using OpenTheBox.Core; using OpenTheBox.Core.Enums; using OpenTheBox.Core.Interactions; using OpenTheBox.Core.Items; using OpenTheBox.Data; using OpenTheBox.Simulation.Events; namespace OpenTheBox.Simulation; /// /// Evaluates interaction rules against newly received items and the current game state, /// triggering automatic interactions or requesting player choices when multiple apply. /// public class InteractionEngine(ContentRegistry registry) { /// /// Checks all interaction rules against newly received items and returns events for /// any auto-activations or choice prompts. /// public List CheckAutoActivations(List newItems, GameState state) { var events = new List(); foreach (var newItem in newItems) { var itemDef = registry.GetItem(newItem.DefinitionId); if (itemDef is null) continue; var matchingRules = FindMatchingRules(itemDef, state); if (matchingRules.Count == 0) { // Special case: key without a matching openable item injects a box into future loot tables if (itemDef.Tags.Contains("Key")) { var hasMatchingOpenable = state.Inventory.Any(i => { var def = registry.GetItem(i.DefinitionId); return def is not null && def.Tags.Contains("Openable"); }); if (!hasMatchingOpenable) { events.Add(new LootTableModifiedEvent( BoxId: "starter_box", AddedEntryId: newItem.DefinitionId, Reason: $"Key '{newItem.DefinitionId}' has no matching Openable item; injecting into future loot tables" )); } } continue; } var automaticRules = matchingRules .Where(r => r.IsAutomatic) .OrderByDescending(r => r.Priority) .ToList(); if (automaticRules.Count == 1) { // Single automatic match: auto-execute var rule = automaticRules[0]; events.AddRange(ExecuteRule(rule, newItem, state)); } else if (automaticRules.Count > 1) { // Multiple automatic matches: let the player choose events.Add(new ChoiceRequiredEvent( Prompt: "prompt.choose_interaction", Options: automaticRules.Select(r => r.DescriptionKey).ToList() )); } else { // Non-automatic rules found but none are automatic -- no auto-activation } } return events; } /// /// Finds all interaction rules that match the given item definition and game state. /// private List FindMatchingRules(ItemDefinition itemDef, GameState state) { var matching = new List(); foreach (var rule in registry.InteractionRules) { // Check required tags var tagsMatch = rule.RequiredItemTags.All(tag => itemDef.Tags.Contains(tag)); if (!tagsMatch) continue; // Check required item ids (if specified, at least one must be in inventory) if (rule.RequiredItemIds is not null && rule.RequiredItemIds.Count > 0) { var hasRequiredItem = rule.RequiredItemIds.All(id => state.HasItem(id)); if (!hasRequiredItem) continue; } matching.Add(rule); } return matching; } /// /// Executes a single interaction rule, consuming the trigger item and producing results. /// private List ExecuteRule(InteractionRule rule, ItemInstance triggerItem, GameState state) { var events = new List(); // Consume the trigger item state.RemoveItem(triggerItem.Id); events.Add(new ItemConsumedEvent(triggerItem.Id)); // Handle result based on ResultType if (rule.ResultData is not null) { if (rule.ResultData.StartsWith("adventure:") || rule.ResultData.StartsWith("adventure_unlock:")) { // Unlock an adventure theme (e.g., "adventure:Pirate" or "adventure_unlock:Space") var themeName = rule.ResultData.Contains("adventure_unlock:") ? rule.ResultData["adventure_unlock:".Length..] : rule.ResultData["adventure:".Length..]; if (Enum.TryParse(themeName, out var theme) && state.UnlockedAdventures.Add(theme)) { events.Add(new AdventureUnlockedEvent(theme)); } } else { // Produce result items (item definition ids, comma-separated) var resultItemIds = rule.ResultData.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); foreach (var resultItemId in resultItemIds) { var resultInstance = ItemInstance.Create(resultItemId); state.AddItem(resultInstance); events.Add(new ItemReceivedEvent(resultInstance)); } } } events.Add(new InteractionTriggeredEvent(rule.Id, rule.DescriptionKey)); return events; } }