diff --git a/apps/client/src/scenes/game/entities/bomb/BombController.ts b/apps/client/src/scenes/game/entities/bomb/BombController.ts index 5ef5991..0f24522 100644 --- a/apps/client/src/scenes/game/entities/bomb/BombController.ts +++ b/apps/client/src/scenes/game/entities/bomb/BombController.ts @@ -11,6 +11,7 @@ y: number; radiusGrid: number; explodeAtElapsedMs: number; + teamId: number; }; /** 爆弾1つ分の状態と描画同期を管理するコントローラー */ @@ -18,13 +19,13 @@ private readonly model: BombModel; private readonly view: BombView; - constructor({ x, y, radiusGrid, explodeAtElapsedMs }: BombControllerOptions) { - this.model = new BombModel({ x, y, radiusGrid, explodeAtElapsedMs }); + constructor({ x, y, radiusGrid, explodeAtElapsedMs, teamId }: BombControllerOptions) { + this.model = new BombModel({ x, y, radiusGrid, explodeAtElapsedMs, teamId }); 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.view.renderState(this.model.getState(), this.model.getExplosionRadiusGrid(), this.model.getTeamId()); } public getDisplayObject() { @@ -33,7 +34,7 @@ public tick(elapsedMs: number): void { this.model.update(elapsedMs); - this.view.renderState(this.model.getState(), this.model.getExplosionRadiusGrid()); + this.view.renderState(this.model.getState(), this.model.getExplosionRadiusGrid(), this.model.getTeamId()); } 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 de4f0f5..cd62580 100644 --- a/apps/client/src/scenes/game/entities/bomb/BombManager.ts +++ b/apps/client/src/scenes/game/entities/bomb/BombManager.ts @@ -23,6 +23,7 @@ y: number; explodeAtElapsedMs: number; radiusGrid: number; + teamId: number; }; /** 爆弾設置時に返す結果型 */ @@ -40,6 +41,8 @@ /** 爆弾エンティティのライフサイクルを管理する */ export class BombManager { + private static readonly UNKNOWN_TEAM_ID = -1; + private worldContainer: Container; private players: GamePlayers; private myId: string; @@ -78,10 +81,11 @@ explodeAtElapsedMs: elapsedMs + BOMB_FUSE_MS, }; const tempBombId = this.createTempBombId(requestId); + const ownTeamId = this.resolveTeamIdBySocketId(this.myId); this.pendingOwnRequestToTempBombId.set(requestId, tempBombId); this.pendingTempBombIdToOwnRequest.set(tempBombId, requestId); - this.upsertBomb(tempBombId, this.toRenderPayload(payload)); + this.upsertBomb(tempBombId, this.toRenderPayload(payload, ownTeamId)); this.lastBombPlacedElapsedMs = elapsedMs; return { tempBombId, @@ -91,7 +95,8 @@ /** 他プレイヤー向けの爆弾確定イベントを反映する */ public applyPlacedBombFromOthers(payload: BombPlacedPayload): void { - this.upsertBomb(payload.bombId, this.toRenderPayload(payload)); + const ownerTeamId = this.resolveTeamIdBySocketId(payload.ownerSocketId); + this.upsertBomb(payload.bombId, this.toRenderPayload(payload, ownerTeamId)); } /** 設置者本人向けACKを反映し,仮IDから正式IDへ置換する */ @@ -169,18 +174,32 @@ return a.x === b.x && a.y === b.y && a.explodeAtElapsedMs === b.explodeAtElapsedMs - && a.radiusGrid === b.radiusGrid; + && a.radiusGrid === b.radiusGrid + && a.teamId === b.teamId; } - private toRenderPayload(payload: { x: number; y: number; explodeAtElapsedMs: number }): BombRenderPayload { + private toRenderPayload( + payload: { x: number; y: number; explodeAtElapsedMs: number }, + teamId: number + ): BombRenderPayload { return { x: payload.x, y: payload.y, explodeAtElapsedMs: payload.explodeAtElapsedMs, radiusGrid: config.GAME_CONFIG.BOMB_RADIUS_GRID, + teamId, }; } + private resolveTeamIdBySocketId(socketId: string): number { + const playerController = this.players[socketId]; + if (!playerController) { + return BombManager.UNKNOWN_TEAM_ID; + } + + return playerController.getSnapshot().teamId; + } + private createRequestId(_elapsedMs: number): string { this.requestSerial += 1; return `${this.requestSerial}`; diff --git a/apps/client/src/scenes/game/entities/bomb/BombModel.ts b/apps/client/src/scenes/game/entities/bomb/BombModel.ts index 339b124..7cf7897 100644 --- a/apps/client/src/scenes/game/entities/bomb/BombModel.ts +++ b/apps/client/src/scenes/game/entities/bomb/BombModel.ts @@ -13,6 +13,7 @@ y: number; radiusGrid: number; explodeAtElapsedMs: number; + teamId: number; }; /** 爆弾の状態と寿命を管理するモデル */ @@ -21,13 +22,15 @@ private y: number; private radiusGrid: number; private explodeAtElapsedMs: number; + private teamId: number; private state: BombState = "armed"; - constructor({ x, y, radiusGrid, explodeAtElapsedMs }: BombModelOptions) { + constructor({ x, y, radiusGrid, explodeAtElapsedMs, teamId }: BombModelOptions) { this.x = x; this.y = y; this.radiusGrid = radiusGrid; this.explodeAtElapsedMs = explodeAtElapsedMs; + this.teamId = teamId; } public getPosition() { @@ -42,6 +45,10 @@ return this.radiusGrid; } + public getTeamId(): number { + return this.teamId; + } + 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 020f29e..d0a2478 100644 --- a/apps/client/src/scenes/game/entities/bomb/BombView.ts +++ b/apps/client/src/scenes/game/entities/bomb/BombView.ts @@ -15,6 +15,7 @@ private explosionGraphic: Graphics; private lastRenderedState: BombState | null = null; private lastRenderedRadiusGrid: number | null = null; + private lastRenderedTeamId: number | null = null; constructor() { this.displayObject = new Container(); @@ -32,8 +33,12 @@ this.displayObject.y = gridY * GRID_CELL_SIZE + GRID_CELL_SIZE / 2; } - public renderState(state: BombState, radiusGrid: number): void { - if (this.lastRenderedState === state && this.lastRenderedRadiusGrid === radiusGrid) { + public renderState(state: BombState, radiusGrid: number, teamId: number): void { + if ( + this.lastRenderedState === state + && this.lastRenderedRadiusGrid === radiusGrid + && this.lastRenderedTeamId === teamId + ) { return; } @@ -43,24 +48,45 @@ this.lastRenderedState = state; this.lastRenderedRadiusGrid = radiusGrid; + this.lastRenderedTeamId = teamId; + + const teamColor = this.resolveTeamColor(teamId); this.bombGraphic.clear(); this.explosionGraphic.clear(); if (state === "armed") { this.bombGraphic.circle(0, 0, bombRadiusPx); - this.bombGraphic.fill({ color: 0x111111, alpha: 0.95 }); + this.bombGraphic.fill({ color: teamColor, alpha: 0.95 }); this.bombGraphic.stroke({ color: 0xffffff, width: 2 }); return; } if (state === "exploded") { this.explosionGraphic.circle(0, 0, explosionRadiusPx); - this.explosionGraphic.fill({ color: 0xffcc00, alpha: 0.35 }); - this.explosionGraphic.stroke({ color: 0xff9900, width: 3 }); + this.explosionGraphic.fill({ color: teamColor, alpha: 0.35 }); + this.explosionGraphic.stroke({ color: teamColor, 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/application/useCases/placeBombUseCase.ts b/apps/server/src/domains/game/application/useCases/placeBombUseCase.ts index beddd68..de21ec8 100644 --- a/apps/server/src/domains/game/application/useCases/placeBombUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/placeBombUseCase.ts @@ -40,6 +40,7 @@ createBombPlacedPayload({ payload: input.payload, bombId, + ownerSocketId: input.socketId, }) ); diff --git a/apps/server/src/domains/game/entities/bomb/bombPlacement.ts b/apps/server/src/domains/game/entities/bomb/bombPlacement.ts index 991944a..21585b6 100644 --- a/apps/server/src/domains/game/entities/bomb/bombPlacement.ts +++ b/apps/server/src/domains/game/entities/bomb/bombPlacement.ts @@ -12,15 +12,18 @@ type CreateBombPlacedPayloadParams = { payload: PlaceBombPayload; bombId: string; + ownerSocketId: string; }; /** 爆弾確定通知で他プレイヤーへ配信するペイロードを生成する */ export const createBombPlacedPayload = ({ payload, bombId, + ownerSocketId, }: CreateBombPlacedPayloadParams): BombPlacedPayload => { return { bombId, + ownerSocketId, x: payload.x, y: payload.y, explodeAtElapsedMs: payload.explodeAtElapsedMs, diff --git a/packages/shared/src/protocol/payloads/gamePayloads.ts b/packages/shared/src/protocol/payloads/gamePayloads.ts index 65c859b..e8543e5 100644 --- a/packages/shared/src/protocol/payloads/gamePayloads.ts +++ b/packages/shared/src/protocol/payloads/gamePayloads.ts @@ -67,6 +67,7 @@ /** BOMB_PLACED イベントで送受信する他プレイヤー向け爆弾確定情報 */ export type BombPlacedPayload = { bombId: string; + ownerSocketId: string; x: number; y: number; explodeAtElapsedMs: number;