diff --git a/apps/client/src/scenes/game/GameManager.ts b/apps/client/src/scenes/game/GameManager.ts index 2d60ceb..5570080 100644 --- a/apps/client/src/scenes/game/GameManager.ts +++ b/apps/client/src/scenes/game/GameManager.ts @@ -2,9 +2,9 @@ import { socketManager } from "@client/network/SocketManager"; import { config } from "@repo/shared"; import type { playerTypes } from "@repo/shared"; -import { BasePlayer, LocalPlayer, RemotePlayer } from "./Player"; -import { GameMap } from "./GameMap"; -import { MAX_DIST } from "./VirtualJoystick"; +import { BasePlayer, LocalPlayer, RemotePlayer } from "./entities/player/Player"; +import { GameMap } from "./entities/map/GameMap"; +import { MAX_DIST } from "./input/joystick/VirtualJoystick"; export class GameManager { private app: Application; diff --git a/apps/client/src/scenes/game/GameMap.ts b/apps/client/src/scenes/game/GameMap.ts deleted file mode 100644 index 19d3cae..0000000 --- a/apps/client/src/scenes/game/GameMap.ts +++ /dev/null @@ -1,117 +0,0 @@ -// apps/client/src/scenes/game/GameMap.ts -import { Container, Graphics } from "pixi.js"; -import { config } from "@repo/shared"; -import type { gridMapTypes } from "@repo/shared"; - -// 親クラスを Graphics から Container に変更し、レイヤー管理を可能にする -export class GameMap extends Container { - private bgGraphics: Graphics; - private gridGraphics: Graphics; - - // 400個(GRID_COLS * GRID_ROWS)のマス目描画用オブジェクトを保持する1次元配列 - private cells: Graphics[] = []; - - constructor() { - super(); - this.bgGraphics = new Graphics(); - this.gridGraphics = new Graphics(); - - // 描画順(追加した順に前面に描画される): 背景 -> マス目 -> グリッド線 - this.addChild(this.bgGraphics); - - // マス目の初期化と Container への追加 - this.initCells(); - - this.addChild(this.gridGraphics); - - // 背景と線の描画(静的なので1回だけ実行) - this.drawBaseMap(); - } - - // 設定値に基づき、空のマス目(Graphics)を400個生成して配列に格納する - private initCells() { - const { GRID_COLS, GRID_ROWS, GRID_CELL_SIZE } = config.GAME_CONFIG; - const totalCells = GRID_COLS * GRID_ROWS; - - for (let i = 0; i < totalCells; i++) { - const col = i % GRID_COLS; - const row = Math.floor(i / GRID_COLS); - - const cell = new Graphics(); - // マスの座標をあらかじめ設定しておく(描画の基準点になる) - cell.x = col * GRID_CELL_SIZE; - cell.y = row * GRID_CELL_SIZE; - - this.addChild(cell); - this.cells.push(cell); - } - } - - // 設定値参照によるマップ外観(背景・グリッド線)の組み立て処理 - private drawBaseMap() { - const { - MAP_WIDTH_PX, MAP_HEIGHT_PX, GRID_CELL_SIZE, - MAP_BG_COLOR, MAP_GRID_COLOR, MAP_BORDER_COLOR - } = config.GAME_CONFIG; - - // マップ全域背景レイヤー - this.bgGraphics.rect(0, 0, MAP_WIDTH_PX, MAP_HEIGHT_PX).fill(MAP_BG_COLOR); - - // 縦方向グリッド線 - for (let x = 0; x <= MAP_WIDTH_PX; x += GRID_CELL_SIZE) { - this.gridGraphics.moveTo(x, 0).lineTo(x, MAP_HEIGHT_PX).stroke({ width: 1, color: MAP_GRID_COLOR }); - } - // 横方向グリッド線 - for (let y = 0; y <= MAP_HEIGHT_PX; y += GRID_CELL_SIZE) { - this.gridGraphics.moveTo(0, y).lineTo(MAP_WIDTH_PX, y).stroke({ width: 1, color: MAP_GRID_COLOR }); - } - - // プレイ領域外枠 - this.gridGraphics.rect(0, 0, MAP_WIDTH_PX, MAP_HEIGHT_PX).stroke({ width: 5, color: MAP_BORDER_COLOR }); - } - - /** - * サーバー(またはテストロジック)から受け取った最新のマップ状態で色を更新する - */ - public updateMapState(state: gridMapTypes.MapState) { - const { GRID_CELL_SIZE, TEAM_COLORS } = config.GAME_CONFIG; - - for (let i = 0; i < state.gridColors.length; i++) { - const teamId = state.gridColors[i]; - const cell = this.cells[i]; - - // 一旦マスの描画をクリア - cell.clear(); - - // 塗布済み(-1以外)の場合のみ色を塗る - if (teamId !== -1) { - // Player.ts と同様に、文字列のカラーコードを PixiJS 用の数値に変換 - const colorString = TEAM_COLORS[teamId] || '#FFFFFF'; - const hexColor = parseInt(colorString.replace("#", "0x"), 16); - - // cell.x, cell.y は設定済みなので、ローカル座標 (0,0) からサイズ分を塗りつぶす - cell.rect(0, 0, GRID_CELL_SIZE, GRID_CELL_SIZE).fill(hexColor); - } - } - } - - /** - * 差分データを受け取って指定のマスだけ色を更新する - */ - public updateCells(updates: gridMapTypes.CellUpdate[]) { - const { GRID_CELL_SIZE, TEAM_COLORS } = config.GAME_CONFIG; - - updates.forEach(({ index, teamId }) => { - const cell = this.cells[index]; - if (!cell) return; - - cell.clear(); - - if (teamId !== -1) { - const colorString = TEAM_COLORS[teamId] || '#FFFFFF'; - const hexColor = parseInt(colorString.replace("#", "0x"), 16); - cell.rect(0, 0, GRID_CELL_SIZE, GRID_CELL_SIZE).fill(hexColor); - } - }); - } -} \ No newline at end of file diff --git a/apps/client/src/scenes/game/GameScene.tsx b/apps/client/src/scenes/game/GameScene.tsx index 2b04995..410ad99 100644 --- a/apps/client/src/scenes/game/GameScene.tsx +++ b/apps/client/src/scenes/game/GameScene.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from "react"; -import { VirtualJoystick } from "./VirtualJoystick"; +import { VirtualJoystick } from "./input/joystick/VirtualJoystick"; import { GameManager } from "./GameManager"; import { config } from "@repo/shared"; diff --git a/apps/client/src/scenes/game/Player.ts b/apps/client/src/scenes/game/Player.ts deleted file mode 100644 index 1408491..0000000 --- a/apps/client/src/scenes/game/Player.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { Graphics } from 'pixi.js'; -import { config } from "@repo/shared"; -import type { playerTypes } from "@repo/shared"; - -/** - * プレイヤーの共通基底クラス(描画と基本データの保持) - */ -export abstract class BasePlayer extends Graphics { - public id: string; - public teamId: number; - public gridX: number; - public gridY: number; - - constructor(data: playerTypes.PlayerData, isLocal: boolean = false) { - super(); - this.id = data.id; - this.teamId = data.teamId; - - // 初期座標のセット(内部はグリッド単位) - this.gridX = data.x; - this.gridY = data.y; - - // gameConfigから定数を取得 - const { - GRID_CELL_SIZE, - PLAYER_RADIUS_PX, - TEAM_COLORS, - PLAYER_LOCAL_STROKE_COLOR, - PLAYER_LOCAL_STROKE_WIDTH, - PLAYER_REMOTE_STROKE_COLOR, - PLAYER_REMOTE_STROKE_WIDTH - } = config.GAME_CONFIG; - - this.position.set(this.gridX * GRID_CELL_SIZE, this.gridY * GRID_CELL_SIZE); - - // チームIDに対応する色をHEX文字列('#RRGGBB')で取得し、PixiJS用の数値(0xRRGGBB)に変換 - const colorString = TEAM_COLORS[this.teamId] || '#FFFFFF'; - const hexColor = parseInt(colorString.replace("#", "0x"), 16); - - // 自プレイヤーか他プレイヤーかで枠線の設定を切り替え - const strokeColor = isLocal ? PLAYER_LOCAL_STROKE_COLOR : PLAYER_REMOTE_STROKE_COLOR; - const strokeWidth = isLocal ? PLAYER_LOCAL_STROKE_WIDTH : PLAYER_REMOTE_STROKE_WIDTH; - - // 塗りつぶしと枠線を同時に描画 - this.circle(0, 0, PLAYER_RADIUS_PX) - .fill(hexColor) - .stroke({ width: strokeWidth, color: strokeColor }); - } - - protected syncDisplayPosition() { - const { GRID_CELL_SIZE } = config.GAME_CONFIG; - this.position.set(this.gridX * GRID_CELL_SIZE, this.gridY * GRID_CELL_SIZE); - } - - // 毎フレーム呼ばれる更新メソッド(サブクラスで具体的な処理を実装させる) - abstract update(deltaTime: number): void; -} - -/** - * 自プレイヤー(キー・ジョイスティック入力で移動・送信する) - */ -export class LocalPlayer extends BasePlayer { - constructor(data: playerTypes.PlayerData) { - super(data, true); - } - - /** - * 入力ベクトルと経過時間基準の座標更新処理 - */ - public move(vx: number, vy: number, deltaTime: number) { - const { PLAYER_SPEED, GRID_COLS, GRID_ROWS, PLAYER_RADIUS } = config.GAME_CONFIG; - - const speed = PLAYER_SPEED * deltaTime; - this.gridX += vx * speed; - this.gridY += vy * speed; - - // 画面外に出ないようにクランプ - 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.syncDisplayPosition(); - } - - public update(_deltaTime: number): void { - // 自プレイヤーは GameScene 側から move() を通じて動かすため、ここでは何もしない - } -} - -/** - * 他プレイヤー(サーバーからの通信を受信して補間・吸着移動する) - */ -export class RemotePlayer extends BasePlayer { - private targetGridX: number; - private targetGridY: number; - - constructor(data: playerTypes.PlayerData) { - super(data, false); - this.targetGridX = data.x; - this.targetGridY = data.y; - } - - /** - * サーバーから受信した最新の座標を目標としてセットする - */ - public setTargetPosition(x?: number, y?: number) { - if (x !== undefined) this.targetGridX = x; - if (y !== undefined) this.targetGridY = y; - } - - /** - * 毎フレームの更新処理(目標座標へのLerp補間) - */ - public update(deltaTime: number): void { - const { PLAYER_LERP_SNAP_THRESHOLD, PLAYER_LERP_SMOOTHNESS } = config.GAME_CONFIG; - - const diffX = this.targetGridX - this.gridX; - const diffY = this.targetGridY - this.gridY; - - // X軸の補間 - if (Math.abs(diffX) < PLAYER_LERP_SNAP_THRESHOLD) { - this.gridX = this.targetGridX; - } else { - this.gridX += diffX * PLAYER_LERP_SMOOTHNESS * deltaTime; - } - - // Y軸の補間 - if (Math.abs(diffY) < PLAYER_LERP_SNAP_THRESHOLD) { - this.gridY = this.targetGridY; - } else { - this.gridY += diffY * PLAYER_LERP_SMOOTHNESS * deltaTime; - } - - this.syncDisplayPosition(); - } -} \ No newline at end of file diff --git a/apps/client/src/scenes/game/VirtualJoystick.tsx b/apps/client/src/scenes/game/VirtualJoystick.tsx deleted file mode 100644 index 90928a1..0000000 --- a/apps/client/src/scenes/game/VirtualJoystick.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { useState } from "react"; - -// ジョイスティック最大入力距離 -export const MAX_DIST = 60; - -type Props = { - // 正規化前入力ベクトル通知コールバック - onMove: (moveX: number, moveY: number) => void; -}; - -// タッチ・マウス両対応仮想ジョイスティック -export const VirtualJoystick = ({ onMove }: Props) => { - // 入力中フラグ - const [isMoving, setIsMoving] = useState(false); - // ジョイスティック基準座標 - const [basePos, setBasePos] = useState({ x: 0, y: 0 }); - // ノブ描画オフセット座標 - const [stickPos, setStickPos] = useState({ x: 0, y: 0 }); - - const handleStart = (e: React.TouchEvent | React.MouseEvent) => { - const clientX = "touches" in e ? e.touches[0].clientX : (e as React.MouseEvent).clientX; - const clientY = "touches" in e ? e.touches[0].clientY : (e as React.MouseEvent).clientY; - setBasePos({ x: clientX, y: clientY }); - setStickPos({ x: 0, y: 0 }); - setIsMoving(true); - }; - - const handleMove = (e: React.TouchEvent | React.MouseEvent) => { - if (!isMoving) return; - const clientX = "touches" in e ? e.touches[0].clientX : (e as React.MouseEvent).clientX; - const clientY = "touches" in e ? e.touches[0].clientY : (e as React.MouseEvent).clientY; - - const dx = clientX - basePos.x; - const dy = clientY - basePos.y; - const dist = Math.sqrt(dx * dx + dy * dy); - const angle = Math.atan2(dy, dx); - - const limitedDist = Math.min(dist, MAX_DIST); - const moveX = Math.cos(angle) * limitedDist; - const moveY = Math.sin(angle) * limitedDist; - - setStickPos({ x: moveX, y: moveY }); - // 距離制限後入力ベクトル通知 - onMove(moveX, moveY); - }; - - const handleEnd = () => { - setIsMoving(false); - setStickPos({ x: 0, y: 0 }); - // 入力終了時停止ベクトル通知 - onMove(0, 0); - }; - - return ( -