openthebox/src/OpenTheBox/Simulation/GameSimulation.cs
Samuel Bouchet 41bfb54a2c Implement chain reaction system replacing simple auto-activation
Redesign the interaction mechanic into a recursive chain reaction system
where items can trigger cascading reactions. Keys now open themed chests
which produce items that may trigger further reactions, with chain bonus
rewards for multi-step chains (x2/x3/x4+).

- Add 6 themed chests + mysterious chest + alchemist stone catalyst
- Rewrite InteractionEngine with recursive chain loop (max depth 10)
- Add ConsumeTrigger field to InteractionRule for catalyst support
- Add ChainBonusEvent and enrich InteractionTriggeredEvent with context
- Update rendering to show both reacting items and chain indicators
- Add item descriptions with anticipation hints for chain partners
- Update GDD Section 5 with full chain reaction specification
2026-03-15 18:43:42 +01:00

210 lines
6.7 KiB
C#

using OpenTheBox.Core;
using OpenTheBox.Core.Enums;
using OpenTheBox.Core.Items;
using OpenTheBox.Data;
using OpenTheBox.Simulation.Actions;
using OpenTheBox.Simulation.Events;
namespace OpenTheBox.Simulation;
/// <summary>
/// The BLACK BOX. Zero I/O. Central orchestrator for all game logic.
/// All game actions are processed through this class, which dispatches to the appropriate
/// engine and runs post-processing passes (auto-activation, meta unlocks).
/// </summary>
public class GameSimulation
{
private readonly ContentRegistry _registry;
private readonly Random _rng;
private readonly BoxEngine _boxEngine;
private readonly InteractionEngine _interactionEngine;
private readonly MetaEngine _metaEngine;
private readonly ResourceEngine _resourceEngine;
private readonly CraftingEngine _craftingEngine;
public GameSimulation(ContentRegistry registry, Random? rng = null)
{
_registry = registry;
_rng = rng ?? new Random();
_boxEngine = new BoxEngine(registry);
_interactionEngine = new InteractionEngine(registry);
_metaEngine = new MetaEngine();
_resourceEngine = new ResourceEngine();
_craftingEngine = new CraftingEngine();
}
/// <summary>
/// Processes a game action against the current state, returning all resulting events in order.
/// This is the single entry point for all game logic.
/// </summary>
public List<GameEvent> ProcessAction(GameAction action, GameState state)
{
var events = new List<GameEvent>();
switch (action)
{
case OpenBoxAction openBox:
events.AddRange(HandleOpenBox(openBox, state));
break;
case UseItemAction useItem:
events.AddRange(HandleUseItem(useItem, state));
break;
case CraftAction craft:
events.AddRange(HandleCraft(craft, state));
break;
case StartAdventureAction startAdventure:
events.AddRange(HandleStartAdventure(startAdventure, state));
break;
case ChangeLocaleAction changeLocale:
events.AddRange(HandleChangeLocale(changeLocale, state));
break;
case EquipCosmeticAction equipCosmetic:
events.AddRange(HandleEquipCosmetic(equipCosmetic, state));
break;
case SaveGameAction:
// Save is handled externally; simulation just acknowledges
events.Add(new MessageEvent("save.acknowledged"));
break;
}
return events;
}
private List<GameEvent> HandleOpenBox(OpenBoxAction action, GameState state)
{
var events = new List<GameEvent>();
// Find the box item in inventory
var boxItem = state.Inventory.FirstOrDefault(i => i.Id == action.BoxInstanceId);
if (boxItem is null)
{
events.Add(new MessageEvent("error.item_not_found"));
return events;
}
// Consume the box item
state.RemoveItem(boxItem.Id);
events.Add(new ItemConsumedEvent(boxItem.Id));
// Open the box
var boxEvents = _boxEngine.Open(boxItem.DefinitionId, state, _rng);
events.AddRange(boxEvents);
state.TotalBoxesOpened++;
// Collect newly received items for post-processing
var newItems = boxEvents.OfType<ItemReceivedEvent>().Select(e => e.Item).ToList();
// Run chain reaction pass
events.AddRange(_interactionEngine.RunChainReactions(newItems, state, _rng));
// Run meta pass
events.AddRange(_metaEngine.ProcessNewItems(newItems, state, _registry));
// Run crafting auto-check (new materials may trigger recipes)
events.AddRange(_craftingEngine.AutoCraftCheck(state, _registry));
return events;
}
private List<GameEvent> HandleUseItem(UseItemAction action, GameState state)
{
var events = new List<GameEvent>();
var item = state.Inventory.FirstOrDefault(i => i.Id == action.ItemInstanceId);
if (item is null)
{
events.Add(new MessageEvent("error.item_not_found"));
return events;
}
var itemDef = _registry.GetItem(item.DefinitionId);
if (itemDef is null)
{
events.Add(new MessageEvent("error.definition_not_found"));
return events;
}
// Check if it's a consumable resource item
if (itemDef.ResourceType.HasValue)
{
events.AddRange(_resourceEngine.ProcessConsumable(item, state, _registry));
return events;
}
// Otherwise, check interaction rules
var interactionEvents = _interactionEngine.RunChainReactions([item], state, _rng);
events.AddRange(interactionEvents);
return events;
}
private List<GameEvent> HandleCraft(CraftAction action, GameState state)
{
// Crafting is now automatic — delegate to the CraftingEngine
return _craftingEngine.AutoCraftCheck(state, _registry);
}
private List<GameEvent> HandleStartAdventure(StartAdventureAction action, GameState state)
{
var events = new List<GameEvent>();
if (!state.UnlockedAdventures.Contains(action.Theme))
{
events.Add(new MessageEvent("error.adventure_locked"));
return events;
}
events.Add(new AdventureStartedEvent(action.Theme));
return events;
}
private static List<GameEvent> HandleChangeLocale(ChangeLocaleAction action, GameState state)
{
var events = new List<GameEvent>();
state.CurrentLocale = action.NewLocale;
events.Add(new MessageEvent("locale.changed", [action.NewLocale.ToString()]));
return events;
}
private List<GameEvent> HandleEquipCosmetic(EquipCosmeticAction action, GameState state)
{
var events = new List<GameEvent>();
var item = state.Inventory.FirstOrDefault(i => i.Id == action.ItemInstanceId);
if (item is null)
{
events.Add(new MessageEvent("error.item_not_found"));
return events;
}
var itemDef = _registry.GetItem(item.DefinitionId);
if (itemDef?.CosmeticSlot is null || itemDef.CosmeticValue is null)
{
events.Add(new MessageEvent("error.not_a_cosmetic"));
return events;
}
var slot = itemDef.CosmeticSlot.Value;
var value = itemDef.CosmeticValue;
if (!state.Appearance.ApplyCosmetic(slot, value))
{
events.Add(new MessageEvent("error.cosmetic_apply_failed"));
return events;
}
events.Add(new CosmeticEquippedEvent(slot, value));
return events;
}
}