Newer
Older
PixelPaintWar / apps / client / src / network / handlers / GameHandler.ts
/**
 * GameHandler
 * ゲーム関連のソケット購読と送信APIを提供する
 * シーン層が利用する通信操作を集約する
 */
import type { Socket } from "socket.io-client";
import { contracts as protocol } from "@repo/shared";
import type {
  BombHitReportPayload,
  BombPlacedAckPayload,
  BombPlacedPayload,
  ClientToServerEventPayloadMap,
  ServerToClientEventPayloadMap,
  CurrentPlayersPayload,
  GameResultPayload,
  GameStartPayload,
  MovePayload,
  NewPlayerPayload,
  PlaceBombPayload,
  PlayerDeadPayload,
  RemovePlayerPayload,
  UpdateMapCellsPayload,
  UpdatePlayersPayload,
} from "@repo/shared";
import { createClientSocketEventBridge } from "./socketEventBridge";

/** ゲームシーンが利用するソケット操作の契約 */
export type GameHandler = {
  onCurrentPlayers: (callback: (players: CurrentPlayersPayload) => void) => void;
  offCurrentPlayers: (callback: (players: CurrentPlayersPayload) => void) => void;
  onNewPlayer: (callback: (player: NewPlayerPayload) => void) => void;
  offNewPlayer: (callback: (player: NewPlayerPayload) => void) => void;
  onUpdatePlayers: (callback: (players: UpdatePlayersPayload) => void) => void;
  offUpdatePlayers: (callback: (players: UpdatePlayersPayload) => void) => void;
  onRemovePlayer: (callback: (id: RemovePlayerPayload) => void) => void;
  offRemovePlayer: (callback: (id: RemovePlayerPayload) => void) => void;
  onUpdateMapCells: (callback: (updates: UpdateMapCellsPayload) => void) => void;
  offUpdateMapCells: (callback: (updates: UpdateMapCellsPayload) => void) => void;
  onGameStart: (callback: (data: GameStartPayload) => void) => void;
  onceGameStart: (callback: (data: GameStartPayload) => void) => void;
  offGameStart: (callback: (data: GameStartPayload) => void) => void;
  onGameEnd: (callback: () => void) => void;
  offGameEnd: (callback: () => void) => void;
  onGameResult: (callback: (payload: GameResultPayload) => void) => void;
  offGameResult: (callback: (payload: GameResultPayload) => void) => void;
  onBombPlaced: (callback: (payload: BombPlacedPayload) => void) => void;
  offBombPlaced: (callback: (payload: BombPlacedPayload) => void) => void;
  onBombPlacedAck: (callback: (payload: BombPlacedAckPayload) => void) => void;
  offBombPlacedAck: (callback: (payload: BombPlacedAckPayload) => void) => void;
  onPlayerDead: (callback: (payload: PlayerDeadPayload) => void) => void;
  offPlayerDead: (callback: (payload: PlayerDeadPayload) => void) => void;
  sendMove: (x: number, y: number) => void;
  sendPlaceBomb: (payload: PlaceBombPayload) => void;
  sendBombHitReport: (payload: BombHitReportPayload) => void;
  readyForGame: () => void;
};

