diff --git a/apps/client/src/config/index.ts b/apps/client/src/config/index.ts index 1e465d4..514fee5 100644 --- a/apps/client/src/config/index.ts +++ b/apps/client/src/config/index.ts @@ -11,6 +11,10 @@ PLAYER_LERP_SMOOTHNESS: 18, PLAYER_LERP_SNAP_THRESHOLD: 0.005, PLAYER_HIT_BLINK_MS: sharedConfig.GAME_CONFIG.PLAYER_HIT_STUN_MS, + PLAYER_HIT_BLINK_INTERVAL_MS: 100, + PLAYER_HIT_BLINK_HIDDEN_ALPHA: 0.2, + PLAYER_HIT_BLINK_MAX_DELTA_MS: 50, + PLAYER_HIT_EFFECT_DEDUP_WINDOW_MS: 300, GRID_CELL_SIZE: 100, diff --git a/apps/client/src/scenes/game/GameManager.ts b/apps/client/src/scenes/game/GameManager.ts index badaa47..4ba00ab 100644 --- a/apps/client/src/scenes/game/GameManager.ts +++ b/apps/client/src/scenes/game/GameManager.ts @@ -37,8 +37,8 @@ private bombHitOrchestrator: BombHitOrchestrator | null = null; private networkSync: GameNetworkSync | null = null; private gameLoop: GameLoop | null = null; - private playerDeathPolicy: PlayerDeathPolicy; - private playerHitEffectOrchestrator: PlayerHitEffectOrchestrator; + private playerDeathPolicy!: PlayerDeathPolicy; + private playerHitEffectOrchestrator!: PlayerHitEffectOrchestrator; private reportedBombHitIds = new Set(); // サーバーからゲーム開始通知(と開始時刻)を受け取った時に呼ぶ @@ -108,6 +108,11 @@ this.app = new Application(); this.worldContainer = new Container(); this.worldContainer.sortableChildren = true; + this.initializeHitSubsystem(); + } + + /** 被弾時の入力制御と演出発火のサブシステムを初期化する */ + private initializeHitSubsystem(): void { this.playerDeathPolicy = new PlayerDeathPolicy({ myId: this.myId, hitStunMs: config.GAME_CONFIG.PLAYER_HIT_STUN_MS, @@ -116,6 +121,7 @@ this.playerHitEffectOrchestrator = new PlayerHitEffectOrchestrator({ players: this.players, blinkDurationMs: config.GAME_CONFIG.PLAYER_HIT_BLINK_MS, + dedupWindowMs: config.GAME_CONFIG.PLAYER_HIT_EFFECT_DEDUP_WINDOW_MS, }); } diff --git a/apps/client/src/scenes/game/application/GameLoop.ts b/apps/client/src/scenes/game/application/GameLoop.ts index b04bdb3..723411a 100644 --- a/apps/client/src/scenes/game/application/GameLoop.ts +++ b/apps/client/src/scenes/game/application/GameLoop.ts @@ -57,7 +57,7 @@ isMoving, }); - this.bombStep.run({ deltaSeconds }); + this.bombStep.run(); this.cameraStep.run({ app: this.app, diff --git a/apps/client/src/scenes/game/application/PlayerHitEffectOrchestrator.ts b/apps/client/src/scenes/game/application/PlayerHitEffectOrchestrator.ts index fb2d989..aeeb2e3 100644 --- a/apps/client/src/scenes/game/application/PlayerHitEffectOrchestrator.ts +++ b/apps/client/src/scenes/game/application/PlayerHitEffectOrchestrator.ts @@ -8,30 +8,69 @@ type PlayerHitEffectOrchestratorOptions = { players: GamePlayers; blinkDurationMs: number; + dedupWindowMs: number; + nowMsProvider?: () => number; +}; + +/** 被弾演出イベント名を表す型 */ +type PlayerHitEffectEventName = "local-bomb-hit" | "network-player-dead"; + +/** 被弾演出イベント入力を表す型 */ +type PlayerHitEffectEvent = { + name: PlayerHitEffectEventName; + playerId: string; + localPlayerId: string; }; /** 被弾演出の発火責務を管理するオーケストレーター */ export class PlayerHitEffectOrchestrator { private readonly players: GamePlayers; private readonly blinkDurationMs: number; + private readonly dedupWindowMs: number; + private readonly nowMsProvider: () => number; + private readonly lastTriggeredAtByPlayerId = new Map(); - constructor({ players, blinkDurationMs }: PlayerHitEffectOrchestratorOptions) { + constructor({ + players, + blinkDurationMs, + dedupWindowMs, + nowMsProvider = () => performance.now(), + }: PlayerHitEffectOrchestratorOptions) { this.players = players; this.blinkDurationMs = blinkDurationMs; + this.dedupWindowMs = dedupWindowMs; + this.nowMsProvider = nowMsProvider; } /** ローカル被弾時の点滅演出を発火する */ public handleLocalBombHit(localPlayerId: string): void { - this.playBombHitBlink(localPlayerId); + this.dispatch({ + name: "local-bomb-hit", + playerId: localPlayerId, + localPlayerId, + }); } /** ネットワーク通知の被弾時に必要な点滅演出を発火する */ public handleNetworkPlayerDead(playerId: string, localPlayerId: string): void { - if (playerId === localPlayerId) { + this.dispatch({ + name: "network-player-dead", + playerId, + localPlayerId, + }); + } + + /** 被弾演出イベント名に応じて処理を分岐する */ + public dispatch(event: PlayerHitEffectEvent): void { + if (event.name === "network-player-dead" && event.playerId === event.localPlayerId) { return; } - this.playBombHitBlink(playerId); + if (!this.shouldTrigger(event.playerId)) { + return; + } + + this.playBombHitBlink(event.playerId); } /** 指定プレイヤーへ被弾点滅演出を適用する */ @@ -43,4 +82,16 @@ target.playBombHitBlink(this.blinkDurationMs); } + + /** 同一プレイヤーへの短時間重複発火を抑止する */ + private shouldTrigger(playerId: string): boolean { + const nowMs = this.nowMsProvider(); + const lastTriggeredAt = this.lastTriggeredAtByPlayerId.get(playerId); + if (lastTriggeredAt !== undefined && nowMs - lastTriggeredAt < this.dedupWindowMs) { + return false; + } + + this.lastTriggeredAtByPlayerId.set(playerId, nowMs); + return true; + } } \ No newline at end of file diff --git a/apps/client/src/scenes/game/application/loopSteps/BombStep.ts b/apps/client/src/scenes/game/application/loopSteps/BombStep.ts index 38c9d0f..f7d9d71 100644 --- a/apps/client/src/scenes/game/application/loopSteps/BombStep.ts +++ b/apps/client/src/scenes/game/application/loopSteps/BombStep.ts @@ -10,11 +10,6 @@ bombManager: BombManager; }; -/** 爆弾更新段の実行入力 */ -type BombStepParams = { - deltaSeconds: number; -}; - /** 爆弾更新処理を担うステップ */ export class BombStep { private bombManager: BombManager; @@ -23,8 +18,8 @@ this.bombManager = bombManager; } - /** 爆弾更新を実行する */ - public run(_params: BombStepParams): void { + /** 爆弾更新を実行する,時間管理は GameTimer 由来の経過時刻を利用する */ + public run(): void { this.bombManager.tick(); } } diff --git a/apps/client/src/scenes/game/entities/player/PlayerController.ts b/apps/client/src/scenes/game/entities/player/PlayerController.ts index 74bce98..1a03876 100644 --- a/apps/client/src/scenes/game/entities/player/PlayerController.ts +++ b/apps/client/src/scenes/game/entities/player/PlayerController.ts @@ -4,6 +4,7 @@ * ローカル入力適用,リモート更新適用,描画同期を分離して扱う */ import type { playerTypes } from "@repo/shared"; +import { config } from "@client/config"; import { AppearanceResolver } from "@client/scenes/game/application/AppearanceResolver"; import { BombHitBlinkRenderer } from "@client/scenes/game/entities/bomb/BombHitBlinkRenderer"; import { PlayerModel } from "./PlayerModel"; @@ -41,6 +42,9 @@ ); this.bombHitBlinkRenderer = new BombHitBlinkRenderer({ target: this.view.displayObject, + blinkIntervalMs: config.GAME_CONFIG.PLAYER_HIT_BLINK_INTERVAL_MS, + hiddenAlpha: config.GAME_CONFIG.PLAYER_HIT_BLINK_HIDDEN_ALPHA, + maxDeltaMs: config.GAME_CONFIG.PLAYER_HIT_BLINK_MAX_DELTA_MS, }); const pos = this.model.getPosition();