diff --git a/apps/client/src/scenes/game/GameView.tsx b/apps/client/src/scenes/game/GameView.tsx index 6f5ccdc..1d67d7a 100644 --- a/apps/client/src/scenes/game/GameView.tsx +++ b/apps/client/src/scenes/game/GameView.tsx @@ -16,6 +16,7 @@ GAME_VIEW_TIMER_STYLE, } from "./styles/GameView.styles"; import { config } from "@client/config"; +import { buildRespawnHeartGauge } from "./input/presentation/GameUiPresenter"; /** 表示と入力に必要なプロパティ */ type Props = { @@ -91,17 +92,6 @@ ); }; -const buildHeartGauge = (localBombHitCount: number): string => { - const maxHearts = Math.max( - 1, - Math.floor(config.GAME_CONFIG.PLAYER_RESPAWN_HIT_COUNT), - ); - const clampedHitCount = Math.min(Math.max(localBombHitCount, 0), maxHearts); - const remainingHearts = maxHearts - clampedHitCount; - - return `${"❤️".repeat(remainingHearts)}${"🤍".repeat(clampedHitCount)}`; -}; - /** 画面描画と入力UIをまとめて描画する */ export const GameView = ({ timeLeft, @@ -116,7 +106,7 @@ const remainingSeconds = parseRemainingSeconds(timeLeft); const isFeverTime = remainingSeconds <= config.GAME_CONFIG.BOMB_FEVER_START_REMAINING_SEC; - const heartGauge = buildHeartGauge(localBombHitCount); + const heartGauge = buildRespawnHeartGauge(localBombHitCount); return (
diff --git a/apps/client/src/scenes/game/application/combat/CombatLifecycleFacade.ts b/apps/client/src/scenes/game/application/combat/CombatLifecycleFacade.ts index 5feea20..f5058e1 100644 --- a/apps/client/src/scenes/game/application/combat/CombatLifecycleFacade.ts +++ b/apps/client/src/scenes/game/application/combat/CombatLifecycleFacade.ts @@ -20,6 +20,8 @@ onLocalBombHitCountChanged: (count: number) => void; }; +type RespawnState = "idle" | "counting" | "pendingRespawn"; + /** 被弾関連ライフサイクルの制御を担当する */ export class CombatLifecycleFacade { private readonly players: GamePlayers; @@ -30,7 +32,7 @@ private readonly playerHitPolicy: PlayerHitPolicy; private readonly playerHitEffectOrchestrator: PlayerHitEffectOrchestrator; private localBombHitCount = 0; - private isRespawnPending = false; + private respawnState: RespawnState = "idle"; private respawnTimer: ReturnType | null = null; constructor({ @@ -64,7 +66,7 @@ public handleBombExploded(payload: BombExplodedPayload): void { const hitPlayerId = this.bombHitOrchestrator.evaluateHit(payload); if (!hitPlayerId) return; - if (this.isRespawnPending) return; + if (this.respawnState === "pendingRespawn") return; this.handleLocalBombHit(); this.playerHitPolicy.applyLocalHitStun(); @@ -97,6 +99,10 @@ } private handleLocalBombHit(): void { + if (this.respawnState === "idle") { + this.respawnState = "counting"; + } + this.localBombHitCount += 1; this.onLocalBombHitCountChanged(this.localBombHitCount); @@ -104,7 +110,7 @@ return; } - this.isRespawnPending = true; + this.respawnState = "pendingRespawn"; if (this.respawnTimer) { clearTimeout(this.respawnTimer); this.respawnTimer = null; @@ -119,7 +125,7 @@ } this.localBombHitCount = 0; - this.isRespawnPending = false; + this.respawnState = "idle"; this.onLocalBombHitCountChanged(this.localBombHitCount); }, config.GAME_CONFIG.PLAYER_HIT_STUN_MS); } diff --git a/apps/client/src/scenes/game/input/presentation/GameUiPresenter.ts b/apps/client/src/scenes/game/input/presentation/GameUiPresenter.ts index b9b8cec..9c29eff 100644 --- a/apps/client/src/scenes/game/input/presentation/GameUiPresenter.ts +++ b/apps/client/src/scenes/game/input/presentation/GameUiPresenter.ts @@ -22,4 +22,16 @@ /** ゲーム画面の初期残り時間表示を返す */ export const getInitialTimeDisplay = (): string => { return formatRemainingTime(config.GAME_CONFIG.GAME_DURATION_SEC); +}; + +/** 被弾回数からハートゲージ表示を生成する */ +export const buildRespawnHeartGauge = (localBombHitCount: number): string => { + const maxHearts = Math.max( + 1, + Math.floor(config.GAME_CONFIG.PLAYER_RESPAWN_HIT_COUNT), + ); + const clampedHitCount = Math.min(Math.max(localBombHitCount, 0), maxHearts); + const remainingHearts = maxHearts - clampedHitCount; + + return `${"❤️".repeat(remainingHearts)}${"🤍".repeat(clampedHitCount)}`; }; \ No newline at end of file