Phase 1 B1/B2/B4-B7: separation, poise, boss lunge, party HP scale, run-failed banner, fire anim

B1 SEPARATION: pairwise soft-collision pass INSIDE EnemyAISystem (sole
enemy-Position writer preserved; O(n^2) fine at MaxAlive<=14). Knocked/
lunging enemies keep committed motion but still push neighbours; the boss is
never pushed; enemies yield a small personal radius around players (below
melee range per review B1-1); displacement goes through the swept move so
nothing shoves through walls. SeparationMaxSpeed = live knob.

B2 POISE: the windup-cancel lived in the knockback CONTINUE branches (grunt/
charger/spitter). Threshold on the EXISTING KnockbackState.Speed channel:
light melee (6) nudges without interrupting; the finisher (10.8) and cone
(8) stagger as before. StaggerKnockbackSpeed = live knob (default 7).

B4 BOSS LUNGE: a telegraphed gap-closer on its own cooldown when the target
sits outside slam reach (repositioning - the slam stays the damage beat).
BossState.PendingAttack (server-only byte) disambiguates the shared windup
elapse (review-confirmed: naive reuse would SLAM on a lunge elapse);
LungeState.UntilTick spans windup+travel so the existing IsLunging ghost bit
replicates the tell, and the client draws a travel wedge instead of the slam
ring while it is set.

B5 PARTY HP SCALE: boss Health x(1 + 0.75/extra LIVING expedition player) at
spawn (not RunParticipant - dead-respawned members park at base). Max is a
GhostField so the bar stays truthful.

B6 RUN-FAILED BANNER: silent wipes used to land players home with zero
explanation. Client detects the (in-run)->Staging edge with a launch-cached
Charge (NEVER Returning - a 1-tick transient that precedes the bank by a
tick) and shows EXPEDITION FAILED for 6 s.

B7 FIRE ANIM: the class ability finally moves the body - a stateless window
from replicated AbilityCooldown.NextFireTick minus derived CooldownTicks
(works for predicted local + interpolated remotes, no cached edges).

456/456 EditMode; live Play smoke: launch -> 5 kills credit the room clear
through the corpse window -> boon window opens -> corpses expire clean, no
exceptions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 18:50:49 -07:00
parent 3836e9c842
commit ea108e48e4
9 changed files with 269 additions and 17 deletions
@@ -171,9 +171,16 @@ namespace ProjectM.Server
ecb.SetComponent(enemy, bxform);
if (SystemAPI.HasComponent<Health>(prefab))
{
// B5: party-size HP scaling by LIVING EXPEDITION players at spawn (NOT the
// RunParticipant count - dead-respawned members keep the tag while parked at
// base). Health.Max is a [GhostField] since DR-046, so the scaled max replicates.
int livingParty = 0;
foreach (var (pHealth, pRegion) in SystemAPI.Query<RefRO<Health>, RefRO<RegionTag>>().WithAll<PlayerTag>())
if (pHealth.ValueRO.Current > 0f && pRegion.ValueRO.Region == RegionId.Expedition) livingParty++;
float partyScale = 1f + Tuning.BossHealthPerExtraPlayer * math.max(0, livingParty - 1);
var hp = SystemAPI.GetComponent<Health>(prefab);
hp.Current *= Tuning.BossHealthMultiplier;
hp.Max *= Tuning.BossHealthMultiplier;
hp.Current *= Tuning.BossHealthMultiplier * partyScale;
hp.Max *= Tuning.BossHealthMultiplier * partyScale;
ecb.SetComponent(enemy, hp);
}
if (SystemAPI.HasComponent<HitRadius>(prefab))