Battle formulas: action values as stat formulas, costs from stat pools, MP gauge for party members, action schema

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cthangoverandClaude Opus 5.5 committed 2026-10-24 10:01:12 +02:00
commit 69390b7743
20 files changed
+724

No files matched your search

+33
View File
@@ -0,0 +1,33 @@
namespace Cthangover.Mods.BattleFormulas.Tests
{
using Cthangover.Core.Characters;
using Cthangover.Core.Mods.Testing;
/// <summary>a.* reads the one acting, b.* the target; live attributes under fixed names; other prefixes are unknown.</summary>
public class BattleFormulaScopeTests
{
[ModTest("scope/sides_and_live_names")]
static void SidesAndLiveNames(ModTestContext ctx)
{
var a = new Character { ID = "test_a_no_stats", Level = 4 };
a.Attributes.Health.Init(40);
a.Attributes.Health.Value = 25;
a.Attributes.Attack.Init(7);
var b = new Character { ID = "test_b_no_stats", Level = 2 };
b.Attributes.Defence.Init(3);
b.Attributes.Point.Init(2);
var scope = new BattleFormulaScope(a, b);
ModAssert.True(scope.TryGet("a.hp", out var hp) && hp == 25, "a.hp is the current health");
ModAssert.True(scope.TryGet("a.hp_max", out var max) && max == 40, "a.hp_max");
ModAssert.True(scope.TryGet("a.attack", out var atk) && atk == 7, "a.attack");
ModAssert.True(scope.TryGet("a.level", out var level) && level == 4, "a.level from the battle character");
ModAssert.True(scope.TryGet("b.defence", out var def) && def == 3, "b.defence");
ModAssert.True(scope.TryGet("b.point", out var points) && points == 2, "b.point");
ModAssert.False(scope.TryGet("c.hp", out _), "unknown side");
ModAssert.False(scope.TryGet("hp", out _), "no side");
ModAssert.False(scope.TryGet("b.no_such_stat", out _), "a stat nobody has");
}
}
}
+101
View File
@@ -0,0 +1,101 @@
namespace Cthangover.Mods.BattleFormulas.Tests
{
using Cthangover.Core.Characters;
using Cthangover.Core.Mods.Testing;
using Cthangover.Core.Utils;
/// <summary>
/// A formula action applied to two plain characters: damage through shields, heals
/// capped at the maximum, refused actions changing nothing.
/// </summary>
public class FormulaActionExecutorTests
{
private static Character Fighter(string id, int hp, int attack = 0, int defence = 0, int points = 3)
{
var c = new Character { ID = id, Level = 1 };
c.Attributes.Health.Init(hp);
c.Attributes.Attack.Init(attack);
c.Attributes.Defence.Init(defence);
c.Attributes.Point.Init(points);
return c;
}
private static ActionCharacter Action(ActionCharacterType type, params string[] values)
{
var props = new PropertyData();
foreach (var value in values)
{
var eq = value.IndexOf('=');
props.Values[value.Substring(0, eq)] = value.Substring(eq + 1);
}
return new ActionCharacter { ID = "test/formula", Type = type, Properties = props };
}
[ModTest("executor/damage_from_live_attributes")]
static void DamageFromLiveAttributes(ModTestContext ctx)
{
var hero = Fighter("test_hero_no_stats", 20, attack: 10);
var slime = Fighter("test_slime_no_stats", 30);
var result = new FormulaActionExecutor("test/formula").Execute(Action(ActionCharacterType.ToEnemy, "Formula=a.attack * 2 - 3"), hero, slime);
ModAssert.True(result.Result, "applied");
ModAssert.Equal(17, result.Target.Damage, "10 * 2 - 3");
ModAssert.Equal(13, slime.Attributes.Health.Value, "health lost");
}
[ModTest("executor/shield_absorbs_unless_pierce")]
static void ShieldAbsorbsUnlessPierce(ModTestContext ctx)
{
var hero = Fighter("test_hero_no_stats", 20);
var guard = Fighter("test_guard_no_stats", 30, defence: 5);
var blocked = new FormulaActionExecutor("test/formula").Execute(Action(ActionCharacterType.ToEnemy, "Formula=12"), hero, guard);
ModAssert.Equal(7, blocked.Target.Damage, "the shield took 5");
ModAssert.Equal(-5, blocked.Target.Defence, "shield change");
ModAssert.Equal(0, guard.Attributes.Defence.Value, "the shield is spent");
guard.Attributes.Defence.Value = 5;
var pierced = new FormulaActionExecutor("test/formula").Execute(Action(ActionCharacterType.ToEnemy, "Formula=12", "Pierce=1"), hero, guard);
ModAssert.Equal(12, pierced.Target.Damage, "pierce ignores the shield");
ModAssert.Equal(5, guard.Attributes.Defence.Value, "the shield is kept");
}
[ModTest("executor/heal_is_capped_and_targets_self")]
static void HealIsCapped(ModTestContext ctx)
{
var hero = Fighter("test_hero_no_stats", 20);
hero.Attributes.Health.Value = 15;
var result = new FormulaActionExecutor("test/formula").Execute(Action(ActionCharacterType.ToSelf, "Formula=10", "Effect=heal"), hero, null);
ModAssert.True(result.Result, "applied");
ModAssert.Equal(5, result.Target.Damage, "only the missing 5 are healed");
ModAssert.Equal(20, hero.Attributes.Health.Value, "full");
}
[ModTest("executor/points_refuse_and_negative_is_zero")]
static void PointsRefuseAndNegativeIsZero(ModTestContext ctx)
{
var hero = Fighter("test_hero_no_stats", 20, points: 1);
var slime = Fighter("test_slime_no_stats", 30);
var refused = new FormulaActionExecutor("test/formula").Execute(Action(ActionCharacterType.ToEnemy, "Formula=50", "RequiredPoint=2"), hero, slime);
ModAssert.False(refused.Result, "not enough action points");
ModAssert.Equal(30, slime.Attributes.Health.Value, "nothing happened");
ModAssert.Equal(1, hero.Attributes.Point.Value, "no points spent");
var zero = new FormulaActionExecutor("test/formula").Execute(Action(ActionCharacterType.ToEnemy, "Formula=b.hp - 999"), hero, slime);
ModAssert.Equal(0, zero.Target.Damage, "a negative amount is 0");
ModAssert.Equal(30, slime.Attributes.Health.Value, "never heals by accident");
}
[ModTest("registry/formula_actions_are_detected")]
static void FormulaActionsAreDetected(ModTestContext ctx)
{
ModAssert.True(FormulaActions.IsFormulaAction(new ActionCharacterInfo { ID = "x", Values = new() { "RequiredPoint=1", "formula=a.attack" } }), "Formula= in any case");
ModAssert.False(FormulaActions.IsFormulaAction(new ActionCharacterInfo { ID = "y", Values = new() { "Attack=2" } }), "a core action");
ModAssert.False(FormulaActions.IsFormulaAction(new ActionCharacterInfo { ID = "z" }), "no values");
}
}
}