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:
parent
fb5c92dd37
commit
cbff678290
10 changed files with 288 additions and 8 deletions
|
|
@ -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.",
|
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.",
|
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.",
|
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>
|
/// <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("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.") }
|
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[]>
|
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_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>
|
/// <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("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."),
|
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(
|
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>
|
/// <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> 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[] RowOrder = { "front", "melee", "splash", "ranged", "support" };
|
||||||
public static readonly string[] BotNames = { "Eichrufer", "Aschenzunge", "Quellwächter", "Glutfaust", "Moosauge", "Basaltherz", "Rindenkrone", "Funkenschritt" };
|
public static readonly string[] BotNames = { "Eichrufer", "Aschenzunge", "Quellwächter", "Glutfaust", "Moosauge", "Basaltherz", "Rindenkrone", "Funkenschritt" };
|
||||||
public static readonly string[] WavesMelee = { "e01", "e02", "e05" };
|
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).
|
/// bot match). Usable as a fixed profile per slot (MatchOptions.Profiles, balance runs).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly OrderedMap<string, BotProfile> ExtraBotProfiles = Ordered(
|
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 = "" }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ namespace Luneraria.Core
|
||||||
reduction += 0.15;
|
reduction += 0.15;
|
||||||
if (hero.Talents.Contains("fernsicht"))
|
if (hero.Talents.Contains("fernsicht"))
|
||||||
definition.AttackRange = definition.AttackRange + 1.5;
|
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;
|
attackSpeed += 0.20;
|
||||||
definition.UltimateRule = UltimateRuleFor(hero);
|
definition.UltimateRule = UltimateRuleFor(hero);
|
||||||
definition.Skin = ValidSkin(hero.HeroId, hero.Skin) ? hero.Skin ?? "" : "";
|
definition.Skin = ValidSkin(hero.HeroId, hero.Skin) ? hero.Skin ?? "" : "";
|
||||||
|
|
|
||||||
|
|
@ -747,6 +747,8 @@ namespace Luneraria.Core
|
||||||
}
|
}
|
||||||
if (unit.HitCount % 3 == 0 && unit.HasTalent("stammschlag"))
|
if (unit.HitCount % 3 == 0 && unit.HasTalent("stammschlag"))
|
||||||
damage *= 1.6;
|
damage *= 1.6;
|
||||||
|
if (unit.Ability() == "brunhild" && HeroLike(target))
|
||||||
|
damage *= BrunhildHunter;
|
||||||
if (unit.LeapStrike)
|
if (unit.LeapStrike)
|
||||||
{
|
{
|
||||||
damage *= SylvaraLeapStrike;
|
damage *= SylvaraLeapStrike;
|
||||||
|
|
@ -811,6 +813,9 @@ namespace Luneraria.Core
|
||||||
case "sylvara":
|
case "sylvara":
|
||||||
unit.AbilityReady = SylvaraFirstThrow;
|
unit.AbilityReady = SylvaraFirstThrow;
|
||||||
break;
|
break;
|
||||||
|
case "brunhild":
|
||||||
|
unit.AbilityReady = BrunhildFirstSpear;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -863,6 +868,8 @@ namespace Luneraria.Core
|
||||||
{
|
{
|
||||||
if (unit.Ability() == "sylvara")
|
if (unit.Ability() == "sylvara")
|
||||||
SylvaraStep(unit, gates);
|
SylvaraStep(unit, gates);
|
||||||
|
else if (unit.Ability() == "brunhild")
|
||||||
|
BrunhildStep(unit, gates);
|
||||||
double regen = unit.Def.Regen;
|
double regen = unit.Def.Regen;
|
||||||
if (regen > 0.0 && unit.Hp < unit.MaxHp)
|
if (regen > 0.0 && unit.Hp < unit.MaxHp)
|
||||||
QueueHeal(unit, unit.MaxHp * regen * Balance.SimStep);
|
QueueHeal(unit, unit.MaxHp * regen * Balance.SimStep);
|
||||||
|
|
@ -969,6 +976,9 @@ namespace Luneraria.Core
|
||||||
case "sylvara":
|
case "sylvara":
|
||||||
success = SylvaraThrow(unit, gates);
|
success = SylvaraThrow(unit, gates);
|
||||||
break;
|
break;
|
||||||
|
case "brunhild":
|
||||||
|
success = BrunhildSpear(unit, gates);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
|
|
@ -1104,6 +1114,97 @@ namespace Luneraria.Core
|
||||||
return true;
|
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)
|
void OnHit(LuneUnit unit, LuneUnit primary, List<LuneUnit> living, Dictionary<string, bool> gates)
|
||||||
{
|
{
|
||||||
string ability = unit.Ability();
|
string ability = unit.Ability();
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,8 @@ namespace Luneraria.Core
|
||||||
public double Mitigated(double raw, int attackType)
|
public double Mitigated(double raw, int attackType)
|
||||||
{
|
{
|
||||||
double value = GdMath.Max(0.0, raw) * DamageMatrix[GdMath.Clamp(attackType, 0, 2), ArmorType()];
|
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>
|
/// <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);
|
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()
|
public double AttackSpeedFactor()
|
||||||
{
|
{
|
||||||
|
if (Stunned())
|
||||||
|
return 0.0;
|
||||||
double slowing = StrongestEffect("attack_slow") * (IsBoss() ? 0.5 : 1.0);
|
double slowing = StrongestEffect("attack_slow") * (IsBoss() ? 0.5 : 1.0);
|
||||||
double bonus = Def.AttackSpeedBonus;
|
double bonus = Def.AttackSpeedBonus;
|
||||||
return 1.0 + GdMath.Clamp(StrongestEffect("attack_speed") - slowing + bonus, -0.25, 0.5);
|
return 1.0 + GdMath.Clamp(StrongestEffect("attack_speed") - slowing + bonus, -0.25, 0.5);
|
||||||
|
|
@ -222,6 +228,8 @@ namespace Luneraria.Core
|
||||||
|
|
||||||
public double MovementFactor()
|
public double MovementFactor()
|
||||||
{
|
{
|
||||||
|
if (Stunned())
|
||||||
|
return 0.0;
|
||||||
return 1.0 - GdMath.Min(0.35, StrongestEffect("movement_slow") * (IsBoss() ? 0.5 : 1.0));
|
return 1.0 - GdMath.Min(0.35, StrongestEffect("movement_slow") * (IsBoss() ? 0.5 : 1.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ namespace Luneraria.Core.Tests
|
||||||
|
|
||||||
public static IList<CheckSuite> All()
|
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() };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ namespace Luneraria.Core.Tests
|
||||||
// Heroes added after the port (H1) follow the reference heroes; those must match exactly.
|
// 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),
|
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");
|
"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)
|
foreach (KeyValuePair<string, object> pair in expected)
|
||||||
{
|
{
|
||||||
var entry = MiniJson.Obj(pair.Value);
|
var entry = MiniJson.Obj(pair.Value);
|
||||||
|
|
|
||||||
148
Assets/Luneraria/Tests/Core/H2HeroSuite.cs
Normal file
148
Assets/Luneraria/Tests/Core/H2HeroSuite.cs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/Luneraria/Tests/Core/H2HeroSuite.cs.meta
Normal file
2
Assets/Luneraria/Tests/Core/H2HeroSuite.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2fa3dc51ddee4a919bec93bf34fc29f9
|
||||||
|
|
@ -55,5 +55,6 @@ namespace Luneraria.Core.Tests
|
||||||
[Test] public void S1Rules() { RunSuite(new S1RulesSuite()); }
|
[Test] public void S1Rules() { RunSuite(new S1RulesSuite()); }
|
||||||
[Test] public void S2Rules() { RunSuite(new S2RulesSuite()); }
|
[Test] public void S2Rules() { RunSuite(new S2RulesSuite()); }
|
||||||
[Test] public void S3Rules() { RunSuite(new S3RulesSuite()); }
|
[Test] public void S3Rules() { RunSuite(new S3RulesSuite()); }
|
||||||
|
[Test] public void H2Heroes() { RunSuite(new H2HeroSuite()); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ using Luneraria.Core;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Program
|
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)
|
public static int Main(string[] args)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue