openthebox/src/OpenTheBox/Simulation/InteractionEngine.cs

156 lines
5.7 KiB
C#
Raw Normal View History

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;
/// <summary>
/// Evaluates interaction rules against newly received items and the current game state,
/// triggering automatic interactions or requesting player choices when multiple apply.
/// </summary>
public class InteractionEngine(ContentRegistry registry)
{
/// <summary>
/// Checks all interaction rules against newly received items and returns events for
/// any auto-activations or choice prompts.
/// </summary>
public List<GameEvent> CheckAutoActivations(List<ItemInstance> newItems, GameState state)
{
var events = new List<GameEvent>();
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;
}
/// <summary>
/// Finds all interaction rules that match the given item definition and game state.
/// </summary>
private List<InteractionRule> FindMatchingRules(ItemDefinition itemDef, GameState state)
{
var matching = new List<InteractionRule>();
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;
}
/// <summary>
/// Executes a single interaction rule, consuming the trigger item and producing results.
/// </summary>
private List<GameEvent> ExecuteRule(InteractionRule rule, ItemInstance triggerItem, GameState state)
{
var events = new List<GameEvent>();
// Consume the trigger item
state.RemoveItem(triggerItem.Id);
events.Add(new ItemConsumedEvent(triggerItem.Id));
2026-03-11 22:19:25 +01:00
// Handle result based on ResultType
if (rule.ResultData is not null)
{
2026-03-11 22:19:25 +01:00
if (rule.ResultData.StartsWith("adventure:") || rule.ResultData.StartsWith("adventure_unlock:"))
{
2026-03-11 22:19:25 +01:00
// 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<AdventureTheme>(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, triggerItem.DefinitionId));
return events;
}
}