diff --git a/apps/client/public/Bomb.svg b/apps/client/public/Bomb.svg new file mode 100644 index 0000000..9ae8186 --- /dev/null +++ b/apps/client/public/Bomb.svg @@ -0,0 +1,997 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/client/src/config/index.ts b/apps/client/src/config/index.ts index 3ec6399..fcdeb0e 100644 --- a/apps/client/src/config/index.ts +++ b/apps/client/src/config/index.ts @@ -1,5 +1,9 @@ import { config as sharedConfig } from "@repo/shared"; +const sharedBombRenderScale = + (sharedConfig.GAME_CONFIG as { BOMB_RENDER_SCALE?: number }) + .BOMB_RENDER_SCALE ?? 1; + const CLIENT_GAME_CONFIG = { TIMER_DISPLAY_UPDATE_MS: 250, JOIN_REQUEST_TIMEOUT_MS: 8000, @@ -28,6 +32,9 @@ get PLAYER_RADIUS_PX(): number { return this.PLAYER_RADIUS * this.GRID_CELL_SIZE; }, + get BOMB_RENDER_RADIUS_PX(): number { + return this.GRID_CELL_SIZE * 0.2 * sharedBombRenderScale; + }, } as const; const NETWORK_CONFIG = { diff --git a/apps/client/src/scenes/game/GameManager.ts b/apps/client/src/scenes/game/GameManager.ts index a12a820..0cea6e5 100644 --- a/apps/client/src/scenes/game/GameManager.ts +++ b/apps/client/src/scenes/game/GameManager.ts @@ -55,7 +55,7 @@ public applyPlacedBombAck(payload: BombPlacedAckPayload): void { this.bombManager?.applyPlacedBombAck(payload); } - + // 入力と状態管理 private joystickInput = { x: 0, y: 0 }; private isInitialized = false; @@ -72,6 +72,7 @@ this.myId = myId; this.app = new Application(); this.worldContainer = new Container(); + this.worldContainer.sortableChildren = true; } /** @@ -79,12 +80,16 @@ */ public async init() { // PixiJS本体の初期化 - await this.app.init({ resizeTo: window, backgroundColor: 0x111111, antialias: true }); + await this.app.init({ + resizeTo: window, + backgroundColor: 0x111111, + antialias: true, + }); // 初期化完了前に destroy() が呼ばれていたら、ここで処理を中断して破棄する if (this.isDestroyed) { - this.app.destroy(true, { children: true }); - return; + this.app.destroy(true, { children: true }); + return; } this.container.appendChild(this.app.canvas); @@ -164,8 +169,8 @@ this.bombManager = null; this.players = {}; this.isInputLocked = false; - + // イベント購読の解除 this.networkSync?.unbind(); } -} \ No newline at end of file +} diff --git a/apps/client/src/scenes/game/entities/bomb/BombView.ts b/apps/client/src/scenes/game/entities/bomb/BombView.ts index 4a71107..12fe1c4 100644 --- a/apps/client/src/scenes/game/entities/bomb/BombView.ts +++ b/apps/client/src/scenes/game/entities/bomb/BombView.ts @@ -4,26 +4,56 @@ * 設置中の見た目と爆風円の表示を管理する */ import { Container, Graphics } from "pixi.js"; +import { Assets, Sprite, Texture } from "pixi.js"; import { config } from "@client/config"; import type { BombState } from "./BombModel"; +const ENABLE_DEBUG_LOG = import.meta.env.DEV; + /** 爆弾の描画表現を管理するビュー */ export class BombView { public readonly displayObject: Container; - private bombGraphic: Graphics; + private bombSprite: Sprite; + private bombFallbackGraphic: Graphics; private explosionGraphic: Graphics; private lastRenderedState: BombState | null = null; private lastRenderedRadiusGrid: number | null = null; private lastRenderedColor: number | null = null; + private isBombTextureReady = false; constructor() { this.displayObject = new Container(); - this.bombGraphic = new Graphics(); + this.displayObject.zIndex = 1000; + this.bombSprite = new Sprite(Texture.WHITE); + this.bombSprite.anchor.set(0.5, 0.5); + this.bombSprite.visible = false; + this.bombFallbackGraphic = new Graphics(); + this.bombFallbackGraphic.visible = false; this.explosionGraphic = new Graphics(); this.displayObject.addChild(this.explosionGraphic); - this.displayObject.addChild(this.bombGraphic); + this.displayObject.addChild(this.bombFallbackGraphic); + this.displayObject.addChild(this.bombSprite); + + void this.applyBombTexture(); + } + + private async applyBombTexture(): Promise { + const imageUrl = `${import.meta.env.BASE_URL}Bomb.svg`; + + try { + const texture = await Assets.load(imageUrl); + this.bombSprite.texture = texture; + this.isBombTextureReady = true; + + if (ENABLE_DEBUG_LOG) { + console.log(`[BombView] Bomb.svg 読み込み成功: ${imageUrl}`); + } + } catch (error) { + this.isBombTextureReady = false; + console.error(`[BombView] Bomb.svg 読み込み失敗: ${imageUrl}`, error); + } } public syncPosition(gridX: number, gridY: number): void { @@ -41,30 +71,40 @@ if ( this.lastRenderedState === state && this.lastRenderedRadiusGrid === radiusGrid && - this.lastRenderedColor === color + this.lastRenderedColor === color && + state !== "armed" ) { return; } - const { GRID_CELL_SIZE } = config.GAME_CONFIG; - const bombRadiusPx = GRID_CELL_SIZE * 0.2; + const { GRID_CELL_SIZE, BOMB_RENDER_RADIUS_PX } = config.GAME_CONFIG; + const bombRadiusPx = BOMB_RENDER_RADIUS_PX; const explosionRadiusPx = radiusGrid * GRID_CELL_SIZE; this.lastRenderedState = state; this.lastRenderedRadiusGrid = radiusGrid; this.lastRenderedColor = color; - this.bombGraphic.clear(); this.explosionGraphic.clear(); + this.bombFallbackGraphic.clear(); if (state === "armed") { - this.bombGraphic.circle(0, 0, bombRadiusPx); - this.bombGraphic.fill({ color, alpha: 0.95 }); - this.bombGraphic.stroke({ color: 0xffffff, width: 2 }); + this.bombSprite.visible = this.isBombTextureReady; + this.bombSprite.tint = 0xffffff; + this.bombSprite.alpha = 1; + this.bombSprite.width = bombRadiusPx * 2; + this.bombSprite.height = bombRadiusPx * 2; + + this.bombFallbackGraphic.visible = true; + this.bombFallbackGraphic.circle(0, 0, bombRadiusPx); + this.bombFallbackGraphic.fill({ color, alpha: 0.92 }); + this.bombFallbackGraphic.stroke({ color: 0xffffff, width: 3 }); return; } if (state === "exploded") { + this.bombSprite.visible = false; + this.bombFallbackGraphic.visible = false; this.explosionGraphic.circle(0, 0, explosionRadiusPx); this.explosionGraphic.fill({ color, alpha: 0.35 }); this.explosionGraphic.stroke({ color, width: 3 }); diff --git a/packages/shared/src/config/gameConfig.ts b/packages/shared/src/config/gameConfig.ts index 97a5515..c9ac7ac 100644 --- a/packages/shared/src/config/gameConfig.ts +++ b/packages/shared/src/config/gameConfig.ts @@ -22,6 +22,7 @@ // 爆弾設定(内部座標はグリッド単位、時間はms、契約値) BOMB_RADIUS_GRID: 1.5, // 爆風半径(グリッド単位、円形当たり判定) + BOMB_RENDER_SCALE: 1.0, // 爆弾見た目サイズ倍率(1=等倍) BOMB_FUSE_MS: 1000, // 設置から爆発までの時間(ms) BOMB_COOLDOWN_MS: 3000, // 設置後に次の爆弾を置けるまでの待機時間(ms) BOMB_DEDUP_EXTRA_TTL_MS: 1000, // 重複排除保持時間の追加分(ms) @@ -48,9 +49,9 @@ /** teamId が有効範囲内かを真偽値で判定する */ export const isKnownTeamId = (teamId: number): boolean => { - return Number.isInteger(teamId) - && teamId >= 0 - && teamId < GAME_CONFIG.TEAM_COUNT; + return ( + Number.isInteger(teamId) && teamId >= 0 && teamId < GAME_CONFIG.TEAM_COUNT + ); }; /** TEAM_COUNT と TEAM_NAMES の整合性を検証する */