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>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
using Unity.Entities;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>07-21 G6 (socket honesty, design review wf_98bf1268): SERVER-ONLY scheduled Cone-socket cleave —
|
||||
/// the <see cref="MeleeCleavePending"/> schedule-and-consume idiom applied to the SpecialSlam (Cone archetype)
|
||||
/// socket, so its damage lands at the animation's visual contact instead of the fire tick. Stamped by
|
||||
/// AbilityFireSystem when a Cone socket fires (ResolveTick = fire + TuningConfig.ConeContactTicks; knob 0 =
|
||||
/// legacy immediate, no stamp), fired on a wrap-safe elapsed compare from LIVE state (cast-turn steers the
|
||||
/// cone until contact; stats re-folded at resolve), CONSUMED by zeroing. A still-armed pending is FLUSHED
|
||||
/// (fired early) before a recast overwrites it. Cleared on death (PlayerDeathStateSystem — an armed pending
|
||||
/// must never fire from the respawn position) and by the dev SetClass swap (socket identity changes).
|
||||
/// At resolve the socket is re-validated (bounds + still a Cone Spark) — consume-drop on mismatch.
|
||||
/// Not replicated; client contact cues are presentation-side (CombatFeedbackSystem). Baked zeroed.</summary>
|
||||
public struct ConeContactPending : IComponentData
|
||||
{
|
||||
/// <summary>NonZero tick the armed cone's slam lands; 0 = nothing pending.</summary>
|
||||
public uint ResolveTick;
|
||||
/// <summary>Socket index (0..SocketId.Count-1) of the ARMED cast — stats/aim re-read live at resolve.</summary>
|
||||
public byte Socket;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8e1f7f8852ab4e548babedafccbc6084
|
||||
@@ -1,32 +1,42 @@
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// SERVER-ONLY periodic-AoE state on a player-cast zone ghost (Vortex / LightZone) — the LANTERN Aoe/zone
|
||||
/// archetype. NOT a <c>[GhostField]</c>: the zone is a server-spawned INTERPOLATED ownerless ghost whose
|
||||
/// position replicates via the stock LocalTransform variant; only its damage schedule lives here, server-side.
|
||||
/// <c>ZonePulseSystem</c> borrows the <c>GeyserEruptSystem</c> skeleton (the INVERTED invalid-tick guard so a
|
||||
/// born-0 tick never storm-fires + the <c>= now + period</c> reschedule) but is ENEMY-ONLY (no friendly fire),
|
||||
/// stamps <see cref="DamageEvent.SourceNetworkId"/> = <see cref="CasterNetworkId"/> (kills credit the caster,
|
||||
/// feeding KillRewardSystem — NOT the -1 environment convention), and has NO region gate (a no-world gym).
|
||||
/// DestroyEntity when <see cref="ExpireTick"/> elapses.
|
||||
/// Periodic-AoE state on a player-cast zone ghost (Vortex / LightZone) — the LANTERN Aoe/zone archetype.
|
||||
/// The zone is a server-spawned INTERPOLATED ownerless ghost; position replicates via the stock LocalTransform
|
||||
/// variant. 07-21 G6 (design review wf_98bf1268): THREE fields are now <c>[GhostField]</c> so the client can
|
||||
/// draw an HONEST fill telegraph (ZoneTelegraphSystem): <see cref="CasterNetworkId"/> (G3 ownership tint),
|
||||
/// <see cref="Radius"/> (rim = the TRUE folded damage radius) and <see cref="NextTick"/> (fill arrival = the
|
||||
/// damage moment — server mutations propagate to all clients on an ownerless ghost). Damage schedule/amount
|
||||
/// stay server-only. <c>ZonePulseSystem</c> borrows the <c>GeyserEruptSystem</c> skeleton (the INVERTED
|
||||
/// invalid-tick guard so a born-0 tick never storm-fires + the <c>= now + period</c> reschedule) but is
|
||||
/// ENEMY-ONLY (no friendly fire), stamps <see cref="DamageEvent.SourceNetworkId"/> = <see cref="CasterNetworkId"/>
|
||||
/// (kills credit the caster, feeding KillRewardSystem — NOT the -1 environment convention), and has NO region
|
||||
/// gate (a no-world gym). DestroyEntity when <see cref="ExpireTick"/> elapses.
|
||||
/// </summary>
|
||||
public struct ZoneEffect : IComponentData
|
||||
{
|
||||
/// <summary>NetworkId of the casting player (damage/kill attribution).</summary>
|
||||
public int CasterNetworkId;
|
||||
/// <summary>Pulse cadence in ticks — single source for ZonePulseSystem (damage) and ZoneTelegraphSystem
|
||||
/// (the client fill window). Hoisted 07-21 G6 (was ZonePulseSystem.k_PulsePeriodTicks).</summary>
|
||||
public const uint PulsePeriodTicks = 30;
|
||||
|
||||
/// <summary>Planar (XZ) damage radius, world units.</summary>
|
||||
public float Radius;
|
||||
/// <summary>NetworkId of the casting player (damage/kill attribution + the client's mine-vs-ally tint).</summary>
|
||||
[GhostField] public int CasterNetworkId;
|
||||
|
||||
/// <summary>Damage dealt to each living enemy in radius per pulse.</summary>
|
||||
/// <summary>Planar (XZ) damage radius, world units. Replicated so the drawn rim is the TRUE folded radius.</summary>
|
||||
[GhostField(Quantization = 100)] public float Radius;
|
||||
|
||||
/// <summary>Raw next-pulse tick (NonZero). <c>0</c> = unscheduled → lazy-stamped born-correct (GeyserErupt H2
|
||||
/// rule, never storm-fires). Replicated for the client fill: ride the ABSOLUTE tick + a value-latch + a
|
||||
/// was-counting-down arm-guard (the Geyser ★ rule — never edge-detect the re-stamp).</summary>
|
||||
[GhostField] public uint NextTick;
|
||||
|
||||
/// <summary>Damage dealt to each living enemy in radius per pulse. Server-only.</summary>
|
||||
public float DamagePerPulse;
|
||||
|
||||
/// <summary>Raw next-pulse tick (NonZero). <c>0</c> = unscheduled → lazy-stamped born-correct (GeyserErupt H2 rule, never storm-fires).</summary>
|
||||
public uint NextTick;
|
||||
|
||||
/// <summary>Raw tick the zone despawns (NonZero). Active while .IsNewerThan(serverTick).</summary>
|
||||
/// <summary>Raw tick the zone despawns (NonZero). Active while .IsNewerThan(serverTick). Server-only.</summary>
|
||||
public uint ExpireTick;
|
||||
|
||||
/// <summary>See <see cref="ZoneEffectFlag"/>. bit0 = Vortex (also pull enemies toward the zone center each pulse).</summary>
|
||||
|
||||
@@ -35,12 +35,22 @@ namespace ProjectM.Simulation
|
||||
public static bool FireActive(uint nextFireRaw, int cooldownTicks, NetworkTick serverTick, uint animTicks)
|
||||
{
|
||||
if (nextFireRaw == 0u || cooldownTicks <= 0 || !serverTick.IsValid) return false;
|
||||
uint startRaw = TickUtil.NonZero(nextFireRaw - (uint)cooldownTicks);
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user