diff --git a/apps/client/src/scenes/game/GameManager.ts b/apps/client/src/scenes/game/GameManager.ts index 9a1b14e..4da0b22 100644 --- a/apps/client/src/scenes/game/GameManager.ts +++ b/apps/client/src/scenes/game/GameManager.ts @@ -10,6 +10,7 @@ import { GameNetworkSync } from "./application/GameNetworkSync"; import { GameLoop } from "./application/GameLoop"; import { BombManager } from "./application/BombManager"; +import type { BombUpsertPayload } from "./application/BombManager"; import type { GamePlayers } from "./application/game.types"; /** ゲームシーンの実行ライフサイクルを管理するマネージャー */ @@ -35,8 +36,17 @@ return this.timer.getRemainingTime(); } - public placeBomb() { - this.bombManager?.placeBomb(); + public placeBomb(): string | null { + if (!this.bombManager) return null; + return this.bombManager.placeBomb(); + } + + public upsertBomb(bombId: string, payload: BombUpsertPayload): void { + this.bombManager?.upsertBomb(bombId, payload); + } + + public removeBomb(bombId: string): void { + this.bombManager?.removeBomb(bombId); } // 入力と状態管理 diff --git a/apps/client/src/scenes/game/GameView.tsx b/apps/client/src/scenes/game/GameView.tsx index e2880c7..3556e13 100644 --- a/apps/client/src/scenes/game/GameView.tsx +++ b/apps/client/src/scenes/game/GameView.tsx @@ -3,7 +3,7 @@ * ゲーム画面の描画専用コンポーネント * タイマー表示,PixiJSの描画領域,入力UIの配置のみを担当する */ -import { JoystickInputPresenter } from "./input/joystick/JoystickInputPresenter"; +import { GameInputOverlay } from "./input/GameInputOverlay"; /** 表示と入力に必要なプロパティ */ type Props = { @@ -45,30 +45,6 @@ zIndex: 1, }; -const UI_LAYER_STYLE: React.CSSProperties = { - position: "absolute", - zIndex: 20, - width: "100%", - height: "100%", -}; - -const BOMB_BUTTON_STYLE: React.CSSProperties = { - position: "fixed", - right: "36px", - bottom: "40px", - width: "96px", - height: "96px", - borderRadius: "50%", - border: "2px solid rgba(255,255,255,0.75)", - background: "rgba(220, 60, 60, 0.85)", - color: "white", - fontSize: "18px", - fontWeight: "bold", - zIndex: 9999, - pointerEvents: "auto", - touchAction: "manipulation", -}; - const TimerOverlay = ({ timeLeft }: { timeLeft: string }) =>
{timeLeft}
; /** 画面描画と入力UIをまとめて描画する */ @@ -81,13 +57,8 @@ {/* PixiJS Canvas 配置領域 */}
- {/* UI 配置領域 */} -
- - -
+ {/* 入力UI レイヤー */} +
); }; diff --git a/apps/client/src/scenes/game/application/BombManager.ts b/apps/client/src/scenes/game/application/BombManager.ts index 9ed623a..08a923c 100644 --- a/apps/client/src/scenes/game/application/BombManager.ts +++ b/apps/client/src/scenes/game/application/BombManager.ts @@ -12,6 +12,14 @@ /** 経過時間ミリ秒を返す関数型 */ export type ElapsedMsProvider = () => number; +/** 爆弾の追加更新に使う入力データ型 */ +export type BombUpsertPayload = { + x: number; + y: number; + radiusGrid: number; + explodeAtElapsedMs: number; +}; + type BombManagerOptions = { worldContainer: Container; players: GamePlayers; @@ -25,7 +33,8 @@ private players: GamePlayers; private myId: string; private getElapsedMs: ElapsedMsProvider; - private bombs: BombController[] = []; + private bombs = new Map(); + private nextLocalBombId = 1; private lastBombPlacedElapsedMs = Number.NEGATIVE_INFINITY; constructor({ worldContainer, players, myId, getElapsedMs }: BombManagerOptions) { @@ -35,53 +44,72 @@ this.getElapsedMs = getElapsedMs; } - /** 自プレイヤー位置に爆弾を設置する */ - public placeBomb(): void { + /** 自プレイヤー位置に爆弾を設置し,生成IDを返す */ + public placeBomb(): string | null { const me = this.players[this.myId]; - if (!me || !(me instanceof LocalPlayerController)) return; + if (!me || !(me instanceof LocalPlayerController)) return null; const elapsedMs = this.getElapsedMs(); const { BOMB_COOLDOWN_MS, BOMB_FUSE_MS, BOMB_RADIUS_GRID } = config.GAME_CONFIG; if (elapsedMs - this.lastBombPlacedElapsedMs < BOMB_COOLDOWN_MS) { - return; + return null; } const position = me.getPosition(); - const bomb = new BombController({ + const payload: BombUpsertPayload = { x: position.x, y: position.y, radiusGrid: BOMB_RADIUS_GRID, explodeAtElapsedMs: elapsedMs + BOMB_FUSE_MS, - }); + }; + const bombId = `local-${this.nextLocalBombId}`; + this.nextLocalBombId += 1; - this.bombs.push(bomb); - this.worldContainer.addChild(bomb.getDisplayObject()); + this.upsertBomb(bombId, payload); this.lastBombPlacedElapsedMs = elapsedMs; + return bombId; + } + + /** 指定IDの爆弾を追加または更新する */ + public upsertBomb(bombId: string, payload: BombUpsertPayload): void { + const current = this.bombs.get(bombId); + if (current) { + this.worldContainer.removeChild(current.getDisplayObject()); + current.destroy(); + } + + const bomb = new BombController(payload); + this.bombs.set(bombId, bomb); + this.worldContainer.addChild(bomb.getDisplayObject()); + } + + /** 指定IDの爆弾を削除する */ + public removeBomb(bombId: string): void { + const bomb = this.bombs.get(bombId); + if (!bomb) return; + + this.worldContainer.removeChild(bomb.getDisplayObject()); + bomb.destroy(); + this.bombs.delete(bombId); } /** 爆弾状態を更新し終了済みを破棄する */ public tick(): void { const elapsedMs = this.getElapsedMs(); - const nextBombs: BombController[] = []; - this.bombs.forEach((bomb) => { + this.bombs.forEach((bomb, bombId) => { bomb.tick(elapsedMs); if (bomb.isFinished()) { - this.worldContainer.removeChild(bomb.getDisplayObject()); - bomb.destroy(); + this.removeBomb(bombId); return; } - - nextBombs.push(bomb); }); - - this.bombs = nextBombs; } /** 管理中の爆弾をすべて破棄する */ public destroy(): void { this.bombs.forEach((bomb) => bomb.destroy()); - this.bombs = []; + this.bombs.clear(); } } diff --git a/apps/client/src/scenes/game/input/GameInputOverlay.tsx b/apps/client/src/scenes/game/input/GameInputOverlay.tsx new file mode 100644 index 0000000..17e30b8 --- /dev/null +++ b/apps/client/src/scenes/game/input/GameInputOverlay.tsx @@ -0,0 +1,30 @@ +/** + * GameInputOverlay + * ゲーム入力UIレイヤーを構成する + * ジョイスティック層と爆弾ボタン層を分離して配置する + */ +import { JoystickInputPresenter } from "./joystick/JoystickInputPresenter"; +import { BombButton } from "./bomb/BombButton"; + +/** 入力UIレイヤーの入力プロパティ */ +type GameInputOverlayProps = { + onJoystickInput: (x: number, y: number) => void; + onPlaceBomb: () => void; +}; + +const UI_LAYER_STYLE: React.CSSProperties = { + position: "absolute", + zIndex: 20, + width: "100%", + height: "100%", +}; + +/** 入力UIレイヤーを描画する */ +export const GameInputOverlay = ({ onJoystickInput, onPlaceBomb }: GameInputOverlayProps) => { + return ( +
+ + +
+ ); +}; diff --git a/apps/client/src/scenes/game/input/bomb/BombButton.tsx b/apps/client/src/scenes/game/input/bomb/BombButton.tsx new file mode 100644 index 0000000..68bd4ad --- /dev/null +++ b/apps/client/src/scenes/game/input/bomb/BombButton.tsx @@ -0,0 +1,36 @@ +/** + * BombButton + * 爆弾設置ボタンの見た目とクリック入力を担う + * 画面右下固定の操作ボタンを提供する + */ + +/** 爆弾設置ボタンの入力プロパティ */ +type BombButtonProps = { + onPress: () => void; +}; + +const BOMB_BUTTON_STYLE: React.CSSProperties = { + position: "fixed", + right: "36px", + bottom: "40px", + width: "96px", + height: "96px", + borderRadius: "50%", + border: "2px solid rgba(255,255,255,0.75)", + background: "rgba(220, 60, 60, 0.85)", + color: "white", + fontSize: "18px", + fontWeight: "bold", + zIndex: 9999, + pointerEvents: "auto", + touchAction: "manipulation", +}; + +/** 画面右下の爆弾設置ボタンを描画する */ +export const BombButton = ({ onPress }: BombButtonProps) => { + return ( + + ); +};