Newer
Older
PixelPaintWar / apps / client / src / scenes / game / entities / bomb / BombManager.ts
/**
 * BombManager
 * 爆弾サブシステムの各サービスを調停する
 * 外部APIを維持しつつ内部責務を分離する
 */
import type { Container } from "pixi.js";
import type {
  BombPlacedAckPayload,
  BombPlacedPayload,
  PlaceBombPayload,
} from "@repo/shared";
import { AppearanceResolver } from "@client/scenes/game/application/AppearanceResolver";
import { BombIdRegistry } from "./BombIdRegistry";
import type { GamePlayers } from "@client/scenes/game/application/game.types";
import { BombRepository, type BombRenderPayload } from "./runtime/BombRepository";
import { BombPlacementService } from "./services/BombPlacementService";
import { BombAckReconciler } from "./services/BombAckReconciler";
import { BombRuntimeSystem } from "./runtime/BombRuntimeSystem";

/** 経過時間ミリ秒を返す関数型 */
export type ElapsedMsProvider = () => number;

/** 爆弾の描画更新に使う入力データ型 */
export type { BombRenderPayload };

/** 爆弾設置時に返す結果型 */
export type BombPlacementResult = {
  tempBombId: string;
  payload: PlaceBombPayload;
};

/** 爆弾爆発時に外部へ通知するペイロード型 */
export type BombExplodedPayload = {
  bombId: string;
  x: number;
  y: number;
  radius: number;
  teamId: number;
};

type BombManagerOptions = {
  worldContainer: Container;
  players: GamePlayers;
  myId: string;
  getElapsedMs: ElapsedMsProvider;
  appearanceResolver: AppearanceResolver;
  onBombExploded?: (payload: BombExplodedPayload) => void;
};

/** 爆弾エンティティのライフサイクルを管理する */
export class BombManager {
  private readonly getElapsedMs: ElapsedMsProvider;
  private readonly bombIdRegistry = new BombIdRegistry();
  private readonly bombRepository: BombRepository;
  private readonly bombPlacementService: BombPlacementService;
  private readonly bombAckReconciler: BombAckReconciler;
  private readonly bombRuntimeSystem: BombRuntimeSystem;

  constructor({ worldContainer, players, myId, getElapsedMs, appearanceResolver, onBombExploded }: BombManagerOptions) {
    this.getElapsedMs = getElapsedMs;

    this.bombRepository = new BombRepository({
      worldContainer,
      bombIdRegistry: this.bombIdRegistry,
    });
    this.bombPlacementService = new BombPlacementService({
      players,
      myId,
      getElapsedMs,
      appearanceResolver,
      bombIdRegistry: this.bombIdRegistry,
    });
    this.bombAckReconciler = new BombAckReconciler({
      bombIdRegistry: this.bombIdRegistry,
      bombRepository: this.bombRepository,
    });
    this.bombRuntimeSystem = new BombRuntimeSystem({
      bombRepository: this.bombRepository,
      onBombExploded,
    });
  }

  /** 自プレイヤー位置に爆弾を仮IDで設置し,設置要求を返す */
  public placeBomb(): BombPlacementResult | null {
    const ownPlacement = this.bombPlacementService.placeOwnBomb();
    if (!ownPlacement) {
      return null;
    }

    this.upsertBomb(ownPlacement.tempBombId, ownPlacement.renderPayload);
    return {
      tempBombId: ownPlacement.tempBombId,
      payload: ownPlacement.payload,
    };
  }

  /** 他プレイヤー向けの爆弾確定イベントを反映する */
  public applyPlacedBombFromOthers(payload: BombPlacedPayload): void {
    this.upsertBomb(
      payload.bombId,
      this.bombPlacementService.createRenderPayload(payload),
    );
  }

  /** 設置者本人向けACKを反映し,仮IDから正式IDへ置換する */
  public applyPlacedBombAck(payload: BombPlacedAckPayload): void {
    this.bombAckReconciler.applyPlacedBombAck(payload);
  }

  /** 描画ペイロードで指定IDの爆弾を追加または更新する */
  public upsertBomb(bombId: string, payload: BombRenderPayload): void {
    this.bombRepository.upsertBomb(bombId, payload);
  }

  /** 指定IDの爆弾を削除する */
  public removeBomb(bombId: string): void {
    this.bombRepository.removeBomb(bombId);
  }

  /** 爆弾状態を更新し終了済みを破棄する */
  public tick(): void {
    this.bombRuntimeSystem.tick(this.getElapsedMs());
  }

  /** 管理中の爆弾をすべて破棄する */
  public destroy(): void {
    this.bombRepository.clear();
    this.bombIdRegistry.clear();
  }
}