champions-of-luneraria/Assets/Luneraria/Core/Content/LuneContent.cs
Martin Stickelbroeck 805f5754c9 H1 Sylvara: Figur, Skin-System, Oberfläche, Nachweise
Meshy-Figur Sylvara (20.709 Dreiecke, Lane-Fassung 4.999) mit zwei Sicheln
als eigene Modelle, Clips leap/ultimate, 116 Credits. Skin-System: hero_skin
als kosmetische Planungswahl, Blutmond als Varianten-Skin (skin_variant.py,
AI_*_Blutmond, Dress mit Skin). Heldenfenster mit Reiter Ultimate & Skin,
Befehlsplatz der Heldenkarte, Mondring. Ultimate zündet nicht im Sprungschritt.
Kern 500/500, EditMode 30/30, PlayMode 15/15, Verify 718/0, Selbsttest 30/30,
Fingerabdrücke unverändert; Balance 50 % wie Ringborke/Funkenlanze; Leistung
gleich der Funkenlanze-Heldin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:06:58 +02:00

241 lines
11 KiB
C#

using System;
using System.Collections.Generic;
namespace Luneraria.Core
{
/// <summary>
/// Heroes, talents, items, attack champions and the boss (reference: lune_content.gd).
/// Hero and champion roles are provisional assignments of existing models.
/// </summary>
public static class LuneContent
{
public const double HpPerLevel = 0.09;
public const double DamagePerLevel = 0.07;
public const int XpParticipation = 40;
public const int XpPerRound = 6;
public const double XpRadius = 12.0;
public const int ItemSlots = 3;
public const double ItemRefund = 0.5;
public const double ChampionHp = 2.2;
public const double ChampionDamage = 1.6;
public const int ChampionBounty = 70;
public const int ChampionBountyPerRound = 6;
public const int MaxBookings = 2;
public const string BossModel = "e10";
public const double BossGrottoHp = 3600.0;
public const double BossGrottoDamage = 55.0;
public const double BossDeployHp = 6800.0;
public const double BossDeployDamage = 88.0;
public const int BossBounty = 520;
public const int BossBountyPerRound = 60;
/// <summary>Victory in round n: deployment in n+1, locked in n+1..n+3, open again from n+4.</summary>
public const int BossLockRounds = 3;
public const int BossXp = 220;
public const int BuildingXp = 60;
public static OrderedMap<string, HeroSpec> Heroes { get { return ContentData.Heroes; } }
public static OrderedMap<string, ItemSpec> Items { get { return ContentData.Items; } }
public static OrderedMap<string, ChampionSpec> Champions { get { return ContentData.Champions; } }
public static int[] LevelXp { get { return ContentData.LevelXp; } }
public static int[] TalentLevels { get { return ContentData.TalentLevels; } }
public static TalentOption[][] TalentsOf(string heroId)
{
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>H1: cosmetic skins of a hero besides the standard look.</summary>
public static TalentOption[] SkinsOf(string heroId)
{
TalentOption[] skins;
return ContentData.HeroSkins.TryGetValue(heroId ?? "", out skins) ? skins : new TalentOption[0];
}
public static bool ValidSkin(string heroId, string skin)
{
if (string.IsNullOrEmpty(skin))
return true;
foreach (TalentOption option in SkinsOf(heroId))
{
if (option.Id == skin)
return true;
}
return false;
}
/// <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;
for (int i = 0; i < ContentData.LevelXp.Length; i++)
{
if (xp >= ContentData.LevelXp[i])
level = i + 1;
}
return level;
}
/// <summary>Combat definition of a hero with its level, items and talents.</summary>
public static UnitDef HeroDefinition(HeroState hero)
{
HeroSpec spec = ContentData.Heroes[hero.HeroId];
UnitDef definition = spec.Base.Clone();
int level = LevelFor(hero.Xp);
definition.Id = spec.Model;
definition.HeroId = hero.HeroId;
definition.DisplayName = spec.Name;
definition.MaxHp = definition.MaxHp * (1.0 + HpPerLevel * (double)(level - 1));
definition.Damage = definition.Damage * (1.0 + DamagePerLevel * (double)(level - 1));
definition.Level = level;
definition.ViewScale = 1.25;
double damageBonus = 0.0;
double reduction = 0.0;
double attackSpeed = 0.0;
foreach (string itemId in hero.Items)
{
ItemSpec item = ContentData.Items[itemId];
definition.MaxHp = definition.MaxHp + item.HpFlat;
damageBonus += item.Damage;
reduction += item.Reduction;
attackSpeed += item.AttackSpeed;
definition.Regen = definition.Regen + item.Regen;
definition.ItemSplash = definition.ItemSplash + item.Splash;
}
if (hero.Talents.Contains("borkenhaut"))
reduction += 0.15;
if (hero.Talents.Contains("fernsicht"))
definition.AttackRange = definition.AttackRange + 1.5;
if (hero.Talents.Contains("glutsalve") || hero.Talents.Contains("neumond"))
attackSpeed += 0.20;
definition.UltimateRule = UltimateRuleFor(hero);
definition.Skin = ValidSkin(hero.HeroId, hero.Skin) ? hero.Skin ?? "" : "";
definition.Talents = new List<string>(hero.Talents);
definition.DamageBonus = damageBonus;
definition.ReductionBonus = reduction;
definition.AttackSpeedBonus = GdMath.Min(0.5, attackSpeed);
return definition;
}
/// <summary>Attack champion of a round: a send type scaled like the waves of that round.</summary>
public static UnitDef ChampionDefinition(string championId, int roundNumber, LuneBalance balance)
{
ChampionSpec spec = ContentData.Champions[championId];
UnitDef definition = DataCatalog.GetSend(spec.Base);
definition.Id = spec.Model;
definition.ChampionId = championId;
definition.DisplayName = spec.Name;
definition.MaxHp = definition.MaxHp * ChampionHp * LuneWaves.Strength(balance, roundNumber, balance.WaveHpGrowth);
definition.Damage = definition.Damage * ChampionDamage * LuneWaves.Strength(balance, roundNumber, balance.WaveDamageGrowth);
definition.Bounty = ChampionBounty + ChampionBountyPerRound * roundNumber;
definition.Priority = spec.Priority;
definition.SiegeFactor = spec.SiegeFactor;
definition.ViewScale = spec.ViewScale;
if (championId == "ch_schutz")
definition.Ability = "ch_schutz";
return definition;
}
/// <summary>The Schlundkrone: resting in the grotto, or deployed on a lane (with bounty).</summary>
public static UnitDef BossDefinition(int roundNumber, LuneBalance balance, bool deployed)
{
UnitDef definition = DataCatalog.GetEnemy(BossModel);
definition.DisplayName = "Schlundkrone";
definition.MaxHp = (deployed ? BossDeployHp : BossGrottoHp) * LuneWaves.Strength(balance, roundNumber, balance.WaveHpGrowth);
definition.Damage = (deployed ? BossDeployDamage : BossGrottoDamage) * LuneWaves.Strength(balance, roundNumber, balance.WaveDamageGrowth);
definition.Bounty = deployed ? BossBounty + BossBountyPerRound * roundNumber : 0;
definition.IsBoss = true;
definition.ViewScale = 1.7;
definition.Speed = 1.6;
return definition;
}
public static int BossBountyFor(int roundNumber)
{
return BossBounty + BossBountyPerRound * roundNumber;
}
}
/// <summary>
/// Endless monster waves (reference: lune_waves.gd). Both teams get the identical composition
/// per lane. The count is capped; strength grows through stats and later monster types.
/// </summary>
public static class LuneWaves
{
public static double Strength(LuneBalance balance, int roundNumber, double growth)
{
double r = (double)(GdMath.Max(1, roundNumber) - 1);
return 1.0 + growth * r + balance.WaveGrowthQuadratic * r * r;
}
public static int MonsterCount(LuneBalance balance, int roundNumber)
{
int extra = (GdMath.Max(1, roundNumber) - 1) / GdMath.Max(1, balance.WaveCountEvery);
return GdMath.Min(balance.WaveMaxCount, balance.WaveBaseCount + extra);
}
public static List<string> UnlockedTypes(LuneBalance balance, int roundNumber)
{
string[] pool = balance.MonsterPool;
int available = GdMath.Clamp(1 + (GdMath.Max(1, roundNumber) - 1) / 2, 1, pool.Length);
var result = new List<string>();
for (int i = 0; i < available; i++)
result.Add(pool[i]);
return result;
}
/// <summary>Monster definitions for one lane of one team, melee first so they lead.</summary>
public static List<UnitDef> Build(LuneBalance balance, int roundNumber)
{
List<string> types = UnlockedTypes(balance, roundNumber);
double hpFactor = Strength(balance, roundNumber, balance.WaveHpGrowth);
double damageFactor = Strength(balance, roundNumber, balance.WaveDamageGrowth);
double bountyFactor = balance.MonsterBountyScale * (1.0 + balance.BountyGrowth * (double)(GdMath.Max(1, roundNumber) - 1));
var melee = new List<UnitDef>();
var ranged = new List<UnitDef>();
int count = MonsterCount(balance, roundNumber);
for (int index = 0; index < count; index++)
{
string typeId = types[(index + roundNumber) % types.Count];
UnitDef source = ContentData.Enemies[typeId];
UnitDef monster = source.Clone();
monster.MaxHp = source.MaxHp * hpFactor;
monster.Damage = source.Damage * damageFactor;
monster.HpFactor = hpFactor;
double bounty = GdMath.Clamp(GdMath.Round((double)source.Bounty * bountyFactor), 1.0, 1.0e12);
// The reference stores a 64-bit int; bounties stay far below int range in any real match.
monster.Bounty = bounty >= int.MaxValue ? int.MaxValue : (int)bounty;
if (Array.IndexOf(ContentData.WavesMelee, typeId) >= 0)
melee.Add(monster);
else
ranged.Add(monster);
}
melee.AddRange(ranged);
return melee;
}
}
}