diff --git a/apps/client/src/scenes/game/entities/player/PlayerController.ts b/apps/client/src/scenes/game/entities/player/PlayerController.ts index c2c51de..3bfda08 100644 --- a/apps/client/src/scenes/game/entities/player/PlayerController.ts +++ b/apps/client/src/scenes/game/entities/player/PlayerController.ts @@ -3,10 +3,10 @@ * 外部入出力とModel/Viewの橋渡しを担うコントローラー群 * ローカル入力適用,リモート更新適用,描画同期を分離して扱う */ -import type { playerTypes } from '@repo/shared'; -import { AppearanceResolver } from '@client/scenes/game/application/AppearanceResolver'; -import { PlayerModel } from './PlayerModel'; -import { PlayerView } from './PlayerView'; +import type { playerTypes } from "@repo/shared"; +import { AppearanceResolver } from "@client/scenes/game/application/AppearanceResolver"; +import { PlayerModel } from "./PlayerModel"; +import { PlayerView } from "./PlayerView"; /** ローカル移動入力を表す型 */ export type LocalInput = { @@ -26,9 +26,17 @@ protected readonly view: PlayerView; /** 共通初期化としてModelとViewを生成する */ - protected constructor(data: playerTypes.PlayerData, isLocal: boolean, appearanceResolver: AppearanceResolver) { + protected constructor( + data: playerTypes.PlayerData, + isLocal: boolean, + appearanceResolver: AppearanceResolver, + ) { this.model = new PlayerModel(data); - this.view = new PlayerView(appearanceResolver.resolvePlayerImageFile(data.teamId), isLocal); + this.view = new PlayerView( + appearanceResolver.resolvePlayerImageFile(data.teamId), + data.name, + isLocal, + ); const pos = this.model.getPosition(); this.view.syncPosition(pos.x, pos.y); @@ -58,7 +66,10 @@ /** ローカルプレイヤーの入力適用と描画同期を担うコントローラー */ export class LocalPlayerController extends BasePlayerController { /** ローカルプレイヤー用コントローラーを初期化する */ - constructor(data: playerTypes.PlayerData, appearanceResolver: AppearanceResolver) { + constructor( + data: playerTypes.PlayerData, + appearanceResolver: AppearanceResolver, + ) { super(data, true, appearanceResolver); } @@ -77,7 +88,10 @@ /** リモートプレイヤーの更新適用と補間同期を担うコントローラー */ export class RemotePlayerController extends BasePlayerController { /** リモートプレイヤー用コントローラーを初期化する */ - constructor(data: playerTypes.PlayerData, appearanceResolver: AppearanceResolver) { + constructor( + data: playerTypes.PlayerData, + appearanceResolver: AppearanceResolver, + ) { super(data, false, appearanceResolver); } @@ -92,4 +106,4 @@ const pos = this.model.getPosition(); this.view.syncPosition(pos.x, pos.y); } -} \ No newline at end of file +} diff --git a/apps/client/src/scenes/game/entities/player/PlayerModel.ts b/apps/client/src/scenes/game/entities/player/PlayerModel.ts index 6f2709d..1e2882f 100644 --- a/apps/client/src/scenes/game/entities/player/PlayerModel.ts +++ b/apps/client/src/scenes/game/entities/player/PlayerModel.ts @@ -3,12 +3,13 @@ * プレイヤーの座標計算と補間計算を担うモデル * ローカル移動,リモート目標座標,送信スナップショットを管理する */ -import { config } from '@client/config'; -import type { playerTypes } from '@repo/shared'; +import { config } from "@client/config"; +import type { playerTypes } from "@repo/shared"; /** プレイヤーの座標計算と補間計算を管理するモデル */ export class PlayerModel { public readonly id: string; + public readonly name: string; public readonly teamId: number; private gridX: number; @@ -19,6 +20,7 @@ /** 共有プレイヤー情報から初期状態を構築する */ constructor(data: playerTypes.PlayerData) { this.id = data.id; + this.name = data.name; this.teamId = data.teamId; this.gridX = data.x; this.gridY = data.y; @@ -35,6 +37,7 @@ public getSnapshot(): playerTypes.PlayerData { return { id: this.id, + name: this.name, teamId: this.teamId, x: this.gridX, y: this.gridY, @@ -43,7 +46,11 @@ /** ローカル入力に基づいて座標を更新する */ public moveLocal(vx: number, vy: number, deltaTime: number): void { - if (!this.isFiniteNumber(vx) || !this.isFiniteNumber(vy) || !this.isFiniteNumber(deltaTime)) { + if ( + !this.isFiniteNumber(vx) || + !this.isFiniteNumber(vy) || + !this.isFiniteNumber(deltaTime) + ) { return; } @@ -58,8 +65,10 @@ /** リモート更新の目標座標を設定する */ public setRemoteTarget(update: Partial): void { - if (update.x !== undefined && this.isFiniteNumber(update.x)) this.targetGridX = update.x; - if (update.y !== undefined && this.isFiniteNumber(update.y)) this.targetGridY = update.y; + if (update.x !== undefined && this.isFiniteNumber(update.x)) + this.targetGridX = update.x; + if (update.y !== undefined && this.isFiniteNumber(update.y)) + this.targetGridY = update.y; } /** 目標座標に向けて補間更新する */ @@ -68,7 +77,8 @@ return; } - const { PLAYER_LERP_SNAP_THRESHOLD, PLAYER_LERP_SMOOTHNESS } = config.GAME_CONFIG; + const { PLAYER_LERP_SNAP_THRESHOLD, PLAYER_LERP_SMOOTHNESS } = + config.GAME_CONFIG; const diffX = this.targetGridX - this.gridX; const diffY = this.targetGridY - this.gridY; @@ -90,12 +100,18 @@ private clampToBounds(): void { const { GRID_COLS, GRID_ROWS, PLAYER_RADIUS } = config.GAME_CONFIG; - this.gridX = Math.max(PLAYER_RADIUS, Math.min(GRID_COLS - PLAYER_RADIUS, this.gridX)); - this.gridY = Math.max(PLAYER_RADIUS, Math.min(GRID_ROWS - PLAYER_RADIUS, this.gridY)); + this.gridX = Math.max( + PLAYER_RADIUS, + Math.min(GRID_COLS - PLAYER_RADIUS, this.gridX), + ); + this.gridY = Math.max( + PLAYER_RADIUS, + Math.min(GRID_ROWS - PLAYER_RADIUS, this.gridY), + ); } /** 有限数かどうかを判定する */ private isFiniteNumber(value: number): boolean { return Number.isFinite(value); } -} \ No newline at end of file +} diff --git a/apps/client/src/scenes/game/entities/player/PlayerView.ts b/apps/client/src/scenes/game/entities/player/PlayerView.ts index 00be3fb..ebd61dc 100644 --- a/apps/client/src/scenes/game/entities/player/PlayerView.ts +++ b/apps/client/src/scenes/game/entities/player/PlayerView.ts @@ -5,27 +5,46 @@ */ import { Assets, Sprite, Texture } from "pixi.js"; import { config } from "@client/config"; +import { Container, Text, TextStyle } from "pixi.js"; const ENABLE_DEBUG_LOG = import.meta.env.DEV; export class PlayerView { - public readonly displayObject: Sprite; + public readonly displayObject: Container; + private readonly sprite: Sprite; + private readonly nameText: Text; - constructor(imageFileName: string, isLocal: boolean) { + constructor(imageFileName: string, playerName: string, isLocal: boolean) { const { PLAYER_RADIUS_PX, PLAYER_RENDER_SCALE } = config.GAME_CONFIG; + this.displayObject = new Container(); + // 🌟 2. スプライト(画像)の生成(初期は1x1テクスチャ) - this.displayObject = new Sprite(Texture.WHITE); + this.sprite = new Sprite(Texture.WHITE); // 🌟 3. 画像の基準点を「中心」にする(ズレ防止) - this.displayObject.anchor.set(0.5, 0.5); + this.sprite.anchor.set(0.5, 0.5); // 🌟 4. 画像サイズを当たり判定(半径×2)に合わせる - this.displayObject.width = PLAYER_RADIUS_PX * 2 * PLAYER_RENDER_SCALE; - this.displayObject.height = PLAYER_RADIUS_PX * 2 * PLAYER_RENDER_SCALE; + this.sprite.width = PLAYER_RADIUS_PX * 2 * PLAYER_RENDER_SCALE; + this.sprite.height = PLAYER_RADIUS_PX * 2 * PLAYER_RENDER_SCALE; // ローカルプレイヤーだけ少し視認性を上げる(未使用引数対策を兼ねる) - this.displayObject.alpha = isLocal ? 1 : 0.95; + this.sprite.alpha = isLocal ? 1 : 0.95; + + this.nameText = new Text({ + text: playerName, + style: new TextStyle({ + fill: "#ffffff", + fontSize: 16, + fontWeight: "700", + stroke: { color: "#000000", width: 3 }, + }), + }); + this.nameText.anchor.set(0.5, 0); + this.nameText.y = PLAYER_RADIUS_PX * PLAYER_RENDER_SCALE + 8; + + this.displayObject.addChild(this.sprite, this.nameText); // 非同期で画像テクスチャを読み込んで差し替える void this.applyTexture(imageFileName); @@ -36,12 +55,13 @@ try { const imageUrl = `${import.meta.env.BASE_URL}${imageFileName}`; const texture = await Assets.load(imageUrl); - this.displayObject.texture = texture; + this.sprite.texture = texture; const { PLAYER_RADIUS_PX, PLAYER_RENDER_SCALE } = config.GAME_CONFIG; - this.displayObject.width = PLAYER_RADIUS_PX * 2 * PLAYER_RENDER_SCALE; - this.displayObject.height = PLAYER_RADIUS_PX * 2 * PLAYER_RENDER_SCALE; + this.sprite.width = PLAYER_RADIUS_PX * 2 * PLAYER_RENDER_SCALE; + this.sprite.height = PLAYER_RADIUS_PX * 2 * PLAYER_RENDER_SCALE; + this.nameText.y = PLAYER_RADIUS_PX * PLAYER_RENDER_SCALE + 8; if (ENABLE_DEBUG_LOG) { console.log( @@ -64,6 +84,6 @@ /** 描画リソースを破棄する */ public destroy(): void { - this.displayObject.destroy(); + this.displayObject.destroy({ children: true }); } } diff --git a/apps/server/src/application/coordinators/startGameCoordinator.ts b/apps/server/src/application/coordinators/startGameCoordinator.ts index 85fc8dc..5018aca 100644 --- a/apps/server/src/application/coordinators/startGameCoordinator.ts +++ b/apps/server/src/application/coordinators/startGameCoordinator.ts @@ -5,7 +5,10 @@ import { type StartGameOutputPort } from "@server/domains/game/application/ports/gameUseCasePorts"; import type { StartGameCoordinatorDeps } from "./coordinatorDeps"; import { startGameUseCase } from "@server/domains/game/application/useCases/startGameUseCase"; -import { createBalancedSessionPlayerIds } from "@server/domains/game/application/services/BotRosterService"; +import { + createBalancedSessionPlayerIds, + isBotPlayerId, +} from "@server/domains/game/application/services/BotRosterService"; import { logEvent } from "@server/logging/logger"; import { gameUseCaseLogEvents, @@ -71,10 +74,20 @@ }); const humanPlayerIds = updatedRoom.players.map((player) => player.id); + const playerNamesById = Object.fromEntries( + updatedRoom.players.map((player) => [player.id, player.name]), + ); const sessionPlayerIds = createBalancedSessionPlayerIds( updatedRoom.roomId, humanPlayerIds, ); + sessionPlayerIds.forEach((playerId) => { + if (!isBotPlayerId(playerId)) { + return; + } + + playerNamesById[playerId] = "BOT"; + }); const gameManager = runtimeRegistry.getGameManagerByRoomId( updatedRoom.roomId, ); @@ -85,6 +98,7 @@ startGameUseCase({ roomId: updatedRoom.roomId, playerIds: sessionPlayerIds, + playerNamesById, recipientPlayerIds: humanPlayerIds, gameSession: gameManager, bombStore: gameManager, diff --git a/apps/server/src/domains/game/GameManager.ts b/apps/server/src/domains/game/GameManager.ts index d97598c..fa7b906 100644 --- a/apps/server/src/domains/game/GameManager.ts +++ b/apps/server/src/domains/game/GameManager.ts @@ -60,12 +60,14 @@ */ startRoomSession( playerIds: string[], + playerNamesById: Record, onTick: (data: gameTypes.TickData) => void, onGameEnd: (payload: GameResultPayload) => void, onBotPlaceBomb?: (ownerId: string, payload: PlaceBombPayload) => void, ) { this.lifecycleService.startRoomSession( playerIds, + playerNamesById, onTick, onGameEnd, onBotPlaceBomb, diff --git a/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts b/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts index 7cf0308..a6a84fa 100644 --- a/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts +++ b/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts @@ -24,6 +24,7 @@ export interface StartGamePort { startRoomSession( playerIds: string[], + playerNamesById: Record, onTick: (data: gameTypes.TickData) => void, onGameEnd: (payload: GameResultPayload) => void, onBotPlaceBomb?: (ownerId: string, payload: PlaceBombPayload) => void, @@ -139,4 +140,7 @@ }; /** 被弾報告ユースケースが利用する出力ポート */ -export type BombHitOutputPort = Pick; +export type BombHitOutputPort = Pick< + BombOutputPort, + "publishPlayerDeadToOthersInRoom" +>; diff --git a/apps/server/src/domains/game/application/services/GameRoomSession.ts b/apps/server/src/domains/game/application/services/GameRoomSession.ts index fff0b67..c932481 100644 --- a/apps/server/src/domains/game/application/services/GameRoomSession.ts +++ b/apps/server/src/domains/game/application/services/GameRoomSession.ts @@ -35,6 +35,7 @@ constructor( private roomId: string, playerIds: string[], + playerNamesById: Record, ) { this.players = new Map(); this.mapStore = new MapStore(); @@ -47,7 +48,8 @@ ); // 算出したチームIDを指定してプレイヤーを生成する - const player = createSpawnedPlayer(playerId, assignedTeamId); + const playerName = playerNamesById[playerId] ?? playerId; + const player = createSpawnedPlayer(playerId, playerName, assignedTeamId); this.players.set(playerId, player); }); @@ -152,7 +154,10 @@ return this.bombStateStore.shouldBroadcastBombPlaced(dedupeKey, nowMs); } - public shouldBroadcastBombHitReport(dedupeKey: string, nowMs: number): boolean { + public shouldBroadcastBombHitReport( + dedupeKey: string, + nowMs: number, + ): boolean { return this.bombStateStore.shouldBroadcastBombHitReport(dedupeKey, nowMs); } diff --git a/apps/server/src/domains/game/application/services/GameSessionLifecycleService.ts b/apps/server/src/domains/game/application/services/GameSessionLifecycleService.ts index a72d78a..a8e0da7 100644 --- a/apps/server/src/domains/game/application/services/GameSessionLifecycleService.ts +++ b/apps/server/src/domains/game/application/services/GameSessionLifecycleService.ts @@ -42,7 +42,10 @@ ); } - public shouldBroadcastBombHitReport(dedupeKey: string, nowMs: number): boolean { + public shouldBroadcastBombHitReport( + dedupeKey: string, + nowMs: number, + ): boolean { return ( this.sessionRef.current?.shouldBroadcastBombHitReport(dedupeKey, nowMs) ?? false @@ -60,6 +63,7 @@ public startRoomSession( playerIds: string[], + playerNamesById: Record, onTick: (data: gameTypes.TickData) => void, onGameEnd: (payload: GameResultPayload) => void, onBotPlaceBomb?: (ownerId: string, payload: PlaceBombPayload) => void, @@ -74,7 +78,11 @@ } const tickRate = config.GAME_CONFIG.PLAYER_POSITION_UPDATE_MS; - const session = new GameRoomSession(this.roomId, playerIds); + const session = new GameRoomSession( + this.roomId, + playerIds, + playerNamesById, + ); this.activePlayerIds.clear(); playerIds.forEach((playerId) => { diff --git a/apps/server/src/domains/game/application/useCases/startGameUseCase.ts b/apps/server/src/domains/game/application/useCases/startGameUseCase.ts index 9000d3d..d386c8c 100644 --- a/apps/server/src/domains/game/application/useCases/startGameUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/startGameUseCase.ts @@ -29,6 +29,7 @@ type StartGameUseCaseParams = { roomId: string; playerIds: string[]; + playerNamesById: Record; recipientPlayerIds?: string[]; gameSession: StartGamePort; bombStore: BombPlacementPort; @@ -40,6 +41,7 @@ export const startGameUseCase = ({ roomId, playerIds, + playerNamesById, recipientPlayerIds, gameSession, bombStore, @@ -55,6 +57,7 @@ gameSession.startRoomSession( playerIds, + playerNamesById, (tickData) => { if (tickData.playerUpdates.length > 0) { updateRecipients.forEach((playerId) => { diff --git a/apps/server/src/domains/game/entities/player/Player.ts b/apps/server/src/domains/game/entities/player/Player.ts index d0f0467..fc4daa5 100644 --- a/apps/server/src/domains/game/entities/player/Player.ts +++ b/apps/server/src/domains/game/entities/player/Player.ts @@ -7,13 +7,15 @@ export class Player implements playerTypes.PlayerData { public id: string; + public name: string; public x: number = 0; public y: number = 0; public teamId: number; // 💡 コンストラクタで teamId を受け取るように変更 - constructor(id: string, teamId: number) { + constructor(id: string, name: string, teamId: number) { this.id = id; + this.name = name; this.teamId = teamId; } } diff --git a/apps/server/src/domains/game/entities/player/playerSpawn.ts b/apps/server/src/domains/game/entities/player/playerSpawn.ts index 8c198c8..6315689 100644 --- a/apps/server/src/domains/game/entities/player/playerSpawn.ts +++ b/apps/server/src/domains/game/entities/player/playerSpawn.ts @@ -7,8 +7,12 @@ /** プレイヤーを生成し,初期スポーン座標を設定して返す */ // 💡 引数に teamId を追加 -export const createSpawnedPlayer = (id: string, teamId: number): Player => { - const player = new Player(id, teamId); // ここにteamIdを渡す! +export const createSpawnedPlayer = ( + id: string, + name: string, + teamId: number, +): Player => { + const player = new Player(id, name, teamId); // ここにteamIdを渡す! const { GRID_COLS, GRID_ROWS, TEAM_COUNT } = config.GAME_CONFIG; diff --git a/packages/shared/src/domains/player/player.type.ts b/packages/shared/src/domains/player/player.type.ts index 809c395..c478463 100644 --- a/packages/shared/src/domains/player/player.type.ts +++ b/packages/shared/src/domains/player/player.type.ts @@ -1,10 +1,11 @@ // クライアント・サーバー間共有プレイヤー基本情報型 export interface PlayerData { id: string; + name: string; // グリッド単位の座標 x: number; y: number; - teamId: number; // 0〜3 のチームID + teamId: number; // 0〜3 のチームID } // 移動イベント送信ペイロード型 @@ -12,4 +13,4 @@ // グリッド単位の座標 x: number; y: number; -} \ No newline at end of file +}