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
@@ -37,11 +37,17 @@ namespace ProjectM.Simulation
[BurstCompile]
public partial struct AbilityFireSystem : ISystem
{
// C3/A4: knockback stamp for the Warrior CONE (guarded HasComponent + boss-immune). Server-only use.
ComponentLookup<KnockbackState> m_KnockbackLookup;
ComponentLookup<BossState> m_BossLookup;
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<AbilityDatabase>();
state.RequireForUpdate<NetworkTime>();
m_KnockbackLookup = state.GetComponentLookup<KnockbackState>(isReadOnly: false);
m_BossLookup = state.GetComponentLookup<BossState>(isReadOnly: true);
}
[BurstCompile]
@@ -63,6 +69,8 @@ namespace ProjectM.Simulation
var abilityDb = SystemAPI.GetSingleton<AbilityDatabase>();
bool isServer = state.WorldUnmanaged.IsServer();
m_KnockbackLookup.Update(ref state);
m_BossLookup.Update(ref state);
// Server-only target set (LIVING enemies/dummies), collected once: positions feed the gamepad
// auto-target assist, and entities+positions feed the Warrior CONE archetype's server-only cleave.
@@ -132,6 +140,19 @@ namespace ProjectM.Simulation
SourceNetworkId = owner.ValueRO.NetworkId,
SourceTick = cStamp,
});
// C3: the cone reads as weak vs the melee cleave without knockback — stamp it like melee
// (guarded: dummies lack KnockbackState → ECB throw; the boss is knockback-immune, A4).
if (m_KnockbackLookup.HasComponent(coneTargets[ci]) && !m_BossLookup.HasComponent(coneTargets[ci]))
{
float3 kd3 = coneTargetPos[ci] - xform.ValueRO.Position;
float2 kdir = math.lengthsq(kd3.xz) > 1e-6f ? math.normalize(kd3.xz) : cFace;
m_KnockbackLookup[coneTargets[ci]] = new KnockbackState
{
Dir = kdir,
Speed = Tuning.KnockbackSpeed,
UntilTick = TickUtil.NonZero(serverTick.TickIndexForValidTick + (uint)math.max(1, Tuning.KnockbackDurationTicks)),
};
}
}
}
uint coneCd = (uint)math.max(1, eff.ValueRO.CooldownTicks);