using System; using System.Collections.Generic; namespace Luneraria.Core { /// Timed effect of one family (e.g. "root_grasp"): kind, strength, time left. public sealed class Effect { /// movement_slow, attack_slow, attack_speed, damage, reduction, healing_reduction. public string Kind = ""; public double Amount; public double Remaining; } /// Damage over time of one family (e.g. "burn"): ticks once per second. public sealed class DamageOverTime { public double Damage; public double Remaining; public double NextTick = 1.0; public int AttackType; } /// /// One combat instance (reference: lune_unit.gd). Persistent ownership stays in LuneMatch; /// this object lives for one combat phase. Damage, heals and effects are queued and applied /// together by LuneCombat, so processing order never decides who strikes first. /// public sealed class LuneUnit { /// Attack type (row) against armour type (column): Klinge, Durchschlag, Impuls. public static readonly double[,] DamageMatrix = { { 1.30, 0.75, 1.00 }, { 1.00, 1.30, 0.75 }, { 0.75, 1.00, 1.30 } }; public static readonly string[] MobileKinds = { "troop", "hero", "monster", "champion", "boss" }; /// Sequential per combat, assigned in spawn order (deterministic). public int Id; /// Spawn index within the own team; mirrored units share it (fair staggering). public int TeamIndex; /// troop, hero, monster, champion, boss, tower or core. public string Kind = "troop"; public int Team; public int OwnerSlot = -1; public string PersistentUid = ""; public string Lane = "mid"; /// lane or grotto. public string Zone = "lane"; public UnitDef Def = new UnitDef(); public string BuildingId = ""; public Vec2 Position; public Vec2 PreviousPosition; public double Facing; public double Hp = 1.0; public double MaxHp = 1.0; public bool Alive = true; public LuneUnit Target; public double Progress; public double Lateral; public double AttackProgress; public int HitCount; public double CombatTime; public double Shield; public double ShieldRemaining = double.PositiveInfinity; public readonly OrderedMap Effects = new OrderedMap(); public readonly OrderedMap Dots = new OrderedMap(); public double AbilityReady = 6.0; public bool ReachedBase; /// The grotto boss waits while two expeditions still fight each other. 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; /// Set for the one step of a leap: the unit lands next to this target. public LuneUnit LeapTarget; /// The first hit after a leap deals extra damage. 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; public bool IsBuilding() { return Kind == "tower" || Kind == "core"; } public bool IsMobile() { return Array.IndexOf(MobileKinds, Kind) >= 0; } public double AttackRange() { return Def.AttackRange; } public double Speed() { return Def.Speed; } public int ArmorType() { return GdMath.Clamp(Def.ArmorType, 0, 2); } public string Ability() { return Def.Ability; } public bool IsBoss() { return Kind == "boss" || Def.IsBoss; } public double Mitigated(double raw, int attackType) { double value = GdMath.Max(0.0, raw) * DamageMatrix[GdMath.Clamp(attackType, 0, 2), ArmorType()]; return value * (1.0 - DamageReduction()); } /// Applies an already mitigated amount through the shield. public void Take(double amount) { if (!Alive || amount <= 0.0) return; double absorbed = GdMath.Min(Shield, amount); Shield -= absorbed; Hp = GdMath.Max(0.0, Hp - (amount - absorbed)); HitFlash = 0.18; } public void Heal(double amount) { if (Alive && !IsBuilding()) Hp = GdMath.Min(MaxHp, Hp + GdMath.Max(0.0, amount) * (1.0 - StrongestEffect("healing_reduction"))); } public void GrantShield(double amount, double duration = double.PositiveInfinity) { if (Alive && amount > Shield) { Shield = amount; ShieldRemaining = duration; } } /// Buildings ignore crowd control and buffs; an existing family keeps the stronger values. public void ApplyEffect(string family, string kind, double amount, double duration) { if (!Alive || IsBuilding()) return; Effect effect; if (Effects.TryGetValue(family, out effect)) { effect.Amount = GdMath.Max(effect.Amount, amount); effect.Remaining = GdMath.Max(effect.Remaining, duration); } else { Effects[family] = new Effect { Kind = kind, Amount = amount, Remaining = duration }; } } public void ApplyDot(string family, double damage, double duration, int attackType) { if (!Alive || IsBuilding()) return; DamageOverTime dot; if (Dots.TryGetValue(family, out dot)) { if (damage > dot.Damage) { dot.Damage = damage; dot.AttackType = attackType; } dot.Remaining = GdMath.Max(dot.Remaining, duration); } else { Dots[family] = new DamageOverTime { Damage = damage, Remaining = duration, NextTick = 1.0, AttackType = attackType }; } } public double StrongestEffect(string kind) { double amount = 0.0; IReadOnlyList values = Effects.Values; for (int i = 0; i < values.Count; i++) { if (values[i].Kind == kind) amount = GdMath.Max(amount, values[i].Amount); } return amount; } public double DamageBonus() { double result = 0.0; IReadOnlyList values = Effects.Values; for (int i = 0; i < values.Count; i++) { if (values[i].Kind == "damage") result += values[i].Amount; } return GdMath.Min(0.30, result) + Def.DamageBonus; } public double DamageReduction() { double result = StrongestEffect("reduction"); switch (Ability()) { case "g03": if (CombatTime < 8.0 - 0.000001) result = GdMath.Max(result, 0.15); break; case "g04": result = GdMath.Max(result, CombatTime < 10.0 - 0.000001 ? 0.25 : 0.10); break; } return GdMath.Min(0.35, result + Def.ReductionBonus); } public double AttackSpeedFactor() { double slowing = StrongestEffect("attack_slow") * (IsBoss() ? 0.5 : 1.0); double bonus = Def.AttackSpeedBonus; return 1.0 + GdMath.Clamp(StrongestEffect("attack_speed") - slowing + bonus, -0.25, 0.5); } public bool HasTalent(string id) { return Def.Talents.Contains(id); } public string Role() { return Def.Role; } public double MovementFactor() { return 1.0 - GdMath.Min(0.35, StrongestEffect("movement_slow") * (IsBoss() ? 0.5 : 1.0)); } /// Advances effect and shield timers; returns the DoT damage due this step. public double AdvanceStatus(double delta) { CombatTime += delta; HitFlash = GdMath.Max(0.0, HitFlash - delta); var families = new List(Effects.Keys); foreach (string family in families) { Effect effect = Effects[family]; effect.Remaining -= delta; if (effect.Remaining <= 0.000001) Effects.Remove(family); } if (!double.IsPositiveInfinity(ShieldRemaining)) { ShieldRemaining -= delta; if (ShieldRemaining <= 0.000001) Shield = 0.0; } double dotDamage = 0.0; var dotFamilies = new List(Dots.Keys); foreach (string family in dotFamilies) { DamageOverTime dot = Dots[family]; dot.NextTick -= GdMath.Min(delta, GdMath.Max(0.0, dot.Remaining)); while (dot.NextTick <= 0.000001) { dotDamage += Mitigated(dot.Damage, dot.AttackType); dot.NextTick += 1.0; } dot.Remaining -= delta; if (dot.Remaining <= 0.000001) Dots.Remove(family); } return dotDamage; } } }