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) {