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 (
+
+ );
+};