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
210 lines
6.7 KiB
C#
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;
|
|
}
|
|
}
|