Boss becomes a real fight + readable-but-fair enemy threat

BossState (server-only) + BossAISystem: sole mover, seek -> telegraphed
radial slam (AttackWindup) -> phase-2 speed + swarmer summon. Excluded from
the EnemyAISystem Charger pass; knockback-immune at all 3 stamp sites;
arena-anchored spawn; scaled Health/HitRadius/AttackRange. Grunt windup now
commits in its last ~30% (whiffs out-of-range); Charger stagger roots; grunt
speed 4.2->5.2; swarmer telegraph synced. SweptMove + zone-enemy spawn-stamp
extracted to shared helpers. Zero new [GhostField].

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-05 20:21:18 -07:00
parent c6b7890212
commit 3995af736c
16 changed files with 456 additions and 70 deletions
@@ -43,6 +43,11 @@ namespace ProjectM.Server
/// <summary>RW lookup to stamp server-only knockback on a hit Husk (Husks bake KnockbackState; players/dummies don't).</summary>
ComponentLookup<KnockbackState> m_KnockbackLookup;
/// <summary>Read-only lookup so a BOSS (BossState) is skipped by the knockback stamp — the boss is
/// knockback-immune (A4) so a solo player can't perma-stunlock it out of its slam wind-ups.</summary>
ComponentLookup<BossState> m_BossLookup;
/// <summary>Extra forgiveness added to a target's hit radius to approximate the projectile's own size.</summary>
const float k_ProjectileRadius = 0.2f;
@@ -51,6 +56,8 @@ namespace ProjectM.Server
{
m_GhostOwnerLookup = state.GetComponentLookup<GhostOwner>(isReadOnly: true);
m_KnockbackLookup = state.GetComponentLookup<KnockbackState>(isReadOnly: false);
m_BossLookup = state.GetComponentLookup<BossState>(isReadOnly: true);
// No projectiles → nothing to expire or hit-test; skip the tick (and its allocations) entirely.
state.RequireForUpdate<Projectile>();
@@ -61,6 +68,8 @@ namespace ProjectM.Server
{
m_GhostOwnerLookup.Update(ref state);
m_KnockbackLookup.Update(ref state);
m_BossLookup.Update(ref state);
bool haveTick = SystemAPI.TryGetSingleton<NetworkTime>(out var nt);
float dt = SystemAPI.Time.DeltaTime;
@@ -133,7 +142,7 @@ namespace ProjectM.Server
SourceTick = haveTick ? TickUtil.NonZero(nt.ServerTick.TickIndexForValidTick) : 0u,
});
var hitTarget = targetEntities[bestIdx];
if (haveTick && Tuning.KnockbackSpeed > 0f && m_KnockbackLookup.HasComponent(hitTarget))
if (haveTick && Tuning.KnockbackSpeed > 0f && m_KnockbackLookup.HasComponent(hitTarget) && !m_BossLookup.HasComponent(hitTarget))
{
m_KnockbackLookup[hitTarget] = new KnockbackState
{