Targeted item actions (action/heal), item.add scenario action, inventory cell Decorate; mod cover
This commit is contained in:
1 parent
d560638501
commit
468e393550
9 files changed
+313
-1
No files matched your search
@@ -0,0 +1,43 @@
|
|||||||
|
using System;
|
||||||
|
using Cthangover.Core.Items;
|
||||||
|
using Cthangover.Core.Settings;
|
||||||
|
|
||||||
|
namespace Cthangover.Mods.Items
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Restores health of the chosen party member (<c>"ItemAction": "action/heal"</c>).
|
||||||
|
/// The amount is item data, not code: the item's <c>"heal"</c> key (an extra JSON
|
||||||
|
/// key, so it lands in <see cref="IItem.Properties"/>), which lets one action serve
|
||||||
|
/// a mushroom and an elixir alike. Health never rises above its base value, and a
|
||||||
|
/// fallen character (health 0) is not revived — that is a different effect.
|
||||||
|
/// The party member's attributes are the very objects the battle uses, so a heal
|
||||||
|
/// in the inventory during a battle shows on the battle widgets at once.
|
||||||
|
/// </summary>
|
||||||
|
public class HealItemAction : ITargetedItemAction
|
||||||
|
{
|
||||||
|
/// <summary>Item property with the amount of health restored.</summary>
|
||||||
|
public const string HealProperty = "heal";
|
||||||
|
|
||||||
|
/// <summary>Action id referenced by item definitions.</summary>
|
||||||
|
public string ID => "action/heal";
|
||||||
|
|
||||||
|
/// <summary>Healing always needs a target; without one nothing happens.</summary>
|
||||||
|
public bool UseAction(IItem item) => false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the item's <c>heal</c> amount to the target's health, capped at the
|
||||||
|
/// base value. Returns <c>false</c> (item not spent) for a missing target, a
|
||||||
|
/// non-positive amount, a fallen target or one already at full health.
|
||||||
|
/// </summary>
|
||||||
|
public bool UseOn(IItem item, CharacterInfoData target)
|
||||||
|
{
|
||||||
|
var health = target?.Attributes?.Health;
|
||||||
|
var amount = item?.Properties?.GetInt(HealProperty) ?? 0;
|
||||||
|
if (health == null || amount <= 0 || health.Value <= 0 || health.Value >= health.BaseValue)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
health.Value = Math.Min(health.BaseValue, health.Value + amount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using Cthangover.Core.Items;
|
||||||
|
using Cthangover.Core.Settings;
|
||||||
|
|
||||||
|
namespace Cthangover.Mods.Items
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Item action that needs to know whom it is applied to. The core
|
||||||
|
/// <see cref="IItemAction"/> contract has no target, so an effect on a party
|
||||||
|
/// member (healing) could not tell which member was picked in
|
||||||
|
/// <see cref="Cthangover.Core.UI.Inventory.CharacterTargetPanel"/>. Items with
|
||||||
|
/// <see cref="ItemType.TargetUsed"/> whose action implements this interface get
|
||||||
|
/// <see cref="UseOn"/> with the chosen character; other actions keep receiving
|
||||||
|
/// the plain <see cref="IItemAction.UseAction"/>.
|
||||||
|
/// </summary>
|
||||||
|
public interface ITargetedItemAction : IItemAction
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Applies the item to <paramref name="target"/>. Returns <c>false</c> when
|
||||||
|
/// nothing happened (e.g. healing at full health), so the item is not spent.
|
||||||
|
/// </summary>
|
||||||
|
bool UseOn(IItem item, CharacterInfoData target);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using Cthangover.Core.Actions;
|
||||||
|
|
||||||
|
namespace Cthangover.Mods.Items
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <c>action item.add id=resource/branches [count=3]</c> — puts items into the
|
||||||
|
/// player's inventory from a scenario: a chest, a gift, a pickup on an RPG map.
|
||||||
|
/// <c>count</c> defaults to 1; an unknown item id is reported by the inventory
|
||||||
|
/// and adds nothing.
|
||||||
|
/// </summary>
|
||||||
|
public class ItemAddAction : IScenarioAction
|
||||||
|
{
|
||||||
|
/// <summary>DSL name.</summary>
|
||||||
|
public string Name => "item.add";
|
||||||
|
|
||||||
|
/// <summary>Reads <c>id</c> and <c>count</c> and adds the stack.</summary>
|
||||||
|
public void Run(IActionContext ctx)
|
||||||
|
{
|
||||||
|
var id = ctx.GetParam("id");
|
||||||
|
if (string.IsNullOrWhiteSpace(id))
|
||||||
|
{
|
||||||
|
ctx.LogWarning("INVENTORY", "item.add: missing 'id'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var countText = ctx.GetParam("count");
|
||||||
|
var count = 1;
|
||||||
|
if (!string.IsNullOrWhiteSpace(countText) && (!int.TryParse(countText, out count) || count <= 0))
|
||||||
|
{
|
||||||
|
ctx.LogWarning("INVENTORY", $"item.add: 'count' must be a positive number, got '{countText}'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.Inventory == null)
|
||||||
|
{
|
||||||
|
ctx.LogWarning("INVENTORY", "item.add: no inventory in this game");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.Inventory.Add(id.Trim(), count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,6 +88,23 @@ namespace Cthangover.Core.UI.Inventory
|
|||||||
_count.Modulate = dimmed ? IconDimmed : Colors.White;
|
_count.Modulate = dimmed ? IconDimmed : Colors.White;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Overrides what <see cref="UpdateInfo"/> derived from the item's action,
|
||||||
|
/// for grids where the action is not the point — the trade window shows a
|
||||||
|
/// price in the corner tag (<paramref name="tag"/>, <c>null</c> hides it) and
|
||||||
|
/// dims what cannot be traded. Call after <see cref="Construct"/>.
|
||||||
|
/// </summary>
|
||||||
|
public void Decorate(string tag, bool dimmed)
|
||||||
|
{
|
||||||
|
if (!_built)
|
||||||
|
return;
|
||||||
|
_tag.Visible = tag != null;
|
||||||
|
if (tag != null)
|
||||||
|
_tagLabel.Text = tag;
|
||||||
|
_icon.Modulate = dimmed ? IconDimmed : Colors.White;
|
||||||
|
_count.Modulate = dimmed ? IconDimmed : Colors.White;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Switches the frame to the gold selected state; the panel owns this flag.</summary>
|
/// <summary>Switches the frame to the gold selected state; the panel owns this flag.</summary>
|
||||||
public void SetSelected(bool selected)
|
public void SetSelected(bool selected)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using Cthangover.Core.UI.Theming;
|
|||||||
using Godot;
|
using Godot;
|
||||||
|
|
||||||
using Cthangover.Mods.Interface.Base.Lists;
|
using Cthangover.Mods.Interface.Base.Lists;
|
||||||
|
using Cthangover.Mods.Items;
|
||||||
using Cthangover.Mods.Interface.Theming;
|
using Cthangover.Mods.Interface.Theming;
|
||||||
namespace Cthangover.Core.UI.Inventory
|
namespace Cthangover.Core.UI.Inventory
|
||||||
{
|
{
|
||||||
@@ -978,7 +979,10 @@ namespace Cthangover.Core.UI.Inventory
|
|||||||
if (!HasActionPoint())
|
if (!HasActionPoint())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!item.ItemAction.UseAction(item))
|
var applied = item.ItemAction is ITargetedItemAction targeted
|
||||||
|
? targeted.UseOn(item, info)
|
||||||
|
: item.ItemAction.UseAction(item);
|
||||||
|
if (!applied)
|
||||||
{
|
{
|
||||||
SetMessage(MessageKind.Error, InventoryItemUi.Translate("ui/inventory/applied_failed"));
|
SetMessage(MessageKind.Error, InventoryItemUi.Translate("ui/inventory/applied_failed"));
|
||||||
_targetOpen = false;
|
_targetOpen = false;
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
namespace Cthangover.Mods.Items.Tests
|
||||||
|
{
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Cthangover.Core.Actions;
|
||||||
|
using Cthangover.Core.Items;
|
||||||
|
using Cthangover.Core.Mods;
|
||||||
|
|
||||||
|
/// <summary>Action context for scenario action tests: dialog variables and an inventory, no other services.</summary>
|
||||||
|
public class FakeActionContext : IActionContext
|
||||||
|
{
|
||||||
|
/// <summary>Dialog variables returned by <see cref="GetParam"/>.</summary>
|
||||||
|
public Dictionary<string, string> Params { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>Warnings the action logged.</summary>
|
||||||
|
public List<string> Warnings { get; } = new();
|
||||||
|
|
||||||
|
public IQuestService Quests => null;
|
||||||
|
public ICharacterService Character => null;
|
||||||
|
public ILightingService Lighting => null;
|
||||||
|
public ISceneNodeService Scene => null;
|
||||||
|
public IModRegistry ModRegistry => null;
|
||||||
|
public IInventory Inventory { get; set; }
|
||||||
|
public IMapZoneService Map => null;
|
||||||
|
|
||||||
|
public string GetParam(string name) => Params.TryGetValue(name, out var value) ? value : null;
|
||||||
|
|
||||||
|
public void Log(string category, string message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogWarning(string category, string message) => Warnings.Add(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
namespace Cthangover.Mods.Items.Tests
|
||||||
|
{
|
||||||
|
using Cthangover.Core.Characters;
|
||||||
|
using Cthangover.Core.Items;
|
||||||
|
using Cthangover.Core.Mods.Testing;
|
||||||
|
using Cthangover.Core.Settings;
|
||||||
|
using Cthangover.Core.Utils;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>action/heal</c>: the amount comes from the item's <c>heal</c> property, health is
|
||||||
|
/// capped at its base value, and an item that does nothing is reported as not used.
|
||||||
|
/// </summary>
|
||||||
|
public class HealItemActionTests
|
||||||
|
{
|
||||||
|
private static Item Food(int heal)
|
||||||
|
{
|
||||||
|
var properties = new PropertyData();
|
||||||
|
properties.SetInt(HealItemAction.HealProperty, heal);
|
||||||
|
return new Item { ID = "test/food", Properties = properties };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CharacterInfoData Hero(int health, int max)
|
||||||
|
{
|
||||||
|
var attributes = new CharacterAttributes();
|
||||||
|
attributes.Health.Value = health;
|
||||||
|
attributes.Health.BaseValue = max;
|
||||||
|
return new CharacterInfoData { ID = "hero", Attributes = attributes };
|
||||||
|
}
|
||||||
|
|
||||||
|
[ModTest("heal/restores_the_amount")]
|
||||||
|
static void RestoresTheAmount(ModTestContext ctx)
|
||||||
|
{
|
||||||
|
var hero = Hero(10, 24);
|
||||||
|
ModAssert.True(new HealItemAction().UseOn(Food(5), hero), "wounded hero is healed");
|
||||||
|
ModAssert.Equal(15, hero.Attributes.Health.Value, "10 + 5");
|
||||||
|
ModAssert.Equal(24, hero.Attributes.Health.BaseValue, "max unchanged");
|
||||||
|
}
|
||||||
|
|
||||||
|
[ModTest("heal/capped_at_max")]
|
||||||
|
static void CappedAtMax(ModTestContext ctx)
|
||||||
|
{
|
||||||
|
var hero = Hero(22, 24);
|
||||||
|
ModAssert.True(new HealItemAction().UseOn(Food(20), hero), "partly wounded hero is healed");
|
||||||
|
ModAssert.Equal(24, hero.Attributes.Health.Value, "never above max");
|
||||||
|
}
|
||||||
|
|
||||||
|
[ModTest("heal/useless_heal_is_not_used")]
|
||||||
|
static void UselessHealIsNotUsed(ModTestContext ctx)
|
||||||
|
{
|
||||||
|
var action = new HealItemAction();
|
||||||
|
|
||||||
|
var full = Hero(24, 24);
|
||||||
|
ModAssert.False(action.UseOn(Food(5), full), "full health");
|
||||||
|
ModAssert.Equal(24, full.Attributes.Health.Value, "full stays full");
|
||||||
|
|
||||||
|
var fallen = Hero(0, 24);
|
||||||
|
ModAssert.False(action.UseOn(Food(5), fallen), "a fallen hero is not revived");
|
||||||
|
ModAssert.Equal(0, fallen.Attributes.Health.Value, "fallen stays fallen");
|
||||||
|
|
||||||
|
var wounded = Hero(10, 24);
|
||||||
|
ModAssert.False(action.UseOn(Food(0), wounded), "no heal amount");
|
||||||
|
ModAssert.False(action.UseOn(new Item { ID = "plain" }, wounded), "no properties");
|
||||||
|
ModAssert.False(action.UseOn(Food(5), null), "no target");
|
||||||
|
ModAssert.False(action.UseOn(Food(5), new CharacterInfoData { ID = "bare" }), "no attributes");
|
||||||
|
ModAssert.False(action.UseAction(Food(5)), "heal needs a target");
|
||||||
|
ModAssert.Equal(10, wounded.Attributes.Health.Value, "untouched");
|
||||||
|
}
|
||||||
|
|
||||||
|
[ModTest("heal/id")]
|
||||||
|
static void Id(ModTestContext ctx)
|
||||||
|
{
|
||||||
|
ModAssert.Equal("action/heal", new HealItemAction().ID, "item definitions reference this id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
namespace Cthangover.Mods.Items.Tests
|
||||||
|
{
|
||||||
|
using System.Linq;
|
||||||
|
using Cthangover.Core.Items;
|
||||||
|
using Cthangover.Core.Mods.Resolvers;
|
||||||
|
using Cthangover.Core.Mods.Testing;
|
||||||
|
|
||||||
|
/// <summary><c>action item.add</c> from a scenario: a count defaults to one, a bad one adds nothing.</summary>
|
||||||
|
public class ItemAddActionTests
|
||||||
|
{
|
||||||
|
private static FakeActionContext Context(string id, string count)
|
||||||
|
{
|
||||||
|
var ctx = new FakeActionContext { Inventory = new Inventory() };
|
||||||
|
if (id != null)
|
||||||
|
ctx.Params["id"] = id;
|
||||||
|
if (count != null)
|
||||||
|
ctx.Params["count"] = count;
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ModTest("item_add/adds_count")]
|
||||||
|
static void AddsCount(ModTestContext test)
|
||||||
|
{
|
||||||
|
var id = Entities.Collect<ItemInfo>().Keys.First();
|
||||||
|
var ctx = Context(id, "3");
|
||||||
|
var before = ctx.Inventory.CheckCount(id);
|
||||||
|
|
||||||
|
new ItemAddAction().Run(ctx);
|
||||||
|
|
||||||
|
ModAssert.Equal(before + 3, ctx.Inventory.CheckCount(id), "three added");
|
||||||
|
ModAssert.Equal(0, ctx.Warnings.Count, "no warnings");
|
||||||
|
}
|
||||||
|
|
||||||
|
[ModTest("item_add/count_defaults_to_one")]
|
||||||
|
static void CountDefaultsToOne(ModTestContext test)
|
||||||
|
{
|
||||||
|
var id = Entities.Collect<ItemInfo>().Keys.First();
|
||||||
|
var ctx = Context(id, null);
|
||||||
|
var before = ctx.Inventory.CheckCount(id);
|
||||||
|
|
||||||
|
new ItemAddAction().Run(ctx);
|
||||||
|
|
||||||
|
ModAssert.Equal(before + 1, ctx.Inventory.CheckCount(id), "one added");
|
||||||
|
}
|
||||||
|
|
||||||
|
[ModTest("item_add/bad_arguments_add_nothing")]
|
||||||
|
static void BadArgumentsAddNothing(ModTestContext test)
|
||||||
|
{
|
||||||
|
var id = Entities.Collect<ItemInfo>().Keys.First();
|
||||||
|
foreach (var count in new[] { "0", "-2", "many" })
|
||||||
|
{
|
||||||
|
var ctx = Context(id, count);
|
||||||
|
var before = ctx.Inventory.CheckCount(id);
|
||||||
|
new ItemAddAction().Run(ctx);
|
||||||
|
ModAssert.Equal(before, ctx.Inventory.CheckCount(id), "count=" + count);
|
||||||
|
ModAssert.Equal(1, ctx.Warnings.Count, "warned about count=" + count);
|
||||||
|
}
|
||||||
|
|
||||||
|
var noId = Context(null, "1");
|
||||||
|
new ItemAddAction().Run(noId);
|
||||||
|
ModAssert.Equal(1, noId.Warnings.Count, "warned about the missing id");
|
||||||
|
|
||||||
|
var noInventory = new FakeActionContext();
|
||||||
|
noInventory.Params["id"] = id;
|
||||||
|
new ItemAddAction().Run(noInventory);
|
||||||
|
ModAssert.Equal(1, noInventory.Warnings.Count, "warned about the missing inventory");
|
||||||
|
}
|
||||||
|
|
||||||
|
[ModTest("item_add/name")]
|
||||||
|
static void Name(ModTestContext test)
|
||||||
|
{
|
||||||
|
ModAssert.Equal("item.add", new ItemAddAction().Name, "DSL name");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user