/** ソケットインスタンスからゲーム向けハンドラを生成する */
export const createGameHandler = (socket: Socket): GameHandler => {
  const { onEvent, onceEvent, offEvent, emitEvent } = createClientSocketEventBridge(socket);
  type ReceiveEventName = Extract<keyof ServerToClientEventPayloadMap, string>;
  type SendEventName = Extract<keyof ClientToServerEventPayloadMap, string>;

  const createSubscriptionPair = <TEvent extends ReceiveEventName>(event: TEvent) => {
    return {
      on: (callback: (payload: ServerToClientEventPayloadMap[TEvent]) => void) => {
        onEvent(event, callback);
      },
      off: (callback: (payload: ServerToClientEventPayloadMap[TEvent]) => void) => {
        offEvent(event, callback);
      },
    };
  };

  const createPayloadSender = <TEvent extends SendEventName>(event: TEvent) => {
    return (payload: ClientToServerEventPayloadMap[TEvent]) => {
      emitEvent(event, payload);
    };
  };

  const createVoidSender = <TEvent extends SendEventName>(event: TEvent) => {
    return () => {
      emitEvent(event);
    };
  };

  const currentPlayersSubscription = createSubscriptionPair(
    protocol.SocketEvents.CURRENT_PLAYERS
  );
  const newPlayerSubscription = createSubscriptionPair(
    protocol.SocketEvents.NEW_PLAYER
  );
  const updatePlayersSubscription = createSubscriptionPair(
    protocol.SocketEvents.UPDATE_PLAYERS
  );
  const removePlayerSubscription = createSubscriptionPair(
    protocol.SocketEvents.REMOVE_PLAYER
  );
  const updateMapCellsSubscription = createSubscriptionPair(
    protocol.SocketEvents.UPDATE_MAP_CELLS
  );
  const gameEndSubscription = createSubscriptionPair(
    protocol.SocketEvents.GAME_END
  );
  const gameResultSubscription = createSubscriptionPair(
    protocol.SocketEvents.GAME_RESULT
  );
  const bombPlacedSubscription = createSubscriptionPair(
    protocol.SocketEvents.BOMB_PLACED
  );
  const bombPlacedAckSubscription = createSubscriptionPair(
    protocol.SocketEvents.BOMB_PLACED_ACK
  );
  const playerDeadSubscription = createSubscriptionPair(
    protocol.SocketEvents.PLAYER_DEAD
  );
  const sendMovePayload = createPayloadSender(protocol.SocketEvents.MOVE);
  const sendPlaceBombPayload = createPayloadSender(protocol.SocketEvents.PLACE_BOMB);
  const sendBombHitReportPayload = createPayloadSender(
    protocol.SocketEvents.BOMB_HIT_REPORT
  );
  const sendReadyForGame = createVoidSender(protocol.SocketEvents.READY_FOR_GAME);

  return {
    onCurrentPlayers: (callback) => {
      currentPlayersSubscription.on(callback);
    },
    offCurrentPlayers: (callback) => {
      currentPlayersSubscription.off(callback);
    },
    onNewPlayer: (callback) => {
      newPlayerSubscription.on(callback);
    },
    offNewPlayer: (callback) => {
      newPlayerSubscription.off(callback);
    },
    onUpdatePlayers: (callback) => {
      updatePlayersSubscription.on(callback);
    },
    offUpdatePlayers: (callback) => {
      updatePlayersSubscription.off(callback);
    },
    onRemovePlayer: (callback) => {
      removePlayerSubscription.on(callback);
    },
    offRemovePlayer: (callback) => {
      removePlayerSubscription.off(callback);
    },
    onUpdateMapCells: (callback) => {
      updateMapCellsSubscription.on(callback);
    },
    offUpdateMapCells: (callback) => {
      updateMapCellsSubscription.off(callback);
    },
    onGameStart: (callback) => {
      onEvent(protocol.SocketEvents.GAME_START, callback);
    },
    onceGameStart: (callback) => {
      onceEvent(protocol.SocketEvents.GAME_START, callback);
    },
    offGameStart: (callback) => {
      offEvent(protocol.SocketEvents.GAME_START, callback);
    },
    onGameEnd: (callback) => {
      gameEndSubscription.on(callback);
    },
    offGameEnd: (callback) => {
      gameEndSubscription.off(callback);
    },
    onGameResult: (callback) => {
      gameResultSubscription.on(callback);
    },
    offGameResult: (callback) => {
      gameResultSubscription.off(callback);
    },
    onBombPlaced: (callback) => {
      bombPlacedSubscription.on(callback);
    },
    offBombPlaced: (callback) => {
      bombPlacedSubscription.off(callback);
    },
    onBombPlacedAck: (callback) => {
      bombPlacedAckSubscription.on(callback);
    },
    offBombPlacedAck: (callback) => {
      bombPlacedAckSubscription.off(callback);
    },
    onPlayerDead: (callback) => {
      playerDeadSubscription.on(callback);
    },
    offPlayerDead: (callback) => {
      playerDeadSubscription.off(callback);
    },
    sendMove: (x, y) => {
      const payload: MovePayload = { x, y };
      sendMovePayload(payload);
    },
    sendPlaceBomb: (payload) => {
      sendPlaceBombPayload(payload);
    },
    sendBombHitReport: (payload) => {
      sendBombHitReportPayload(payload);
    },
    readyForGame: () => {
      sendReadyForGame();
    }
  };
};