From 6c345ba5953a06412d346f9166db0fad883a39a2 Mon Sep 17 00:00:00 2001 From: Syncer Deploy Date: Mon, 31 Aug 2026 13:21:31 -0400 Subject: [PATCH] stabilize dead air camera reconciliation --- apps/web/src/DeadAir3D.tsx | 2 +- apps/web/src/useDeadAirClient.ts | 33 +++++++++++++++++++++++---- packages/shared/src/dead-air-game.ts | 24 +++++++++++++++++-- packages/shared/src/dead-air-types.ts | 2 ++ 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/apps/web/src/DeadAir3D.tsx b/apps/web/src/DeadAir3D.tsx index 5959d1d..5edc5d6 100644 --- a/apps/web/src/DeadAir3D.tsx +++ b/apps/web/src/DeadAir3D.tsx @@ -172,7 +172,7 @@ function updateRuntime( const local = world.players.find((player) => player.id === playerId); const fractionalSeconds = frame.interpolationAlpha / DEAD_AIR_TICK_RATE; if (local) { - const correctionLife = Math.max(0, 1 - (time - frame.localCorrection.updatedAt) / 130); + const correctionLife = Math.exp(-(time - frame.localCorrection.updatedAt) / 115); runtime.camera.position.set( local.x + local.velocityX * fractionalSeconds + frame.localCorrection.x * correctionLife, 1.62, diff --git a/apps/web/src/useDeadAirClient.ts b/apps/web/src/useDeadAirClient.ts index 20390c3..d3b5d0d 100644 --- a/apps/web/src/useDeadAirClient.ts +++ b/apps/web/src/useDeadAirClient.ts @@ -203,11 +203,36 @@ export function useDeadAirClient(): DeadAirClientView { const localAfter = (engine.currentState as DeadAirClientState).players.find( (player) => player.id === engine.localPlayerId, ); - if (localBefore && localAfter && decoded.snapshot.state.round === renderSource.current.state.round) { + if ( + localBefore && + localAfter && + localBefore.alive === localAfter.alive && + decoded.snapshot.state.round === renderSource.current.state.round + ) { + const now = performance.now(); const correction = renderSource.current.localCorrection; - correction.x += localBefore.x - localAfter.x; - correction.z += localBefore.z - localAfter.z; - correction.updatedAt = performance.now(); + const decay = Math.exp(-(now - correction.updatedAt) / 115); + const leadSeconds = clock.interpolationAlpha / deadAirGame.tickRateHz; + const deltaX = + localBefore.x + localBefore.velocityX * leadSeconds - + localAfter.x - localAfter.velocityX * leadSeconds; + const deltaZ = + localBefore.z + localBefore.velocityZ * leadSeconds - + localAfter.z - localAfter.velocityZ * leadSeconds; + if (Math.hypot(deltaX, deltaZ) < 3) { + correction.x = correction.x * decay + deltaX; + correction.z = correction.z * decay + deltaZ; + } else { + correction.x = 0; + correction.z = 0; + } + correction.updatedAt = now; + } else { + renderSource.current.localCorrection = { + x: 0, + z: 0, + updatedAt: performance.now(), + }; } break; } diff --git a/packages/shared/src/dead-air-game.ts b/packages/shared/src/dead-air-game.ts index 1468029..e532376 100644 --- a/packages/shared/src/dead-air-game.ts +++ b/packages/shared/src/dead-air-game.ts @@ -67,6 +67,20 @@ const baseDeadAirGame = defineMultiplayerGame({ applyInput(state, input, { playerId, emit }) { const player = findPlayer(state, playerId); if (!player || player.bot || !player.alive || state.resetTicks > 0) return; + if ( + Math.abs(input.forward) > 0.01 || + Math.abs(input.strafe) > 0.01 || + input.fire || + input.sprint || + input.reload || + input.interact || + input.toggleFlashlight || + input.throwDecoy || + angleDifference(input.yaw, player.yaw) > 0.003 || + Math.abs(input.pitch - player.pitch) > 0.003 + ) { + player.engaged = true; + } applyCommand(player, input); if (input.reload) beginReload(player); if (input.toggleFlashlight) player.flashlight = !player.flashlight; @@ -370,6 +384,7 @@ function createPlayer(id: number, bot: boolean, round: number): DeadAirAuthority return { id, bot, + engaged: bot, alive: true, x: spawn.x, z: spawn.z, @@ -517,7 +532,12 @@ function fire( let hit: DeadAirAuthorityPlayer | null = null; let hitDistance = shotRange; for (const target of state.players) { - if (!target.alive || target.id === shooter.id || target.bot === shooter.bot) continue; + if ( + !target.alive || + target.id === shooter.id || + target.bot === shooter.bot || + (!target.bot && !target.engaged) + ) continue; const offsetX = target.x - shooter.x; const offsetZ = target.z - shooter.z; const forwardDistance = offsetX * directionX + offsetZ * directionZ; @@ -673,7 +693,7 @@ function updateWarden( tick: number, ): void { const visibleTargets = state.players.filter( - (target) => !target.bot && target.alive && canSeePlayer(state, warden, target), + (target) => !target.bot && target.engaged && target.alive && canSeePlayer(state, warden, target), ); visibleTargets.sort( (left, right) => diff --git a/packages/shared/src/dead-air-types.ts b/packages/shared/src/dead-air-types.ts index 30da7ef..59e27d2 100644 --- a/packages/shared/src/dead-air-types.ts +++ b/packages/shared/src/dead-air-types.ts @@ -31,6 +31,8 @@ export interface DeadAirBotState { export interface DeadAirAuthorityPlayer { id: number; bot: boolean; + /** Humans remain untargetable until they produce deliberate play input. */ + engaged: boolean; alive: boolean; x: number; z: number;