diff --git a/apps/client/src/network/handlers/socketEventBridge.ts b/apps/client/src/network/handlers/socketEventBridge.ts index 143fe00..655f720 100644 --- a/apps/client/src/network/handlers/socketEventBridge.ts +++ b/apps/client/src/network/handlers/socketEventBridge.ts @@ -8,6 +8,7 @@ createSocketEventBridge, type ClientToServerEventPayloadMap, type ConnectionLifecycleEventPayloadMap, + type SocketBridgeTarget, type ServerToClientEventPayloadMap, } from "@repo/shared"; @@ -17,10 +18,30 @@ /** クライアント向けの型付きソケットイベント bridge を生成する */ export const createClientSocketEventBridge = (socket: Socket) => { + const bridgeTarget: SocketBridgeTarget = { + on: (event, callback) => { + socket.on(event, callback); + }, + once: (event, callback) => { + socket.once(event, callback); + }, + off: (event, callback) => { + socket.off(event, callback); + }, + emit: (event, payload?: unknown) => { + if (payload === undefined) { + socket.emit(event); + return; + } + + socket.emit(event, payload); + }, + }; + const { onEvent, onceEvent, offEvent, emitEvent } = createSocketEventBridge< ClientInboundEventPayloadMap, ClientToServerEventPayloadMap - >(socket as any); + >(bridgeTarget); return { onEvent, diff --git a/packages/shared/src/config/gameConfig.ts b/packages/shared/src/config/gameConfig.ts index c3dcdbd..7c11d92 100644 --- a/packages/shared/src/config/gameConfig.ts +++ b/packages/shared/src/config/gameConfig.ts @@ -43,36 +43,3 @@ /** プレイヤー情報から teamId を解決できない場合に利用する既定値 */ export const UNKNOWN_TEAM_ID = -1; - -/** teamId が unknown を表す値か判定する */ -export const isUnknownTeamId = (teamId: number): boolean => { - return teamId === UNKNOWN_TEAM_ID; -}; - -/** teamId が有効範囲内かを真偽値で判定する */ -export const isKnownTeamId = (teamId: number): boolean => { - return ( - Number.isInteger(teamId) && teamId >= 0 && teamId < GAME_CONFIG.TEAM_COUNT - ); -}; - -/** TEAM_COUNT と TEAM_NAMES の整合性を検証する */ -export const validateTeamConfig = (): void => { - const { TEAM_COUNT } = GAME_CONFIG; - - if (TEAM_NAMES.length !== TEAM_COUNT) { - throw new Error( - `GAME_CONFIG mismatch: TEAM_NAMES length (${TEAM_NAMES.length}) must equal TEAM_COUNT (${TEAM_COUNT})`, - ); - } -}; - -/** teamId が有効範囲内かを検証する */ -export const assertValidTeamId = (teamId: number): void => { - validateTeamConfig(); - - const { TEAM_COUNT } = GAME_CONFIG; - if (!Number.isInteger(teamId) || teamId < 0 || teamId >= TEAM_COUNT) { - throw new Error(`Invalid teamId: ${teamId}`); - } -}; diff --git a/packages/shared/src/config/index.ts b/packages/shared/src/config/index.ts index d183320..3d3230d 100644 --- a/packages/shared/src/config/index.ts +++ b/packages/shared/src/config/index.ts @@ -16,6 +16,6 @@ assertValidTeamId, isUnknownTeamId, isKnownTeamId, -} from "./gameConfig"; +} from "./teamValidators"; /** ネットワーク共有設定値を再公開する */ export { NETWORK_CONFIG } from "./networkConfig"; diff --git a/packages/shared/src/config/teamValidators.ts b/packages/shared/src/config/teamValidators.ts new file mode 100644 index 0000000..badcc25 --- /dev/null +++ b/packages/shared/src/config/teamValidators.ts @@ -0,0 +1,39 @@ +/** + * teamValidators + * チーム設定に対する検証関数を定義する + * 設定値と参照IDの整合性検査を集約する + */ +import { GAME_CONFIG, TEAM_NAMES, UNKNOWN_TEAM_ID } from "./gameConfig"; + +/** teamId が unknown を表す値か判定する */ +export const isUnknownTeamId = (teamId: number): boolean => { + return teamId === UNKNOWN_TEAM_ID; +}; + +/** teamId が有効範囲内かを真偽値で判定する */ +export const isKnownTeamId = (teamId: number): boolean => { + return ( + Number.isInteger(teamId) && teamId >= 0 && teamId < GAME_CONFIG.TEAM_COUNT + ); +}; + +/** TEAM_COUNT と TEAM_NAMES の整合性を検証する */ +export const validateTeamConfig = (): void => { + const { TEAM_COUNT } = GAME_CONFIG; + + if (TEAM_NAMES.length !== TEAM_COUNT) { + throw new Error( + `GAME_CONFIG mismatch: TEAM_NAMES length (${TEAM_NAMES.length}) must equal TEAM_COUNT (${TEAM_COUNT})`, + ); + } +}; + +/** teamId が有効範囲内かを検証する */ +export const assertValidTeamId = (teamId: number): void => { + validateTeamConfig(); + + const { TEAM_COUNT } = GAME_CONFIG; + if (!Number.isInteger(teamId) || teamId < 0 || teamId >= TEAM_COUNT) { + throw new Error(`Invalid teamId: ${teamId}`); + } +}; diff --git a/packages/shared/src/domains/app/index.ts b/packages/shared/src/domains/app/index.ts new file mode 100644 index 0000000..026f0bb --- /dev/null +++ b/packages/shared/src/domains/app/index.ts @@ -0,0 +1,10 @@ +/** + * index + * app ドメインの公開要素を集約して再公開する + * 画面遷移フェーズの型と定数を外部利用向けに束ねる + */ + +/** 画面遷移フェーズ定数を再公開する */ +export { ScenePhase } from "./app.const"; +/** 画面遷移フェーズ型を再公開する */ +export type { ScenePhase as ScenePhaseType } from "./app.type"; diff --git a/packages/shared/src/domains/game/index.ts b/packages/shared/src/domains/game/index.ts new file mode 100644 index 0000000..3b3cd6b --- /dev/null +++ b/packages/shared/src/domains/game/index.ts @@ -0,0 +1,8 @@ +/** + * index + * game ドメインの公開要素を集約して再公開する + * ゲーム進行で利用する型を外部利用向けに束ねる + */ + +/** ゲーム進行関連の型を再公開する */ +export type { PlayerPositionUpdate, TickData } from "./game.type"; diff --git a/packages/shared/src/domains/gridMap/index.ts b/packages/shared/src/domains/gridMap/index.ts new file mode 100644 index 0000000..5daa48f --- /dev/null +++ b/packages/shared/src/domains/gridMap/index.ts @@ -0,0 +1,10 @@ +/** + * index + * gridMap ドメインの公開要素を集約して再公開する + * 型定義と座標変換ロジックを外部利用向けに束ねる + */ + +/** グリッドマップ関連の型を再公開する */ +export type { MapState, CellUpdate } from "./gridMap.type"; +/** グリッド座標変換ロジックを再公開する */ +export { getGridIndexFromPosition } from "./gridMap.logic"; diff --git a/packages/shared/src/domains/index.ts b/packages/shared/src/domains/index.ts new file mode 100644 index 0000000..15dcb80 --- /dev/null +++ b/packages/shared/src/domains/index.ts @@ -0,0 +1,16 @@ +/** + * index + * domains 配下の公開要素を集約して再公開する + * ドメイン単位で参照できる安定公開面を提供する + */ + +/** app ドメインを再公開する */ +export * as app from "./app"; +/** game ドメインを再公開する */ +export * as game from "./game"; +/** gridMap ドメインを再公開する */ +export * as gridMap from "./gridMap"; +/** player ドメインを再公開する */ +export * as player from "./player"; +/** room ドメインを再公開する */ +export * as room from "./room"; diff --git a/packages/shared/src/domains/player/index.ts b/packages/shared/src/domains/player/index.ts new file mode 100644 index 0000000..9ce56af --- /dev/null +++ b/packages/shared/src/domains/player/index.ts @@ -0,0 +1,8 @@ +/** + * index + * player ドメインの公開要素を集約して再公開する + * プレイヤー契約で利用する型を外部利用向けに束ねる + */ + +/** プレイヤー契約関連の型を再公開する */ +export type { PlayerData, MovePayload } from "./player.type"; diff --git a/packages/shared/src/domains/room/index.ts b/packages/shared/src/domains/room/index.ts new file mode 100644 index 0000000..cd19cd4 --- /dev/null +++ b/packages/shared/src/domains/room/index.ts @@ -0,0 +1,18 @@ +/** + * index + * room ドメインの公開要素を集約して再公開する + * ルーム状態契約の型と定数を外部利用向けに束ねる + */ + +/** ルーム進行フェーズ定数を再公開する */ +export { RoomPhase } from "./room.const"; +/** ルーム進行フェーズ型を再公開する */ +export type { RoomPhase as RoomPhaseType } from "./room.const"; +/** ルーム契約関連の型を再公開する */ +export type { + RoomMember, + Room, + JoinRoomPayload, + JoinRoomRejectedReason, + JoinRoomRejectedPayload, +} from "./room.type"; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 8eb0070..4f69fcd 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,8 +1,14 @@ /** * index * shared パッケージの公開 API を集約して再公開するエントリ - * ドメイン型,プロトコル型,設定値を外部利用向けに束ねる + * 安定公開面と既存互換公開面を併存して外部利用向けに束ねる */ +/** 安定公開面として domains の集約を再公開 */ +export * as domain from "./domains"; +/** 安定公開面として protocol 契約の集約を再公開 */ +export * as contracts from "./protocol"; + +/** 既存互換のため,以下は従来どおり再公開する */ /** グリッドマップ関連の型定義を再公開 */ export * as gridMapTypes from "./domains/gridMap/gridMap.type"; /** グリッドマップ関連のロジックを再公開 */ diff --git a/packages/shared/src/protocol/eventPayloads.ts b/packages/shared/src/protocol/eventPayloads.ts index e444c18..3276144 100644 --- a/packages/shared/src/protocol/eventPayloads.ts +++ b/packages/shared/src/protocol/eventPayloads.ts @@ -16,6 +16,8 @@ /** ゲームイベントのペイロード型を再公開する */ export type { + PlayerSnapshotPayload, + PlayerDeltaPayload, InitialPlayerSyncPayload, DeltaPlayerSyncPayload, UpdatePlayersPayload, diff --git a/packages/shared/src/protocol/index.ts b/packages/shared/src/protocol/index.ts new file mode 100644 index 0000000..f6e00f1 --- /dev/null +++ b/packages/shared/src/protocol/index.ts @@ -0,0 +1,12 @@ +/** + * index + * protocol 配下の公開要素を集約して再公開する + * イベント契約と bridge をまとめた安定公開面を提供する + */ + +/** ソケットイベント契約を再公開する */ +export * from "./events"; +/** ソケットイベント bridge を再公開する */ +export { createSocketEventBridge } from "./socketEventBridge"; +/** ソケットイベント bridge の対象インターフェースを再公開する */ +export type { SocketBridgeTarget } from "./socketEventBridge"; diff --git a/packages/shared/src/protocol/maps/gameEventPayloadMap.ts b/packages/shared/src/protocol/maps/gameEventPayloadMap.ts index 3c69019..2772b08 100644 --- a/packages/shared/src/protocol/maps/gameEventPayloadMap.ts +++ b/packages/shared/src/protocol/maps/gameEventPayloadMap.ts @@ -38,11 +38,11 @@ /** ゲーム関連のサーバー送信イベントペイロード対応表 */ export type GameServerToClientEventPayloadMap = { [SocketEvents.GAME_START]: GameStartPayload; - [SocketEvents.CURRENT_PLAYERS]: CurrentPlayersPayload; - [SocketEvents.NEW_PLAYER]: NewPlayerPayload; - [SocketEvents.UPDATE_PLAYERS]: UpdatePlayersPayload; - [SocketEvents.REMOVE_PLAYER]: RemovePlayerPayload; - [SocketEvents.UPDATE_MAP_CELLS]: UpdateMapCellsPayload; + [SocketEvents.CURRENT_PLAYERS_SYNC]: CurrentPlayersPayload; + [SocketEvents.NEW_PLAYER_SYNC]: NewPlayerPayload; + [SocketEvents.UPDATE_PLAYERS_SYNC]: UpdatePlayersPayload; + [SocketEvents.REMOVE_PLAYER_SYNC]: RemovePlayerPayload; + [SocketEvents.UPDATE_MAP_CELLS_SYNC]: UpdateMapCellsPayload; [SocketEvents.BOMB_PLACED]: BombPlacedPayload; [SocketEvents.BOMB_PLACED_ACK]: BombPlacedAckPayload; [SocketEvents.PLAYER_DEAD]: PlayerDeadPayload; diff --git a/packages/shared/src/protocol/payloads/gamePayloads.ts b/packages/shared/src/protocol/payloads/gamePayloads.ts index 87fb74c..6e1d6b2 100644 --- a/packages/shared/src/protocol/payloads/gamePayloads.ts +++ b/packages/shared/src/protocol/payloads/gamePayloads.ts @@ -10,7 +10,7 @@ PlayerData, } from "../../domains/player/player.type"; -/** GAME_RESULT イベントで送受信するランキング1行 */ +/** game-result イベントで送受信するランキング1行 */ export type GameResultRanking = { rank: number; teamId: number; @@ -18,28 +18,37 @@ paintRate: number; }; -/** GAME_RESULT イベントで送受信する最終結果 */ +/** game-result イベントで送受信する最終結果 */ export type GameResultPayload = { rankings: GameResultRanking[]; }; +/** current-players で配信するプレイヤー全体スナップショット */ +export type PlayerSnapshotPayload = PlayerData[]; + +/** + * update-players で配信するプレイヤー差分配列 + * 帯域最適化のため teamId は含めず,id/x/y のみを配信する + */ +export type PlayerDeltaPayload = PlayerPositionUpdate[]; + /** * 初期同期(current-players)で利用するプレイヤー一覧 * 初期同期用のため teamId を含む完全な PlayerData を配信する */ -export type InitialPlayerSyncPayload = PlayerData[]; +export type InitialPlayerSyncPayload = PlayerSnapshotPayload; /** * 差分同期(update-players)で利用するプレイヤー差分配列 * 帯域最適化のため teamId は含めず,id/x/y のみを配信する */ -export type DeltaPlayerSyncPayload = PlayerPositionUpdate[]; +export type DeltaPlayerSyncPayload = PlayerDeltaPayload; /** update-players イベントで送受信するプレイヤー差分配列 */ -export type UpdatePlayersPayload = DeltaPlayerSyncPayload; +export type UpdatePlayersPayload = PlayerDeltaPayload; /** current-players イベントで送受信するプレイヤー一覧 */ -export type CurrentPlayersPayload = InitialPlayerSyncPayload; +export type CurrentPlayersPayload = PlayerSnapshotPayload; /** update-map-cells イベントで送受信するマップ差分配列 */ export type UpdateMapCellsPayload = CellUpdate[]; @@ -53,18 +62,18 @@ /** remove-player イベントで送受信するプレイヤーID */ export type RemovePlayerPayload = PlayerData["id"]; -/** GAME_START イベントで送受信するゲーム開始情報 */ +/** game-start イベントで送受信するゲーム開始情報 */ export type GameStartPayload = { startTime: number }; -/** START_GAME イベントで受信するゲーム開始要求 */ +/** start-game イベントで受信するゲーム開始要求 */ export type StartGameRequestPayload = { targetPlayerCount?: number; }; -/** MOVE イベントで送受信する移動入力情報 */ +/** move イベントで送受信する移動入力情報 */ export type MovePayload = PlayerMovePayload; -/** PLACE_BOMB イベントで送受信する爆弾設置要求 */ +/** place-bomb イベントで送受信する爆弾設置要求 */ export type PlaceBombPayload = { requestId: string; x: number; @@ -72,7 +81,7 @@ explodeAtElapsedMs: number; }; -/** BOMB_PLACED イベントで送受信する他プレイヤー向け爆弾確定情報,設置者識別は ownerSocketId で扱う */ +/** bomb-placed イベントで送受信する他プレイヤー向け爆弾確定情報,設置者識別は ownerSocketId で扱う */ export type BombPlacedPayload = { bombId: string; ownerSocketId: string; @@ -81,18 +90,18 @@ explodeAtElapsedMs: number; }; -/** BOMB_PLACED_ACK イベントで送受信する設置者向け確定情報 */ +/** bomb-placed-ack イベントで送受信する設置者向け確定情報 */ export type BombPlacedAckPayload = { bombId: string; requestId: string; }; -/** BOMB_HIT_REPORT イベントで送受信する被弾報告 */ +/** bomb-hit-report イベントで送受信する被弾報告 */ export type BombHitReportPayload = { bombId: string; }; -/** PLAYER_DEAD イベントで送受信する死亡プレイヤー情報 */ +/** player-dead イベントで送受信する死亡プレイヤー情報 */ export type PlayerDeadPayload = { playerId: string; }; diff --git a/packages/shared/src/protocol/socketEventBridge.ts b/packages/shared/src/protocol/socketEventBridge.ts index a5f61e2..8055445 100644 --- a/packages/shared/src/protocol/socketEventBridge.ts +++ b/packages/shared/src/protocol/socketEventBridge.ts @@ -8,10 +8,13 @@ /** ソケットブリッジ生成に必要な最小インターフェース */ export type SocketBridgeTarget = { - on: (event: string, callback: (payload: unknown) => void) => void; - once: (event: string, callback: (payload: unknown) => void) => void; - off: (event: string, callback: (payload: unknown) => void) => void; - emit: (event: string, payload?: unknown) => void; + on: (event: string, callback: (payload: TPayload) => void) => void; + once: (event: string, callback: (payload: TPayload) => void) => void; + off: (event: string, callback: (payload: TPayload) => void) => void; + emit: { + (event: string): void; + (event: string, payload: TPayload): void; + }; }; /** @@ -25,21 +28,21 @@ event: TEvent, callback: (payload: TInboundMap[TEvent]) => void ) => { - socket.on(event, callback as (payload: unknown) => void); + socket.on(event, callback); }; const onceEvent = >( event: TEvent, callback: (payload: TInboundMap[TEvent]) => void ) => { - socket.once(event, callback as (payload: unknown) => void); + socket.once(event, callback); }; const offEvent = >( event: TEvent, callback: (payload: TInboundMap[TEvent]) => void ) => { - socket.off(event, callback as (payload: unknown) => void); + socket.off(event, callback); }; function emitEvent>(event: TEvent): void; @@ -50,7 +53,7 @@ return; } - socket.emit(event, payload as unknown); + socket.emit(event, payload); } return { diff --git a/packages/shared/src/protocol/socketEvents.ts b/packages/shared/src/protocol/socketEvents.ts index 9d46c45..f896567 100644 --- a/packages/shared/src/protocol/socketEvents.ts +++ b/packages/shared/src/protocol/socketEvents.ts @@ -19,6 +19,13 @@ READY_FOR_GAME: "ready-for-game", // ゲームプレイ関連イベント名 + CURRENT_PLAYERS_SYNC: "current-players", + NEW_PLAYER_SYNC: "new-player", + UPDATE_PLAYERS_SYNC: "update-players", + REMOVE_PLAYER_SYNC: "remove-player", + UPDATE_MAP_CELLS_SYNC: "update-map-cells", + + // 互換維持のため残す旧キー名 CURRENT_PLAYERS: "current-players", NEW_PLAYER: "new-player", UPDATE_PLAYERS: "update-players",