diff --git a/apps/client/src/config/index.ts b/apps/client/src/config/index.ts index a786100..1e465d4 100644 --- a/apps/client/src/config/index.ts +++ b/apps/client/src/config/index.ts @@ -10,6 +10,7 @@ PLAYER_LERP_SMOOTHNESS: 18, PLAYER_LERP_SNAP_THRESHOLD: 0.005, + PLAYER_HIT_BLINK_MS: sharedConfig.GAME_CONFIG.PLAYER_HIT_STUN_MS, GRID_CELL_SIZE: 100, diff --git a/apps/client/src/scenes/game/GameManager.ts b/apps/client/src/scenes/game/GameManager.ts index 74e558c..badaa47 100644 --- a/apps/client/src/scenes/game/GameManager.ts +++ b/apps/client/src/scenes/game/GameManager.ts @@ -19,6 +19,7 @@ import { BombHitContextProvider } from "./application/BombHitContextProvider"; import { BombHitOrchestrator } from "./application/BombHitOrchestrator"; import { PlayerDeathPolicy } from "./application/PlayerDeathPolicy"; +import { PlayerHitEffectOrchestrator } from "./application/PlayerHitEffectOrchestrator"; import type { BombHitEvaluationResult } from "./application/BombHitOrchestrator"; import type { GamePlayers } from "./application/game.types"; @@ -37,6 +38,7 @@ private networkSync: GameNetworkSync | null = null; private gameLoop: GameLoop | null = null; private playerDeathPolicy: PlayerDeathPolicy; + private playerHitEffectOrchestrator: PlayerHitEffectOrchestrator; private reportedBombHitIds = new Set(); // サーバーからゲーム開始通知(と開始時刻)を受け取った時に呼ぶ @@ -111,6 +113,10 @@ hitStunMs: config.GAME_CONFIG.PLAYER_HIT_STUN_MS, acquireInputLock: this.lockInput.bind(this), }); + this.playerHitEffectOrchestrator = new PlayerHitEffectOrchestrator({ + players: this.players, + blinkDurationMs: config.GAME_CONFIG.PLAYER_HIT_BLINK_MS, + }); } /** @@ -189,9 +195,7 @@ }, onPlayerDeadFromNetwork: (payload) => { this.playerDeathPolicy.applyPlayerDeadEvent(payload); - if (payload.playerId !== this.myId) { - this.playBombHitBlink(payload.playerId); - } + this.playerHitEffectOrchestrator.handleNetworkPlayerDead(payload.playerId, this.myId); }, }); this.networkSync.bind(); @@ -246,20 +250,11 @@ } this.playerDeathPolicy.applyLocalHitStun(); - this.playBombHitBlink(this.myId); + this.playerHitEffectOrchestrator.handleLocalBombHit(this.myId); socketManager.game.sendBombHitReport({ bombId }); } - private playBombHitBlink(playerId: string): void { - const target = this.players[playerId]; - if (!target) { - return; - } - - target.playBombHitBlink(config.GAME_CONFIG.PLAYER_HIT_STUN_MS); - } - private shouldSendBombHitReport( result: BombHitEvaluationResult | undefined, bombId: string, diff --git a/apps/client/src/scenes/game/application/PlayerHitEffectOrchestrator.ts b/apps/client/src/scenes/game/application/PlayerHitEffectOrchestrator.ts new file mode 100644 index 0000000..fb2d989 --- /dev/null +++ b/apps/client/src/scenes/game/application/PlayerHitEffectOrchestrator.ts @@ -0,0 +1,46 @@ +/** + * PlayerHitEffectOrchestrator + * 被弾時のプレイヤー演出発火を管理する + * ローカル被弾とネットワーク通知被弾を同じ窓口で扱う + */ +import type { GamePlayers } from "./game.types"; + +type PlayerHitEffectOrchestratorOptions = { + players: GamePlayers; + blinkDurationMs: number; +}; + +/** 被弾演出の発火責務を管理するオーケストレーター */ +export class PlayerHitEffectOrchestrator { + private readonly players: GamePlayers; + private readonly blinkDurationMs: number; + + constructor({ players, blinkDurationMs }: PlayerHitEffectOrchestratorOptions) { + this.players = players; + this.blinkDurationMs = blinkDurationMs; + } + + /** ローカル被弾時の点滅演出を発火する */ + public handleLocalBombHit(localPlayerId: string): void { + this.playBombHitBlink(localPlayerId); + } + + /** ネットワーク通知の被弾時に必要な点滅演出を発火する */ + public handleNetworkPlayerDead(playerId: string, localPlayerId: string): void { + if (playerId === localPlayerId) { + return; + } + + this.playBombHitBlink(playerId); + } + + /** 指定プレイヤーへ被弾点滅演出を適用する */ + private playBombHitBlink(playerId: string): void { + const target = this.players[playerId]; + if (!target) { + return; + } + + target.playBombHitBlink(this.blinkDurationMs); + } +} \ No newline at end of file diff --git a/apps/client/src/scenes/game/entities/bomb/BombHitBlinkRenderer.ts b/apps/client/src/scenes/game/entities/bomb/BombHitBlinkRenderer.ts index 570f959..964aa70 100644 --- a/apps/client/src/scenes/game/entities/bomb/BombHitBlinkRenderer.ts +++ b/apps/client/src/scenes/game/entities/bomb/BombHitBlinkRenderer.ts @@ -3,57 +3,66 @@ * 爆弾被弾時の点滅演出を描画オブジェクトへ適用する * 点滅開始,停止,破棄を一元管理する */ -import type { Container } from "pixi.js"; +import { Ticker, type Container } from "pixi.js"; type BombHitBlinkRendererOptions = { target: Container; + ticker?: Ticker; blinkIntervalMs?: number; hiddenAlpha?: number; + maxDeltaMs?: number; }; /** 爆弾被弾時の点滅演出を制御するレンダラー */ export class BombHitBlinkRenderer { private readonly target: Container; + private readonly ticker: Ticker; private readonly blinkIntervalMs: number; private readonly hiddenAlpha: number; - private blinkIntervalId: ReturnType | null = null; - private stopTimeoutId: ReturnType | null = null; + private readonly maxDeltaMs: number; private isVisibleFrame = true; + private isPlaying = false; + private remainingDurationMs = 0; + private elapsedSinceToggleMs = 0; - constructor({ target, blinkIntervalMs = 100, hiddenAlpha = 0.2 }: BombHitBlinkRendererOptions) { + constructor({ + target, + ticker = Ticker.shared, + blinkIntervalMs = 100, + hiddenAlpha = 0.2, + maxDeltaMs = 50, + }: BombHitBlinkRendererOptions) { this.target = target; + this.ticker = ticker; this.blinkIntervalMs = blinkIntervalMs; this.hiddenAlpha = hiddenAlpha; + this.maxDeltaMs = maxDeltaMs; } /** 指定時間だけ点滅演出を再生する */ public play(durationMs: number): void { this.stop(); + if (durationMs <= 0) { + return; + } + + this.isPlaying = true; + this.remainingDurationMs = durationMs; + this.elapsedSinceToggleMs = 0; this.isVisibleFrame = true; this.target.alpha = 1; - - this.blinkIntervalId = setInterval(() => { - this.isVisibleFrame = !this.isVisibleFrame; - this.target.alpha = this.isVisibleFrame ? 1 : this.hiddenAlpha; - }, this.blinkIntervalMs); - - this.stopTimeoutId = setTimeout(() => { - this.stop(); - }, durationMs); + this.ticker.add(this.handleTick); } /** 点滅演出を停止し描画状態を初期化する */ public stop(): void { - if (this.blinkIntervalId) { - clearInterval(this.blinkIntervalId); - this.blinkIntervalId = null; + if (this.isPlaying) { + this.ticker.remove(this.handleTick); } - if (this.stopTimeoutId) { - clearTimeout(this.stopTimeoutId); - this.stopTimeoutId = null; - } - + this.isPlaying = false; + this.remainingDurationMs = 0; + this.elapsedSinceToggleMs = 0; this.isVisibleFrame = true; this.target.alpha = 1; } @@ -62,4 +71,24 @@ public destroy(): void { this.stop(); } + + private handleTick = (ticker: Ticker): void => { + if (!this.isPlaying) { + return; + } + + const deltaMs = Math.min(ticker.deltaMS, this.maxDeltaMs); + this.remainingDurationMs -= deltaMs; + this.elapsedSinceToggleMs += deltaMs; + + while (this.elapsedSinceToggleMs >= this.blinkIntervalMs) { + this.elapsedSinceToggleMs -= this.blinkIntervalMs; + this.isVisibleFrame = !this.isVisibleFrame; + this.target.alpha = this.isVisibleFrame ? 1 : this.hiddenAlpha; + } + + if (this.remainingDurationMs <= 0) { + this.stop(); + } + }; } \ No newline at end of file