H1A Sylvara: Kernregeln, Ultimate-Auslöser, Netz, Bot-Profil, Prüfsuite
Sylvara (held_sylvara) als vierter Held nach der Referenz: Mondsprung, Sichelwurf, Mondsicheltanz mit in der Planung gewählter Auslöseregel, Mondschleier-Schild, Talente Mondsilber/Sichelsturm und Neumond/Vollmond. Befehl ultimate_rule, HeroState.Ultimate und UnitDef.UltimateRule im Netz, Sprung-/Ultimate-Zähler in den Flag-Bits des Kampf-Frames. Bot-Profil assassine außerhalb der Seed-Rotation. Golden 0 Abweichungen, h1 35/35. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
845556e2b5
commit
60992f1091
17 changed files with 589 additions and 11 deletions
|
|
@ -55,9 +55,14 @@ namespace Luneraria.Core
|
|||
public static readonly OrderedMap<string, HeroSpec> Heroes = Ordered(
|
||||
new HeroSpec { Id = "held_ringborke", Name = "Ringborke", Title = "Wächterin der Wurzeln", Model = "w04", Cost = 260, RoleText = "Front / Schutz", Provisional = true, Summary = "Hält die Front, heilt sich selbst und schützt nahe Verbündete.", Base = new UnitDef { MaxHp = 2200.0, Damage = 34.0, AttackType = 1, ArmorType = 1, AttackRange = 1.4, Speed = 2.4, Ability = "w04" } },
|
||||
new HeroSpec { Id = "held_quellgeflecht", Name = "Quellgeflecht", Title = "Hüterin der Quelle", Model = "w10", Cost = 260, RoleText = "Unterstützung / Heilung", Provisional = true, Summary = "Heilt Verbündete in 4,5 m und stärkt die Armee im Gefecht.", Base = new UnitDef { MaxHp = 1300.0, Damage = 28.0, AttackType = 2, ArmorType = 2, AttackRange = 4.5, Speed = 2.0, Ability = "w10" } },
|
||||
new HeroSpec { Id = "held_funkenlanze", Name = "Funkenlanze", Title = "Schützin des Glutpakts", Model = "g05", Cost = 260, RoleText = "Fernkampf / Schaden", Provisional = true, Summary = "Hoher Einzelschaden aus der zweiten Reihe.", Base = new UnitDef { MaxHp = 950.0, Damage = 46.0, AttackType = 1, ArmorType = 0, AttackRange = 6.0, Speed = 2.0, Ability = "g05" } }
|
||||
new HeroSpec { Id = "held_funkenlanze", Name = "Funkenlanze", Title = "Schützin des Glutpakts", Model = "g05", Cost = 260, RoleText = "Fernkampf / Schaden", Provisional = true, Summary = "Hoher Einzelschaden aus der zweiten Reihe.", Base = new UnitDef { MaxHp = 950.0, Damage = 46.0, AttackType = 1, ArmorType = 0, AttackRange = 6.0, Speed = 2.0, Ability = "g05" } },
|
||||
// H1 (not in the Godot reference): first hero of the champions concept, docs/luneraria/CHAMPIONS-KONZEPT.md.
|
||||
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.", Base = new UnitDef { MaxHp = 1450.0, Damage = 42.0, AttackType = 0, ArmorType = 0, AttackRange = 1.2, Speed = 2.8, Ability = "sylvara" } }
|
||||
);
|
||||
|
||||
/// <summary>Heroes of the Godot reference; ContentTests compares exactly these, later heroes follow them.</summary>
|
||||
public static readonly string[] ReferenceHeroes = { "held_ringborke", "held_quellgeflecht", "held_funkenlanze" };
|
||||
|
||||
public static readonly int[] LevelXp = { 0, 120, 280, 480, 720, 1000, 1320, 1680, 2080, 2520 };
|
||||
public static readonly int[] TalentLevels = { 3, 6 };
|
||||
|
||||
|
|
@ -78,9 +83,30 @@ namespace Luneraria.Core
|
|||
{
|
||||
new[] { new TalentOption("durchschlag", "Durchschlag", "Einzelziel: jeder 4. Treffer ×2,1 statt ×1,5."), new TalentOption("splitterschuss", "Splitterschuss", "Fläche: jeder 4. Treffer zusätzlich 45 % an bis zu 3 Gegner in 2 m.") },
|
||||
new[] { new TalentOption("fernsicht", "Fernsicht", "+1,5 m Reichweite."), new TalentOption("glutsalve", "Glutsalve", "+20 % Angriffstempo.") }
|
||||
} },
|
||||
{ "held_sylvara", new[]
|
||||
{
|
||||
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.") }
|
||||
} }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// H1: trigger rules of a hero's ultimate, chosen in planning; the first is the default.
|
||||
/// Heroes without an entry have no ultimate. The rule decides when the single charge per
|
||||
/// combat phase is released; the combat itself stays automatic.
|
||||
/// </summary>
|
||||
public static readonly Dictionary<string, TalentOption[]> UltimateRules = new Dictionary<string, TalentOption[]>
|
||||
{
|
||||
{ "held_sylvara", new[]
|
||||
{
|
||||
new TalentOption("gruppe", "Gruppe", "Sobald mindestens 3 Gegner in 4 m stehen."),
|
||||
new TalentOption("zweite_reihe", "Zweite Reihe", "Sobald ein Fernkämpfer oder Unterstützer in 5 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."),
|
||||
} },
|
||||
};
|
||||
|
||||
public static readonly OrderedMap<string, ItemSpec> Items = Ordered(
|
||||
new ItemSpec { Id = "basaltschild", Name = "Basaltschild", Cost = 180, Text = "+350 Lebenspunkte", HpFlat = 350.0 },
|
||||
new ItemSpec { Id = "glutklinge", Name = "Glutklinge", Cost = 180, Text = "+18 % Schaden", Damage = 0.18 },
|
||||
|
|
@ -98,7 +124,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");
|
||||
public static readonly OrderedMap<string, string> HeroRoles = Pairs("held_ringborke", "front", "held_quellgeflecht", "support", "held_funkenlanze", "ranged", "held_sylvara", "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" };
|
||||
|
|
@ -115,5 +141,13 @@ namespace Luneraria.Core
|
|||
new BotProfile { Name = "teamhilfe", Front = 0.3, Melee = 0.2, Ranged = 0.25, Splash = 0.1, Support = 0.15, UpgradeFrom = 6, SupportLane = 0.22, Hero = "held_quellgeflecht", HeroRound = 4, Items = new[] { "quellamulett", "rankenmantel", "basaltschild" }, TalentChoices = new[] { 1, 0 }, Champion = "ch_schutz" },
|
||||
new BotProfile { Name = "bossjagd", Front = 0.35, Melee = 0.25, Ranged = 0.2, Splash = 0.1, Support = 0.1, UpgradeFrom = 6, SupportLane = 0.1, Hero = "held_ringborke", HeroRound = 3, Items = new[] { "glutklinge", "basaltschild", "rankenmantel" }, TalentChoices = new[] { 1, 1 }, Champion = "ch_belagerung", BossFrom = 5, BossTroops = 5 }
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// H1: profiles outside the seed rotation (adding them there would change every reference
|
||||
/// 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 = "ch_druck" }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ namespace Luneraria.Core
|
|||
public double DamageBonus;
|
||||
public double ReductionBonus;
|
||||
public double AttackSpeedBonus;
|
||||
/// <summary>Heroes with an ultimate (H1): the trigger rule chosen in planning, "" = none.</summary>
|
||||
public string UltimateRule = "";
|
||||
// Attack champions.
|
||||
public string ChampionId = "";
|
||||
public string Priority = "";
|
||||
|
|
|
|||
|
|
@ -44,6 +44,32 @@ namespace Luneraria.Core
|
|||
return ContentData.Talents[heroId];
|
||||
}
|
||||
|
||||
/// <summary>H1: delay before an ultimate may fire in a combat phase.</summary>
|
||||
public const double UltimateDelay = 2.0;
|
||||
|
||||
/// <summary>H1: trigger rules of the hero's ultimate (first = default); empty without an ultimate.</summary>
|
||||
public static TalentOption[] UltimateRulesOf(string heroId)
|
||||
{
|
||||
TalentOption[] rules;
|
||||
return ContentData.UltimateRules.TryGetValue(heroId ?? "", out rules) ? rules : new TalentOption[0];
|
||||
}
|
||||
|
||||
public static bool HasUltimate(string heroId) { return UltimateRulesOf(heroId).Length > 0; }
|
||||
|
||||
/// <summary>The rule a hero's ultimate follows: the chosen one if valid, else the default, "" without ultimate.</summary>
|
||||
public static string UltimateRuleFor(HeroState hero)
|
||||
{
|
||||
TalentOption[] rules = UltimateRulesOf(hero.HeroId);
|
||||
if (rules.Length == 0)
|
||||
return "";
|
||||
foreach (TalentOption rule in rules)
|
||||
{
|
||||
if (rule.Id == hero.Ultimate)
|
||||
return rule.Id;
|
||||
}
|
||||
return rules[0].Id;
|
||||
}
|
||||
|
||||
public static int LevelFor(int xp)
|
||||
{
|
||||
int level = 1;
|
||||
|
|
@ -85,8 +111,9 @@ namespace Luneraria.Core
|
|||
reduction += 0.15;
|
||||
if (hero.Talents.Contains("fernsicht"))
|
||||
definition.AttackRange = definition.AttackRange + 1.5;
|
||||
if (hero.Talents.Contains("glutsalve"))
|
||||
if (hero.Talents.Contains("glutsalve") || hero.Talents.Contains("neumond"))
|
||||
attackSpeed += 0.20;
|
||||
definition.UltimateRule = UltimateRuleFor(hero);
|
||||
definition.Talents = new List<string>(hero.Talents);
|
||||
definition.DamageBonus = damageBonus;
|
||||
definition.ReductionBonus = reduction;
|
||||
|
|
|
|||
|
|
@ -697,6 +697,9 @@ namespace Luneraria.Core
|
|||
unit.DeathTime = elapsed;
|
||||
unit.Alive = (flags & 1) != 0;
|
||||
unit.InRange = (flags & 2) != 0;
|
||||
// H1: leap and ultimate counters ride in the flag bits (8 bits each, compared for change only).
|
||||
unit.LeapSerial = (flags >> 2) & 0xFF;
|
||||
unit.UltimateSerial = (flags >> 10) & 0xFF;
|
||||
unit.AttackProgress = data[baseIndex + 7];
|
||||
unit.HitCount = (int)data[baseIndex + 8];
|
||||
unit.AttackSerial = (int)data[baseIndex + 9];
|
||||
|
|
@ -799,7 +802,7 @@ namespace Luneraria.Core
|
|||
{
|
||||
if (!unit.IsMobile())
|
||||
continue;
|
||||
int flags = (unit.Alive ? 1 : 0) | (unit.InRange ? 2 : 0);
|
||||
int flags = (unit.Alive ? 1 : 0) | (unit.InRange ? 2 : 0) | ((unit.LeapSerial & 0xFF) << 2) | ((unit.UltimateSerial & 0xFF) << 10);
|
||||
data.Add(unit.Id);
|
||||
data.Add((float)unit.Position.X);
|
||||
data.Add((float)unit.Position.Y);
|
||||
|
|
|
|||
|
|
@ -25,7 +25,9 @@ namespace Luneraria.Core
|
|||
public static void Plan(LuneMatch match, int slot, string profileName = "")
|
||||
{
|
||||
string name = string.IsNullOrEmpty(profileName) ? ProfileFor(match, slot) : profileName;
|
||||
BotProfile profile = ContentData.BotProfiles[name];
|
||||
BotProfile profile;
|
||||
if (!ContentData.BotProfiles.TryGetValue(name, out profile))
|
||||
profile = ContentData.ExtraBotProfiles[name];
|
||||
Player player = match.Players[slot];
|
||||
List<string> ids = match.BaseIds(player.Bund);
|
||||
var roles = new Dictionary<string, string>();
|
||||
|
|
|
|||
|
|
@ -287,14 +287,16 @@ namespace Luneraria.Core
|
|||
foreach (LuneUnit unit in living)
|
||||
{
|
||||
if (unit.IsMobile())
|
||||
AbilityStep(unit, living);
|
||||
AbilityStep(unit, living, gates);
|
||||
}
|
||||
foreach (LuneUnit unit in living)
|
||||
{
|
||||
if (unit.IsMobile() || unit.Kind == "tower")
|
||||
{
|
||||
bool keep = unit.Target != null && unit.Target.Alive;
|
||||
if (keep || (Steps + unit.TeamIndex) % Retarget == 0)
|
||||
if (unit.LeapTarget != null)
|
||||
unit.Target = unit.LeapTarget;
|
||||
else if (keep || (Steps + unit.TeamIndex) % Retarget == 0)
|
||||
unit.Target = ChooseTarget(unit, living, gates);
|
||||
}
|
||||
}
|
||||
|
|
@ -314,6 +316,7 @@ namespace Luneraria.Core
|
|||
unit.Position = next;
|
||||
if (heading.LengthSquared() > 0.000001)
|
||||
unit.Facing = GdMath.Atan2(heading.X, heading.Y);
|
||||
unit.LeapTarget = null;
|
||||
if (unit.Zone == "lane")
|
||||
UpdateProgress(unit, plan.Value);
|
||||
}
|
||||
|
|
@ -543,6 +546,8 @@ namespace Luneraria.Core
|
|||
/// </summary>
|
||||
KeyValuePair<Vec2, double> PlanMove(LuneUnit unit, List<LuneUnit> living, double dt)
|
||||
{
|
||||
if (unit.LeapTarget != null)
|
||||
return new KeyValuePair<Vec2, double>(LeapLanding(unit, unit.LeapTarget), unit.Progress);
|
||||
double stepLength = unit.Speed() * Balance.MarchSpeedFactor * unit.MovementFactor() * dt;
|
||||
Vec2 goal = unit.Position;
|
||||
double travel = 0.0;
|
||||
|
|
@ -676,6 +681,11 @@ namespace Luneraria.Core
|
|||
}
|
||||
if (unit.HitCount % 3 == 0 && unit.HasTalent("stammschlag"))
|
||||
damage *= 1.6;
|
||||
if (unit.LeapStrike)
|
||||
{
|
||||
damage *= SylvaraLeapStrike;
|
||||
unit.LeapStrike = false;
|
||||
}
|
||||
if (target.IsBuilding())
|
||||
damage *= unit.Def.SiegeFactor;
|
||||
QueueDamage(target, target.Mitigated(damage, unit.Def.AttackType), gates);
|
||||
|
|
@ -730,6 +740,9 @@ namespace Luneraria.Core
|
|||
case "e08":
|
||||
unit.GrantShield(unit.MaxHp * 0.20);
|
||||
break;
|
||||
case "sylvara":
|
||||
unit.AbilityReady = SylvaraFirstThrow;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -778,8 +791,10 @@ namespace Luneraria.Core
|
|||
_pendingDots.Add(new PendingDot { Unit = target, Family = family, Damage = damage, Duration = duration, AttackType = attackType });
|
||||
}
|
||||
|
||||
void AbilityStep(LuneUnit unit, List<LuneUnit> living)
|
||||
void AbilityStep(LuneUnit unit, List<LuneUnit> living, Dictionary<string, bool> gates)
|
||||
{
|
||||
if (unit.Ability() == "sylvara")
|
||||
SylvaraStep(unit, gates);
|
||||
double regen = unit.Def.Regen;
|
||||
if (regen > 0.0 && unit.Hp < unit.MaxHp)
|
||||
QueueHeal(unit, unit.MaxHp * regen * Balance.SimStep);
|
||||
|
|
@ -883,6 +898,9 @@ namespace Luneraria.Core
|
|||
success = victims.Count > 0;
|
||||
break;
|
||||
}
|
||||
case "sylvara":
|
||||
success = SylvaraThrow(unit, gates);
|
||||
break;
|
||||
}
|
||||
if (success)
|
||||
{
|
||||
|
|
@ -891,6 +909,130 @@ namespace Luneraria.Core
|
|||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- H1 hero kits
|
||||
public const double SylvaraLeapRange = 8.0;
|
||||
public const double SylvaraLeapStrike = 1.5;
|
||||
public const double SylvaraFirstThrow = 4.0;
|
||||
public const double SylvaraThrowRange = 6.0;
|
||||
/// <summary>The leap only targets back-row enemies with at most this many allies within 3 m.</summary>
|
||||
public const int SylvaraLeapGuards = 2;
|
||||
/// <summary>Mondschleier: shield after the leap and after the ultimate, share of max HP, for 4 s.</summary>
|
||||
public const double SylvaraVeil = 0.25;
|
||||
public const double SylvaraDance = 2.2;
|
||||
public const double SylvaraFullMoon = 2.8;
|
||||
/// <summary>Back-row enemies: the leap and the "zweite_reihe" rule look for these.</summary>
|
||||
public const double BackRowRange = 3.0;
|
||||
|
||||
static bool BackRow(LuneUnit other) { return other.IsMobile() && other.AttackRange() >= BackRowRange - 0.000001; }
|
||||
|
||||
List<LuneUnit> NearbyMobile(LuneUnit unit, double radius, int maximum)
|
||||
{
|
||||
List<LuneUnit> result = Nearby(unit, false, unit.Position, radius, 1000);
|
||||
result.RemoveAll(other => !other.IsMobile());
|
||||
if (result.Count > maximum)
|
||||
result.RemoveRange(maximum, result.Count - maximum);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Mobile allies of the target within 3 m of it (its guards against a leap).</summary>
|
||||
int Guards(LuneUnit unit, LuneUnit target)
|
||||
{
|
||||
int count = 0;
|
||||
foreach (LuneUnit other in NearMobile(target.Position, 3.0, new[] { target.Team }))
|
||||
{
|
||||
if (other != target && other.Alive && other.Zone == target.Zone && other.Position.DistanceTo(target.Position) <= 3.0 + 0.000001)
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>Lands 0.9 m in front of the target, on the side the unit comes from.</summary>
|
||||
static Vec2 LeapLanding(LuneUnit unit, LuneUnit target)
|
||||
{
|
||||
Vec2 away = unit.Position - target.Position;
|
||||
if (away.LengthSquared() < 0.000001)
|
||||
away = unit.Team == 0 ? Vec2.Right : Vec2.Left;
|
||||
Vec2 landing = target.Position + away.Normalized() * 0.9;
|
||||
landing.X = (float)GdMath.Clamp(landing.X, -LuneMap.HalfExtent, LuneMap.HalfExtent);
|
||||
landing.Y = (float)GdMath.Clamp(landing.Y, -LuneMap.HalfExtent, LuneMap.HalfExtent);
|
||||
return landing;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mondsprung (passive, once per combat): the first back-row enemy within 8 m draws the
|
||||
/// leap; she lands next to it, takes it as target and strikes the first hit at 150 %.
|
||||
/// Mondsicheltanz (ultimate, once per combat after 2 s): released by the planned rule.
|
||||
/// Both decide on the state at the start of the step; the landing is applied with the moves.
|
||||
/// </summary>
|
||||
void SylvaraStep(LuneUnit unit, Dictionary<string, bool> gates)
|
||||
{
|
||||
if (!unit.LeapUsed)
|
||||
{
|
||||
foreach (LuneUnit other in NearbyMobile(unit, SylvaraLeapRange, 1000))
|
||||
{
|
||||
if (BackRow(other) && !other.IsBoss() && Guards(unit, other) <= SylvaraLeapGuards)
|
||||
{
|
||||
_pendingShields.Add(new PendingShield { Unit = unit, Amount = unit.MaxHp * SylvaraVeil, Duration = 4.0 });
|
||||
unit.LeapUsed = true;
|
||||
unit.LeapTarget = other;
|
||||
unit.LeapStrike = true;
|
||||
unit.LeapSerial += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unit.UltimateUsed || string.IsNullOrEmpty(unit.Def.UltimateRule) || unit.CombatTime + 0.000001 < LuneContent.UltimateDelay)
|
||||
return;
|
||||
bool release = false;
|
||||
switch (unit.Def.UltimateRule)
|
||||
{
|
||||
case "gruppe":
|
||||
release = NearbyMobile(unit, 4.0, 3).Count >= 3;
|
||||
break;
|
||||
case "zweite_reihe":
|
||||
release = NearbyMobile(unit, 5.0, 1000).Exists(BackRow);
|
||||
break;
|
||||
case "notfall":
|
||||
release = unit.Hp < unit.MaxHp * 0.4 && NearbyMobile(unit, 4.0, 1).Count > 0;
|
||||
break;
|
||||
case "sofort":
|
||||
release = NearbyMobile(unit, 4.0, 1).Count > 0;
|
||||
break;
|
||||
}
|
||||
if (!release)
|
||||
return;
|
||||
bool full = unit.HasTalent("vollmond");
|
||||
double damage = unit.Def.Damage * (1.0 + unit.DamageBonus()) * (full ? SylvaraFullMoon : SylvaraDance);
|
||||
_pendingShields.Add(new PendingShield { Unit = unit, Amount = unit.MaxHp * SylvaraVeil, Duration = 4.0 });
|
||||
foreach (LuneUnit victim in NearbyMobile(unit, 5.0, full ? 7 : 5))
|
||||
{
|
||||
QueueDamage(victim, victim.Mitigated(damage, unit.Def.AttackType), gates);
|
||||
AddEffect(victim, "moon_mark", "movement_slow", 0.30, 2.0);
|
||||
}
|
||||
unit.UltimateUsed = true;
|
||||
unit.UltimateSerial += 1;
|
||||
}
|
||||
|
||||
/// <summary>Sichelwurf (every 6 s): 90 % to the target and up to 2 more enemies within 2.5 m of it.</summary>
|
||||
bool SylvaraThrow(LuneUnit unit, Dictionary<string, bool> gates)
|
||||
{
|
||||
LuneUnit primary = unit.Target;
|
||||
if (primary == null || !Hostile(unit, primary) || !primary.IsMobile() || unit.Position.DistanceTo(primary.Position) > SylvaraThrowRange)
|
||||
return false;
|
||||
double damage = unit.Def.Damage * (1.0 + unit.DamageBonus());
|
||||
if (unit.HasTalent("mondsilber"))
|
||||
{
|
||||
QueueDamage(primary, primary.Mitigated(damage * 2.0, unit.Def.AttackType), gates);
|
||||
return true;
|
||||
}
|
||||
bool storm = unit.HasTalent("sichelsturm");
|
||||
var victims = new List<LuneUnit> { primary };
|
||||
victims.AddRange(Nearby(unit, false, primary.Position, storm ? 3.5 : 2.5, storm ? 4 : 2, primary));
|
||||
foreach (LuneUnit victim in victims)
|
||||
QueueDamage(victim, victim.Mitigated(damage * 0.9, unit.Def.AttackType), gates);
|
||||
return true;
|
||||
}
|
||||
|
||||
void OnHit(LuneUnit unit, LuneUnit primary, List<LuneUnit> living, Dictionary<string, bool> gates)
|
||||
{
|
||||
string ability = unit.Ability();
|
||||
|
|
|
|||
|
|
@ -291,6 +291,21 @@ namespace Luneraria.Core
|
|||
hero.Talents.Add(option.Id);
|
||||
return Result(true, "Talent " + option.Name + " gewählt.");
|
||||
}
|
||||
case "ultimate_rule":
|
||||
{
|
||||
if (hero == null || !LuneContent.HasUltimate(hero.HeroId))
|
||||
return Result(false, "Dein Held hat kein Ultimate.");
|
||||
string rule = command.TypeId ?? "";
|
||||
foreach (TalentOption option in LuneContent.UltimateRulesOf(hero.HeroId))
|
||||
{
|
||||
if (option.Id == rule)
|
||||
{
|
||||
hero.Ultimate = rule;
|
||||
return Result(true, "Ultimate-Auslöser: " + option.Name + ".");
|
||||
}
|
||||
}
|
||||
return Result(false, "Unbekannter Ultimate-Auslöser.");
|
||||
}
|
||||
case "champion_book":
|
||||
{
|
||||
string championId = command.ChampionId ?? "";
|
||||
|
|
@ -459,6 +474,8 @@ namespace Luneraria.Core
|
|||
}
|
||||
if (hero.Talents.Count > LuneContent.TalentLevels.Length)
|
||||
return false;
|
||||
if (!string.IsNullOrEmpty(hero.Ultimate) && LuneContent.UltimateRuleFor(hero) != hero.Ultimate)
|
||||
return false;
|
||||
}
|
||||
return draft.NewBookings.Count <= LuneContent.MaxBookings;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,9 +67,18 @@ namespace Luneraria.Core
|
|||
/// <summary>The grotto boss waits while two expeditions still fight each other.</summary>
|
||||
public bool Dormant;
|
||||
public double AuraReady = 4.0;
|
||||
// H1 hero kit: Sylvara's leap (once per combat) and the ultimate charge (once per combat).
|
||||
public bool LeapUsed;
|
||||
/// <summary>Set for the one step of a leap: the unit lands next to this target.</summary>
|
||||
public LuneUnit LeapTarget;
|
||||
/// <summary>The first hit after a leap deals extra damage.</summary>
|
||||
public bool LeapStrike;
|
||||
public bool UltimateUsed;
|
||||
// Presentation-only counters; never read by the rules.
|
||||
public int AttackSerial;
|
||||
public int AbilitySerial;
|
||||
public int LeapSerial;
|
||||
public int UltimateSerial;
|
||||
public double HitFlash;
|
||||
public bool InRange;
|
||||
public double DeathTime = -1.0;
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ namespace Luneraria.Core
|
|||
public bool Fresh;
|
||||
/// <summary>Items bought in the current planning (full refund on sale).</summary>
|
||||
public List<string> FreshItems = new List<string>();
|
||||
/// <summary>Trigger rule of the ultimate (H1), "" = the hero's default rule.</summary>
|
||||
public string Ultimate = "";
|
||||
|
||||
public HeroState Clone()
|
||||
{
|
||||
|
|
@ -232,5 +234,7 @@ namespace Luneraria.Core
|
|||
public static Command ChampionBook(string championId, string lane = null) { return new Command { Type = "champion_book", ChampionId = championId, Lane = lane }; }
|
||||
public static Command ChampionCancel(string uid) { return new Command { Type = "champion_cancel", Uid = uid }; }
|
||||
public static Command BossLaneChoice(string lane) { return new Command { Type = "boss_lane", Lane = lane }; }
|
||||
/// <summary>H1: trigger rule of the hero's ultimate; the rule id travels in TypeId.</summary>
|
||||
public static Command UltimateRule(string rule) { return new Command { Type = "ultimate_rule", TypeId = rule }; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -324,6 +324,7 @@ namespace Luneraria.Net
|
|||
Str(text, hero.Items != null ? string.Join("\x1d", hero.Items) : "");
|
||||
Str(text, hero.Talents != null ? string.Join("\x1d", hero.Talents) : "");
|
||||
Str(text, hero.FreshItems != null ? string.Join("\x1d", hero.FreshItems) : "");
|
||||
Str(text, hero.Ultimate ?? "");
|
||||
}
|
||||
|
||||
static void WriteUnit(StringBuilder text, NetUnitRecord unit)
|
||||
|
|
@ -374,6 +375,7 @@ namespace Luneraria.Net
|
|||
Num(text, def.HpFactor);
|
||||
Str(text, def.Tier ?? "");
|
||||
Str(text, def.Talents != null ? string.Join("\x1d", def.Talents) : "");
|
||||
Str(text, def.UltimateRule ?? "");
|
||||
}
|
||||
|
||||
static void Str(StringBuilder text, string value)
|
||||
|
|
@ -756,6 +758,7 @@ namespace Luneraria.Net
|
|||
AddSplit(hero.Items, Text());
|
||||
AddSplit(hero.Talents, Text());
|
||||
AddSplit(hero.FreshItems, Text());
|
||||
hero.Ultimate = Text() ?? "";
|
||||
return hero;
|
||||
}
|
||||
|
||||
|
|
@ -824,6 +827,7 @@ namespace Luneraria.Net
|
|||
}
|
||||
};
|
||||
AddSplit(unit.Def.Talents, Text());
|
||||
unit.Def.UltimateRule = Text() ?? "";
|
||||
return unit;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() };
|
||||
return new List<CheckSuite> { new ContentSuite(), new CoreRulesSuite(), new M2RulesSuite(), new GoldenSuite(), new DeterminismSuite(), new NetSuite(), new NetRoundsSuite(), new H1HeroSuite() };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,7 +87,10 @@ namespace Luneraria.Core.Tests
|
|||
|
||||
void CompareHeroes(OrderedMap<string, object> expected)
|
||||
{
|
||||
Check(expected.Keys.SequenceEqual(ContentData.Heroes.Keys), "heroes: same ids in the same order");
|
||||
// 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)");
|
||||
foreach (KeyValuePair<string, object> pair in expected)
|
||||
{
|
||||
var entry = MiniJson.Obj(pair.Value);
|
||||
|
|
@ -156,7 +159,7 @@ namespace Luneraria.Core.Tests
|
|||
|
||||
void CompareTalents(OrderedMap<string, object> expected)
|
||||
{
|
||||
bool same = expected.Count == ContentData.Talents.Count;
|
||||
bool same = expected.Count == ContentData.Talents.Count - (ContentData.Heroes.Count - ContentData.ReferenceHeroes.Length);
|
||||
foreach (KeyValuePair<string, object> pair in expected)
|
||||
{
|
||||
TalentOption[][] tiers;
|
||||
|
|
|
|||
240
Assets/Luneraria/Tests/Core/H1HeroSuite.cs
Normal file
240
Assets/Luneraria/Tests/Core/H1HeroSuite.cs
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Luneraria.Core.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
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<LuneMatch.ArmyEntry> 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");
|
||||
}
|
||||
|
||||
/// <summary>A small fight on the mid lane without buildings: Sylvara (team 0) and given enemies (team 1).</summary>
|
||||
static LuneCombat Arena(HeroState hero, out LuneUnit sylvara, 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";
|
||||
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<string, double> 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<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 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<string> { "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<string> { "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<string> { "assassine", "druck", "fernkampf", "ausgewogen", "druck", "fernkampf" } });
|
||||
var log = new List<string>();
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Luneraria/Tests/Core/H1HeroSuite.cs.meta
Normal file
2
Assets/Luneraria/Tests/Core/H1HeroSuite.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 0d43a7513f38410191603ecb1036ff4c
|
||||
|
|
@ -51,5 +51,6 @@ namespace Luneraria.Core.Tests
|
|||
[Test] public void Determinism() { RunSuite(new DeterminismSuite()); }
|
||||
[Test] public void NetProtocol() { RunSuite(new NetSuite()); }
|
||||
[Test] public void NetRounds() { RunSuite(new NetRoundsSuite()); }
|
||||
[Test] public void H1Heroes() { RunSuite(new H1HeroSuite()); }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
tools/dotnet/Luneraria.Balance/Luneraria.Balance.csproj
Normal file
16
tools/dotnet/Luneraria.Balance/Luneraria.Balance.csproj
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- H1 balance runs: bot matches with one hero swapped, both sides, many seeds. -->
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>disable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../Luneraria.Core/Luneraria.Core.csproj" />
|
||||
<Compile Include="Program.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
72
tools/dotnet/Luneraria.Balance/Program.cs
Normal file
72
tools/dotnet/Luneraria.Balance/Program.cs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Luneraria.Core;
|
||||
|
||||
/// <summary>
|
||||
/// H1 balance runs. Every hero gets the same probe bot (shares of "ausgewogen", same items,
|
||||
/// first talent choices, recruited in round 3) on slot 0 or slot 3; the other five slots are
|
||||
/// fixed reference profiles with Ringborke's probe on the opposite seat. Reports the win rate
|
||||
/// of the probe seat per hero over many seeds and both sides, plus how often Sylvara leaps and
|
||||
/// releases her ultimate. Usage: dotnet run -c Release --project tools/dotnet/Luneraria.Balance [seeds]
|
||||
/// </summary>
|
||||
public static class Program
|
||||
{
|
||||
static readonly string[] Heroes = { "held_ringborke", "held_quellgeflecht", "held_funkenlanze", "held_sylvara" };
|
||||
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
int seeds = args.Length > 0 ? int.Parse(args[0], CultureInfo.InvariantCulture) : 40;
|
||||
BotProfile reference = ContentData.BotProfiles["ausgewogen"];
|
||||
foreach (string hero in Heroes)
|
||||
{
|
||||
ContentData.ExtraBotProfiles["probe_" + hero] = new BotProfile
|
||||
{
|
||||
Name = "probe_" + hero, Front = reference.Front, Melee = reference.Melee, Ranged = reference.Ranged, Splash = reference.Splash,
|
||||
Support = reference.Support, UpgradeFrom = reference.UpgradeFrom, SupportLane = 0.0, Hero = hero, HeroRound = 3,
|
||||
Items = new[] { "glutklinge", "basaltschild", "funkenfokus" }, TalentChoices = new[] { 0, 0 }, Champion = "",
|
||||
};
|
||||
}
|
||||
Console.WriteLine("hero; seat wins; losses; draws; open; win rate; mean rounds; leaps/combat; ultimates/combat");
|
||||
foreach (string hero in Heroes)
|
||||
{
|
||||
int wins = 0, losses = 0, draws = 0, open = 0, rounds = 0, combats = 0, leaps = 0, ults = 0;
|
||||
for (int seed = 1; seed <= seeds; seed++)
|
||||
{
|
||||
for (int side = 0; side < 2; side++)
|
||||
{
|
||||
var profiles = new List<string> { "probe_held_ringborke", "druck", "fernkampf", "probe_held_ringborke", "druck", "fernkampf" };
|
||||
profiles[side * 3] = "probe_" + hero;
|
||||
var match = new LuneMatch();
|
||||
match.Setup(new MatchOptions { Seed = seed, HumanSlot = -1, Profiles = profiles });
|
||||
while (match.Phase != Phase.Over && match.RoundNumber <= 80)
|
||||
{
|
||||
for (int slot = 0; slot < 6; slot++)
|
||||
LuneBot.Plan(match, slot);
|
||||
match.BeginCombat();
|
||||
match.FinishCombat();
|
||||
combats++;
|
||||
foreach (LuneUnit unit in match.Combat.Units)
|
||||
{
|
||||
if (unit.Kind == "hero" && unit.OwnerSlot == side * 3)
|
||||
{
|
||||
leaps += unit.LeapSerial;
|
||||
ults += unit.UltimateSerial;
|
||||
}
|
||||
}
|
||||
}
|
||||
rounds += match.RoundNumber;
|
||||
if (match.Phase != Phase.Over) open++;
|
||||
else if (match.Winner == side) wins++;
|
||||
else if (match.Winner == LuneMatch.Draw) draws++;
|
||||
else losses++;
|
||||
}
|
||||
}
|
||||
int games = seeds * 2;
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, "{0}; {1}; {2}; {3}; {4}; {5:P0}; {6:F1}; {7:F2}; {8:F2}",
|
||||
hero, wins, losses, draws, open, (double)wins / Math.Max(1, wins + losses + draws), (double)rounds / games, (double)leaps / combats, (double)ults / combats));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue