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 chain reactions that can cascade recursively. /// public class InteractionEngine(ContentRegistry registry) { private const int MaxChainDepth = 10; /// /// Runs the full chain reaction loop: checks new items for interactions, /// executes them, and re-checks any produced items until no more reactions fire. /// Returns all events produced across the entire chain. /// public List RunChainReactions(List newItems, GameState state, Random rng) { var allEvents = new List(); var itemsToCheck = new List(newItems); int chainStep = 0; while (itemsToCheck.Count > 0 && chainStep < MaxChainDepth) { var producedItems = new List(); foreach (var newItem in itemsToCheck) { 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 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) { allEvents.Add(new MessageEvent("interaction.key_no_match")); } } continue; } var automaticRules = matchingRules .Where(r => r.IsAutomatic) .OrderByDescending(r => r.Priority) .ToList(); if (automaticRules.Count >= 1) { var rule = automaticRules[0]; var ruleEvents = ExecuteRule(rule, newItem, state, chainStep); allEvents.AddRange(ruleEvents); // Collect items produced by this reaction for the next chain iteration foreach (var evt in ruleEvents) { if (evt is ItemReceivedEvent received) producedItems.Add(received.Item); } } } if (producedItems.Count == 0) break; itemsToCheck = producedItems; chainStep++; } // Award chain bonus if chain was 2+ steps if (chainStep >= 2) { string bonusItemId = chainStep >= 4 ? "box_legendary" : chainStep >= 3 ? "box_cool" : "box_ok_tier"; var bonusInstance = ItemInstance.Create(bonusItemId); state.AddItem(bonusInstance); allEvents.Add(new ItemReceivedEvent(bonusInstance)); allEvents.Add(new ChainBonusEvent(chainStep, bonusItemId)); } return allEvents; } /// /// 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, all 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 items and producing results. /// private List ExecuteRule(InteractionRule rule, ItemInstance triggerItem, GameState state, int chainStep) { var events = new List(); // Find the partner item (first matching required item id) for the interaction message string? partnerItemId = null; if (rule.RequiredItemIds is not null && rule.RequiredItemIds.Count > 0) { partnerItemId = rule.RequiredItemIds[0]; } // Consume the trigger item (unless it's a catalyst) if (rule.ConsumeTrigger) { state.RemoveItem(triggerItem.Id); events.Add(new ItemConsumedEvent(triggerItem.Id)); } // Consume required inventory items (the partner items) if (rule.RequiredItemIds is not null) { foreach (var requiredId in rule.RequiredItemIds) { var invItem = state.Inventory.FirstOrDefault(i => i.DefinitionId == requiredId); if (invItem is not null) { state.RemoveItem(invItem.Id); events.Add(new ItemConsumedEvent(invItem.Id)); } } } // Emit the interaction event with context events.Add(new InteractionTriggeredEvent(rule.Id, rule.DescriptionKey, triggerItem.DefinitionId, partnerItemId, chainStep)); // Handle result based on ResultData if (rule.ResultData is not null) { if (rule.ResultData.StartsWith("adventure:") || rule.ResultData.StartsWith("adventure_unlock:")) { 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 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)); } } } return events; } }