using System.Collections.Generic; using System.Linq; namespace Luneraria.Core.Tests { /// /// H1: Sylvara, the first hero after the port, and the planned ultimate rules. Checks the /// purchase and the rule command, the kit in small controlled combats (leap, leap strike, /// sickle throw, ultimate per rule, talents), the bot profile outside the seed rotation and /// determinism of matches with her. The reference matches stay untouched (golden suite). /// public sealed class H1HeroSuite : CheckSuite { public override string Name { get { return "h1"; } } protected override void RunChecks() { TestPurchaseAndRules(); TestLeap(); TestThrow(); TestUltimate(); TestTalents(); TestBots(); } static LuneMatch NewMatch(int seed = 31) { var state = new LuneMatch(); state.Setup(new MatchOptions { Seed = seed, HumanSlot = -1 }); return state; } void TestPurchaseAndRules() { LuneMatch state = NewMatch(); state.Players[0].Draft.Gold += 1000; Check(!state.Submit(0, Command.UltimateRule("sofort")).Ok, "ultimate rule needs a hero"); int gold = state.GoldOf(0); Check(state.Submit(0, Command.HeroBuy("held_sylvara")).Ok && state.GoldOf(0) == gold - 260, "Sylvara costs 260 like every hero"); HeroState hero = state.Players[0].Draft.Hero; Check(LuneContent.UltimateRuleFor(hero) == "gruppe", "default ultimate rule is the first (Gruppe)"); Check(!state.Submit(0, Command.UltimateRule("ueberall")).Ok, "unknown ultimate rule rejected"); Check(state.Submit(0, Command.UltimateRule("zweite_reihe")).Ok && hero.Ultimate == "zweite_reihe", "ultimate rule accepted in planning"); Check(state.Undo(0).Ok && state.Players[0].Draft.Hero.Ultimate == "", "rule change can be undone like any planning step"); state.Submit(0, Command.UltimateRule("notfall")); state.BeginCombat(); List army = state.ArmyEntries(0); LuneUnit spawned = state.Combat.Units.FirstOrDefault(u => u.Kind == "hero" && u.OwnerSlot == 0); Check(spawned != null && spawned.Def.UltimateRule == "notfall" && spawned.Role() == "melee" && spawned.Def.Id == "sylvara", "combat unit carries rule, melee role and model id"); state.FinishCombat(); Check(state.Players[0].Hero.Ultimate == "notfall", "rule persists after the round"); state.Submit(0, Command.HeroCancel()); Check(state.Players[0].Draft.Hero != null, "committed Sylvara cannot be returned"); LuneMatch other = NewMatch(); other.Players[1].Draft.Gold += 1000; other.Submit(1, Command.HeroBuy("held_ringborke")); Check(!other.Submit(1, Command.UltimateRule("gruppe")).Ok, "reference heroes have no ultimate"); Check(LuneContent.HeroDefinition(other.Players[1].Draft.Hero).UltimateRule == "", "reference hero definitions carry no rule"); Check(army.Count(e => e.Kind == "hero") == 1 && army.First(e => e.Kind == "hero").Role == "melee", "Sylvara stands in the melee row"); } /// A small fight on the mid lane without buildings: Sylvara (team 0) and given enemies (team 1). static LuneCombat Arena(HeroState hero, out LuneUnit sylvara, params KeyValuePair[] enemies) { var balance = LuneBalance.Default(); var combat = new LuneCombat(new LuneMap(balance), balance, 5); UnitDef definition = LuneContent.HeroDefinition(hero); definition.Role = "melee"; sylvara = 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) { UnitDef monster = ContentData.Enemies[enemy.Key].Clone(); monster.MaxHp = 50000.0; monster.Speed = 0.0; LuneUnit unit = combat.Spawn("monster", 1, "mid", monster, 40.0, 0.0); unit.Position = sylvara.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 TestLeap() { LuneUnit sylvara; // A melee monster 3 m ahead, a ranged one (Fadenschütze, 6 m reach) 7 m ahead. LuneCombat combat = Arena(new HeroState { HeroId = "held_sylvara" }, out sylvara, At("e01", 3.0), At("e04", 7.0)); LuneUnit ranged = combat.Units.First(u => u.Def.Id == "e04"); combat.Step(); Check(sylvara.LeapUsed && sylvara.LeapSerial == 1, "leap fires on the first step with a back-row enemy within 8 m"); Check(sylvara.Position.DistanceTo(ranged.Position) < 1.0 && sylvara.Target == ranged, "she lands next to the back-row enemy and targets it"); Run(combat, 3.0); Check(sylvara.LeapSerial == 1, "leap only once per combat"); // Leap strike: the first hit deals 150 %, later hits 100 %. LuneUnit lone; LuneCombat strike = Arena(new HeroState { HeroId = "held_sylvara" }, out lone, At("e04", 5.0)); LuneUnit victim = strike.Units.First(u => u.Def.Id == "e04"); double before = victim.Hp; int guard = 0; while (lone.HitCount == 0 && guard++ < 200) strike.Step(); double first = before - victim.Hp; before = victim.Hp; while (lone.HitCount == 1 && guard++ < 400) strike.Step(); double second = before - victim.Hp; double expected = victim.Mitigated(lone.Def.Damage * (1.0 + lone.DamageBonus()), lone.Def.AttackType); Check(GdMath.IsEqualApprox(first, expected * 1.5) && GdMath.IsEqualApprox(second, expected), string.Format("leap strike 150 % then 100 % ({0:F1} / {1:F1}, base {2:F1})", first, second, expected)); // No leap without a back-row enemy, and never onto a boss. LuneUnit plain; LuneCombat melee = Arena(new HeroState { HeroId = "held_sylvara" }, out plain, At("e01", 3.0), At("e07", 6.0)); Run(melee, 1.0); Check(!plain.LeapUsed, "no leap against melee enemies or bosses"); } void TestThrow() { LuneUnit sylvara; LuneCombat combat = Arena(new HeroState { HeroId = "held_sylvara" }, out sylvara, At("e01", 1.0), At("e01", 2.0), At("e01", 2.6), At("e01", 3.2)); Run(combat, LuneCombat.SylvaraFirstThrow - 0.2); Check(sylvara.AbilitySerial == 0, "no sickle throw before 4 s"); Run(combat, 0.4); Check(sylvara.AbilitySerial == 1, "first sickle throw at 4 s"); Run(combat, 6.0); Check(sylvara.AbilitySerial == 2, "sickle throw every 6 s"); // The throw itself: 90 % to the target and two more within 2.5 m (one direct check). LuneUnit probe; LuneCombat single = Arena(new HeroState { HeroId = "held_sylvara" }, out probe, At("e01", 1.0), At("e01", 2.0), At("e01", 3.0), At("e01", 9.5)); Run(single, LuneCombat.SylvaraFirstThrow - 0.1); var hpBefore = single.Units.Where(u => u.Kind == "monster").ToDictionary(u => u.Id, u => u.Hp); Run(single, 0.15); int touched = single.Units.Count(u => u.Kind == "monster" && u.Hp < hpBefore[u.Id]); Check(probe.AbilitySerial == 1 && touched >= 3 && single.Units.First(u => u.Kind == "monster" && u.Position.DistanceTo(probe.Position) > 8.0).Hp == 50000.0, "sickle throw hits three enemies near the target, not the far one"); } void TestUltimate() { LuneUnit sylvara; LuneCombat combat = Arena(new HeroState { HeroId = "held_sylvara", Ultimate = "sofort" }, out sylvara, At("e01", 1.0), At("e01", 1.5), At("e01", 2.0), At("e01", 2.5), At("e01", 3.0), At("e01", 3.5), At("e01", 4.0)); Run(combat, LuneContent.UltimateDelay - 0.1); Check(sylvara.UltimateSerial == 0, "no ultimate before 2 s"); Run(combat, 0.2); Check(sylvara.UltimateSerial == 1, "rule Sofort releases at 2 s with an enemy in 4 m"); int marked = combat.Units.Count(u => u.Effects.ContainsKey("moon_mark")); Check(marked == 5, "Mondsicheltanz hits five enemies (" + marked + ")"); Run(combat, 10.0); Check(sylvara.UltimateSerial == 1, "ultimate once per combat"); // Notfall waits for low health. LuneUnit calm; LuneCombat emergency = Arena(new HeroState { HeroId = "held_sylvara", Ultimate = "notfall" }, out calm, At("e01", 1.0)); Run(emergency, 3.0); bool waited = calm.UltimateSerial == 0; calm.Hp = calm.MaxHp * 0.3; Run(emergency, 0.1); Check(waited && calm.UltimateSerial == 1, "rule Notfall waits until health falls below 40 %"); // Gruppe needs three enemies within 4 m. LuneUnit grouped; LuneCombat few = Arena(new HeroState { HeroId = "held_sylvara" }, out grouped, At("e01", 1.0), At("e01", 2.0)); Run(few, 4.0); Check(grouped.UltimateSerial == 0, "rule Gruppe ignores two enemies"); LuneUnit crowd; LuneCombat many = Arena(new HeroState { HeroId = "held_sylvara" }, out crowd, At("e01", 1.0), At("e01", 2.0), At("e01", 3.0)); Run(many, 2.2); Check(crowd.UltimateSerial == 1, "rule Gruppe fires with three enemies in 4 m"); // Zweite Reihe: a ranged enemy within 5 m. LuneUnit hunter; LuneCombat back = Arena(new HeroState { HeroId = "held_sylvara", Ultimate = "zweite_reihe" }, out hunter, At("e01", 1.0)); Run(back, 3.0); Check(hunter.UltimateSerial == 0, "rule Zweite Reihe ignores melee enemies"); } void TestTalents() { var hero = new HeroState { HeroId = "held_sylvara", Xp = 1000, Talents = new List { "sichelsturm", "neumond" } }; UnitDef definition = LuneContent.HeroDefinition(hero); Check(GdMath.IsEqualApprox(definition.AttackSpeedBonus, 0.20) && definition.Level == 6, "Neumond: +20 % attack tempo"); LuneUnit full; LuneCombat combat = Arena(new HeroState { HeroId = "held_sylvara", Ultimate = "sofort", Talents = new List { "mondsilber", "vollmond" } }, out full, At("e01", 1.0), At("e01", 1.4), At("e01", 1.8), At("e01", 2.2), At("e01", 2.6), At("e01", 3.0), At("e01", 3.4), At("e01", 3.8)); Run(combat, 2.2); Check(combat.Units.Count(u => u.Effects.ContainsKey("moon_mark")) == 7, "Vollmond: the dance hits seven enemies"); TalentOption[][] tiers = LuneContent.TalentsOf("held_sylvara"); Check(tiers.Length == 2 && tiers.All(t => t.Length == 2), "two talent tiers with two choices each"); } void TestBots() { bool rotationClean = true; for (int seed = 1; seed <= 60; seed++) { LuneMatch probe = NewMatch(seed); for (int slot = 0; slot < 6; slot++) rotationClean &= LuneBot.ProfileFor(probe, slot) != "assassine"; } Check(rotationClean, "the seed rotation never picks the H1 profile (reference bot matches unchanged)"); string first = PlayWithSylvara(12, out LuneMatch played); string second = PlayWithSylvara(12, out LuneMatch _); Check(first == second, "matches with Sylvara bots are deterministic"); HeroState hero = played.Players[0].Hero; Check(hero != null && hero.HeroId == "held_sylvara" && hero.Xp > 0, "the assassine bot recruits Sylvara and she gains XP"); Note("sylvara-bot match seed 12: " + first.Split('|')[0]); } static string PlayWithSylvara(int seed, out LuneMatch match) { match = new LuneMatch(); match.Setup(new MatchOptions { Seed = seed, HumanSlot = -1, Profiles = new List { "assassine", "druck", "fernkampf", "ausgewogen", "druck", "fernkampf" } }); var log = new List(); while (match.Phase != Phase.Over && match.RoundNumber <= 60) { for (int slot = 0; slot < 6; slot++) LuneBot.Plan(match, slot); match.BeginCombat(); int leaps = 0; int ultimates = 0; while (match.Phase == Phase.Combat) { match.StepCombat(); } foreach (LuneUnit unit in match.Combat.Units) { leaps += unit.LeapSerial; ultimates += unit.UltimateSerial; } log.Add(leaps + "/" + ultimates); } return match.RoundNumber + "/" + match.Winner + "|" + string.Join(",", log) + "|" + string.Join(",", match.Players.Select(p => p.Gold + ":" + p.Earned)); } } }