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
@@ -61,6 +61,11 @@ namespace ProjectM.Simulation
// than a normal one; GoalReachedSystem also math.max(1, ...) at the use-site.
public float FinalSiegeMultiplier;
// Phase 1 B1/B2 (design review wf_fd177263): poise threshold on the EXISTING KnockbackState.Speed channel
// (light melee 6 nudges; finisher 6x1.8=10.8 and cone 8 stagger), and the separation pass's push-speed cap.
public float StaggerKnockbackSpeed;
public float SeparationMaxSpeed;
/// <summary>The baked feel defaults == the pre-MC-0 consts. Single source of truth for the fallback path.</summary>
public static TuningConfig Defaults() => new TuningConfig
{
@@ -88,6 +93,8 @@ namespace ProjectM.Simulation
CoreRegenIntervalTicks = 18f, // END-1: +1 integrity / 0.3s in Calm (~30s to refill 100 from 0)
CoreOverrunDrainPct = 0.5f, // END-1: a breach costs half the shared ledger (soft-loss penalty)
FinalSiegeMultiplier = 2.5f, // END-2: the final siege is ~2.5x the would-be-next normal siege
StaggerKnockbackSpeed = 7f, // B2 poise: kb.Speed >= this interrupts windups/lunges; below = nudge only
SeparationMaxSpeed = 3f, // B1: max separation push (units/s) so soft-collision can't fling
};
/// <summary>Clamp a knob to its safe floor: tick knobs &gt;= 1, value knobs &gt;= 0. Used by every write path
@@ -109,6 +116,8 @@ namespace ProjectM.Simulation
case TuningKnob.StructureAggroWeight:
case TuningKnob.CoreDamagePerHusk:
case TuningKnob.CoreOverrunDrainPct:
case TuningKnob.StaggerKnockbackSpeed:
case TuningKnob.SeparationMaxSpeed:
return math.max(0f, value);
// tick knobs: >= 1 (a 0 tick count is degenerate; a 0 i-frame window divides-by-zero in DashSystem).
// FinalSiegeMultiplier also lands here on purpose — a final siege should never be < 1x a normal one.
@@ -147,6 +156,8 @@ namespace ProjectM.Simulation
case TuningKnob.CoreRegenIntervalTicks: c.CoreRegenIntervalTicks = value; break;
case TuningKnob.CoreOverrunDrainPct: c.CoreOverrunDrainPct = value; break;
case TuningKnob.FinalSiegeMultiplier: c.FinalSiegeMultiplier = value; break;
case TuningKnob.StaggerKnockbackSpeed: c.StaggerKnockbackSpeed = value; break;
case TuningKnob.SeparationMaxSpeed: c.SeparationMaxSpeed = value; break;
// unknown index -> no-op (matches the no-default switch convention in DebugCommandReceiveSystem)
}
}
@@ -180,6 +191,8 @@ namespace ProjectM.Simulation
case TuningKnob.CoreRegenIntervalTicks: return c.CoreRegenIntervalTicks;
case TuningKnob.CoreOverrunDrainPct: return c.CoreOverrunDrainPct;
case TuningKnob.FinalSiegeMultiplier: return c.FinalSiegeMultiplier;
case TuningKnob.StaggerKnockbackSpeed: return c.StaggerKnockbackSpeed;
case TuningKnob.SeparationMaxSpeed: return c.SeparationMaxSpeed;
default: return 0f;
}
}
@@ -211,6 +224,8 @@ namespace ProjectM.Simulation
CoreRegenIntervalTicks = c.CoreRegenIntervalTicks,
CoreOverrunDrainPct = c.CoreOverrunDrainPct,
FinalSiegeMultiplier = c.FinalSiegeMultiplier,
StaggerKnockbackSpeed = c.StaggerKnockbackSpeed,
SeparationMaxSpeed = c.SeparationMaxSpeed,
};
/// <summary>Reconstruct the full config from a wire snapshot (FULL state, not a delta).</summary>
@@ -240,6 +255,8 @@ namespace ProjectM.Simulation
CoreRegenIntervalTicks = r.CoreRegenIntervalTicks,
CoreOverrunDrainPct = r.CoreOverrunDrainPct,
FinalSiegeMultiplier = r.FinalSiegeMultiplier,
StaggerKnockbackSpeed = r.StaggerKnockbackSpeed,
SeparationMaxSpeed = r.SeparationMaxSpeed,
};
}
@@ -270,9 +287,11 @@ namespace ProjectM.Simulation
public const byte CoreRegenIntervalTicks = 21;
public const byte CoreOverrunDrainPct = 22;
public const byte FinalSiegeMultiplier = 23;
public const byte StaggerKnockbackSpeed = 24;
public const byte SeparationMaxSpeed = 25;
/// <summary>Knob count (overlay iteration bound).</summary>
public const byte Count = 24;
public const byte Count = 26;
}
/// <summary>
@@ -307,5 +326,7 @@ namespace ProjectM.Simulation
public float CoreRegenIntervalTicks;
public float CoreOverrunDrainPct;
public float FinalSiegeMultiplier;
public float StaggerKnockbackSpeed;
public float SeparationMaxSpeed;
}
}