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_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.
|
||||||
@@ -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') >= число.
|
||||||
@@ -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": [
|
||||||
|
|||||||
@@ -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.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>
|
||||||
|
|||||||
@@ -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