Newer
Older
PixelPaintWar / apps / server / src / domains / game / application / services / GameSessionLifecycleService.ts
/**
 * GameSessionLifecycleService
 * ゲームセッションの開始,参照,終了時クリーンアップを管理する
 */
import { config } from "@server/config";
import type {
  ActiveBombSnapshot,
  ActiveBombRegistration,
  GameFieldConfig,
} from "../ports/gameUseCasePorts";
import type {
  domain,
  GameResultPayload,
} from "@repo/shared";
import { logEvent } from "@server/logging/logger";
import {
  gameDomainLogEvents,
  logResults,
  logScopes,
} from "@server/logging/index";
import { GameRoomSession, type GameSessionCallbacks } from "./GameRoomSession";

type GameSessionRef = { current: GameRoomSession | null };
type ActivePlayerIndex = Set<string>;

/** ゲームセッションのライフサイクル操作を提供するサービス */
export class GameSessionLifecycleService {
  constructor(
    private sessionRef: GameSessionRef,
    private activePlayerIds: ActivePlayerIndex,
    private roomId: string,
  ) {}

  public getRoomStartTime(): number | undefined {
    return this.sessionRef.current?.getStartTime();
  }

  public getRoomPlayers() {
    return this.sessionRef.current?.getPlayers() ?? [];
  }

  public getRoomFieldConfig(): GameFieldConfig | undefined {
    return this.sessionRef.current?.getFieldConfig();
  }

  public shouldBroadcastBombPlaced(dedupeKey: string, nowMs: number): boolean {
    return (
      this.sessionRef.current?.shouldBroadcastBombPlaced(dedupeKey, nowMs) ??
      false
    );
  }

  public shouldBroadcastBombHitReport(
    dedupeKey: string,
    nowMs: number,
  ): boolean {
    return (
      this.sessionRef.current?.shouldBroadcastBombHitReport(dedupeKey, nowMs) ??
      false
    );
  }

  public issueServerBombId(): string {
    const session = this.sessionRef.current;
    if (!session) {
      throw new Error("Game session not found");
    }

    return session.issueServerBombId();
  }

  /** 指定プレイヤーのチームIDを返す,未参加時は UNKNOWN_TEAM_ID を返す */
  public getPlayerTeamId(playerId: string): number {
    return this.sessionRef.current?.getPlayerTeamId(playerId) ?? -1;
  }

  /** 設置済み爆弾をアクティブレジストリに登録する */
  public registerActiveBomb(registration: ActiveBombRegistration): void {
    this.sessionRef.current?.registerActiveBomb(registration);
  }

  /** 指定爆弾の所有者の bombHitCount を加算する */
  public recordBombHitForOwner(bombId: string): void {
    this.sessionRef.current?.recordBombHitForOwner(bombId);
  }

  /** 現在アクティブな爆弾一覧を返す */
  public getActiveBombs(): ActiveBombSnapshot[] {
    return this.sessionRef.current?.getActiveBombs() ?? [];
  }

  public startRoomSession(
    playerIds: string[],
    playerNamesById: Record<string, string>,
    fieldConfig: GameFieldConfig,
    callbacks: GameSessionCallbacks,
  ) {
    if (this.sessionRef.current) {
      logEvent(logScopes.GAME_SESSION_LIFECYCLE_SERVICE, {
        event: gameDomainLogEvents.SESSION_START,
        result: logResults.IGNORED_ALREADY_RUNNING,
        roomId: this.roomId,
      });
      return;
    }

    const tickRate = config.GAME_CONFIG.NETWORK_SYNC.PLAYER_POSITION_UPDATE_MS;
    const session = new GameRoomSession(
      this.roomId,
      playerIds,
      playerNamesById,
      fieldConfig,
    );

    this.activePlayerIds.clear();
    playerIds.forEach((playerId) => {
      this.activePlayerIds.add(playerId);
    });

    this.sessionRef.current = session;
    session.start(tickRate, {
      ...callbacks,
      onGameEnd: (payload) => {
        this.activePlayerIds.clear();
        this.sessionRef.current = null;
        callbacks.onGameEnd(payload);
      },
    });

    logEvent(logScopes.GAME_SESSION_LIFECYCLE_SERVICE, {
      event: gameDomainLogEvents.SESSION_START,
      result: logResults.STARTED,
      roomId: this.roomId,
      playerCount: playerIds.length,
    });
  }

  public dispose(): void {
    this.sessionRef.current?.dispose();
    this.sessionRef.current = null;
    this.activePlayerIds.clear();
  }
}