Newer
Older
PixelPaintWar / apps / client / src / scenes / game / application / PlayerHitEffectOrchestrator.ts
/**
 * PlayerHitEffectOrchestrator
 * 被弾時のプレイヤー演出発火を管理する
 * ローカル被弾とネットワーク通知被弾を同じ窓口で扱う
 */
import type { GamePlayers } from "./game.types";
import type {
  PlayerHitEffectEventName,
  PlayerHitEffectEventPayloadMap,
} from "@repo/shared";

type PlayerHitEffectOrchestratorOptions = {
  players: GamePlayers;
  blinkDurationMs: number;
  dedupWindowMs: number;
  nowMsProvider?: () => number;
};

/** 被弾演出イベント入力を表す型 */
type PlayerHitEffectEvent<TEventName extends PlayerHitEffectEventName = PlayerHitEffectEventName> = {
  name: TEventName;
  payload: PlayerHitEffectEventPayloadMap[TEventName];
};

/** 被弾演出の発火責務を管理するオーケストレーター */
export class PlayerHitEffectOrchestrator {
  private readonly players: GamePlayers;
  private readonly blinkDurationMs: number;
  private readonly dedupWindowMs: number;
  private readonly nowMsProvider: () => number;
  private readonly lastTriggeredAtByPlayerId = new Map<string, number>();

  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.dispatch({
      name: "local-bomb-hit",
      payload: {
        playerId: localPlayerId,
        localPlayerId,
      },
    });
  }

  /** ネットワーク通知の被弾時に必要な点滅演出を発火する */
  public handleNetworkPlayerHit(playerId: string, localPlayerId: string): void {
    this.dispatch({
      name: "network-player-hit",
      payload: {
        playerId,
        localPlayerId,
      },
    });
  }

  /** 被弾演出イベント名に応じて処理を分岐する */
  public dispatch(event: PlayerHitEffectEvent): void {
    if (
      event.name === "network-player-hit"
      && event.payload.playerId === event.payload.localPlayerId
    ) {
      return;
    }

    if (!this.shouldTrigger(event.payload.playerId)) {
      return;
    }

    this.playBombHitBlink(event.payload.playerId);
  }

  /** 指定プレイヤーへ被弾点滅演出を適用する */
  private playBombHitBlink(playerId: string): void {
    const target = this.players[playerId];
    if (!target) {
      return;
    }

    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;
  }
}