Add inventory count conditions and item removal actions

This commit is contained in:
cthangover committed 2026-10-27 01:07:31 +01:00
1 parent 10fbfe614c
commit 1604bd283e
8 files changed
+156

No files matched your search

+3
View File
@@ -12,3 +12,6 @@ items/schema/field/item_type = Item types
items/schema/field/item_type_desc = Comma separated: Resource, Food, Used, TargetUsed, Quest, Recipe, CantDrop items/schema/field/item_type_desc = Comma separated: Resource, Food, Used, TargetUsed, Quest, Recipe, CantDrop
items/schema/field/item_action = Item action items/schema/field/item_action = Item action
items/schema/field/item_action_desc = action/heal, action/equip, action/stat_up… or null items/schema/field/item_action_desc = action/heal, action/equip, action/stat_up… or null
items/schema/remove = Remove item
items/schema/remove_desc = Removes a positive stack. Gate the delivery with item('id') >= count first.
+3
View File
@@ -12,3 +12,6 @@ items/schema/field/item_type = Типы предмета
items/schema/field/item_type_desc = Через запятую: Resource, Food, Used, TargetUsed, Quest, Recipe, CantDrop items/schema/field/item_type_desc = Через запятую: Resource, Food, Used, TargetUsed, Quest, Recipe, CantDrop
items/schema/field/item_action = Действие предмета items/schema/field/item_action = Действие предмета
items/schema/field/item_action_desc = action/heal, action/equip, action/stat_up… или null items/schema/field/item_action_desc = action/heal, action/equip, action/stat_up… или null
items/schema/remove = Забрать предмет
items/schema/remove_desc = Убирает указанное число предметов. Сначала проверьте количество условием item('id') >= число.
+21
View File
@@ -20,6 +20,27 @@
"Name": "items/schema/field/count" "Name": "items/schema/field/count"
} }
] ]
},
{
"Id": "item.remove",
"Name": "items/schema/remove",
"Description": "items/schema/remove_desc",
"Category": "items",
"Params": [
{
"Key": "id",
"Type": "ref",
"Kind": "item",
"Required": true,
"Name": "items/schema/field/item"
},
{
"Key": "count",
"Type": "int",
"Default": "1",
"Name": "items/schema/field/count"
}
]
} }
], ],
"Kinds": [ "Kinds": [
+25
View File
@@ -0,0 +1,25 @@
namespace Cthangover.Mods.Items
{
using Cthangover.Core.Scenes;
using Cthangover.Core.Settings;
/// <summary>Reads the current bag on evaluation, so saves and new games never leave a cached inventory behind.</summary>
public class ItemCountCondition : ConditionNode
{
private readonly string id;
private readonly string op;
private readonly int count;
/// <summary>Compares the owned count of an item with a whole-number threshold.</summary>
public ItemCountCondition(string id, string op, int count)
{
this.id = id;
this.op = op;
this.count = count;
}
/// <inheritdoc />
public override bool Evaluate(EvaluationContext ctx)
=> ConditionCompare.Compare(op, GameData.Instance?.Runtime?.Inventory?.CheckCount(id) ?? 0, count);
}
}
+36
View File
@@ -0,0 +1,36 @@
namespace Cthangover.Mods.Items
{
using System;
using Cthangover.Core.Scenes;
using Cthangover.Core.Scenes.Parsing;
/// <summary>Adds item('id') OP count to the shared condition language, usable by quests, choices and map pages alike.</summary>
public class ItemCountParser : ILeafParser
{
/// <inheritdoc />
public bool CanParse(TokenPeeker peeker) => !peeker.AtEnd
&& peeker.Peek.Type == ConditionParser.TokenType.Ident && peeker.Peek.Value == "item"
&& peeker.PeekAhead(1).Type == ConditionParser.TokenType.LParen;
/// <inheritdoc />
public ICondition Parse(TokenPeeker peeker, Func<ICondition> parseSubExpr)
{
peeker.Advance();
peeker.Expect(ConditionParser.TokenType.LParen, "expected '(' after item");
if (!peeker.Check(ConditionParser.TokenType.String) && !peeker.Check(ConditionParser.TokenType.Ident))
throw new FormatException("item() expects an item id");
var id = peeker.Advance().Value;
peeker.Expect(ConditionParser.TokenType.RParen, "expected ')' after item id");
var op = peeker.Peek.Type switch
{
ConditionParser.TokenType.Eq => "==", ConditionParser.TokenType.Neq => "!=",
ConditionParser.TokenType.Ge => ">=", ConditionParser.TokenType.Le => "<=",
ConditionParser.TokenType.Gt => ">", ConditionParser.TokenType.Lt => "<",
_ => throw new FormatException("item() expects a comparison")
};
peeker.Advance();
var count = peeker.Expect(ConditionParser.TokenType.Number, "item() expects a whole-number count");
return new ItemCountCondition(id, op, int.Parse(count.Value));
}
}
}
+26
View File
@@ -0,0 +1,26 @@
namespace Cthangover.Mods.Items
{
using Cthangover.Core.Actions;
/// <summary>Consumes a positive stack from a scenario. Gate deliveries with item('id') >= count before running it.</summary>
public class ItemRemoveAction : IScenarioAction
{
/// <summary>Scenario id.</summary>
public string Name => "item.remove";
/// <summary>Refuses an invalid or insufficient stack without changing inventory.</summary>
public void Run(IActionContext ctx)
{
var id = ctx.GetParam("id")?.Trim();
var text = ctx.GetParam("count");
var count = 1;
if (string.IsNullOrWhiteSpace(id) || (!string.IsNullOrWhiteSpace(text) && !int.TryParse(text, out count)) || count <= 0)
{
ctx.LogWarning("INVENTORY", "item.remove: expected an item id and a positive count");
return;
}
if (ctx.Inventory?.Remove(id, count) != true)
ctx.LogWarning("INVENTORY", $"item.remove: insufficient '{id}' (need {count})");
}
}
}
+9
View File
@@ -1,4 +1,7 @@
using Cthangover.Core.Factories.Impls; using Cthangover.Core.Factories.Impls;
using Cthangover.Core.Events;
using Cthangover.Core.Scenes;
using Cthangover.Core.Scenes.Parsing;
using Cthangover.Core.Items; using Cthangover.Core.Items;
using Cthangover.Core.Mods; using Cthangover.Core.Mods;
using Cthangover.Core.Settings; using Cthangover.Core.Settings;
@@ -23,9 +26,15 @@ namespace Cthangover.Mods.Items
/// </summary> /// </summary>
public class ItemsModInitializer : IModInitializer public class ItemsModInitializer : IModInitializer
{ {
private static bool conditionsRegistered;
/// <summary>No per-mod work is needed on load; discovery is reflection-based.</summary> /// <summary>No per-mod work is needed on load; discovery is reflection-based.</summary>
public void OnModLoaded(string modId) public void OnModLoaded(string modId)
{ {
if (conditionsRegistered)
return;
conditionsRegistered = true;
LeafParserRegistry.RegisterFirst(new ItemCountParser());
ModEventBus.Subscribe<InventoryChangedEvent>(ConditionStateMonitor.Notify);
} }
/// <summary>Installs the RuntimeData factory hooks and binds the item-action bridge (both idempotent).</summary> /// <summary>Installs the RuntimeData factory hooks and binds the item-action bridge (both idempotent).</summary>
+33
View File
@@ -0,0 +1,33 @@
namespace Cthangover.Mods.Items.Tests
{
using Cthangover.Core.Items;
using Cthangover.Core.Mods.Resolvers;
using Cthangover.Core.Mods.Testing;
using Cthangover.Core.Scenes;
using Cthangover.Core.Settings;
using System.Linq;
/// <summary>Conditions read current inventory state and compose with the scenario language's existing operators.</summary>
public class ItemCountConditionTests
{
[ModTest("items/count_condition_follows_bag_changes")]
static void CountFollowsBag(ModTestContext ctx)
{
var bag = GameData.Instance.Runtime.Inventory;
var before = bag.Items;
bag.Items = new();
try
{
var id = Entities.Collect<ItemInfo>().Keys.First();
ModAssert.True(ScenarioCondition.Evaluate($"item('{id}') == 0"), "empty bag");
bag.Add(id, 3);
ModAssert.True(ScenarioCondition.Evaluate($"item('{id}') >= 3 && item('{id}') < 4"), "count range");
ModAssert.False(ScenarioCondition.Evaluate($"item('{id}') > 3"), "upper bound");
bag.Remove(id, 2);
ModAssert.True(ScenarioCondition.Evaluate($"item('{id}') == 1"), "updated bag");
ModAssert.True(ScenarioCondition.Evaluate("item('missing/item') == 0"), "unknown id counts zero");
}
finally { bag.Items = before; }
}
}
}