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:
@@ -73,6 +73,17 @@ namespace ProjectM.Server
|
||||
structurePositions.Add(sx.ValueRO.Position);
|
||||
structureRegions.Add(sr.ValueRO.Region);
|
||||
}
|
||||
// LANTERN decoy-wisp aggro: a decoy draws Husks like a fake target. Region-agnostic gather (gym).
|
||||
var decoyEntities = new NativeList<Entity>(Allocator.Temp);
|
||||
var decoyPositions = new NativeList<float3>(Allocator.Temp);
|
||||
foreach (var (dx, dh, de) in
|
||||
SystemAPI.Query<RefRO<LocalTransform>, RefRO<Health>>()
|
||||
.WithAll<DecoyTag>().WithEntityAccess())
|
||||
{
|
||||
if (dh.ValueRO.Current <= 0f) continue; // a spent decoy is being despawned; don't aggro a corpse
|
||||
decoyEntities.Add(de);
|
||||
decoyPositions.Add(dx.ValueRO.Position);
|
||||
}
|
||||
|
||||
// END-1: the Engine Core is a FALLBACK target. When no living player/structure remains, undefended
|
||||
// Husks march on the base heart (PlotCenter) so the base can be overrun instead of the swarm idling.
|
||||
@@ -80,7 +91,7 @@ namespace ProjectM.Server
|
||||
&& SystemAPI.TryGetSingleton<CoreIntegrity>(out var coreInteg) && coreInteg.Current > 0;
|
||||
float3 corePos = coreAlive ? BaseGridMath.PlotCenter(SystemAPI.GetSingleton<BaseAnchor>()) : float3.zero;
|
||||
|
||||
if (playerEntities.Length == 0 && structureEntities.Length == 0 && !coreAlive)
|
||||
if (playerEntities.Length == 0 && structureEntities.Length == 0 && decoyEntities.Length == 0 && !coreAlive)
|
||||
{
|
||||
playerEntities.Dispose();
|
||||
playerPositions.Dispose();
|
||||
@@ -88,12 +99,15 @@ namespace ProjectM.Server
|
||||
structureEntities.Dispose();
|
||||
structurePositions.Dispose();
|
||||
structureRegions.Dispose();
|
||||
decoyEntities.Dispose();
|
||||
decoyPositions.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
float dt = SystemAPI.Time.DeltaTime;
|
||||
var serverTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
|
||||
uint now = serverTick.TickIndexForValidTick;
|
||||
float decoyAggroSq = 30f * 30f; // LANTERN decoy-wisp aggro radius (world units, squared)
|
||||
// Live feel knobs (MC-0): one read, guarded at use. Server-only — clients never simulate enemies.
|
||||
var tune = SystemAPI.TryGetSingleton<TuningConfig>(out var tcfg) ? tcfg : TuningConfig.Defaults();
|
||||
float structAggro = math.max(0f, tune.StructureAggroWeight);
|
||||
@@ -147,12 +161,30 @@ namespace ProjectM.Server
|
||||
// EB-1 fortress aggro: nearest of players (weight 1) + structures (StructureAggroWeight) — a wall/
|
||||
// turret is the preferred target unless a player is in the way (closer after weighting).
|
||||
EnemyAIMath.PickWeightedNearest(pos, playerPositions, playerRegions, structurePositions, structureRegions, huskRegion, structAggro, out bool tgtIsStruct, out int tgtIdx);
|
||||
if (tgtIdx < 0 && !huskCoreAlive)
|
||||
continue; // no player/structure and no Core -> nothing to seek
|
||||
Entity targetEntity = tgtIdx < 0 ? Entity.Null
|
||||
: (tgtIsStruct ? structureEntities[tgtIdx] : playerEntities[tgtIdx]);
|
||||
float3 targetPos = tgtIdx < 0 ? corePos
|
||||
: (tgtIsStruct ? structurePositions[tgtIdx] : playerPositions[tgtIdx]);
|
||||
// Decoy aggro (LANTERN): a decoy-wisp within range is the PREFERRED target -- overrides player/structure/core.
|
||||
int decoyIdx = -1;
|
||||
float decoyBestSq = decoyAggroSq;
|
||||
for (int di = 0; di < decoyPositions.Length; di++)
|
||||
{
|
||||
float dsq = math.distancesq(pos.xz, decoyPositions[di].xz);
|
||||
if (dsq <= decoyBestSq) { decoyBestSq = dsq; decoyIdx = di; }
|
||||
}
|
||||
Entity targetEntity;
|
||||
float3 targetPos;
|
||||
if (decoyIdx >= 0)
|
||||
{
|
||||
targetEntity = decoyEntities[decoyIdx];
|
||||
targetPos = decoyPositions[decoyIdx];
|
||||
}
|
||||
else
|
||||
{
|
||||
if (tgtIdx < 0 && !huskCoreAlive)
|
||||
continue; // no decoy, no player/structure, and no Core -> nothing to seek
|
||||
targetEntity = tgtIdx < 0 ? Entity.Null
|
||||
: (tgtIsStruct ? structureEntities[tgtIdx] : playerEntities[tgtIdx]);
|
||||
targetPos = tgtIdx < 0 ? corePos
|
||||
: (tgtIsStruct ? structurePositions[tgtIdx] : playerPositions[tgtIdx]);
|
||||
}
|
||||
|
||||
// Seek: stop just inside strike range so the Husk holds position to attack.
|
||||
float stopDistance = stats.ValueRO.AttackRange * 0.9f;
|
||||
@@ -665,6 +697,8 @@ namespace ProjectM.Server
|
||||
structureEntities.Dispose();
|
||||
structurePositions.Dispose();
|
||||
structureRegions.Dispose();
|
||||
decoyEntities.Dispose();
|
||||
decoyPositions.Dispose();
|
||||
}
|
||||
|
||||
// Swept collide-and-slide for server-authoritative enemy movement — delegates to the shared
|
||||
|
||||
Reference in New Issue
Block a user