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>
311 lines
20 KiB
C#
311 lines
20 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
|
|
namespace Luneraria.Core.Tests
|
|
{
|
|
/// <summary>
|
|
/// Compares every rule table and the computed map of the C# core with the export of the
|
|
/// Godot reference (Golden/content.json, tools/godot-golden/content_export.gd): units,
|
|
/// monsters, sends, heroes, talents, items, champions, bot profiles (order), balance
|
|
/// defaults, lane polylines and building positions. Tables must match exactly; the lane
|
|
/// geometry is computed and must match within 1e-5 m (the largest deviation is noted).
|
|
/// </summary>
|
|
public sealed class ContentSuite : CheckSuite
|
|
{
|
|
public override string Name { get { return "content"; } }
|
|
|
|
const double GeometryTolerance = 0.00001;
|
|
|
|
protected override void RunChecks()
|
|
{
|
|
var data = MiniJson.Obj(MiniJson.Parse(File.ReadAllText(Path.Combine(GoldenDirectory, "content.json"))));
|
|
CompareUnits("units", MiniJson.Obj(data["units"]), ContentData.Units);
|
|
CompareUnits("enemies", MiniJson.Obj(data["enemies"]), ContentData.Enemies);
|
|
CompareUnits("sends", MiniJson.Obj(data["sends"]), ContentData.Sends);
|
|
CompareHeroes(MiniJson.Obj(data["heroes"]));
|
|
CompareItems(MiniJson.Obj(data["items"]));
|
|
CompareChampions(MiniJson.Obj(data["champions"]));
|
|
CompareTalents(MiniJson.Obj(data["talents"]));
|
|
CompareConstants(data);
|
|
CompareProfiles(MiniJson.Obj(data["bot_profiles"]));
|
|
CompareBalance(MiniJson.Obj(data["balance"]));
|
|
CompareMap(MiniJson.Obj(data["lanes"]), MiniJson.Arr(data["buildings"]));
|
|
}
|
|
|
|
void CompareUnits(string label, OrderedMap<string, object> expected, OrderedMap<string, UnitDef> actual)
|
|
{
|
|
Check(expected.Keys.SequenceEqual(actual.Keys), label + ": same ids in the same order");
|
|
foreach (KeyValuePair<string, object> pair in expected)
|
|
{
|
|
var entry = MiniJson.Obj(pair.Value);
|
|
UnitDef unit;
|
|
if (!actual.TryGetValue(pair.Key, out unit))
|
|
{
|
|
Check(false, label + ": missing " + pair.Key);
|
|
continue;
|
|
}
|
|
var problems = new List<string>();
|
|
Compare(problems, entry, "id", unit.Id);
|
|
Compare(problems, entry, "display_name", unit.DisplayName);
|
|
Compare(problems, entry, "cost", unit.Cost);
|
|
Compare(problems, entry, "upgrade_id", unit.UpgradeId);
|
|
Compare(problems, entry, "max_hp", unit.MaxHp);
|
|
Compare(problems, entry, "damage", unit.Damage);
|
|
Compare(problems, entry, "attack_type", unit.AttackType);
|
|
Compare(problems, entry, "armor_type", unit.ArmorType);
|
|
Compare(problems, entry, "attack_range", unit.AttackRange);
|
|
Compare(problems, entry, "speed", unit.Speed);
|
|
Compare(problems, entry, "ability", unit.Ability);
|
|
Compare(problems, entry, "leak_value", unit.LeakValue);
|
|
Compare(problems, entry, "bounty", unit.Bounty);
|
|
Compare(problems, entry, "ability_description", unit.AbilityDescription);
|
|
Compare(problems, entry, "is_boss", unit.IsBoss);
|
|
Compare(problems, entry, "income_gain", unit.IncomeGain);
|
|
Check(problems.Count == 0, label + " " + pair.Key + " matches the reference " + string.Join(", ", problems));
|
|
}
|
|
}
|
|
|
|
static void Compare(List<string> problems, OrderedMap<string, object> entry, string key, object actual)
|
|
{
|
|
object expected;
|
|
if (!entry.TryGetValue(key, out expected))
|
|
return;
|
|
bool same;
|
|
if (actual is double d)
|
|
same = MiniJson.Num(expected) == d;
|
|
else if (actual is int i)
|
|
same = MiniJson.Int(expected) == i;
|
|
else if (actual is bool b)
|
|
same = expected is bool eb && eb == b;
|
|
else
|
|
same = MiniJson.Str(expected) == MiniJson.Str(actual);
|
|
if (!same)
|
|
problems.Add(key + ": " + MiniJson.Str(expected) + " != " + MiniJson.Str(actual));
|
|
}
|
|
|
|
void CompareHeroes(OrderedMap<string, object> expected)
|
|
{
|
|
// 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);
|
|
HeroSpec hero = ContentData.Heroes[pair.Key];
|
|
var problems = new List<string>();
|
|
Compare(problems, entry, "name", hero.Name);
|
|
Compare(problems, entry, "title", hero.Title);
|
|
Compare(problems, entry, "model", hero.Model);
|
|
Compare(problems, entry, "cost", hero.Cost);
|
|
Compare(problems, entry, "role", hero.RoleText);
|
|
Compare(problems, entry, "summary", hero.Summary);
|
|
var baseDef = MiniJson.Obj(entry["def"]);
|
|
Compare(problems, baseDef, "max_hp", hero.Base.MaxHp);
|
|
Compare(problems, baseDef, "damage", hero.Base.Damage);
|
|
Compare(problems, baseDef, "attack_type", hero.Base.AttackType);
|
|
Compare(problems, baseDef, "armor_type", hero.Base.ArmorType);
|
|
Compare(problems, baseDef, "attack_range", hero.Base.AttackRange);
|
|
Compare(problems, baseDef, "speed", hero.Base.Speed);
|
|
Compare(problems, baseDef, "ability", hero.Base.Ability);
|
|
Check(problems.Count == 0, "hero " + pair.Key + " matches " + string.Join(", ", problems));
|
|
}
|
|
}
|
|
|
|
void CompareItems(OrderedMap<string, object> expected)
|
|
{
|
|
Check(expected.Keys.SequenceEqual(ContentData.Items.Keys), "items: same ids in the same order");
|
|
foreach (KeyValuePair<string, object> pair in expected)
|
|
{
|
|
var entry = MiniJson.Obj(pair.Value);
|
|
ItemSpec item = ContentData.Items[pair.Key];
|
|
var problems = new List<string>();
|
|
Compare(problems, entry, "name", item.Name);
|
|
Compare(problems, entry, "cost", item.Cost);
|
|
Compare(problems, entry, "text", item.Text);
|
|
Compare(problems, entry, "hp_flat", item.HpFlat);
|
|
Compare(problems, entry, "damage", item.Damage);
|
|
Compare(problems, entry, "reduction", item.Reduction);
|
|
Compare(problems, entry, "attack_speed", item.AttackSpeed);
|
|
Compare(problems, entry, "regen", item.Regen);
|
|
Compare(problems, entry, "splash", item.Splash);
|
|
Compare(problems, entry, "min_level", item.MinLevel);
|
|
Check(problems.Count == 0, "item " + pair.Key + " matches " + string.Join(", ", problems));
|
|
}
|
|
}
|
|
|
|
void CompareChampions(OrderedMap<string, object> expected)
|
|
{
|
|
Check(expected.Keys.SequenceEqual(ContentData.Champions.Keys), "champions: same ids in the same order");
|
|
foreach (KeyValuePair<string, object> pair in expected)
|
|
{
|
|
var entry = MiniJson.Obj(pair.Value);
|
|
ChampionSpec spec = ContentData.Champions[pair.Key];
|
|
var problems = new List<string>();
|
|
Compare(problems, entry, "name", spec.Name);
|
|
Compare(problems, entry, "role", spec.RoleText);
|
|
Compare(problems, entry, "model", spec.Model);
|
|
Compare(problems, entry, "cost", spec.Cost);
|
|
Compare(problems, entry, "base", spec.Base);
|
|
Compare(problems, entry, "text", spec.Text);
|
|
Compare(problems, entry, "priority", spec.Priority);
|
|
Compare(problems, entry, "siege_factor", spec.SiegeFactor);
|
|
Compare(problems, entry, "view_scale", spec.ViewScale);
|
|
Check(problems.Count == 0, "champion " + pair.Key + " matches " + string.Join(", ", problems));
|
|
}
|
|
}
|
|
|
|
void CompareTalents(OrderedMap<string, object> expected)
|
|
{
|
|
bool same = expected.Count == ContentData.Talents.Count - (ContentData.Heroes.Count - ContentData.ReferenceHeroes.Length);
|
|
foreach (KeyValuePair<string, object> pair in expected)
|
|
{
|
|
TalentOption[][] tiers;
|
|
if (!ContentData.Talents.TryGetValue(pair.Key, out tiers))
|
|
{
|
|
same = false;
|
|
continue;
|
|
}
|
|
List<object> expectedTiers = MiniJson.Arr(pair.Value);
|
|
same = same && expectedTiers.Count == tiers.Length;
|
|
for (int tier = 0; tier < Math.Min(expectedTiers.Count, tiers.Length); tier++)
|
|
{
|
|
List<object> options = MiniJson.Arr(expectedTiers[tier]);
|
|
same = same && options.Count == tiers[tier].Length;
|
|
for (int choice = 0; choice < Math.Min(options.Count, tiers[tier].Length); choice++)
|
|
{
|
|
var option = MiniJson.Obj(options[choice]);
|
|
same = same && MiniJson.Str(option["id"]) == tiers[tier][choice].Id && MiniJson.Str(option["name"]) == tiers[tier][choice].Name
|
|
&& MiniJson.Str(option["text"]) == tiers[tier][choice].Text;
|
|
}
|
|
}
|
|
}
|
|
Check(same, "talents: every tier and choice matches");
|
|
}
|
|
|
|
void CompareConstants(OrderedMap<string, object> data)
|
|
{
|
|
List<object> levels = MiniJson.Arr(data["level_xp"]);
|
|
Check(levels.Select(MiniJson.Int).SequenceEqual(ContentData.LevelXp), "level XP table");
|
|
Check(MiniJson.Arr(data["talent_levels"]).Select(MiniJson.Int).SequenceEqual(ContentData.TalentLevels), "talent levels");
|
|
var constants = MiniJson.Obj(data["content_constants"]);
|
|
var expected = new Dictionary<string, double>
|
|
{
|
|
{ "HP_PER_LEVEL", LuneContent.HpPerLevel }, { "DAMAGE_PER_LEVEL", LuneContent.DamagePerLevel }, { "XP_PARTICIPATION", LuneContent.XpParticipation },
|
|
{ "XP_PER_ROUND", LuneContent.XpPerRound }, { "XP_RADIUS", LuneContent.XpRadius }, { "ITEM_SLOTS", LuneContent.ItemSlots },
|
|
{ "ITEM_REFUND", LuneContent.ItemRefund }, { "CHAMPION_HP", LuneContent.ChampionHp }, { "CHAMPION_DAMAGE", LuneContent.ChampionDamage },
|
|
{ "CHAMPION_BOUNTY", LuneContent.ChampionBounty }, { "CHAMPION_BOUNTY_PER_ROUND", LuneContent.ChampionBountyPerRound },
|
|
{ "MAX_BOOKINGS", LuneContent.MaxBookings }, { "BOSS_GROTTO_HP", LuneContent.BossGrottoHp }, { "BOSS_GROTTO_DAMAGE", LuneContent.BossGrottoDamage },
|
|
{ "BOSS_DEPLOY_HP", LuneContent.BossDeployHp }, { "BOSS_DEPLOY_DAMAGE", LuneContent.BossDeployDamage }, { "BOSS_BOUNTY", LuneContent.BossBounty },
|
|
{ "BOSS_BOUNTY_PER_ROUND", LuneContent.BossBountyPerRound }, { "BOSS_LOCK_ROUNDS", LuneContent.BossLockRounds }, { "BOSS_XP", LuneContent.BossXp },
|
|
{ "BUILDING_XP", LuneContent.BuildingXp },
|
|
};
|
|
var wrong = expected.Where(p => MiniJson.Num(constants[p.Key]) != p.Value).Select(p => p.Key).ToList();
|
|
Check(wrong.Count == 0 && MiniJson.Str(constants["BOSS_MODEL"]) == LuneContent.BossModel, "content constants " + string.Join(", ", wrong));
|
|
Check(MiniJson.Obj(data["match_roles"]).Keys.SequenceEqual(ContentData.MatchRoles.Keys)
|
|
&& MiniJson.Obj(data["match_roles"]).All(p => ContentData.MatchRoles[p.Key] == MiniJson.Str(p.Value)), "troop roles in reference order");
|
|
Check(MiniJson.Obj(data["hero_roles"]).All(p => ContentData.HeroRoles[p.Key] == MiniJson.Str(p.Value)), "hero roles");
|
|
Check(MiniJson.Arr(data["row_order"]).Select(MiniJson.Str).SequenceEqual(ContentData.RowOrder), "formation row order");
|
|
Check(MiniJson.Arr(data["bot_names"]).Select(MiniJson.Str).SequenceEqual(ContentData.BotNames), "bot names");
|
|
Check(MiniJson.Arr(data["waves_melee"]).Select(MiniJson.Str).SequenceEqual(ContentData.WavesMelee), "melee monster types");
|
|
var bot = MiniJson.Obj(data["bot_constants"]);
|
|
Check(MiniJson.Num(bot["BOSS_BAR"]) == LuneBot.BossBar && MiniJson.Num(bot["BOSS_BAR_STEP"]) == LuneBot.BossBarStep, "bot boss thresholds");
|
|
}
|
|
|
|
void CompareProfiles(OrderedMap<string, object> expected)
|
|
{
|
|
Check(expected.Keys.SequenceEqual(ContentData.BotProfiles.Keys), "bot profiles in reference order (seed rotation)");
|
|
foreach (KeyValuePair<string, object> pair in expected)
|
|
{
|
|
var entry = MiniJson.Obj(pair.Value);
|
|
BotProfile profile = ContentData.BotProfiles[pair.Key];
|
|
bool same = MiniJson.Num(entry["front"]) == profile.Front && MiniJson.Num(entry["melee"]) == profile.Melee && MiniJson.Num(entry["ranged"]) == profile.Ranged
|
|
&& MiniJson.Num(entry["splash"]) == profile.Splash && MiniJson.Num(entry["support"]) == profile.Support
|
|
&& MiniJson.Int(entry["upgrade_from"]) == profile.UpgradeFrom && MiniJson.Num(entry["support_lane"]) == profile.SupportLane
|
|
&& MiniJson.Str(entry["hero"]) == profile.Hero && MiniJson.Int(entry["hero_round"]) == profile.HeroRound
|
|
&& MiniJson.Arr(entry["items"]).Select(MiniJson.Str).SequenceEqual(profile.Items)
|
|
&& MiniJson.Arr(entry["talents"]).Select(MiniJson.Int).SequenceEqual(profile.TalentChoices)
|
|
&& MiniJson.Str(entry["champion"]) == profile.Champion
|
|
&& (entry.ContainsKey("boss_from") ? MiniJson.Int(entry["boss_from"]) == profile.BossFrom && MiniJson.Int(entry["boss_troops"]) == profile.BossTroops : !profile.HuntsBoss);
|
|
Check(same, "bot profile " + pair.Key);
|
|
}
|
|
}
|
|
|
|
void CompareBalance(OrderedMap<string, object> expected)
|
|
{
|
|
LuneBalance balance = LuneBalance.Default();
|
|
var actual = new Dictionary<string, object>
|
|
{
|
|
{ "sim_step", balance.SimStep }, { "combat_time_limit", balance.CombatTimeLimit }, { "march_speed_factor", balance.MarchSpeedFactor },
|
|
{ "aggro_radius", balance.AggroRadius }, { "leash_radius", balance.LeashRadius }, { "lane_half_width", balance.LaneHalfWidth },
|
|
{ "start_gold", balance.StartGold }, { "base_income", balance.BaseIncome }, { "income_per_round", balance.IncomePerRound },
|
|
{ "income_growth_cap", balance.IncomeGrowthCap }, { "supply_cap", balance.SupplyCap }, { "sell_refund", balance.SellRefund },
|
|
{ "lane_share", balance.LaneShare }, { "wave_base_count", balance.WaveBaseCount }, { "wave_count_every", balance.WaveCountEvery },
|
|
{ "wave_max_count", balance.WaveMaxCount }, { "wave_hp_growth", balance.WaveHpGrowth }, { "wave_damage_growth", balance.WaveDamageGrowth },
|
|
{ "wave_growth_quadratic", balance.WaveGrowthQuadratic }, { "monster_bounty_scale", balance.MonsterBountyScale },
|
|
{ "bounty_growth", balance.BountyGrowth }, { "tower_outer_hp", balance.TowerOuterHp }, { "tower_inner_hp", balance.TowerInnerHp },
|
|
{ "base_tower_hp", balance.BaseTowerHp }, { "core_hp", balance.CoreHp }, { "tower_damage", balance.TowerDamage },
|
|
{ "base_tower_damage", balance.BaseTowerDamage }, { "tower_range", balance.TowerRange }, { "tower_attack_interval", balance.TowerAttackInterval },
|
|
{ "building_radius", balance.BuildingRadius }, { "bounty_outer_tower", balance.BountyOuterTower }, { "bounty_inner_tower", balance.BountyInnerTower },
|
|
{ "bounty_base_tower", balance.BountyBaseTower }, { "inner_tower_fraction", balance.InnerTowerFraction },
|
|
{ "outer_tower_fraction", balance.OuterTowerFraction }, { "tower_side_offset", balance.TowerSideOffset },
|
|
};
|
|
var problems = new List<string>();
|
|
foreach (KeyValuePair<string, object> pair in expected)
|
|
{
|
|
if (pair.Key == "monster_pool")
|
|
{
|
|
if (!MiniJson.Arr(pair.Value).Select(MiniJson.Str).SequenceEqual(balance.MonsterPool))
|
|
problems.Add(pair.Key);
|
|
continue;
|
|
}
|
|
object value;
|
|
if (!actual.TryGetValue(pair.Key, out value))
|
|
{
|
|
problems.Add("missing " + pair.Key);
|
|
continue;
|
|
}
|
|
if (MiniJson.Num(pair.Value) != Convert.ToDouble(value))
|
|
problems.Add(pair.Key);
|
|
}
|
|
Check(problems.Count == 0, "balance defaults match lune_balance.gd " + string.Join(", ", problems));
|
|
}
|
|
|
|
void CompareMap(OrderedMap<string, object> lanes, List<object> buildings)
|
|
{
|
|
var map = new LuneMap();
|
|
double worst = 0.0;
|
|
foreach (string lane in LuneMap.Lanes)
|
|
{
|
|
var expected = MiniJson.Obj(lanes[lane]);
|
|
LanePath path = map.LanePaths[lane];
|
|
List<object> points = MiniJson.Arr(expected["points"]);
|
|
bool countOk = points.Count == path.Points.Length;
|
|
double deviation = Math.Abs(MiniJson.Num(expected["length"]) - path.Length);
|
|
for (int i = 0; countOk && i < points.Count; i++)
|
|
{
|
|
List<object> xy = MiniJson.Arr(points[i]);
|
|
deviation = Math.Max(deviation, Math.Max(Math.Abs(MiniJson.Num(xy[0]) - path.Points[i].X), Math.Abs(MiniJson.Num(xy[1]) - path.Points[i].Y)));
|
|
}
|
|
worst = Math.Max(worst, deviation);
|
|
Check(countOk && deviation <= GeometryTolerance, string.Format("{0} lane polyline and length match ({1} points, deviation {2:E2})", lane, path.Points.Length, deviation));
|
|
}
|
|
bool buildingsOk = buildings.Count == map.Buildings.Count;
|
|
double buildingWorst = 0.0;
|
|
for (int i = 0; buildingsOk && i < buildings.Count; i++)
|
|
{
|
|
var expected = MiniJson.Obj(buildings[i]);
|
|
BuildingDef actual = map.Buildings[i];
|
|
List<object> position = MiniJson.Arr(expected["position"]);
|
|
buildingsOk = MiniJson.Str(expected["id"]) == actual.Id && MiniJson.Str(expected["tier"]) == actual.Tier && MiniJson.Str(expected["lane"]) == actual.Lane
|
|
&& MiniJson.Num(expected["max_hp"]) == actual.MaxHp && MiniJson.Int(expected["bounty"]) == actual.Bounty;
|
|
buildingWorst = Math.Max(buildingWorst, Math.Abs(MiniJson.Num(expected["progress"]) - actual.Progress));
|
|
buildingWorst = Math.Max(buildingWorst, Math.Max(Math.Abs(MiniJson.Num(position[0]) - actual.Position.X), Math.Abs(MiniJson.Num(position[1]) - actual.Position.Y)));
|
|
}
|
|
Check(buildingsOk && buildingWorst <= GeometryTolerance, string.Format("18 buildings in reference order, positions match (deviation {0:E2})", buildingWorst));
|
|
Note(string.Format("map geometry: largest deviation lanes {0:E2} m, buildings {1:E2} m", worst, buildingWorst));
|
|
}
|
|
}
|
|
}
|