Vault Re-Alignment

This commit is contained in:
2026-06-09 23:26:20 -07:00
parent a7405c3f38
commit da522efe7a
63 changed files with 119048 additions and 15 deletions
@@ -71,7 +71,7 @@ namespace ProjectM.Server
foreach (var (xform, stats, cooldown, knockback, windup) in
SystemAPI.Query<RefRW<LocalTransform>, RefRO<EnemyStats>, RefRW<EnemyAttackCooldown>,
RefRW<KnockbackState>, RefRW<AttackWindup>>()
.WithAll<EnemyTag>())
.WithAll<EnemyTag>().WithNone<LungeState>())
{
float3 pos = xform.ValueRO.Position;
@@ -145,6 +145,7 @@ namespace ProjectM.Server
{
Amount = stats.ValueRO.AttackDamage,
SourceNetworkId = -1, // environment / Husk, not a player
SourceTick = TickUtil.NonZero(now),
});
uint cooldownTicks = (uint)math.max(1, stats.ValueRO.AttackCooldownTicks);
cooldown.ValueRW.NextAttackTick = TickUtil.NonZero(now + cooldownTicks);
@@ -170,6 +171,138 @@ namespace ProjectM.Server
}
}
// --- Charger pass: a Husk variant baked with LungeState commits to a punishable fixed-direction lunge.
// Component-presence is the discriminator; the Grunt pass above excludes these via .WithNone<LungeState>().
const float ChargerLungeSpeed = 16f; // units/s while lunging
const uint ChargerLungeDurationTicks = 18; // ~0.30 s of committed travel
const uint ChargerWindupTicks = 30; // ~0.50 s readable telegraph (>= interp delay + reaction)
const uint ChargerWhiffStaggerTicks = 36; // ~0.60 s punish window on a whiff
uint chargerWhiffsThisTick = 0;
foreach (var (xform, stats, cooldown, knockback, windup, lunge) in
SystemAPI.Query<RefRW<LocalTransform>, RefRO<EnemyStats>, RefRW<EnemyAttackCooldown>,
RefRW<KnockbackState>, RefRW<AttackWindup>, RefRW<LungeState>>()
.WithAll<EnemyTag>())
{
float3 pos = xform.ValueRO.Position;
// 1. Knockback wins (and cancels any in-flight lunge so Position keeps a single writer).
var kb = knockback.ValueRO;
if (kb.UntilTick != 0)
{
var kbTick = new NetworkTick(kb.UntilTick);
if (kbTick.IsValid && kbTick.IsNewerThan(serverTick))
{
float3 kpos = pos + new float3(kb.Dir.x, 0f, kb.Dir.y) * (kb.Speed * dt);
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;
continue;
}
knockback.ValueRW.UntilTick = 0;
}
// Nearest living player (reuse the snapshot taken above).
int cbest = -1; float cbestSq = float.MaxValue;
for (int i = 0; i < playerPositions.Length; i++)
{
float2 dd = playerPositions[i].xz - pos.xz;
float sq = math.lengthsq(dd);
if (sq < cbestSq) { cbestSq = sq; cbest = i; }
}
float3 cTargetPos = playerPositions[cbest];
// 2. Lunge active: travel the locked direction; damage on contact, or stagger on a wall-stop whiff.
var lg = lunge.ValueRO;
if (lg.UntilTick != 0)
{
var lgTick = new NetworkTick(lg.UntilTick);
if (lgTick.IsValid && lgTick.IsNewerThan(serverTick))
{
float3 intended = pos + new float3(lg.Dir.x, 0f, lg.Dir.y) * (lg.Speed * dt);
intended.y = pos.y;
float3 moved = sweep ? SweptMove(in physics, pos, intended, SweepRadius, envFilter) : intended;
xform.ValueRW.Position = moved;
if (math.lengthsq(lg.Dir) > 1e-6f)
xform.ValueRW.Rotation = quaternion.LookRotationSafe(new float3(lg.Dir.x, 0f, lg.Dir.y), math.up());
if (EnemyAIMath.InAttackRange(moved, cTargetPos, stats.ValueRO.AttackRange))
{
ecb.AppendToBuffer(playerEntities[cbest], new DamageEvent
{
Amount = stats.ValueRO.AttackDamage,
SourceNetworkId = -1,
SourceTick = TickUtil.NonZero(now),
});
uint cdTicks = (uint)math.max(1, stats.ValueRO.AttackCooldownTicks);
cooldown.ValueRW.NextAttackTick = TickUtil.NonZero(now + cdTicks);
lunge.ValueRW.UntilTick = 0; // landed -> end the lunge
}
else
{
float intendedDist = math.distance(pos.xz, intended.xz);
float actualDist = math.distance(pos.xz, moved.xz);
if (intendedDist > 1e-4f && actualDist < intendedDist * 0.5f)
{
cooldown.ValueRW.NextAttackTick = TickUtil.NonZero(now + ChargerWhiffStaggerTicks);
lunge.ValueRW.UntilTick = 0; // wall-stop whiff -> stagger (the punish window)
chargerWhiffsThisTick++;
lunge.ValueRW.StaggerUntilTick = TickUtil.NonZero(now + ChargerWhiffStaggerTicks); // scoreable punish window
}
}
continue; // committed this tick
}
// Timer elapsed without landing -> overshoot whiff -> stagger, then seek this tick.
cooldown.ValueRW.NextAttackTick = TickUtil.NonZero(now + ChargerWhiffStaggerTicks);
lunge.ValueRW.UntilTick = 0;
chargerWhiffsThisTick++;
lunge.ValueRW.StaggerUntilTick = TickUtil.NonZero(now + ChargerWhiffStaggerTicks); // scoreable punish window
}
// 3. Seek + face (shared shape with the Grunt path).
float cStop = stats.ValueRO.AttackRange * 0.9f;
float3 cvel = EnemyAIMath.SeekVelocity(pos, cTargetPos, stats.ValueRO.MoveSpeed, cStop);
float3 cNewPos = pos + cvel * dt; cNewPos.y = pos.y;
if (sweep) cNewPos = SweptMove(in physics, pos, cNewPos, SweepRadius, envFilter);
xform.ValueRW.Position = cNewPos;
float3 cToTarget = cTargetPos - pos; cToTarget.y = 0f;
if (math.lengthsq(cToTarget) > 1e-6f)
xform.ValueRW.Rotation = quaternion.LookRotationSafe(math.normalize(cToTarget), math.up());
// 4. Commit: a wind-up elapses -> LOCK the lunge direction + fire. NO cancel-on-leave-range — the
// whole point is the commit lands even if the player dodged out of range (the punishable tell).
uint cWindRaw = windup.ValueRO.WindUpUntilTick;
if (cWindRaw != 0)
{
var cWindTick = new NetworkTick(cWindRaw);
if (!(cWindTick.IsValid && cWindTick.IsNewerThan(serverTick)))
{
float3 toT = cTargetPos - pos; toT.y = 0f;
float2 ldir = math.lengthsq(toT) > 1e-6f ? math.normalize(toT.xz) : new float2(0f, 1f);
lunge.ValueRW.Dir = ldir;
lunge.ValueRW.Speed = ChargerLungeSpeed;
lunge.ValueRW.UntilTick = TickUtil.NonZero(now + ChargerLungeDurationTicks);
windup.ValueRW.WindUpUntilTick = 0;
}
}
else
{
bool cInRange = EnemyAIMath.InAttackRange(pos, cTargetPos, stats.ValueRO.AttackRange);
if (cInRange)
{
bool cReady = cooldown.ValueRO.NextAttackTick == 0
|| !new NetworkTick(cooldown.ValueRO.NextAttackTick).IsNewerThan(serverTick);
if (cReady)
windup.ValueRW.WindUpUntilTick = TickUtil.NonZero(now + ChargerWindupTicks);
}
}
}
if (chargerWhiffsThisTick != 0 && SystemAPI.HasSingleton<DevTelemetry>())
SystemAPI.GetSingletonRW<DevTelemetry>().ValueRW.ChargerWhiffWindowsOpened += chargerWhiffsThisTick;
ecb.Playback(state.EntityManager);
ecb.Dispose();