diff --git a/apps/client/src/config/index.ts b/apps/client/src/config/index.ts index 3a98026..d131dba 100644 --- a/apps/client/src/config/index.ts +++ b/apps/client/src/config/index.ts @@ -1,4 +1,40 @@ import { config as sharedConfig } from "@repo/shared"; +import type { FieldSizePreset, GameStartPayload } from "@repo/shared"; + +const runtimeMapSize = { + gridCols: sharedConfig.GAME_CONFIG.GRID_COLS, + gridRows: sharedConfig.GAME_CONFIG.GRID_ROWS, +}; + +/** フィールドサイズ種別からクライアント実行中のマップサイズを更新する */ +export const setRuntimeMapSizeByPreset = (preset: FieldSizePreset): void => { + const resolved = sharedConfig.resolveFieldGridSize(preset); + runtimeMapSize.gridCols = resolved.cols; + runtimeMapSize.gridRows = resolved.rows; +}; + +/** GAME_STARTペイロードを優先してクライアント実行中のマップサイズを更新する */ +export const applyRuntimeMapSizeFromGameStart = ( + payload: Pick, +): void => { + if ( + Number.isFinite(payload.gridCols) + && Number.isFinite(payload.gridRows) + && payload.gridCols > 0 + && payload.gridRows > 0 + ) { + runtimeMapSize.gridCols = payload.gridCols; + runtimeMapSize.gridRows = payload.gridRows; + return; + } + + setRuntimeMapSizeByPreset(payload.fieldSizePreset); +}; + +/** クライアント実行中マップサイズを既定値へ戻す */ +export const resetRuntimeMapSizeToDefault = (): void => { + setRuntimeMapSizeByPreset(sharedConfig.GAME_CONFIG.DEFAULT_FIELD_PRESET); +}; const sharedBombRenderScale = (sharedConfig.GAME_CONFIG as { BOMB_RENDER_SCALE?: number }) @@ -66,6 +102,12 @@ const GAME_CONFIG = { ...sharedConfig.GAME_CONFIG, ...CLIENT_GAME_CONFIG, + get GRID_COLS(): number { + return runtimeMapSize.gridCols; + }, + get GRID_ROWS(): number { + return runtimeMapSize.gridRows; + }, get MAP_WIDTH_PX(): number { return this.GRID_COLS * this.GRID_CELL_SIZE; }, diff --git a/apps/client/src/hooks/useSocketSubscriptions.ts b/apps/client/src/hooks/useSocketSubscriptions.ts index 3af4fa3..25b53a7 100644 --- a/apps/client/src/hooks/useSocketSubscriptions.ts +++ b/apps/client/src/hooks/useSocketSubscriptions.ts @@ -5,8 +5,12 @@ */ import { useEffect } from "react"; import { socketManager } from "@client/network/SocketManager"; +import { + applyRuntimeMapSizeFromGameStart, + setRuntimeMapSizeByPreset, +} from "@client/config"; import { domain } from "@repo/shared"; -import type { GameResultPayload } from "@repo/shared"; +import type { GameResultPayload, GameStartPayload } from "@repo/shared"; import type { AppFlowAction } from "./types/appFlowState"; type UseSocketSubscriptionsParams = { @@ -18,7 +22,7 @@ type AppSocketHandlers = { handleConnect: (id: string) => void; handleRoomUpdate: (updatedRoom: domain.room.Room) => void; - handleGameStart: () => void; + handleGameStart: (payload: GameStartPayload) => void; handleGameResult: (payload: GameResultPayload) => void; }; @@ -76,6 +80,7 @@ handleRoomUpdate: (updatedRoom: domain.room.Room) => { completeJoinRequest(); + setRuntimeMapSizeByPreset(updatedRoom.fieldSizePreset); if ( scenePhase === domain.app.ScenePhase.PLAYING || scenePhase === domain.app.ScenePhase.RESULT @@ -86,7 +91,8 @@ } }, - handleGameStart: () => { + handleGameStart: (payload) => { + applyRuntimeMapSizeFromGameStart(payload); dispatchAppFlow({ type: "setPlaying" }); }, diff --git a/apps/client/src/scenes/game/hooks/useGameSceneController.ts b/apps/client/src/scenes/game/hooks/useGameSceneController.ts index aabba96..9d57079 100644 --- a/apps/client/src/scenes/game/hooks/useGameSceneController.ts +++ b/apps/client/src/scenes/game/hooks/useGameSceneController.ts @@ -19,9 +19,12 @@ const DEFAULT_TEAM_PAINT_RATES = new Array( config.GAME_CONFIG.TEAM_COUNT, ).fill(0); -const DEFAULT_MINIMAP_TEAM_IDS = new Array( - config.GAME_CONFIG.GRID_COLS * config.GAME_CONFIG.GRID_ROWS, -).fill(-1); + +const createDefaultMiniMapTeamIds = (): number[] => { + return new Array( + config.GAME_CONFIG.GRID_COLS * config.GAME_CONFIG.GRID_ROWS, + ).fill(-1); +}; type SceneControllerState = { timeLeft: string; @@ -38,14 +41,16 @@ | { type: "syncMiniMap"; payload: MiniMapState } | { type: "reset" }; -const INITIAL_SCENE_CONTROLLER_STATE: SceneControllerState = { - timeLeft: getInitialTimeDisplay(), - startCountdownText: null, - isInputEnabled: false, - teamPaintRates: DEFAULT_TEAM_PAINT_RATES, - miniMapTeamIds: DEFAULT_MINIMAP_TEAM_IDS, - localBombHitCount: 0, - localPlayerPosition: null, +const createInitialSceneControllerState = (): SceneControllerState => { + return { + timeLeft: getInitialTimeDisplay(), + startCountdownText: null, + isInputEnabled: false, + teamPaintRates: DEFAULT_TEAM_PAINT_RATES, + miniMapTeamIds: createDefaultMiniMapTeamIds(), + localBombHitCount: 0, + localPlayerPosition: null, + }; }; const sceneControllerReducer = ( @@ -73,7 +78,7 @@ }; } case "reset": { - return INITIAL_SCENE_CONTROLLER_STATE; + return createInitialSceneControllerState(); } default: { return state; @@ -87,7 +92,7 @@ const gameManagerRef = useRef(null); const [state, dispatch] = useReducer( sceneControllerReducer, - INITIAL_SCENE_CONTROLLER_STATE, + createInitialSceneControllerState(), ); useEffect(() => { diff --git a/apps/server/src/application/coordinators/startGameCoordinator.ts b/apps/server/src/application/coordinators/startGameCoordinator.ts index 51c2d2c..f974987 100644 --- a/apps/server/src/application/coordinators/startGameCoordinator.ts +++ b/apps/server/src/application/coordinators/startGameCoordinator.ts @@ -5,6 +5,7 @@ import { type StartGameOutputPort } from "@server/domains/game/application/ports/gameUseCasePorts"; import type { FieldSizePreset } from "@repo/shared"; import { config } from "@server/config"; +import { config as sharedConfig } from "@repo/shared"; import type { StartGameCoordinatorDeps } from "./coordinatorDeps"; import { startGameUseCase } from "@server/domains/game/application/useCases/startGameUseCase"; import { @@ -75,6 +76,9 @@ requestedFieldSizePreset ?? updatedRoom.fieldSizePreset ?? config.GAME_CONFIG.DEFAULT_FIELD_PRESET; + const resolvedGridSize = sharedConfig.resolveFieldGridSize( + resolvedFieldSizePreset, + ); updatedRoom.fieldSizePreset = resolvedFieldSizePreset; logEvent(logScopes.GAME_USE_CASE, { @@ -111,7 +115,11 @@ startGameUseCase({ roomId: updatedRoom.roomId, - fieldSizePreset: resolvedFieldSizePreset, + fieldConfig: { + fieldSizePreset: resolvedFieldSizePreset, + gridCols: resolvedGridSize.cols, + gridRows: resolvedGridSize.rows, + }, playerIds: sessionPlayerIds, playerNamesById, gameSession: gameManager, diff --git a/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts b/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts index adadf12..0ff7535 100644 --- a/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts +++ b/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts @@ -24,6 +24,8 @@ /** ゲーム開始時に固定するフィールド設定情報 */ export type GameFieldConfig = { fieldSizePreset: FieldSizePreset; + gridCols: number; + gridRows: number; }; /** ゲーム開始ユースケースが利用するゲーム管理入力ポート */ diff --git a/apps/server/src/domains/game/application/services/GameRoomSession.ts b/apps/server/src/domains/game/application/services/GameRoomSession.ts index 056e8aa..1958875 100644 --- a/apps/server/src/domains/game/application/services/GameRoomSession.ts +++ b/apps/server/src/domains/game/application/services/GameRoomSession.ts @@ -50,7 +50,10 @@ fieldConfig: GameFieldConfig, ) { this.players = new Map(); - this.mapStore = new MapStore(); + this.mapStore = new MapStore({ + gridCols: fieldConfig.gridCols, + gridRows: fieldConfig.gridRows, + }); this.bombStateStore = new BombStateStore(); this.fieldConfig = fieldConfig; @@ -62,7 +65,15 @@ // 算出したチームIDを指定してプレイヤーを生成する const playerName = playerNamesById[playerId] ?? playerId; - const player = createSpawnedPlayer(playerId, playerName, assignedTeamId); + const player = createSpawnedPlayer( + playerId, + playerName, + assignedTeamId, + { + gridCols: fieldConfig.gridCols, + gridRows: fieldConfig.gridRows, + }, + ); this.players.set(playerId, player); }); @@ -99,6 +110,8 @@ this.gameLoop = new GameLoop({ roomId: this.roomId, tickRate, + gridCols: this.fieldConfig.gridCols, + gridRows: this.fieldConfig.gridRows, players: this.players, mapStore: this.mapStore, activeBombRegistry: this.bombStateStore.activeBombRegistry, diff --git a/apps/server/src/domains/game/application/services/bot/movement/MovePlanner.ts b/apps/server/src/domains/game/application/services/bot/movement/MovePlanner.ts index 49d8cdb..915be6e 100644 --- a/apps/server/src/domains/game/application/services/bot/movement/MovePlanner.ts +++ b/apps/server/src/domains/game/application/services/bot/movement/MovePlanner.ts @@ -4,6 +4,11 @@ */ import { config } from "@server/config"; +type MapGridSize = { + gridCols: number; + gridRows: number; +}; + const clamp = (value: number, min: number, max: number): number => { return Math.max(min, Math.min(max, value)); }; @@ -14,6 +19,7 @@ y: number, targetCol: number, targetRow: number, + size: MapGridSize, ): { nextX: number; nextY: number } => { const targetX = targetCol + 0.5; const targetY = targetRow + 0.5; @@ -35,7 +41,7 @@ const nextY = y + diffY * ratio; return { - nextX: clamp(nextX, 0, config.GAME_CONFIG.GRID_COLS - 0.001), - nextY: clamp(nextY, 0, config.GAME_CONFIG.GRID_ROWS - 0.001), + nextX: clamp(nextX, 0, size.gridCols - 0.001), + nextY: clamp(nextY, 0, size.gridRows - 0.001), }; }; diff --git a/apps/server/src/domains/game/application/services/bot/orchestrators/BotTurnOrchestrator.ts b/apps/server/src/domains/game/application/services/bot/orchestrators/BotTurnOrchestrator.ts index 470c413..9550ec8 100644 --- a/apps/server/src/domains/game/application/services/bot/orchestrators/BotTurnOrchestrator.ts +++ b/apps/server/src/domains/game/application/services/bot/orchestrators/BotTurnOrchestrator.ts @@ -12,18 +12,28 @@ import { BotStateStore } from "../state/BotStateStore.js"; import type { BotDecision } from "../types/BotTypes.js"; +type MapGridSize = { + gridCols: number; + gridRows: number; +}; + const clamp = (value: number, min: number, max: number): number => { return Math.max(min, Math.min(max, value)); }; /** Botの1tick分の意思決定を提供するオーケストレータ */ export class BotTurnOrchestrator { + private readonly mapSize: MapGridSize; private stateStore = new BotStateStore(); private readonly hitStunPolicy = new BotHitStunPolicy({ hitStunMs: config.GAME_CONFIG.PLAYER_HIT_STUN_MS, }); private readonly respawnAtMsByBotId = new Map(); + constructor(mapSize: MapGridSize) { + this.mapSize = mapSize; + } + public decide( botPlayerId: BotPlayerId, player: Player, @@ -31,9 +41,8 @@ nowMs: number, elapsedMs: number, ): BotDecision { - const { GRID_COLS, GRID_ROWS } = config.GAME_CONFIG; - const currentCol = clamp(Math.floor(player.x), 0, GRID_COLS - 1); - const currentRow = clamp(Math.floor(player.y), 0, GRID_ROWS - 1); + const currentCol = clamp(Math.floor(player.x), 0, this.mapSize.gridCols - 1); + const currentRow = clamp(Math.floor(player.y), 0, this.mapSize.gridRows - 1); const currentState = this.stateStore.getOrCreate(botPlayerId, { targetCol: currentCol, @@ -49,8 +58,8 @@ this.respawnAtMsByBotId.delete(botPlayerId); this.stateStore.update(botPlayerId, (state) => ({ ...state, - targetCol: clamp(Math.floor(player.initialX), 0, GRID_COLS - 1), - targetRow: clamp(Math.floor(player.initialY), 0, GRID_ROWS - 1), + targetCol: clamp(Math.floor(player.initialX), 0, this.mapSize.gridCols - 1), + targetRow: clamp(Math.floor(player.initialY), 0, this.mapSize.gridRows - 1), })); return { nextX: player.initialX, @@ -78,7 +87,7 @@ config.BOT_AI_CONFIG.TARGET_REACHED_EPSILON; const nextTarget = reachedTarget - ? chooseNextTarget(currentCol, currentRow, gridColors) + ? chooseNextTarget(currentCol, currentRow, gridColors, this.mapSize) : { col: currentState.targetCol, row: currentState.targetRow }; const moved = moveTowardsTarget( @@ -86,6 +95,7 @@ player.y, nextTarget.col, nextTarget.row, + this.mapSize, ); const bombDecision = decideBombPlacement( diff --git a/apps/server/src/domains/game/application/services/bot/policies/TargetSelectionPolicy.ts b/apps/server/src/domains/game/application/services/bot/policies/TargetSelectionPolicy.ts index 1024753..06788e5 100644 --- a/apps/server/src/domains/game/application/services/bot/policies/TargetSelectionPolicy.ts +++ b/apps/server/src/domains/game/application/services/bot/policies/TargetSelectionPolicy.ts @@ -5,6 +5,11 @@ import { config } from "@server/config"; import type { BotTarget } from "../types/BotTypes.js"; +type MapGridSize = { + gridCols: number; + gridRows: number; +}; + const UNPAINTED_TEAM_ID = -1; const clamp = (value: number, min: number, max: number): number => { @@ -29,9 +34,10 @@ col: number, row: number, gridColors: number[], + size: MapGridSize, ): BotTarget => { const { UNPAINTED_PRIORITY_STRENGTH } = config.BOT_AI_CONFIG; - const { GRID_COLS, GRID_ROWS } = config.GAME_CONFIG; + const { gridCols, gridRows } = size; const candidates = [ { col: col + 1, row }, { col: col - 1, row }, @@ -40,9 +46,9 @@ ].filter((candidate) => { return ( candidate.col >= 0 && - candidate.col < GRID_COLS && + candidate.col < gridCols && candidate.row >= 0 && - candidate.row < GRID_ROWS + candidate.row < gridRows ); }); @@ -52,7 +58,7 @@ const unpaintedCandidates = candidates.filter((candidate) => { return ( - getCellTeamId(gridColors, candidate.col, candidate.row, GRID_COLS) === + getCellTeamId(gridColors, candidate.col, candidate.row, gridCols) === UNPAINTED_TEAM_ID ); }); diff --git a/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts b/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts index b1ef6f3..1cb24f0 100644 --- a/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts @@ -6,6 +6,7 @@ import type { GameOutputPort } from "../ports/gameUseCasePorts"; import { config } from "@server/config"; import { logEvent } from "@server/logging/logger"; +import { config as sharedConfig } from "@repo/shared"; import { gameUseCaseLogEvents, logResults, logScopes } from "@server/logging/index"; type ReadyForGameUseCaseParams = { @@ -55,6 +56,16 @@ serverNow: Date.now(), fieldSizePreset: fieldConfig?.fieldSizePreset ?? config.GAME_CONFIG.DEFAULT_FIELD_PRESET, + gridCols: + fieldConfig?.gridCols + ?? sharedConfig.resolveFieldGridSize( + config.GAME_CONFIG.DEFAULT_FIELD_PRESET, + ).cols, + gridRows: + fieldConfig?.gridRows + ?? sharedConfig.resolveFieldGridSize( + config.GAME_CONFIG.DEFAULT_FIELD_PRESET, + ).rows, }); logEvent(logScopes.GAME_USE_CASE, { event: gameUseCaseLogEvents.GAME_START, diff --git a/apps/server/src/domains/game/application/useCases/startGameUseCase.ts b/apps/server/src/domains/game/application/useCases/startGameUseCase.ts index 9cf60b2..5301633 100644 --- a/apps/server/src/domains/game/application/useCases/startGameUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/startGameUseCase.ts @@ -18,7 +18,7 @@ type StartGameUseCaseParams = { roomId: string; - fieldSizePreset: GameFieldConfig["fieldSizePreset"]; + fieldConfig: GameFieldConfig; playerIds: string[]; playerNamesById: Record; gameSession: StartGamePort; @@ -58,7 +58,7 @@ /** ゲームセッション開始とティック通知,終了通知を実行する */ export const startGameUseCase = ({ roomId, - fieldSizePreset, + fieldConfig, playerIds, playerNamesById, gameSession, @@ -83,7 +83,7 @@ playerIds, playerNamesById, { - fieldSizePreset, + ...fieldConfig, }, { onTick: (tickData) => { @@ -111,10 +111,12 @@ ); const startTime = gameSession.getRoomStartTime() || Date.now(); - const fieldConfig = gameSession.getRoomFieldConfig(); + const sessionFieldConfig = gameSession.getRoomFieldConfig() ?? fieldConfig; output.publishGameStartToRoom(roomId, { startTime, serverNow: Date.now(), - fieldSizePreset: fieldConfig?.fieldSizePreset ?? fieldSizePreset, + fieldSizePreset: sessionFieldConfig.fieldSizePreset, + gridCols: sessionFieldConfig.gridCols, + gridRows: sessionFieldConfig.gridRows, }); }; diff --git a/apps/server/src/domains/game/entities/map/MapStore.ts b/apps/server/src/domains/game/entities/map/MapStore.ts index ec06150..6742b1c 100644 --- a/apps/server/src/domains/game/entities/map/MapStore.ts +++ b/apps/server/src/domains/game/entities/map/MapStore.ts @@ -6,6 +6,11 @@ import { createInitialGridColors } from "./mapGrid.js"; import { paintCellIfChanged } from "./mapPainting.js"; +type MapGridSize = { + gridCols: number; + gridRows: number; +}; + /** ルーム内マップの塗り状態と更新差分を管理するストア */ export class MapStore { // 全マスの現在の色(teamId)を保持 @@ -13,9 +18,9 @@ // 次回の送信ループで送る差分リスト private pendingUpdates: domain.game.gridMap.CellUpdate[]; - constructor() { + constructor(size?: MapGridSize) { // 初期状態は -1 (無色) などで初期化 - this.gridColors = createInitialGridColors(); + this.gridColors = createInitialGridColors(size); this.pendingUpdates = []; } diff --git a/apps/server/src/domains/game/entities/map/mapGrid.ts b/apps/server/src/domains/game/entities/map/mapGrid.ts index 9cae240..1de2039 100644 --- a/apps/server/src/domains/game/entities/map/mapGrid.ts +++ b/apps/server/src/domains/game/entities/map/mapGrid.ts @@ -4,8 +4,15 @@ */ import { config } from "@server/config"; +type MapGridSize = { + gridCols: number; + gridRows: number; +}; + /** マップ全セルを未塗り状態で初期化した配列を返す */ -export const createInitialGridColors = (): number[] => { - const totalCells = config.GAME_CONFIG.GRID_COLS * config.GAME_CONFIG.GRID_ROWS; +export const createInitialGridColors = (size?: MapGridSize): number[] => { + const gridCols = size?.gridCols ?? config.GAME_CONFIG.GRID_COLS; + const gridRows = size?.gridRows ?? config.GAME_CONFIG.GRID_ROWS; + const totalCells = gridCols * gridRows; return new Array(totalCells).fill(-1); }; diff --git a/apps/server/src/domains/game/entities/player/playerPosition.ts b/apps/server/src/domains/game/entities/player/playerPosition.ts index 316e400..8ae376f 100644 --- a/apps/server/src/domains/game/entities/player/playerPosition.ts +++ b/apps/server/src/domains/game/entities/player/playerPosition.ts @@ -5,7 +5,24 @@ import { domain } from "@repo/shared"; import { Player } from "./Player.js"; +type MapGridSize = { + gridCols: number; + gridRows: number; +}; + /** プレイヤー座標に対応するグリッドインデックスを返す */ -export const getPlayerGridIndex = (player: Player): number | null => { +export const getPlayerGridIndex = ( + player: Player, + size?: MapGridSize, +): number | null => { + if (size) { + return domain.game.gridMap.getGridIndexFromPositionWithSize( + player.x, + player.y, + size.gridCols, + size.gridRows, + ); + } + return domain.game.gridMap.getGridIndexFromPosition(player.x, player.y); }; diff --git a/apps/server/src/domains/game/entities/player/playerSpawn.ts b/apps/server/src/domains/game/entities/player/playerSpawn.ts index 62340bd..b613979 100644 --- a/apps/server/src/domains/game/entities/player/playerSpawn.ts +++ b/apps/server/src/domains/game/entities/player/playerSpawn.ts @@ -5,19 +5,27 @@ import { config } from "@server/config"; import { Player } from "./Player.js"; +type SpawnMapSize = { + gridCols: number; + gridRows: number; +}; + /** プレイヤーを生成し,初期スポーン座標を設定して返す */ // 💡 引数に teamId を追加 export const createSpawnedPlayer = ( id: string, name: string, teamId: number, + mapSize?: SpawnMapSize, ): Player => { const player = new Player(id, name, teamId); // ここにteamIdを渡す! - const { GRID_COLS, GRID_ROWS, TEAM_COUNT } = config.GAME_CONFIG; + const { TEAM_COUNT } = config.GAME_CONFIG; + const gridCols = mapSize?.gridCols ?? config.GAME_CONFIG.GRID_COLS; + const gridRows = mapSize?.gridRows ?? config.GAME_CONFIG.GRID_ROWS; - let baseX = GRID_COLS / 2; - let baseY = GRID_ROWS / 2; + let baseX = gridCols / 2; + let baseY = gridRows / 2; switch (player.teamId % TEAM_COUNT) { case 0: // 左上 @@ -25,24 +33,24 @@ baseY = 2; break; case 1: // 右下 - baseX = GRID_COLS - 2; - baseY = GRID_ROWS - 2; + baseX = gridCols - 2; + baseY = gridRows - 2; break; case 2: // 右上 - baseX = GRID_COLS - 2; + baseX = gridCols - 2; baseY = 2; break; case 3: // 左下 baseX = 2; - baseY = GRID_ROWS - 2; + baseY = gridRows - 2; break; } const scatterX = (Math.random() - 0.5) * 2; const scatterY = (Math.random() - 0.5) * 2; - player.x = Math.max(1, Math.min(GRID_COLS - 1, baseX + scatterX)); - player.y = Math.max(1, Math.min(GRID_ROWS - 1, baseY + scatterY)); + player.x = Math.max(1, Math.min(gridCols - 1, baseX + scatterX)); + player.y = Math.max(1, Math.min(gridRows - 1, baseY + scatterY)); // リスポーン時に戻る座標として初期位置を保持する player.initialX = player.x; diff --git a/apps/server/src/domains/game/loop/GameLoop.ts b/apps/server/src/domains/game/loop/GameLoop.ts index 0cbfaad..3a8ae13 100644 --- a/apps/server/src/domains/game/loop/GameLoop.ts +++ b/apps/server/src/domains/game/loop/GameLoop.ts @@ -34,6 +34,8 @@ export type GameLoopOptions = { roomId: string; tickRate: number; + gridCols: number; + gridRows: number; players: Map; mapStore: MapStore; activeBombRegistry: ActiveBombRegistry; @@ -65,9 +67,10 @@ private lastSentPlayers: Map = new Map(); private disconnectedBotControlledPlayerIds: Set = new Set(); - private botTurnOrchestrator: BotTurnOrchestrator = new BotTurnOrchestrator(); + private readonly mapSize: { gridCols: number; gridRows: number }; + private botTurnOrchestrator: BotTurnOrchestrator; private readonly botReceivedHitCountById = new Map(); - private readonly hurricaneSystem = new HurricaneSystem(); + private readonly hurricaneSystem: HurricaneSystem; private readonly roomId: string; private readonly tickRate: number; @@ -79,6 +82,12 @@ constructor(options: GameLoopOptions) { this.roomId = options.roomId; this.tickRate = options.tickRate; + this.mapSize = { + gridCols: options.gridCols, + gridRows: options.gridRows, + }; + this.botTurnOrchestrator = new BotTurnOrchestrator(this.mapSize); + this.hurricaneSystem = new HurricaneSystem(this.mapSize); this.players = options.players; this.mapStore = options.mapStore; this.activeBombRegistry = options.activeBombRegistry; @@ -311,7 +320,7 @@ this.players.forEach((player) => { gridEntries.push({ playerId: player.id, - gridIndex: getPlayerGridIndex(player), + gridIndex: getPlayerGridIndex(player, this.mapSize), teamId: player.teamId, player, }); diff --git a/apps/server/src/domains/game/loop/HurricaneSystem.ts b/apps/server/src/domains/game/loop/HurricaneSystem.ts index b97d518..7c49b1a 100644 --- a/apps/server/src/domains/game/loop/HurricaneSystem.ts +++ b/apps/server/src/domains/game/loop/HurricaneSystem.ts @@ -20,6 +20,11 @@ rotationRad: number; }; +type MapGridSize = { + gridCols: number; + gridRows: number; +}; + type HurricaneSyncSnapshot = { x: number; y: number; @@ -68,6 +73,7 @@ /** ハリケーン状態の生成更新と被弾判定を管理する */ export class HurricaneSystem { + private readonly mapSize: MapGridSize; private hasSpawned = false; private hurricanes: HurricaneState[] = []; private readonly lastHitAtMsByPlayerId = new Map(); @@ -76,6 +82,10 @@ HurricaneSyncSnapshot >(); + constructor(mapSize: MapGridSize) { + this.mapSize = mapSize; + } + /** 残り時間しきい値到達時にハリケーンを一度だけ生成する */ public ensureSpawned(elapsedMs: number): void { if (!config.GAME_CONFIG.HURRICANE_ENABLED || this.hasSpawned) { @@ -101,8 +111,8 @@ return; } - const maxX = config.GAME_CONFIG.GRID_COLS; - const maxY = config.GAME_CONFIG.GRID_ROWS; + const maxX = this.mapSize.gridCols; + const maxY = this.mapSize.gridRows; this.hurricanes.forEach((hurricane) => { hurricane.x += hurricane.vx * deltaSec; @@ -210,8 +220,8 @@ /** ハリケーン初期状態を生成する */ private createHurricane(index: number): HurricaneState { const radius = config.GAME_CONFIG.HURRICANE_DIAMETER_GRID / 2; - const x = this.randomInRange(radius, config.GAME_CONFIG.GRID_COLS - radius); - const y = this.randomInRange(radius, config.GAME_CONFIG.GRID_ROWS - radius); + const x = this.randomInRange(radius, this.mapSize.gridCols - radius); + const y = this.randomInRange(radius, this.mapSize.gridRows - radius); const directionRad = this.randomInRange(0, Math.PI * 2); const speed = config.GAME_CONFIG.HURRICANE_MOVE_SPEED; diff --git a/packages/shared/src/domains/game/gridMap/gridMap.logic.ts b/packages/shared/src/domains/game/gridMap/gridMap.logic.ts index bb9d466..226feed 100644 --- a/packages/shared/src/domains/game/gridMap/gridMap.logic.ts +++ b/packages/shared/src/domains/game/gridMap/gridMap.logic.ts @@ -1,20 +1,35 @@ import { GAME_CONFIG } from "../../../config/gameConfig"; +const toGridIndexWithSize = ( + x: number, + y: number, + gridCols: number, + gridRows: number, +): number | null => { + const col = Math.floor(x); + const row = Math.floor(y); + + if (col < 0 || col >= gridCols || row < 0 || row >= gridRows) { + return null; + } + + return row * gridCols + col; +}; + +/** グリッド座標から1次元配列インデックスを取得する(サイズ指定版) */ +export function getGridIndexFromPositionWithSize( + x: number, + y: number, + gridCols: number, + gridRows: number, +): number | null { + return toGridIndexWithSize(x, y, gridCols, gridRows); +} + /** * グリッド座標から1次元配列インデックスを取得する(中心点判定) */ export function getGridIndexFromPosition(x: number, y: number): number | null { const { GRID_COLS, GRID_ROWS } = GAME_CONFIG; - - // 座標がどのマス(列・行)に属するか計算 - const col = Math.floor(x); - const row = Math.floor(y); - - // マップ外の場合は null を返す - if (col < 0 || col >= GRID_COLS || row < 0 || row >= GRID_ROWS) { - return null; - } - - // 1次元配列のインデックスに変換 (row * 幅 + col) - return row * GRID_COLS + col; + return toGridIndexWithSize(x, y, GRID_COLS, GRID_ROWS); } diff --git a/packages/shared/src/domains/game/gridMap/index.ts b/packages/shared/src/domains/game/gridMap/index.ts index af9db0a..4a6e22e 100644 --- a/packages/shared/src/domains/game/gridMap/index.ts +++ b/packages/shared/src/domains/game/gridMap/index.ts @@ -7,6 +7,9 @@ /** グリッドマップ関連の型を再公開する */ export type { MapState, CellUpdate, GroupedCellUpdates } from "./gridMap.type"; /** グリッド座標変換ロジックを再公開する */ -export { getGridIndexFromPosition } from "./gridMap.logic"; +export { + getGridIndexFromPosition, + getGridIndexFromPositionWithSize, +} from "./gridMap.logic"; /** CellUpdate配列とGroupedCellUpdatesの相互変換を再公開する */ export { groupCellUpdates, ungroupCellUpdates } from "./groupedCellUpdates"; diff --git a/packages/shared/src/protocol/payloads/gamePayloads.ts b/packages/shared/src/protocol/payloads/gamePayloads.ts index 01218a0..76a60ee 100644 --- a/packages/shared/src/protocol/payloads/gamePayloads.ts +++ b/packages/shared/src/protocol/payloads/gamePayloads.ts @@ -107,6 +107,10 @@ serverNow: number; /** 今回のゲームで採用するフィールドサイズ種別 */ fieldSizePreset: FieldSizePreset; + /** 今回のゲームで採用するマップ横幅(グリッド単位) */ + gridCols: number; + /** 今回のゲームで採用するマップ縦幅(グリッド単位) */ + gridRows: number; }; /** start-game イベントで受信するゲーム開始要求 */