Newer
Older
PixelPaintWar / apps / server / src / domains / game / application / useCases / startGameUseCase.ts
/**
 * startGameUseCase
 * ルーム内プレイヤーでゲームセッションを開始し,進行イベントを通知する
 */
import type {
  BombPlacementPort,
  GameFieldConfig,
  StartGameOutputPort,
  StartGamePort,
} from "../ports/gameUseCasePorts";
import { logEvent } from "@server/logging/logger";
import {
  gameUseCaseLogEvents,
  logResults,
  logScopes,
} from "@server/logging/index";
import { createBotBombActionHandler } from "../services/bot/index.js";

type StartGameUseCaseParams = {
  roomId: string;
  fieldConfig: GameFieldConfig;
  playerIds: string[];
  playerNamesById: Record<string, string>;
  gameSession: StartGamePort;
  bombStore: BombPlacementPort;
  onGameEnd: () => void;
  output: StartGameOutputPort;
};

type TickUpdatePublishParams = {
  roomId: string;
  output: StartGameOutputPort;
  tickData: Parameters<StartGamePort["startRoomSession"]>[3] extends {
    onTick: (data: infer TTickData) => void;
  }
    ? TTickData
    : never;
};

type TickPublishStep = {
  key: "hurricaneSnapshot" | "hurricaneDelta" | "player" | "map";
  run: (params: TickUpdatePublishParams) => void;
};

const TICK_PUBLISH_STEPS: TickPublishStep[] = [
  {
    key: "hurricaneSnapshot",
    run: ({ roomId, output, tickData }) => {
      if (tickData.hurricaneSync.snapshotUpdates.length === 0) {
        return;
      }

      output.publishCurrentHurricanesToRoom(
        roomId,
        tickData.hurricaneSync.snapshotUpdates,
      );
    },
  },
  {
    key: "hurricaneDelta",
    run: ({ roomId, output, tickData }) => {
      if (tickData.hurricaneSync.deltaUpdates.length === 0) {
        return;
      }

      output.publishUpdateHurricanesToRoom(
        roomId,
        tickData.hurricaneSync.deltaUpdates,
      );
    },
  },
  {
    key: "player",
    run: ({ roomId, output, tickData }) => {
      if (tickData.playerUpdates.length === 0) {
        return;
      }

      output.publishUpdatePlayersToRoom(roomId, tickData.playerUpdates);
    },
  },
  {
    key: "map",
    run: ({ roomId, output, tickData }) => {
      if (tickData.cellUpdates.length === 0) {
        return;
      }

      output.publishMapCellUpdatesToRoom(roomId, tickData.cellUpdates);
    },
  },
];

const publishTickUpdates = ({
  roomId,
  output,
  tickData,
}: TickUpdatePublishParams): void => {
  const params: TickUpdatePublishParams = {
    roomId,
    output,
    tickData,
  };

  TICK_PUBLISH_STEPS.forEach((step) => {
    step.run(params);
  });
};

/** ゲームセッション開始とティック通知,終了通知を実行する */
export const startGameUseCase = ({
  roomId,
  fieldConfig,
  playerIds,
  playerNamesById,
  gameSession,
  bombStore,
  onGameEnd,
  output,
}: StartGameUseCaseParams) => {
  const handleBotBombAction = createBotBombActionHandler({
    roomId,
    bombStore,
    output,
  });

  /** Bot被弾検出時にPLAYER_HITをルームへ配信する */
  const handleBotBombHit = (targetPlayerId: string, _bombId: string): void => {
    output.publishPlayerHitToOthersInRoom(roomId, targetPlayerId, {
      playerId: targetPlayerId,
    });
  };

  gameSession.startRoomSession(
    playerIds,
    playerNamesById,
    {
      ...fieldConfig,
    },
    {
      onTick: (tickData) => {
        publishTickUpdates({ roomId, output, tickData });
      },
      onGameEnd: (resultPayload) => {
        logEvent(logScopes.GAME_USE_CASE, {
          event: gameUseCaseLogEvents.GAME_END,
          result: logResults.EMITTED,
          roomId,
          reason: "duration_elapsed",
        });
        output.publishGameEndToRoom(roomId);
        output.publishGameResultToRoom(roomId, resultPayload);
        onGameEnd();
      },
      onBotPlaceBomb: handleBotBombAction,
      onBotBombHit: handleBotBombHit,
      onHurricanePlayerHit: (targetPlayerId) => {
        output.publishHurricaneHitToRoom(roomId, {
          playerId: targetPlayerId,
        });
      },
    },
  );

  const startTime = gameSession.getRoomStartTime() || Date.now();
  const sessionFieldConfig = gameSession.getRoomFieldConfig() ?? fieldConfig;
  output.publishGameStartToRoom(roomId, {
    startTime,
    serverNow: Date.now(),
    fieldSizePreset: sessionFieldConfig.fieldSizePreset,
    gridCols: sessionFieldConfig.gridCols,
    gridRows: sessionFieldConfig.gridRows,
  });
};