champions-of-luneraria/Assets/Luneraria/Core/Simulation/LuneUnit.cs
Martin Stickelbroeck 7b86973197 S4 Feinschliff: Betäubung sichtbar, Nebel im Gelände, Glyphe für Gäste
Betäubungs-Ring über dem Kopf (Gäste über Bit 18 der Rahmen-Flags),
Nebel-Gitter LuneFogOverlay mit Shader Luneraria/FogOverlay aus
LuneFog.Vision (10 Hz, weiche Kante), laufende Glyphe je Team als vier
Zusatzwerte nach den Gebäude-Lebenspunkten. Reine Darstellung.
Kern 627/627 (s4 8), EditMode 35/35, PlayMode 20/20, Verify 718/0,
Selbsttest 30/30, Golden 0, Fingerabdruck unverändert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:55:42 +02:00

278 lines
11 KiB
C#

using System;
using System.Collections.Generic;
namespace Luneraria.Core
{
/// <summary>Timed effect of one family (e.g. "root_grasp"): kind, strength, time left.</summary>
public sealed class Effect
{
/// <summary>movement_slow, attack_slow, attack_speed, damage, reduction, healing_reduction.</summary>
public string Kind = "";
public double Amount;
public double Remaining;
}
/// <summary>Damage over time of one family (e.g. "burn"): ticks once per second.</summary>
public sealed class DamageOverTime
{
public double Damage;
public double Remaining;
public double NextTick = 1.0;
public int AttackType;
}
/// <summary>
/// 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.
/// </summary>
public sealed class LuneUnit
{
/// <summary>Attack type (row) against armour type (column): Klinge, Durchschlag, Impuls.</summary>
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", "camp" };
/// <summary>Sequential per combat, assigned in spawn order (deterministic).</summary>
public int Id;
/// <summary>Spawn index within the own team; mirrored units share it (fair staggering).</summary>
public int TeamIndex;
/// <summary>troop, hero, monster, champion, boss, tower or core.</summary>
public string Kind = "troop";
public int Team;
public int OwnerSlot = -1;
public string PersistentUid = "";
public string Lane = "mid";
/// <summary>lane or grotto.</summary>
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<string, Effect> Effects = new OrderedMap<string, Effect>();
public readonly OrderedMap<string, DamageOverTime> Dots = new OrderedMap<string, DamageOverTime>();
public double AbilityReady = 6.0;
public bool ReachedBase;
/// <summary>The grotto boss waits while two expeditions still fight each other.</summary>
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;
/// <summary>Set for the one step of a leap: the unit lands next to this target.</summary>
public LuneUnit LeapTarget;
/// <summary>The first hit after a leap deals extra damage.</summary>
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;
/// <summary>S4: stunned as reported by the host's frame (guests have no effects); presentation only.</summary>
public bool NetStunned;
/// <summary>Stunned for the view: the own effect on the host, the frame flag on a guest.</summary>
public bool ShowsStun() { return NetStunned || Stunned(); }
public double HitFlash;
public bool InRange;
public double DeathTime = -1.0;
public bool IsBuilding() { return Kind == "tower" || Kind == "core"; }
/// <summary>Buildings and S3 camp guardians stay where they are and only fight in reach.</summary>
public bool IsStationary() { return IsBuilding() || Kind == "camp"; }
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()];
// H2: a unit marked by Brunhild's spear takes more damage ("vulnerable"; 0 for everyone else).
return value * (1.0 - DamageReduction()) * (1.0 + StrongestEffect("vulnerable"));
}
/// <summary>Applies an already mitigated amount through the shield.</summary>
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;
}
}
/// <summary>Buildings ignore crowd control and buffs; an existing family keeps the stronger values.</summary>
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<Effect> 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<Effect> 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);
}
/// <summary>H2: Brunhild's stun stops attacks and movement for its duration.</summary>
public bool Stunned() { return StrongestEffect("stun") > 0.0; }
public double AttackSpeedFactor()
{
if (Stunned())
return 0.0;
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()
{
if (Stunned())
return 0.0;
return 1.0 - GdMath.Min(0.35, StrongestEffect("movement_slow") * (IsBoss() ? 0.5 : 1.0));
}
/// <summary>Advances effect and shield timers; returns the DoT damage due this step.</summary>
public double AdvanceStatus(double delta)
{
CombatTime += delta;
HitFlash = GdMath.Max(0.0, HitFlash - delta);
var families = new List<string>(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<string>(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;
}
}
}