diff --git a/Assets/Luneraria/Core/Content/ContentData.cs b/Assets/Luneraria/Core/Content/ContentData.cs
index 78b0133..e377e39 100644
--- a/Assets/Luneraria/Core/Content/ContentData.cs
+++ b/Assets/Luneraria/Core/Content/ContentData.cs
@@ -60,7 +60,13 @@ namespace Luneraria.Core
new HeroSpec { Id = "held_sylvara", Name = "Sylvara", Title = "Die Mondklinge", Model = "sylvara", Cost = 260, RoleText = "Assassine / Nahkampf", Provisional = false, Summary = "Springt in die zweite Reihe, wirft Sicheln und wirbelt mit dem Mondsicheltanz durch Gruppen.",
PassiveText = "Mondsprung: einmal pro Kampf auf den nächsten Fernkämpfer oder Unterstützer in 8 m, der höchstens 2 Begleiter in 3 m hat. Der erste Treffer danach macht 150 % Schaden; Mondschleier schützt 4 s mit 25 % ihrer Lebenspunkte.",
AbilityText = "Sichelwurf: ab 4 s alle 6 s 90 % Schaden an ihrem Ziel und bis zu 2 weiteren Gegnern in 2,5 m.",
- UltimateName = "Mondsicheltanz", UltimateText = "Einmal pro Kampf, frühestens nach 2 s: 220 % Schaden an bis zu 5 Gegnern in 5 m, die 2 s um 30 % verlangsamt werden; danach Mondschleier. Wann er ausgelöst wird, legst du in der Planung fest.", Base = new UnitDef { MaxHp = 1450.0, Damage = 42.0, AttackType = 0, ArmorType = 0, AttackRange = 1.2, Speed = 2.8, Ability = "sylvara", AbilityDescription = "Mondsprung auf ungeschützte Fernkämpfer · Sichelwurf alle 6 s · Ultimate Mondsicheltanz nach Planungsregel." } }
+ UltimateName = "Mondsicheltanz", UltimateText = "Einmal pro Kampf, frühestens nach 2 s: 220 % Schaden an bis zu 5 Gegnern in 5 m, die 2 s um 30 % verlangsamt werden; danach Mondschleier. Wann er ausgelöst wird, legst du in der Planung fest.", Base = new UnitDef { MaxHp = 1450.0, Damage = 42.0, AttackType = 0, ArmorType = 0, AttackRange = 1.2, Speed = 2.8, Ability = "sylvara", AbilityDescription = "Mondsprung auf ungeschützte Fernkämpfer · Sichelwurf alle 6 s · Ultimate Mondsicheltanz nach Planungsregel." } },
+ // H2: Brunhild, the second hero of the champions concept.
+ new HeroSpec { Id = "held_brunhild", Name = "Brunhild", Title = "Die Walküre", Model = "brunhild", Cost = 260, RoleText = "Heldenjägerin / Nahkampf", Provisional = false, Summary = "Markiert mit dem Speer, stürzt sich auf Helden und betäubt mit dem Walkürensturz.",
+ PassiveText = "Heldenjägerin: +25 % Schaden gegen Helden und Angriffschampions.",
+ AbilityText = "Speerwurf: ab 3 s alle 6 s 120 % Schaden am fernsten Gegner in 7 m; er ist 4 s markiert und nimmt 15 % mehr Schaden.",
+ UltimateName = "Walkürensturz", UltimateText = "Einmal pro Kampf, frühestens nach 2 s: Sprung auf ihr Ziel (ein Held oder der markierte Gegner zuerst), 200 % Schaden an allen Gegnern in 3 m und 1 s Betäubung (Bosse sind immun); danach Walkürenschild (30 % ihrer Lebenspunkte, 5 s). Wann sie springt, legst du in der Planung fest.",
+ Base = new UnitDef { MaxHp = 2000.0, Damage = 42.0, AttackType = 1, ArmorType = 1, AttackRange = 2.0, Speed = 2.4, Ability = "brunhild", AbilityDescription = "Heldenjägerin · Speerwurf markiert alle 6 s · Ultimate Walkürensturz nach Planungsregel." } }
);
/// Heroes of the Godot reference; ContentTests compares exactly these, later heroes follow them.
@@ -91,6 +97,11 @@ namespace Luneraria.Core
{
new[] { new TalentOption("mondsilber", "Mondsilber", "Einzelziel: Der Sichelwurf trifft nur das Ziel, dafür mit 200 % statt 90 % Schaden."), new TalentOption("sichelsturm", "Sichelsturm", "Fläche: Der Sichelwurf trifft bis zu 5 statt 3 Gegner in 3,5 m.") },
new[] { new TalentOption("neumond", "Neumond", "+20 % Angriffstempo."), new TalentOption("vollmond", "Vollmond", "Mondsicheltanz: bis zu 7 statt 5 Ziele, 280 % statt 220 % Schaden.") }
+ } },
+ { "held_brunhild", new[]
+ {
+ new[] { new TalentOption("walkuerenblut", "Walkürenblut", "Einzelziel: Der Speerwurf macht gegen Helden und Champions doppelten Schaden."), new TalentOption("speerregen", "Speerregen", "Fläche: Der Speerwurf trifft die drei fernsten Gegner in 7 m.") },
+ new[] { new TalentOption("rabenschwinge", "Rabenschwinge", "+20 % Angriffstempo."), new TalentOption("goetterdaemmerung", "Götterdämmerung", "Walkürensturz: 2 s Betäubung in 4 m statt 1 s in 3 m.") }
} }
};
@@ -102,6 +113,7 @@ namespace Luneraria.Core
public static readonly Dictionary HeroSkins = new Dictionary
{
{ "held_sylvara", new[] { new TalentOption("blutmond", "Blutmond", "Variante: rote Klingen und Runen, dunkelrotes Tuch, nachgedunkeltes Silber.") } },
+ { "held_brunhild", new[] { new TalentOption("eiswalkuere", "Eiswalküre", "Variante: eisblauer Umhang und Federn statt Rot, frostiges Gold.") } },
};
///
@@ -118,6 +130,13 @@ namespace Luneraria.Core
new TalentOption("notfall", "Notfall", "Wenn Sylvara unter 40 % Leben fällt und ein Gegner in 4 m steht."),
new TalentOption("sofort", "Sofort", "Beim ersten Gegner in 4 m."),
} },
+ { "held_brunhild", new[]
+ {
+ new TalentOption("gruppe", "Gruppe", "Sobald mindestens 3 Gegner in 4 m um ihr Ziel stehen."),
+ new TalentOption("held", "Heldenjagd", "Sobald ein gegnerischer Held oder Angriffschampion in 7 m steht."),
+ new TalentOption("notfall", "Notfall", "Wenn Brunhild unter 40 % Leben fällt und ein Gegner in 7 m steht."),
+ new TalentOption("sofort", "Sofort", "Beim ersten Gegner in 7 m."),
+ } },
};
public static readonly OrderedMap Items = Ordered(
@@ -145,7 +164,7 @@ namespace Luneraria.Core
/// Base troop id to role; iteration order matters for RoleOf (first match).
public static readonly OrderedMap MatchRoles = Pairs("g01", "melee", "g03", "front", "g05", "ranged", "g07", "splash", "g09", "support", "w01", "melee", "w03", "front", "w05", "ranged", "w07", "splash", "w09", "support");
- public static readonly OrderedMap HeroRoles = Pairs("held_ringborke", "front", "held_quellgeflecht", "support", "held_funkenlanze", "ranged", "held_sylvara", "melee");
+ public static readonly OrderedMap HeroRoles = Pairs("held_ringborke", "front", "held_quellgeflecht", "support", "held_funkenlanze", "ranged", "held_sylvara", "melee", "held_brunhild", "melee");
public static readonly string[] RowOrder = { "front", "melee", "splash", "ranged", "support" };
public static readonly string[] BotNames = { "Eichrufer", "Aschenzunge", "Quellwächter", "Glutfaust", "Moosauge", "Basaltherz", "Rindenkrone", "Funkenschritt" };
public static readonly string[] WavesMelee = { "e01", "e02", "e05" };
@@ -168,7 +187,8 @@ namespace Luneraria.Core
/// bot match). Usable as a fixed profile per slot (MatchOptions.Profiles, balance runs).
///
public static readonly OrderedMap ExtraBotProfiles = Ordered(
- new BotProfile { Name = "assassine", Front = 0.3, Melee = 0.25, Ranged = 0.25, Splash = 0.1, Support = 0.1, UpgradeFrom = 6, SupportLane = 0.0, Hero = "held_sylvara", HeroRound = 3, Items = new[] { "glutklinge", "funkenfokus", "basaltschild" }, TalentChoices = new[] { 1, 0 }, Champion = "" }
+ new BotProfile { Name = "assassine", Front = 0.3, Melee = 0.25, Ranged = 0.25, Splash = 0.1, Support = 0.1, UpgradeFrom = 6, SupportLane = 0.0, Hero = "held_sylvara", HeroRound = 3, Items = new[] { "glutklinge", "funkenfokus", "basaltschild" }, TalentChoices = new[] { 1, 0 }, Champion = "" },
+ new BotProfile { Name = "walkuere", Front = 0.3, Melee = 0.25, Ranged = 0.25, Splash = 0.1, Support = 0.1, UpgradeFrom = 6, SupportLane = 0.0, Hero = "held_brunhild", HeroRound = 3, Items = new[] { "glutklinge", "basaltschild", "funkenfokus" }, TalentChoices = new[] { 0, 1 }, Champion = "" }
);
}
}
diff --git a/Assets/Luneraria/Core/Content/LuneContent.cs b/Assets/Luneraria/Core/Content/LuneContent.cs
index 3ac7983..4cc4a16 100644
--- a/Assets/Luneraria/Core/Content/LuneContent.cs
+++ b/Assets/Luneraria/Core/Content/LuneContent.cs
@@ -130,7 +130,7 @@ namespace Luneraria.Core
reduction += 0.15;
if (hero.Talents.Contains("fernsicht"))
definition.AttackRange = definition.AttackRange + 1.5;
- if (hero.Talents.Contains("glutsalve") || hero.Talents.Contains("neumond"))
+ if (hero.Talents.Contains("glutsalve") || hero.Talents.Contains("neumond") || hero.Talents.Contains("rabenschwinge"))
attackSpeed += 0.20;
definition.UltimateRule = UltimateRuleFor(hero);
definition.Skin = ValidSkin(hero.HeroId, hero.Skin) ? hero.Skin ?? "" : "";
diff --git a/Assets/Luneraria/Core/Simulation/LuneCombat.cs b/Assets/Luneraria/Core/Simulation/LuneCombat.cs
index 7665bbc..4d87d3d 100644
--- a/Assets/Luneraria/Core/Simulation/LuneCombat.cs
+++ b/Assets/Luneraria/Core/Simulation/LuneCombat.cs
@@ -747,6 +747,8 @@ namespace Luneraria.Core
}
if (unit.HitCount % 3 == 0 && unit.HasTalent("stammschlag"))
damage *= 1.6;
+ if (unit.Ability() == "brunhild" && HeroLike(target))
+ damage *= BrunhildHunter;
if (unit.LeapStrike)
{
damage *= SylvaraLeapStrike;
@@ -811,6 +813,9 @@ namespace Luneraria.Core
case "sylvara":
unit.AbilityReady = SylvaraFirstThrow;
break;
+ case "brunhild":
+ unit.AbilityReady = BrunhildFirstSpear;
+ break;
}
}
@@ -863,6 +868,8 @@ namespace Luneraria.Core
{
if (unit.Ability() == "sylvara")
SylvaraStep(unit, gates);
+ else if (unit.Ability() == "brunhild")
+ BrunhildStep(unit, gates);
double regen = unit.Def.Regen;
if (regen > 0.0 && unit.Hp < unit.MaxHp)
QueueHeal(unit, unit.MaxHp * regen * Balance.SimStep);
@@ -969,6 +976,9 @@ namespace Luneraria.Core
case "sylvara":
success = SylvaraThrow(unit, gates);
break;
+ case "brunhild":
+ success = BrunhildSpear(unit, gates);
+ break;
}
if (success)
{
@@ -1104,6 +1114,97 @@ namespace Luneraria.Core
return true;
}
+ // H2 Brunhild
+ public const double BrunhildFirstSpear = 3.0;
+ public const double BrunhildSpearRange = 7.0;
+ public const double BrunhildHunter = 1.25;
+ public const double BrunhildShield = 0.30;
+
+ static bool HeroLike(LuneUnit unit) { return unit.Kind == "hero" || unit.Kind == "champion"; }
+
+ /// Enemies within the radius, farthest first (ties by id), mobile only.
+ List Farthest(LuneUnit unit, double radius, int maximum)
+ {
+ List result = NearbyMobile(unit, radius, 1000);
+ result.Reverse();
+ GdMath.SortStable(result, (a, b) =>
+ {
+ double ad = a.Position.DistanceTo(unit.Position);
+ double bd = b.Position.DistanceTo(unit.Position);
+ return GdMath.IsEqualApprox(ad, bd) ? a.Id < b.Id : ad > bd;
+ });
+ if (result.Count > maximum)
+ result.RemoveRange(maximum, result.Count - maximum);
+ return result;
+ }
+
+ /// Speerwurf: 120 % to the farthest enemy in 7 m (Speerregen: three), marked 4 s (+15 % damage taken).
+ bool BrunhildSpear(LuneUnit unit, Dictionary gates)
+ {
+ List victims = Farthest(unit, BrunhildSpearRange, unit.HasTalent("speerregen") ? 3 : 1);
+ if (victims.Count == 0)
+ return false;
+ double damage = unit.Def.Damage * (1.0 + unit.DamageBonus()) * 1.2;
+ foreach (LuneUnit victim in victims)
+ {
+ double hit = damage * (HeroLike(victim) ? BrunhildHunter * (unit.HasTalent("walkuerenblut") ? 2.0 : 1.0) : 1.0);
+ QueueDamage(victim, victim.Mitigated(hit, unit.Def.AttackType), gates);
+ AddEffect(victim, "valkyrie_mark", "vulnerable", 0.15, 4.0);
+ }
+ return true;
+ }
+
+ ///
+ /// Walkürensturz (once per combat after 2 s, by the planned rule): she leaps to her target
+ /// (an enemy hero or champion in 7 m first, then the marked enemy, then her current target),
+ /// deals 200 % to every enemy within 3 m of it and stuns them 1 s (Götterdämmerung: 2 s in
+ /// 4 m); bosses are not stunned. Decided on the state at the start of the step.
+ ///
+ void BrunhildStep(LuneUnit unit, Dictionary gates)
+ {
+ if (unit.UltimateUsed || string.IsNullOrEmpty(unit.Def.UltimateRule) || unit.CombatTime + 0.000001 < LuneContent.UltimateDelay)
+ return;
+ List reach = NearbyMobile(unit, BrunhildSpearRange, 1000);
+ if (reach.Count == 0)
+ return;
+ LuneUnit target = reach.Find(HeroLike) ?? reach.Find(other => other.Effects.ContainsKey("valkyrie_mark"))
+ ?? (unit.Target != null && reach.Contains(unit.Target) ? unit.Target : reach[0]);
+ bool full = unit.HasTalent("goetterdaemmerung");
+ double radius = full ? 4.0 : 3.0;
+ bool release = false;
+ switch (unit.Def.UltimateRule)
+ {
+ case "gruppe":
+ release = Nearby(unit, false, target.Position, 4.0, 1000).FindAll(other => other.IsMobile()).Count >= 3;
+ break;
+ case "held":
+ release = HeroLike(target);
+ break;
+ case "notfall":
+ release = unit.Hp < unit.MaxHp * 0.4;
+ break;
+ case "sofort":
+ release = true;
+ break;
+ }
+ if (!release)
+ return;
+ double damage = unit.Def.Damage * (1.0 + unit.DamageBonus()) * 2.0;
+ foreach (LuneUnit victim in Nearby(unit, false, target.Position, radius, 1000))
+ {
+ if (!victim.IsMobile())
+ continue;
+ QueueDamage(victim, victim.Mitigated(damage * (HeroLike(victim) ? BrunhildHunter : 1.0), unit.Def.AttackType), gates);
+ if (!victim.IsBoss())
+ AddEffect(victim, "valkyrie_stun", "stun", 1.0, full ? 2.0 : 1.0);
+ }
+ // Walkürenschild: after the leap she is shielded like Sylvara after hers.
+ _pendingShields.Add(new PendingShield { Unit = unit, Amount = unit.MaxHp * BrunhildShield, Duration = 5.0 });
+ unit.LeapTarget = target;
+ unit.UltimateUsed = true;
+ unit.UltimateSerial += 1;
+ }
+
void OnHit(LuneUnit unit, LuneUnit primary, List living, Dictionary gates)
{
string ability = unit.Ability();
diff --git a/Assets/Luneraria/Core/Simulation/LuneUnit.cs b/Assets/Luneraria/Core/Simulation/LuneUnit.cs
index 3d09d16..a26a92e 100644
--- a/Assets/Luneraria/Core/Simulation/LuneUnit.cs
+++ b/Assets/Luneraria/Core/Simulation/LuneUnit.cs
@@ -103,7 +103,8 @@ namespace Luneraria.Core
public double Mitigated(double raw, int attackType)
{
double value = GdMath.Max(0.0, raw) * DamageMatrix[GdMath.Clamp(attackType, 0, 2), ArmorType()];
- return value * (1.0 - DamageReduction());
+ // H2: a unit marked by Brunhild's spear takes more damage ("vulnerable"; 0 for everyone else).
+ return value * (1.0 - DamageReduction()) * (1.0 + StrongestEffect("vulnerable"));
}
/// Applies an already mitigated amount through the shield.
@@ -209,8 +210,13 @@ namespace Luneraria.Core
return GdMath.Min(0.35, result + Def.ReductionBonus);
}
+ /// H2: Brunhild's stun stops attacks and movement for its duration.
+ public bool Stunned() { return StrongestEffect("stun") > 0.0; }
+
public double AttackSpeedFactor()
{
+ if (Stunned())
+ return 0.0;
double slowing = StrongestEffect("attack_slow") * (IsBoss() ? 0.5 : 1.0);
double bonus = Def.AttackSpeedBonus;
return 1.0 + GdMath.Clamp(StrongestEffect("attack_speed") - slowing + bonus, -0.25, 0.5);
@@ -222,6 +228,8 @@ namespace Luneraria.Core
public double MovementFactor()
{
+ if (Stunned())
+ return 0.0;
return 1.0 - GdMath.Min(0.35, StrongestEffect("movement_slow") * (IsBoss() ? 0.5 : 1.0));
}
diff --git a/Assets/Luneraria/Tests/Core/CheckSuite.cs b/Assets/Luneraria/Tests/Core/CheckSuite.cs
index 0f9f0df..fe2ddda 100644
--- a/Assets/Luneraria/Tests/Core/CheckSuite.cs
+++ b/Assets/Luneraria/Tests/Core/CheckSuite.cs
@@ -50,7 +50,7 @@ namespace Luneraria.Core.Tests
public static IList All()
{
- return new List { new ContentSuite(), new CoreRulesSuite(), new M2RulesSuite(), new GoldenSuite(), new DeterminismSuite(), new NetSuite(), new NetRoundsSuite(), new H1HeroSuite(), new S1RulesSuite(), new S2RulesSuite(), new S3RulesSuite() };
+ return new List { new ContentSuite(), new CoreRulesSuite(), new M2RulesSuite(), new GoldenSuite(), new DeterminismSuite(), new NetSuite(), new NetRoundsSuite(), new H1HeroSuite(), new S1RulesSuite(), new S2RulesSuite(), new S3RulesSuite(), new H2HeroSuite() };
}
}
}
diff --git a/Assets/Luneraria/Tests/Core/ContentSuite.cs b/Assets/Luneraria/Tests/Core/ContentSuite.cs
index 37ef2a6..1cae402 100644
--- a/Assets/Luneraria/Tests/Core/ContentSuite.cs
+++ b/Assets/Luneraria/Tests/Core/ContentSuite.cs
@@ -90,7 +90,7 @@ namespace Luneraria.Core.Tests
// Heroes added after the port (H1) follow the reference heroes; those must match exactly.
Check(expected.Keys.SequenceEqual(ContentData.ReferenceHeroes) && ContentData.Heroes.Keys.Take(ContentData.ReferenceHeroes.Length).SequenceEqual(ContentData.ReferenceHeroes),
"heroes: same reference ids in the same order, later heroes after them");
- Check(ContentData.Heroes.Keys.Skip(ContentData.ReferenceHeroes.Length).SequenceEqual(new[] { "held_sylvara" }), "heroes: exactly the documented later heroes (H1: held_sylvara)");
+ Check(ContentData.Heroes.Keys.Skip(ContentData.ReferenceHeroes.Length).SequenceEqual(new[] { "held_sylvara", "held_brunhild" }), "heroes: exactly the documented later heroes (H1 Sylvara, H2 Brunhild)");
foreach (KeyValuePair pair in expected)
{
var entry = MiniJson.Obj(pair.Value);
diff --git a/Assets/Luneraria/Tests/Core/H2HeroSuite.cs b/Assets/Luneraria/Tests/Core/H2HeroSuite.cs
new file mode 100644
index 0000000..c942637
--- /dev/null
+++ b/Assets/Luneraria/Tests/Core/H2HeroSuite.cs
@@ -0,0 +1,148 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Luneraria.Core.Tests
+{
+ ///
+ /// H2: Brunhild, the Valkyrie. Purchase and rules, hunter passive, spear with mark, the leap
+ /// with area damage and stun (bosses immune), talents, skin, bot profile; reference untouched.
+ ///
+ public sealed class H2HeroSuite : CheckSuite
+ {
+ public override string Name { get { return "h2"; } }
+
+ protected override void RunChecks()
+ {
+ TestPurchase();
+ TestSpear();
+ TestUltimate();
+ TestPassiveAndTalents();
+ TestBots();
+ }
+
+ static LuneCombat Arena(HeroState hero, out LuneUnit brunhild, params KeyValuePair[] enemies)
+ {
+ var balance = LuneBalance.Default();
+ var combat = new LuneCombat(new LuneMap(balance), balance, 5);
+ UnitDef definition = LuneContent.HeroDefinition(hero);
+ definition.Role = "melee";
+ brunhild = combat.Spawn("hero", 0, "mid", definition, 40.0, 0.0, 0, "h0-1");
+ Vec2 forward = combat.Map.LaneForward(0, "mid", 40.0);
+ foreach (KeyValuePair enemy in enemies)
+ {
+ bool isHero = enemy.Key == "held";
+ UnitDef def = isHero ? LuneContent.HeroDefinition(new HeroState { HeroId = "held_ringborke" }) : ContentData.Enemies[enemy.Key].Clone();
+ def.MaxHp = 50000.0;
+ def.Speed = 0.0;
+ LuneUnit unit = combat.Spawn(isHero ? "hero" : "monster", 1, "mid", def, 40.0, 0.0, isHero ? 3 : -1, isHero ? "h3-1" : "");
+ unit.Position = brunhild.Position + forward * enemy.Value;
+ unit.PreviousPosition = unit.Position;
+ }
+ return combat;
+ }
+
+ static KeyValuePair At(string id, double distance) { return new KeyValuePair(id, distance); }
+
+ static void Run(LuneCombat combat, double seconds)
+ {
+ int steps = (int)(seconds / combat.Balance.SimStep + 0.5);
+ for (int i = 0; i < steps && !combat.Finished; i++)
+ combat.Step();
+ }
+
+ void TestPurchase()
+ {
+ var state = new LuneMatch();
+ state.Setup(new MatchOptions { Seed = 71, HumanSlot = -1 });
+ state.Players[0].Draft.Gold += 1000;
+ Check(state.Submit(0, Command.HeroBuy("held_brunhild")).Ok, "Brunhild can be recruited for 260");
+ Check(LuneContent.UltimateRuleFor(state.Players[0].Draft.Hero) == "gruppe", "default rule Gruppe");
+ Check(state.Submit(0, Command.UltimateRule("held")).Ok && state.Submit(0, Command.HeroSkin("eiswalkuere")).Ok, "rule Heldenjagd and skin Eiswalküre");
+ Check(!state.Submit(0, Command.HeroSkin("blutmond")).Ok, "Sylvara's skin is not hers");
+ UnitDef definition = LuneContent.HeroDefinition(state.Players[0].Draft.Hero);
+ Check(definition.Id == "brunhild" && definition.Skin == "eiswalkuere" && definition.UltimateRule == "held" && GdMath.IsEqualApprox(definition.AttackRange, 2.0), "definition carries model, skin, rule and the spear's reach");
+ }
+
+ void TestSpear()
+ {
+ LuneUnit brunhild;
+ LuneCombat combat = Arena(new HeroState { HeroId = "held_brunhild", Ultimate = "notfall" }, out brunhild, At("e01", 1.5), At("e01", 4.0), At("e04", 6.5));
+ LuneUnit far = combat.Units.First(u => u.Def.Id == "e04");
+ Run(combat, LuneCombat.BrunhildFirstSpear - 0.1);
+ Check(brunhild.AbilitySerial == 0, "no spear before 3 s");
+ Run(combat, 0.2);
+ Check(brunhild.AbilitySerial == 1 && far.Effects.ContainsKey("valkyrie_mark"), "the first spear marks the farthest enemy in 7 m");
+ double raw = 100.0;
+ var probe = new LuneUnit { Def = far.Def };
+ Check(GdMath.IsEqualApprox(far.Mitigated(raw, 0), probe.Mitigated(raw, 0) * 1.15), "a marked enemy takes 15 % more damage");
+ }
+
+ void TestUltimate()
+ {
+ LuneUnit brunhild;
+ LuneCombat combat = Arena(new HeroState { HeroId = "held_brunhild", Ultimate = "held" }, out brunhild, At("e01", 1.5), At("held", 6.0), At("e01", 6.8), At("e07", 6.5));
+ LuneUnit hero = combat.Units.First(u => u.Kind == "hero" && u.Team == 1);
+ LuneUnit boss = combat.Units.First(u => u.Def.Id == "e07");
+ Run(combat, LuneContent.UltimateDelay - 0.1);
+ Check(brunhild.UltimateSerial == 0, "no ultimate before 2 s");
+ Run(combat, 0.1);
+ Check(brunhild.UltimateSerial == 1, "Heldenjagd fires with an enemy hero in 7 m");
+ Check(brunhild.Position.DistanceTo(hero.Position) < 1.5, "she leaps next to the hero");
+ Check(hero.Stunned() && !boss.Stunned(), "the hero is stunned, the boss is immune");
+ Check(hero.AttackSpeedFactor() == 0.0 && hero.MovementFactor() == 0.0, "a stunned unit neither attacks nor moves");
+ Run(combat, 1.1);
+ Check(!hero.Stunned(), "the stun ends after 1 s");
+ Run(combat, 8.0);
+ Check(brunhild.UltimateSerial == 1, "once per combat");
+ LuneUnit calm;
+ LuneCombat group = Arena(new HeroState { HeroId = "held_brunhild" }, out calm, At("e01", 3.0), At("e01", 3.5));
+ Run(group, 4.0);
+ Check(calm.UltimateSerial == 0, "Gruppe waits for three enemies at the target");
+ }
+
+ void TestPassiveAndTalents()
+ {
+ LuneUnit brunhild;
+ LuneCombat combat = Arena(new HeroState { HeroId = "held_brunhild", Ultimate = "notfall" }, out brunhild, At("held", 1.5));
+ LuneUnit hero = combat.Units.First(u => u.Kind == "hero" && u.Team == 1);
+ double before = hero.Hp;
+ int guard = 0;
+ while (brunhild.HitCount == 0 && guard++ < 200)
+ combat.Step();
+ double expected = hero.Mitigated(brunhild.Def.Damage * (1.0 + brunhild.DamageBonus()) * LuneCombat.BrunhildHunter, brunhild.Def.AttackType);
+ Check(GdMath.IsEqualApprox(before - hero.Hp, expected), "Heldenjägerin: +25 % against heroes");
+ UnitDef raven = LuneContent.HeroDefinition(new HeroState { HeroId = "held_brunhild", Xp = 1000, Talents = new List { "speerregen", "rabenschwinge" } });
+ Check(GdMath.IsEqualApprox(raven.AttackSpeedBonus, 0.20), "Rabenschwinge: +20 % attack tempo");
+ LuneUnit rain;
+ LuneCombat spears = Arena(new HeroState { HeroId = "held_brunhild", Ultimate = "notfall", Talents = new List { "speerregen", "goetterdaemmerung" } }, out rain,
+ At("e01", 4.0), At("e01", 5.0), At("e01", 6.0), At("e01", 1.0));
+ Run(spears, 3.1);
+ Check(spears.Units.Count(u => u.Effects.ContainsKey("valkyrie_mark")) == 3, "Speerregen: three marked enemies");
+ LuneUnit dusk;
+ LuneCombat stun = Arena(new HeroState { HeroId = "held_brunhild", Ultimate = "sofort", Talents = new List { "speerregen", "goetterdaemmerung" } }, out dusk, At("e01", 2.0));
+ Run(stun, 2.05);
+ LuneUnit victim = stun.Units.First(u => u.Kind == "monster");
+ Run(stun, 1.5);
+ Check(victim.Stunned(), "Götterdämmerung: the stun lasts 2 s");
+ }
+
+ void TestBots()
+ {
+ bool picked = false;
+ for (int seed = 1; seed <= 16 && !picked; seed++)
+ {
+ var probe = new LuneMatch();
+ probe.Setup(new MatchOptions { Seed = seed, HumanSlot = -1, Extended = true });
+ for (int slot = 0; slot < 6; slot++)
+ picked |= LuneBot.ProfileFor(probe, slot) == "walkuere";
+ }
+ Check(picked, "extended bots pick Brunhild's profile");
+ var reference = new LuneMatch();
+ reference.Setup(new MatchOptions { Seed = 3, HumanSlot = -1 });
+ bool clean = true;
+ for (int slot = 0; slot < 6; slot++)
+ clean &= !ContentData.ExtraBotProfiles.ContainsKey(LuneBot.ProfileFor(reference, slot));
+ Check(clean, "reference rotation untouched");
+ }
+ }
+}
diff --git a/Assets/Luneraria/Tests/Core/H2HeroSuite.cs.meta b/Assets/Luneraria/Tests/Core/H2HeroSuite.cs.meta
new file mode 100644
index 0000000..4fe19ac
--- /dev/null
+++ b/Assets/Luneraria/Tests/Core/H2HeroSuite.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 2fa3dc51ddee4a919bec93bf34fc29f9
diff --git a/Assets/Luneraria/Tests/Core/Unity/CoreSuiteTests.cs b/Assets/Luneraria/Tests/Core/Unity/CoreSuiteTests.cs
index 7cad221..5066a35 100644
--- a/Assets/Luneraria/Tests/Core/Unity/CoreSuiteTests.cs
+++ b/Assets/Luneraria/Tests/Core/Unity/CoreSuiteTests.cs
@@ -55,5 +55,6 @@ namespace Luneraria.Core.Tests
[Test] public void S1Rules() { RunSuite(new S1RulesSuite()); }
[Test] public void S2Rules() { RunSuite(new S2RulesSuite()); }
[Test] public void S3Rules() { RunSuite(new S3RulesSuite()); }
+ [Test] public void H2Heroes() { RunSuite(new H2HeroSuite()); }
}
}
diff --git a/tools/dotnet/Luneraria.Balance/Program.cs b/tools/dotnet/Luneraria.Balance/Program.cs
index af11108..999268f 100644
--- a/tools/dotnet/Luneraria.Balance/Program.cs
+++ b/tools/dotnet/Luneraria.Balance/Program.cs
@@ -13,7 +13,7 @@ using Luneraria.Core;
///
public static class Program
{
- static readonly string[] Heroes = { "held_ringborke", "held_quellgeflecht", "held_funkenlanze", "held_sylvara" };
+ static readonly string[] Heroes = { "held_ringborke", "held_quellgeflecht", "held_funkenlanze", "held_sylvara", "held_brunhild" };
public static int Main(string[] args)
{