using System; using System.Collections.Generic; namespace Luneraria.Core { /// /// Heroes, talents, items, attack champions and the boss (reference: lune_content.gd). /// Hero and champion roles are provisional assignments of existing models. /// 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; /// Victory in round n: deployment in n+1, locked in n+1..n+3, open again from n+4. public const int BossLockRounds = 3; public const int BossXp = 220; public const int BuildingXp = 60; public static OrderedMap Heroes { get { return ContentData.Heroes; } } public static OrderedMap Items { get { return ContentData.Items; } } public static OrderedMap 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]; } /// H1: delay before an ultimate may fire in a combat phase. public const double UltimateDelay = 2.0; /// H1: trigger rules of the hero's ultimate (first = default); empty without an ultimate. 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; } /// H1: cosmetic skins of a hero besides the standard look. 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; } /// The rule a hero's ultimate follows: the chosen one if valid, else the default, "" without ultimate. 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; } /// Combat definition of a hero with its level, items and talents. 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") || hero.Talents.Contains("rabenschwinge")) attackSpeed += 0.20; definition.UltimateRule = UltimateRuleFor(hero); definition.Skin = ValidSkin(hero.HeroId, hero.Skin) ? hero.Skin ?? "" : ""; definition.Talents = new List(hero.Talents); definition.DamageBonus = damageBonus; definition.ReductionBonus = reduction; definition.AttackSpeedBonus = GdMath.Min(0.5, attackSpeed); return definition; } /// Attack champion of a round: a send type scaled like the waves of that round. 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; } /// The Schlundkrone: resting in the grotto, or deployed on a lane (with bounty). 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; } } /// /// 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. /// 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 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(); for (int i = 0; i < available; i++) result.Add(pool[i]); return result; } /// Monster definitions for one lane of one team, melee first so they lead. public static List Build(LuneBalance balance, int roundNumber) { List 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(); var ranged = new List(); 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; } } }