222 lines
7.1 KiB
C#
222 lines
7.1 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;
|
||
|
|
|
||
|
|
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();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <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 auto-activation pass
|
||
|
|
events.AddRange(_interactionEngine.CheckAutoActivations(newItems, state));
|
||
|
|
|
||
|
|
// Run meta pass
|
||
|
|
events.AddRange(_metaEngine.ProcessNewItems(newItems, 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.CheckAutoActivations([item], state);
|
||
|
|
events.AddRange(interactionEvents);
|
||
|
|
|
||
|
|
return events;
|
||
|
|
}
|
||
|
|
|
||
|
|
private List<GameEvent> HandleCraft(CraftAction action, GameState state)
|
||
|
|
{
|
||
|
|
var events = new List<GameEvent>();
|
||
|
|
|
||
|
|
if (!state.UnlockedWorkstations.Contains(action.Station))
|
||
|
|
{
|
||
|
|
events.Add(new MessageEvent("error.workstation_locked"));
|
||
|
|
return events;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Consume all material items
|
||
|
|
foreach (var materialId in action.MaterialIds)
|
||
|
|
{
|
||
|
|
var material = state.Inventory.FirstOrDefault(i => i.Id == materialId);
|
||
|
|
if (material is null)
|
||
|
|
{
|
||
|
|
events.Add(new MessageEvent("error.material_not_found", [materialId.ToString()]));
|
||
|
|
return events;
|
||
|
|
}
|
||
|
|
|
||
|
|
state.RemoveItem(materialId);
|
||
|
|
events.Add(new ItemConsumedEvent(materialId));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Crafting result is determined by interaction rules matching the materials
|
||
|
|
// The interaction engine will handle rule matching and result production
|
||
|
|
events.Add(new MessageEvent("craft.materials_consumed"));
|
||
|
|
|
||
|
|
return events;
|
||
|
|
}
|
||
|
|
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
|
||
|
|
events.Add(new CosmeticEquippedEvent(itemDef.CosmeticSlot.Value, itemDef.CosmeticValue));
|
||
|
|
|
||
|
|
return events;
|
||
|
|
}
|
||
|
|
}
|