Add inventory count conditions and item removal actions
This commit is contained in:
1 parent
10fbfe614c
commit
1604bd283e
8 files changed
+156
No files matched your search
@@ -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_action = Item action
|
||||
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.
|
||||
@@ -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_action = Действие предмета
|
||||
items/schema/field/item_action_desc = action/heal, action/equip, action/stat_up… или null
|
||||
|
||||
items/schema/remove = Забрать предмет
|
||||
items/schema/remove_desc = Убирает указанное число предметов. Сначала проверьте количество условием item('id') >= число.
|
||||
@@ -20,6 +20,27 @@
|
||||
"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": [
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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})");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
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.Mods;
|
||||
using Cthangover.Core.Settings;
|
||||
@@ -23,9 +26,15 @@ namespace Cthangover.Mods.Items
|
||||
/// </summary>
|
||||
public class ItemsModInitializer : IModInitializer
|
||||
{
|
||||
private static bool conditionsRegistered;
|
||||
/// <summary>No per-mod work is needed on load; discovery is reflection-based.</summary>
|
||||
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>
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user