diff --git a/apps/client/src/scenes/game/entities/bomb/BombController.ts b/apps/client/src/scenes/game/entities/bomb/BombController.ts index 0f24522..09552ab 100644 --- a/apps/client/src/scenes/game/entities/bomb/BombController.ts +++ b/apps/client/src/scenes/game/entities/bomb/BombController.ts @@ -12,6 +12,7 @@ radiusGrid: number; explodeAtElapsedMs: number; teamId: number; + color: number; }; /** 爆弾1つ分の状態と描画同期を管理するコントローラー */ @@ -19,13 +20,13 @@ private readonly model: BombModel; private readonly view: BombView; - constructor({ x, y, radiusGrid, explodeAtElapsedMs, teamId }: BombControllerOptions) { - this.model = new BombModel({ x, y, radiusGrid, explodeAtElapsedMs, teamId }); + constructor({ x, y, radiusGrid, explodeAtElapsedMs, teamId, color }: BombControllerOptions) { + this.model = new BombModel({ x, y, radiusGrid, explodeAtElapsedMs, teamId, color }); this.view = new BombView(); const pos = this.model.getPosition(); this.view.syncPosition(pos.x, pos.y); - this.view.renderState(this.model.getState(), this.model.getExplosionRadiusGrid(), this.model.getTeamId()); + this.view.renderState(this.model.getState(), this.model.getExplosionRadiusGrid(), this.model.getColor()); } public getDisplayObject() { @@ -34,7 +35,7 @@ public tick(elapsedMs: number): void { this.model.update(elapsedMs); - this.view.renderState(this.model.getState(), this.model.getExplosionRadiusGrid(), this.model.getTeamId()); + this.view.renderState(this.model.getState(), this.model.getExplosionRadiusGrid(), this.model.getColor()); } public isFinished(): boolean { diff --git a/apps/client/src/scenes/game/entities/bomb/BombManager.ts b/apps/client/src/scenes/game/entities/bomb/BombManager.ts index cd62580..ab4efcd 100644 --- a/apps/client/src/scenes/game/entities/bomb/BombManager.ts +++ b/apps/client/src/scenes/game/entities/bomb/BombManager.ts @@ -10,6 +10,7 @@ BombPlacedPayload, PlaceBombPayload, } from "@repo/shared"; +import { config as sharedConfig } from "@repo/shared"; import { LocalPlayerController } from "@client/scenes/game/entities/player/PlayerController"; import { BombController } from "./BombController"; import type { GamePlayers } from "@client/scenes/game/application/game.types"; @@ -24,6 +25,7 @@ explodeAtElapsedMs: number; radiusGrid: number; teamId: number; + color: number; }; /** 爆弾設置時に返す結果型 */ @@ -41,12 +43,11 @@ /** 爆弾エンティティのライフサイクルを管理する */ export class BombManager { - private static readonly UNKNOWN_TEAM_ID = -1; - private worldContainer: Container; private players: GamePlayers; private myId: string; private getElapsedMs: ElapsedMsProvider; + private readonly cachedTeamColors: number[]; private bombs = new Map(); private bombRenderPayloadById = new Map(); private pendingOwnRequestToTempBombId = new Map(); @@ -59,6 +60,7 @@ this.players = players; this.myId = myId; this.getElapsedMs = getElapsedMs; + this.cachedTeamColors = config.GAME_CONFIG.TEAM_COLORS.map((colorCode) => this.parseColorCode(colorCode)); } /** 自プレイヤー位置に爆弾を仮IDで設置し,設置要求を返す */ @@ -81,10 +83,10 @@ explodeAtElapsedMs: elapsedMs + BOMB_FUSE_MS, }; const tempBombId = this.createTempBombId(requestId); + // 自分の爆弾は設置時点で teamId を確定して保持する const ownTeamId = this.resolveTeamIdBySocketId(this.myId); - this.pendingOwnRequestToTempBombId.set(requestId, tempBombId); - this.pendingTempBombIdToOwnRequest.set(tempBombId, requestId); + this.registerPendingOwnRequest(requestId, tempBombId); this.upsertBomb(tempBombId, this.toRenderPayload(payload, ownTeamId)); this.lastBombPlacedElapsedMs = elapsedMs; return { @@ -95,13 +97,14 @@ /** 他プレイヤー向けの爆弾確定イベントを反映する */ public applyPlacedBombFromOthers(payload: BombPlacedPayload): void { + // 通信では ownerSocketId を受け取り,受信時点で teamId を確定する const ownerTeamId = this.resolveTeamIdBySocketId(payload.ownerSocketId); this.upsertBomb(payload.bombId, this.toRenderPayload(payload, ownerTeamId)); } /** 設置者本人向けACKを反映し,仮IDから正式IDへ置換する */ public applyPlacedBombAck(payload: BombPlacedAckPayload): void { - const tempBombId = this.pendingOwnRequestToTempBombId.get(payload.requestId); + const tempBombId = this.getPendingTempBombId(payload.requestId); if (!tempBombId) { return; } @@ -175,7 +178,8 @@ && a.y === b.y && a.explodeAtElapsedMs === b.explodeAtElapsedMs && a.radiusGrid === b.radiusGrid - && a.teamId === b.teamId; + && a.teamId === b.teamId + && a.color === b.color; } private toRenderPayload( @@ -188,18 +192,42 @@ explodeAtElapsedMs: payload.explodeAtElapsedMs, radiusGrid: config.GAME_CONFIG.BOMB_RADIUS_GRID, teamId, + color: this.resolveTeamColorByTeamId(teamId), }; } private resolveTeamIdBySocketId(socketId: string): number { const playerController = this.players[socketId]; if (!playerController) { - return BombManager.UNKNOWN_TEAM_ID; + // 参照できない場合でも描画継続できるように未知チームで扱う + return sharedConfig.UNKNOWN_TEAM_ID; } return playerController.getSnapshot().teamId; } + private resolveTeamColorByTeamId(teamId: number): number { + const teamColor = this.cachedTeamColors[teamId]; + if (!Number.isInteger(teamColor)) { + return config.GAME_CONFIG.MAP_GRID_COLOR; + } + + return teamColor; + } + + private parseColorCode(colorCode: string): number { + const normalizedColorCode = colorCode.startsWith("#") + ? colorCode.slice(1) + : colorCode; + + const parsedColor = Number.parseInt(normalizedColorCode, 16); + if (Number.isNaN(parsedColor)) { + return config.GAME_CONFIG.MAP_GRID_COLOR; + } + + return parsedColor; + } + private createRequestId(_elapsedMs: number): string { this.requestSerial += 1; return `${this.requestSerial}`; @@ -209,6 +237,15 @@ return `temp:${requestId}`; } + private registerPendingOwnRequest(requestId: string, tempBombId: string): void { + this.pendingOwnRequestToTempBombId.set(requestId, tempBombId); + this.pendingTempBombIdToOwnRequest.set(tempBombId, requestId); + } + + private getPendingTempBombId(requestId: string): string | undefined { + return this.pendingOwnRequestToTempBombId.get(requestId); + } + private removePendingRequestByRequestId(requestId: string): void { const tempBombId = this.pendingOwnRequestToTempBombId.get(requestId); if (!tempBombId) { diff --git a/apps/client/src/scenes/game/entities/bomb/BombModel.ts b/apps/client/src/scenes/game/entities/bomb/BombModel.ts index 7cf7897..240c9fd 100644 --- a/apps/client/src/scenes/game/entities/bomb/BombModel.ts +++ b/apps/client/src/scenes/game/entities/bomb/BombModel.ts @@ -14,6 +14,7 @@ radiusGrid: number; explodeAtElapsedMs: number; teamId: number; + color: number; }; /** 爆弾の状態と寿命を管理するモデル */ @@ -23,14 +24,16 @@ private radiusGrid: number; private explodeAtElapsedMs: number; private teamId: number; + private color: number; private state: BombState = "armed"; - constructor({ x, y, radiusGrid, explodeAtElapsedMs, teamId }: BombModelOptions) { + constructor({ x, y, radiusGrid, explodeAtElapsedMs, teamId, color }: BombModelOptions) { this.x = x; this.y = y; this.radiusGrid = radiusGrid; this.explodeAtElapsedMs = explodeAtElapsedMs; this.teamId = teamId; + this.color = color; } public getPosition() { @@ -49,6 +52,10 @@ return this.teamId; } + public getColor(): number { + return this.color; + } + public update(elapsedMs: number): void { if (this.state === "finished") return; diff --git a/apps/client/src/scenes/game/entities/bomb/BombView.ts b/apps/client/src/scenes/game/entities/bomb/BombView.ts index d0a2478..4ce6d8a 100644 --- a/apps/client/src/scenes/game/entities/bomb/BombView.ts +++ b/apps/client/src/scenes/game/entities/bomb/BombView.ts @@ -15,7 +15,7 @@ private explosionGraphic: Graphics; private lastRenderedState: BombState | null = null; private lastRenderedRadiusGrid: number | null = null; - private lastRenderedTeamId: number | null = null; + private lastRenderedColor: number | null = null; constructor() { this.displayObject = new Container(); @@ -33,11 +33,11 @@ this.displayObject.y = gridY * GRID_CELL_SIZE + GRID_CELL_SIZE / 2; } - public renderState(state: BombState, radiusGrid: number, teamId: number): void { + public renderState(state: BombState, radiusGrid: number, color: number): void { if ( this.lastRenderedState === state && this.lastRenderedRadiusGrid === radiusGrid - && this.lastRenderedTeamId === teamId + && this.lastRenderedColor === color ) { return; } @@ -48,45 +48,25 @@ this.lastRenderedState = state; this.lastRenderedRadiusGrid = radiusGrid; - this.lastRenderedTeamId = teamId; - - const teamColor = this.resolveTeamColor(teamId); + this.lastRenderedColor = color; this.bombGraphic.clear(); this.explosionGraphic.clear(); if (state === "armed") { this.bombGraphic.circle(0, 0, bombRadiusPx); - this.bombGraphic.fill({ color: teamColor, alpha: 0.95 }); + this.bombGraphic.fill({ color, alpha: 0.95 }); this.bombGraphic.stroke({ color: 0xffffff, width: 2 }); return; } if (state === "exploded") { this.explosionGraphic.circle(0, 0, explosionRadiusPx); - this.explosionGraphic.fill({ color: teamColor, alpha: 0.35 }); - this.explosionGraphic.stroke({ color: teamColor, width: 3 }); + this.explosionGraphic.fill({ color, alpha: 0.35 }); + this.explosionGraphic.stroke({ color, width: 3 }); } } - private resolveTeamColor(teamId: number): number { - const teamColorCode = config.GAME_CONFIG.TEAM_COLORS[teamId]; - if (typeof teamColorCode !== "string") { - return config.GAME_CONFIG.MAP_GRID_COLOR; - } - - const normalizedColorCode = teamColorCode.startsWith("#") - ? teamColorCode.slice(1) - : teamColorCode; - - const parsedColor = Number.parseInt(normalizedColorCode, 16); - if (Number.isNaN(parsedColor)) { - return config.GAME_CONFIG.MAP_GRID_COLOR; - } - - return parsedColor; - } - public destroy(): void { this.displayObject.destroy({ children: true }); } diff --git a/apps/server/src/domains/game/entities/bomb/bombPlacement.ts b/apps/server/src/domains/game/entities/bomb/bombPlacement.ts index 21585b6..00c4604 100644 --- a/apps/server/src/domains/game/entities/bomb/bombPlacement.ts +++ b/apps/server/src/domains/game/entities/bomb/bombPlacement.ts @@ -21,6 +21,7 @@ bombId, ownerSocketId, }: CreateBombPlacedPayloadParams): BombPlacedPayload => { + // 設置者の識別情報はサーバー確定の socketId を利用する return { bombId, ownerSocketId, diff --git a/packages/shared/src/config/gameConfig.ts b/packages/shared/src/config/gameConfig.ts index d263ef7..6d503a3 100644 --- a/packages/shared/src/config/gameConfig.ts +++ b/packages/shared/src/config/gameConfig.ts @@ -38,6 +38,9 @@ "黄チーム", ] as const; +/** プレイヤー情報から teamId を解決できない場合に利用する既定値 */ +export const UNKNOWN_TEAM_ID = -1; + /** TEAM_COUNT と TEAM_NAMES の整合性を検証する */ export const validateTeamConfig = (): void => { const { TEAM_COUNT } = GAME_CONFIG; diff --git a/packages/shared/src/config/index.ts b/packages/shared/src/config/index.ts index 92611fe..ca34ae3 100644 --- a/packages/shared/src/config/index.ts +++ b/packages/shared/src/config/index.ts @@ -1,4 +1,5 @@ export { GAME_CONFIG } from "./gameConfig"; export { TEAM_NAMES } from "./gameConfig"; +export { UNKNOWN_TEAM_ID } from "./gameConfig"; export { validateTeamConfig, assertValidTeamId } from "./gameConfig"; export { NETWORK_CONFIG } from "./networkConfig"; diff --git a/packages/shared/src/protocol/payloads/gamePayloads.ts b/packages/shared/src/protocol/payloads/gamePayloads.ts index e8543e5..22f5900 100644 --- a/packages/shared/src/protocol/payloads/gamePayloads.ts +++ b/packages/shared/src/protocol/payloads/gamePayloads.ts @@ -64,7 +64,7 @@ explodeAtElapsedMs: number; }; -/** BOMB_PLACED イベントで送受信する他プレイヤー向け爆弾確定情報 */ +/** BOMB_PLACED イベントで送受信する他プレイヤー向け爆弾確定情報,設置者識別は ownerSocketId で扱う */ export type BombPlacedPayload = { bombId: string; ownerSocketId: string;