H2A Brunhild: Kernregeln, Markierung, Betäubung, Bot-Profil, Prüfsuite

Brunhild (held_brunhild): Heldenjägerin (+25 % gegen Helden/Champions),
Speerwurf markiert (+15 % erlittener Schaden), Walkürensturz (Sprung,
200 % in 3 m, 1 s Betäubung, Bosse immun, Walkürenschild 30 %), Talente,
Auslöser, Skin-Eintrag Eiswalküre, Bot-Profil walkuere (nur erweitert).
Neue Effektarten vulnerable und stun wirken nur durch Brunhild; Golden 0.
Probe-Vergleich 50 % wie die übrigen Helden; h2 22/22.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Martin Stickelbroeck 2026-10-11 16:44:42 +02:00
parent fb5c92dd37
commit cbff678290
10 changed files with 288 additions and 8 deletions

View file

@ -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." } }
);
/// <summary>Heroes of the Godot reference; ContentTests compares exactly these, later heroes follow them.</summary>
@ -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<string, TalentOption[]> HeroSkins = new Dictionary<string, TalentOption[]>
{
{ "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.") } },
};
/// <summary>
@ -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<string, ItemSpec> Items = Ordered(
@ -145,7 +164,7 @@ namespace Luneraria.Core
/// <summary>Base troop id to role; iteration order matters for RoleOf (first match).</summary>
public static readonly OrderedMap<string, string> 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<string, string> HeroRoles = Pairs("held_ringborke", "front", "held_quellgeflecht", "support", "held_funkenlanze", "ranged", "held_sylvara", "melee");
public static readonly OrderedMap<string, string> 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).
/// </summary>
public static readonly OrderedMap<string, BotProfile> 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 = "" }
);
}
}

View file

@ -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 ?? "" : "";

View file

@ -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"; }
/// <summary>Enemies within the radius, farthest first (ties by id), mobile only.</summary>
List<LuneUnit> Farthest(LuneUnit unit, double radius, int maximum)
{
List<LuneUnit> 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;
}
/// <summary>Speerwurf: 120 % to the farthest enemy in 7 m (Speerregen: three), marked 4 s (+15 % damage taken).</summary>
bool BrunhildSpear(LuneUnit unit, Dictionary<string, bool> gates)
{
List<LuneUnit> 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;
}
/// <summary>
/// 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.
/// </summary>
void BrunhildStep(LuneUnit unit, Dictionary<string, bool> gates)
{
if (unit.UltimateUsed || string.IsNullOrEmpty(unit.Def.UltimateRule) || unit.CombatTime + 0.000001 < LuneContent.UltimateDelay)
return;
List<LuneUnit> 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<LuneUnit> living, Dictionary<string, bool> gates)
{
string ability = unit.Ability();

View file

@ -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"));
}
/// <summary>Applies an already mitigated amount through the shield.</summary>
@ -209,8 +210,13 @@ namespace Luneraria.Core
return GdMath.Min(0.35, result + Def.ReductionBonus);
}
/// <summary>H2: Brunhild's stun stops attacks and movement for its duration.</summary>
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));
}

View file

@ -50,7 +50,7 @@ namespace Luneraria.Core.Tests
public static IList<CheckSuite> All()
{
return new List<CheckSuite> { 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<CheckSuite> { new ContentSuite(), new CoreRulesSuite(), new M2RulesSuite(), new GoldenSuite(), new DeterminismSuite(), new NetSuite(), new NetRoundsSuite(), new H1HeroSuite(), new S1RulesSuite(), new S2RulesSuite(), new S3RulesSuite(), new H2HeroSuite() };
}
}
}

View file

@ -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<string, object> pair in expected)
{
var entry = MiniJson.Obj(pair.Value);

View file

@ -0,0 +1,148 @@
using System.Collections.Generic;
using System.Linq;
namespace Luneraria.Core.Tests
{
/// <summary>
/// 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.
/// </summary>
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<string, double>[] 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<string, double> 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<string, double> At(string id, double distance) { return new KeyValuePair<string, double>(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<string> { "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<string> { "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<string> { "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");
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2fa3dc51ddee4a919bec93bf34fc29f9

View file

@ -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()); }
}
}

View file

@ -13,7 +13,7 @@ using Luneraria.Core;
/// </summary>
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)
{