Files
Project-M/Assets/_Project/Scripts/Simulation/Combat/TickWindowMath.cs
T
kronic 2aebc37115 G6+G4: Cone damage-at-contact + zone fill telegraph (ZoneEffect GhostFields) + co-op saturation budget
Cone/SpecialSlam damage lands at its visual contact via ConeContactPending (knob 32, 0=legacy;
early-flush + resolve re-validate; death + BOTH class-swap paths drop armed pendings — fixes the
shipped melee death-strand in the same stroke). Zones: [GhostField] Caster/Radius/NextTick +
ZoneTelegraphSystem (Geyser latch contract on InterpolationTick; rim = true radius; arm grows,
persistent phase drains). Cone cues latch to contact (FireStartRaw, C14); TuningConfig.Defaults()
fallback at client cue sites (release-build timing fix). G4: SaturationMath ally-FX degrade
(living-enemy census, solo-exempt; enemy telegraphs structurally exempt) + CombatStressDebug +
overlay saturation rows. Reviews wf_98bf1268 (13 confirmed folded) / wf_9757d214 (5 confirmed fixed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 13:27:50 -07:00

85 lines
5.0 KiB
C#

using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
namespace ProjectM.Simulation
{
/// <summary>
/// Pure tick-window predicates over replicated combat state (MeleeCombo swing window, per-socket
/// cooldown-derived fire windows). Extracted VERBATIM from PlayerAnimationDriveSystem (MC-4/B7) so the
/// predicted facing path (PlayerAimSystem) and the client animation path share one implementation.
/// Window lengths stay CALLER parameters (the anim path passes its k_AttackAnimTicks; the facing path
/// passes its own) — never baked constants. Movement-archetype sockets (dash/blink) NEVER count as
/// firing: a blink is a dodge, not a cast (this also kills the latent cooldown-tail phantom IsFiring
/// pulse the old include-all check produced — the blink socket's stamped cooldown row used to open a
/// fake window ~2.5s after each blink). Burst-safe statics (byte archetypes, no enums), EditMode-tested.
/// </summary>
public static class TickWindowMath
{
/// <summary>True while now is within [SwingStartTick, SwingStartTick + animTicks) — a per-swing pulse
/// that re-triggers on each chained swing. NetworkTick arithmetic (wrap-safe).</summary>
public static bool SwingActive(in MeleeCombo mc, NetworkTick serverTick, uint animTicks)
{
if (mc.SwingStartTick == 0u || !serverTick.IsValid) return false;
var start = new NetworkTick(mc.SwingStartTick);
var end = new NetworkTick(TickUtil.NonZero(mc.SwingStartTick + animTicks));
return start.IsValid && end.IsValid && !start.IsNewerThan(serverTick) && end.IsNewerThan(serverTick);
}
/// <summary>True while now is within the stateless fire window [NextFireTick - CooldownTicks, +animTicks)
/// reconstructed from the replicated per-socket cooldown stamp minus the locally-derived CooldownTicks
/// (recomputes identically on every world; no cached edges — edge-caches false-fire on
/// relevancy/join/rollback). Window start shifts if cooldown modifiers change mid-window — documented
/// acceptable (B7).</summary>
public static bool FireActive(uint nextFireRaw, int cooldownTicks, NetworkTick serverTick, uint animTicks)
{
if (nextFireRaw == 0u || cooldownTicks <= 0 || !serverTick.IsValid) return false;
uint startRaw = FireStartRaw(nextFireRaw, cooldownTicks);
var start = new NetworkTick(startRaw);
var end = new NetworkTick(TickUtil.NonZero(startRaw + animTicks));
return start.IsValid && end.IsValid && !start.IsNewerThan(serverTick) && end.IsNewerThan(serverTick);
}
/// <summary>Reconstructed fire-window START (raw NonZero tick) from the replicated per-socket cooldown
/// stamp minus the locally-derived CooldownTicks; 0 = no window (unstamped / degenerate inputs). The ONE
/// home of this reconstruction (07-21 G6 review wf_98bf1268: the cone connect-cue latches
/// contact = FireStartRaw + ConeContactTicks at the fire edge — never re-derive it inline).</summary>
public static uint FireStartRaw(uint nextFireRaw, int cooldownTicks)
{
if (nextFireRaw == 0u || cooldownTicks <= 0) return 0u;
return TickUtil.NonZero(nextFireRaw - (uint)cooldownTicks);
}
/// <summary>LANTERN 4-socket fire/cone resolution (any-socket model): firing if ANY socketed,
/// NON-Movement Spark's per-socket window is mid-fire; cone if any such active socket holds a
/// Cone-archetype Spark. Movement sockets are skipped OUTRIGHT (blink = dodge, not cast). Without a
/// blob the archetype is unknown — the socket is included (matches the old fallback behavior).</summary>
public static void SocketFireAndCone(in SocketCooldown cd, DynamicBuffer<AbilitySocket> sockets,
DynamicBuffer<EffectiveSocketStats> effSockets, BlobAssetReference<AbilityDatabaseBlob> abilityDb,
NetworkTick serverTick, uint animTicks, out bool firing, out bool cone)
{
firing = false; cone = false;
int n = math.min(SocketId.Count, math.min(sockets.Length, effSockets.Length));
bool haveDb = abilityDb.IsCreated;
for (int sk = 0; sk < n; sk++)
{
byte sid = sockets[sk].SparkId;
if (sid == 0) continue;
bool isCone = false;
if (haveDb)
{
ref var adb = ref abilityDb.Value;
if (adb.TryGetAbility(sid, out var d))
{
if (d.Archetype == (byte)AbilityArchetype.Movement) continue; // dodge, never a cast
isCone = d.Archetype == (byte)AbilityArchetype.Cone;
}
}
if (!FireActive(cd.Get(sk), effSockets[sk].CooldownTicks, serverTick, animTicks)) continue;
firing = true;
if (isCone) cone = true;
}
}
}
}