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;
}
}
}