diff --git a/apps/client/src/scenes/game/GameManager.ts b/apps/client/src/scenes/game/GameManager.ts index a27851a..90b6e2a 100644 --- a/apps/client/src/scenes/game/GameManager.ts +++ b/apps/client/src/scenes/game/GameManager.ts @@ -8,6 +8,7 @@ BombPlacedAckPayload, BombPlacedPayload, } from "@repo/shared"; +import { config } from "@client/config"; import { socketManager } from "@client/network/SocketManager"; import { AppearanceResolver } from "./application/AppearanceResolver"; import { GameMapController } from "./entities/map/GameMapController"; @@ -48,7 +49,7 @@ } private canAcceptInput(): boolean { - return !this.isInputLocked && this.timer.isStarted(); + return this.inputLockCount === 0 && this.timer.isStarted(); } // 現在の残り秒数を取得する @@ -78,11 +79,21 @@ private joystickInput = { x: 0, y: 0 }; private isInitialized = false; private isDestroyed = false; - private isInputLocked = false; + private inputLockCount = 0; - public lockInput() { - this.isInputLocked = true; + public lockInput(): () => void { + this.inputLockCount += 1; this.joystickInput = { x: 0, y: 0 }; + + let released = false; + return () => { + if (released) { + return; + } + + released = true; + this.inputLockCount = Math.max(0, this.inputLockCount - 1); + }; } constructor(container: HTMLDivElement, myId: string) { @@ -93,7 +104,8 @@ this.worldContainer.sortableChildren = true; this.playerDeathPolicy = new PlayerDeathPolicy({ myId: this.myId, - lockInput: this.lockInput.bind(this), + hitStunMs: config.GAME_CONFIG.PLAYER_HIT_STUN_MS, + acquireInputLock: this.lockInput.bind(this), }); } @@ -226,6 +238,8 @@ return; } + this.playerDeathPolicy.applyLocalHitStun(); + socketManager.game.sendBombHitReport({ bombId }); } @@ -257,9 +271,11 @@ this.bombManager = null; this.bombHitOrchestrator?.clear(); this.bombHitOrchestrator = null; + this.playerDeathPolicy.dispose(); this.reportedBombHitIds.clear(); this.players = {}; - this.isInputLocked = false; + this.inputLockCount = 0; + this.joystickInput = { x: 0, y: 0 }; // イベント購読の解除 this.networkSync?.unbind(); diff --git a/apps/client/src/scenes/game/application/PlayerDeathPolicy.ts b/apps/client/src/scenes/game/application/PlayerDeathPolicy.ts index 207d74f..56b33b1 100644 --- a/apps/client/src/scenes/game/application/PlayerDeathPolicy.ts +++ b/apps/client/src/scenes/game/application/PlayerDeathPolicy.ts @@ -2,17 +2,22 @@ type PlayerDeathPolicyParams = { myId: string; - lockInput: () => void; + hitStunMs: number; + acquireInputLock: () => () => void; }; /** 被弾後のローカルプレイヤー処理ポリシーを管理する */ export class PlayerDeathPolicy { private myId: string; - private lockInput: () => void; + private hitStunMs: number; + private acquireInputLock: () => () => void; + private activeLockRelease: (() => void) | null = null; + private unlockTimer: ReturnType | null = null; - constructor({ myId, lockInput }: PlayerDeathPolicyParams) { + constructor({ myId, hitStunMs, acquireInputLock }: PlayerDeathPolicyParams) { this.myId = myId; - this.lockInput = lockInput; + this.hitStunMs = hitStunMs; + this.acquireInputLock = acquireInputLock; } /** サーバーからの死亡通知に応じてローカル被弾後処理を適用する */ @@ -21,6 +26,43 @@ return; } - this.lockInput(); + this.applyHitStun(); + } + + /** ローカル被弾判定時に硬直を適用する */ + public applyLocalHitStun(): void { + this.applyHitStun(); + } + + /** ポリシーが保持するタイマーと入力ロックを解放する */ + public dispose(): void { + if (this.unlockTimer) { + clearTimeout(this.unlockTimer); + this.unlockTimer = null; + } + + if (this.activeLockRelease) { + this.activeLockRelease(); + this.activeLockRelease = null; + } + } + + private applyHitStun(): void { + if (!this.activeLockRelease) { + this.activeLockRelease = this.acquireInputLock(); + } + + if (this.unlockTimer) { + clearTimeout(this.unlockTimer); + this.unlockTimer = null; + } + + this.unlockTimer = setTimeout(() => { + this.unlockTimer = null; + if (this.activeLockRelease) { + this.activeLockRelease(); + this.activeLockRelease = null; + } + }, this.hitStunMs); } } diff --git a/packages/shared/src/config/README.md b/packages/shared/src/config/README.md index 027f1af..bb57564 100644 --- a/packages/shared/src/config/README.md +++ b/packages/shared/src/config/README.md @@ -21,6 +21,7 @@ - `GAME_CONFIG.GRID_ROWS`: マップ縦方向のグリッド数 - `GAME_CONFIG.PLAYER_RADIUS`: プレイヤー当たり判定半径(グリッド単位) - `GAME_CONFIG.PLAYER_SPEED`: プレイヤー移動速度(1秒あたりのグリッド移動量) +- `GAME_CONFIG.PLAYER_HIT_STUN_MS`: 被弾時に入力を停止する時間(ms) - `GAME_CONFIG.BOMB_RADIUS_GRID`: 爆風半径(グリッド単位) - `GAME_CONFIG.BOMB_FUSE_MS`: 爆弾設置から爆発までの時間(ms) - `GAME_CONFIG.BOMB_COOLDOWN_MS`: 次の爆弾を置けるまでの待機時間(ms) diff --git a/packages/shared/src/config/gameConfig.ts b/packages/shared/src/config/gameConfig.ts index 8f0ff0f..27a5d17 100644 --- a/packages/shared/src/config/gameConfig.ts +++ b/packages/shared/src/config/gameConfig.ts @@ -20,6 +20,7 @@ PLAYER_RADIUS: 0.5, // プレイヤー半径(グリッド単位、目安: 0.05〜0.2) PLAYER_SPEED: 3, // 1秒当たりの移動量(グリッド単位) PLAYER_RENDER_SCALE: 1, // プレイヤー見た目サイズ倍率(1=等倍) + PLAYER_HIT_STUN_MS: 1000, // 被弾時に入力を停止する時間(ms) // 爆弾設定(内部座標はグリッド単位、時間はms、契約値) BOMB_RADIUS_GRID: 1.5, // 爆風半径(グリッド単位、円形当たり判定)