LANTERN P1 Group B: 5-Spark systems — Aoe/Hitscan dispatch + reel/zone/decoy

Aoe/Hitscan archetype dispatch in AbilityFireSystem (both-worlds cooldown;
server-only ecb.Instantiate — the client-never-instantiates invariant; ZoneEffect
vs DecoyTag chosen by prefab membership). Harpooner REEL as a homing state
(ReelState + ReelSystem re-aim the hit Husk's KnockbackState toward the caster's
live position each tick; EnemyAISystem stays sole Position writer), stamped by
ProjectileDamageSystem on a Reel-flag hit. ZonePulseSystem (enemy-only periodic
AoE, caster attribution, no friendly fire, Vortex pull, born-0 lazy-stamp).
Decoy aggro in EnemyAISystem + DecoySystem despawn. AbilityDefBlob gains
EffectFlags (Reel) + DurationTicks. +3 ZonePulseSystem EditMode tests (497 green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-15 00:00:41 -07:00
parent 8f070ff83c
commit 36950ca21d
24 changed files with 738 additions and 20 deletions
@@ -0,0 +1,54 @@
using Unity.Entities;
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.
/// </summary>
public struct ZoneEffect : IComponentData
{
/// <summary>NetworkId of the casting player (damage/kill attribution).</summary>
public int CasterNetworkId;
/// <summary>Planar (XZ) damage radius, world units.</summary>
public float Radius;
/// <summary>Damage dealt to each living enemy in radius per pulse.</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>
public uint ExpireTick;
/// <summary>See <see cref="ZoneEffectFlag"/>. bit0 = Vortex (also pull enemies toward the zone center each pulse).</summary>
public byte Flags;
}
/// <summary>Bit masks for <see cref="ZoneEffect.Flags"/>.</summary>
public static class ZoneEffectFlag
{
public const byte Vortex = 1;
}
/// <summary>
/// SERVER-ONLY decoy-wisp state (the Aoe/spawn Spark) — a server-spawned INTERPOLATED ownerless ghost that
/// draws Husk aggro. <c>EnemyAISystem</c> gathers DecoyTag entities into its target set so Husks path to and
/// strike the decoy (soaking hits via its Health/DamageEvent buffer); it despawns on <c>Health &lt;= 0</c> or
/// when <see cref="ExpireTick"/> elapses (both handled where EnemyAISystem already iterates decoys). Position
/// replicates via the stock LocalTransform variant — NOT a <c>[GhostField]</c>.
/// </summary>
public struct DecoyTag : IComponentData
{
/// <summary>Raw tick the decoy despawns (NonZero). <c>0</c> = no timeout; active while .IsNewerThan(serverTick).</summary>
public uint ExpireTick;
}
}