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:
@@ -126,7 +126,8 @@ namespace ProjectM.Server
|
||||
if (sweep)
|
||||
kpos = SweptMove(in physics, pos, kpos, SweepRadius, envFilter);
|
||||
xform.ValueRW.Position = kpos;
|
||||
windup.ValueRW.WindUpUntilTick = 0; // a recoiling Husk does not wind up
|
||||
if (kb.Speed >= tune.StaggerKnockbackSpeed)
|
||||
windup.ValueRW.WindUpUntilTick = 0; // B2 poise: only a HEAVY hit interrupts; a light hit nudges while the wind-up keeps counting
|
||||
continue; // recoiling: skip seek + strike this tick
|
||||
}
|
||||
knockback.ValueRW.UntilTick = 0; // window elapsed
|
||||
@@ -240,8 +241,11 @@ namespace ProjectM.Server
|
||||
kpos.y = pos.y;
|
||||
if (sweep) kpos = SweptMove(in physics, pos, kpos, SweepRadius, envFilter);
|
||||
xform.ValueRW.Position = kpos;
|
||||
windup.ValueRW.WindUpUntilTick = 0;
|
||||
lunge.ValueRW.UntilTick = 0;
|
||||
if (kb.Speed >= tune.StaggerKnockbackSpeed)
|
||||
{
|
||||
windup.ValueRW.WindUpUntilTick = 0; // B2 poise: only a HEAVY hit breaks the windup / committed lunge
|
||||
lunge.ValueRW.UntilTick = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
knockback.ValueRW.UntilTick = 0;
|
||||
@@ -380,7 +384,8 @@ namespace ProjectM.Server
|
||||
kpos.y = pos.y;
|
||||
if (sweep) kpos = SweptMove(in physics, pos, kpos, SweepRadius, envFilter);
|
||||
xform.ValueRW.Position = kpos;
|
||||
windup.ValueRW.WindUpUntilTick = 0;
|
||||
if (kb.Speed >= tune.StaggerKnockbackSpeed)
|
||||
windup.ValueRW.WindUpUntilTick = 0; // B2 poise: light hits nudge, only heavy interrupts
|
||||
continue;
|
||||
}
|
||||
knockback.ValueRW.UntilTick = 0;
|
||||
@@ -475,6 +480,74 @@ namespace ProjectM.Server
|
||||
isLunging.ValueRW = lunge.ValueRO.UntilTick != 0u; // lunging iff a committed lunge is live this tick
|
||||
}
|
||||
|
||||
// --- Phase 1 B1: SEPARATION (soft-collision) so hordes stop interpenetrating. Lives INSIDE
|
||||
// EnemyAISystem (the sole enemy-Position writer; BossAISystem runs after and re-owns the boss).
|
||||
// Pairwise among LIVING enemies (MaxAlive <= ~14 + boss -> O(n^2) is trivial); an enemy mid-knockback
|
||||
// or mid-lunge keeps its committed motion (immovable this tick) but still pushes neighbours away, and
|
||||
// the BOSS is never pushed (knockback-immune identity). Enemies also yield a SMALL personal radius
|
||||
// around each player (below melee range so grunts can still strike - review B1-1). Displacement goes
|
||||
// through the same swept move so separation cannot shove anything through walls.
|
||||
{
|
||||
var sepEnt = new NativeList<Entity>(Allocator.Temp);
|
||||
var sepPos = new NativeList<float3>(Allocator.Temp);
|
||||
var sepRad = new NativeList<float>(Allocator.Temp);
|
||||
var sepMov = new NativeList<bool>(Allocator.Temp);
|
||||
foreach (var (sxf, shr, se) in SystemAPI.Query<RefRO<LocalTransform>, RefRO<HitRadius>>()
|
||||
.WithAll<EnemyTag>().WithNone<Dying>().WithEntityAccess())
|
||||
{
|
||||
bool movable = !SystemAPI.HasComponent<BossState>(se);
|
||||
if (movable && SystemAPI.HasComponent<KnockbackState>(se))
|
||||
{
|
||||
var k = SystemAPI.GetComponent<KnockbackState>(se);
|
||||
if (k.UntilTick != 0 && new NetworkTick(k.UntilTick).IsNewerThan(serverTick)) movable = false;
|
||||
}
|
||||
if (movable && SystemAPI.HasComponent<LungeState>(se))
|
||||
{
|
||||
var l = SystemAPI.GetComponent<LungeState>(se);
|
||||
if (l.UntilTick != 0 && new NetworkTick(l.UntilTick).IsNewerThan(serverTick)) movable = false;
|
||||
}
|
||||
sepEnt.Add(se); sepPos.Add(sxf.ValueRO.Position); sepRad.Add(shr.ValueRO.Value); sepMov.Add(movable);
|
||||
}
|
||||
float sepMaxStep = math.max(0f, tune.SeparationMaxSpeed) * dt;
|
||||
for (int i = 0; i < sepEnt.Length && sepMaxStep > 0f; i++)
|
||||
{
|
||||
if (!sepMov[i]) continue;
|
||||
float3 pi = sepPos[i];
|
||||
float2 push = float2.zero;
|
||||
for (int j = 0; j < sepEnt.Length; j++)
|
||||
{
|
||||
if (j == i) continue;
|
||||
float2 d = pi.xz - sepPos[j].xz;
|
||||
float want = (sepRad[i] + sepRad[j]) * 0.9f;
|
||||
float distSq = math.lengthsq(d);
|
||||
if (want <= 0f || distSq >= want * want) continue;
|
||||
float dist = math.sqrt(distSq);
|
||||
// Exact stacks de-overlap deterministically by index (golden angle), never randomly.
|
||||
float2 dir = dist > 1e-4f ? d / dist : new float2(math.sin(i * 2.399963f), math.cos(i * 2.399963f));
|
||||
push += dir * (want - dist) * 0.5f;
|
||||
}
|
||||
for (int pj = 0; pj < playerPositions.Length; pj++)
|
||||
{
|
||||
float2 d = pi.xz - playerPositions[pj].xz;
|
||||
float want = sepRad[i] + 0.35f; // small: never blocks strike range or the Spitter cornered-hold
|
||||
float distSq = math.lengthsq(d);
|
||||
if (distSq >= want * want) continue;
|
||||
float dist = math.sqrt(distSq);
|
||||
float2 dir = dist > 1e-4f ? d / dist : new float2(1f, 0f);
|
||||
push += dir * (want - dist);
|
||||
}
|
||||
if (math.lengthsq(push) < 1e-8f) continue;
|
||||
float pushLen = math.length(push);
|
||||
if (pushLen > sepMaxStep) push *= sepMaxStep / pushLen;
|
||||
float3 sepTarget = pi + new float3(push.x, 0f, push.y);
|
||||
sepTarget.y = pi.y;
|
||||
if (sweep) sepTarget = SweptMove(in physics, pi, sepTarget, SweepRadius, envFilter);
|
||||
var sepXf = SystemAPI.GetComponentRW<LocalTransform>(sepEnt[i]);
|
||||
sepXf.ValueRW.Position = sepTarget;
|
||||
}
|
||||
sepEnt.Dispose(); sepPos.Dispose(); sepRad.Dispose(); sepMov.Dispose();
|
||||
}
|
||||
|
||||
if (chargerWhiffsThisTick != 0 && SystemAPI.HasSingleton<DevTelemetry>())
|
||||
SystemAPI.GetSingletonRW<DevTelemetry>().ValueRW.ChargerWhiffWindowsOpened += chargerWhiffsThisTick;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user