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:
@@ -0,0 +1,99 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.NetCode;
|
||||
using Unity.Transforms;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// SERVER-ONLY Harpooner reel driver. For every Husk carrying an active <see cref="ReelState"/> (stamped by
|
||||
/// <see cref="ProjectileDamageSystem"/> on a Reel-flagged HookPull hit), it re-aims the Husk's
|
||||
/// <see cref="KnockbackState"/> toward the CASTER's CURRENT position every tick — so the pull HOMES (unlike
|
||||
/// the frozen-Dir pull flag). <see cref="EnemyAISystem"/> then DRIVES that re-aimed knockback, staying the
|
||||
/// SOLE writer of the Husk's Position; this system writes only KnockbackState.Dir/Speed/UntilTick (never a
|
||||
/// position). Runs in the plain <see cref="SimulationSystemGroup"/> AFTER the predicted group (so THIS tick's
|
||||
/// stamp is visible) and BEFORE <see cref="EnemyAISystem"/>. Releases (clears both states) when the target is
|
||||
/// within release range, the leash (<see cref="ReelState.ExpireTick"/>) elapses, or the caster is gone.
|
||||
/// Bosses never reel (they don't carry KnockbackState/ReelState — knockback-immune, A4). Ticks via
|
||||
/// <see cref="TickUtil.NonZero"/>; compared with <see cref="NetworkTick"/> only.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateInGroup(typeof(SimulationSystemGroup))]
|
||||
[UpdateAfter(typeof(PredictedSimulationSystemGroup))]
|
||||
[UpdateBefore(typeof(EnemyAISystem))]
|
||||
public partial struct ReelSystem : ISystem
|
||||
{
|
||||
const float k_ReleaseDistance = 1.6f; // stop reeling once the target is this close to the caster (world units)
|
||||
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<NetworkTime>();
|
||||
state.RequireForUpdate(state.GetEntityQuery(ComponentType.ReadOnly<ReelState>()));
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var serverTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
|
||||
if (!serverTick.IsValid) return;
|
||||
uint now = serverTick.TickIndexForValidTick;
|
||||
|
||||
// Snapshot living players by NetworkId (<=4 in co-op; linear lookup is cheaper than a hashmap).
|
||||
var casterIds = new NativeList<int>(Allocator.Temp);
|
||||
var casterPos = new NativeList<float3>(Allocator.Temp);
|
||||
foreach (var (owner, lt, hp) in
|
||||
SystemAPI.Query<RefRO<GhostOwner>, RefRO<LocalTransform>, RefRO<Health>>().WithAll<PlayerTag>())
|
||||
{
|
||||
if (hp.ValueRO.Current <= 0f) continue;
|
||||
casterIds.Add(owner.ValueRO.NetworkId);
|
||||
casterPos.Add(lt.ValueRO.Position);
|
||||
}
|
||||
|
||||
float releaseSq = k_ReleaseDistance * k_ReleaseDistance;
|
||||
|
||||
foreach (var (reel, knockback, lt) in
|
||||
SystemAPI.Query<RefRW<ReelState>, RefRW<KnockbackState>, RefRO<LocalTransform>>()
|
||||
.WithAll<EnemyTag>().WithNone<Dying>())
|
||||
{
|
||||
uint expire = reel.ValueRO.ExpireTick;
|
||||
bool active = expire != 0u && new NetworkTick(expire).IsNewerThan(serverTick);
|
||||
if (!active)
|
||||
{
|
||||
// Leash elapsed (or never reeling): release so EnemyAISystem resumes seek next tick.
|
||||
if (expire != 0u) { reel.ValueRW.ExpireTick = 0u; knockback.ValueRW.UntilTick = 0u; }
|
||||
continue;
|
||||
}
|
||||
|
||||
int idx = -1;
|
||||
for (int i = 0; i < casterIds.Length; i++)
|
||||
if (casterIds[i] == reel.ValueRO.CasterNetworkId) { idx = i; break; }
|
||||
if (idx < 0) { reel.ValueRW.ExpireTick = 0u; knockback.ValueRW.UntilTick = 0u; continue; } // caster gone
|
||||
|
||||
float3 pos = lt.ValueRO.Position;
|
||||
float2 to = casterPos[idx].xz - pos.xz;
|
||||
if (math.lengthsq(to) <= releaseSq)
|
||||
{
|
||||
// Arrived at the caster: release (don't jitter on top of the player).
|
||||
reel.ValueRW.ExpireTick = 0u;
|
||||
knockback.ValueRW.UntilTick = 0u;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Home: re-aim the knockback toward the caster's LIVE position for exactly this tick; EnemyAISystem
|
||||
// applies it. Re-stamped every tick while reeling so it tracks the caster as they move.
|
||||
float2 dir = math.normalize(to);
|
||||
knockback.ValueRW.Dir = dir;
|
||||
knockback.ValueRW.Speed = reel.ValueRO.Speed;
|
||||
knockback.ValueRW.UntilTick = TickUtil.NonZero(now + 1u);
|
||||
}
|
||||
|
||||
casterIds.Dispose();
|
||||
casterPos.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user