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>
221 lines
10 KiB
C#
221 lines
10 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>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.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;
|
|
}
|
|
}
|
|
}
|