diff --git a/apps/client/src/app.tsx b/apps/client/src/app.tsx index 8f7d4ac..5db6e0d 100644 --- a/apps/client/src/app.tsx +++ b/apps/client/src/app.tsx @@ -1,91 +1,83 @@ import { useEffect, useState, useRef } from "react"; import { Stage, Container } from "@pixi/react"; +// 👇 インポートパスを src ディレクトリ基準(./)に修正 import { socketClient } from "./network/SocketClient"; import { GameMap, MAP_SIZE } from "./entities/GameMap"; import { PlayerSprite } from "./entities/PlayerSprite"; import { VirtualJoystick, MAX_DIST } from "./input/VirtualJoystick"; -type Player = { - id: string; - x: number; - y: number; - color: string; -}; +import { TitleScene } from "./scenes/TitleScene"; +import { LobbyScene } from "./scenes/LobbyScene"; +// 👇 共有パッケージへのパスを src/app.tsx からの相対パスに修正 +import type { Room } from "../../../packages/shared/src/types/room"; + +type Player = { id: string; x: number; y: number; color: string; }; + +// 👇 変更: export default function App() に戻しました export default function App() { + const [gameState, setGameState] = useState<"title" | "lobby" | "playing">("title"); + const [room, setRoom] = useState(null); + const [myId, setMyId] = useState(null); const [players, setPlayers] = useState>({}); - const [viewport, setViewport] = useState({ - w: window.innerWidth, - h: window.innerHeight, - }); + const [viewport, setViewport] = useState({ w: window.innerWidth, h: window.innerHeight }); const myPosRef = useRef({ x: MAP_SIZE / 2, y: MAP_SIZE / 2 }); - - // 🌟 追加1: ジョイスティックの「現在の傾き」を常に記憶しておく変数 const joystickInputRef = useRef({ x: 0, y: 0 }); - // 🌟 変更2: ジョイスティックが動いた時は「傾きを記憶するだけ」にする const handleJoystickMove = (moveX: number, moveY: number) => { joystickInputRef.current = { x: moveX, y: moveY }; }; - // 🌟 追加3: 毎秒60回実行される「ゲームループ(エンジン)」 useEffect(() => { - let animationFrameId: number; + if (gameState !== "playing") return; + let animationFrameId: number; const tick = () => { const { x: dx, y: dy } = joystickInputRef.current; - - // 入力(傾き)がある場合のみ移動処理を行う if (myId && (dx !== 0 || dy !== 0)) { const speed = 5.0; let nextX = myPosRef.current.x + (dx / MAX_DIST) * speed; let nextY = myPosRef.current.y + (dy / MAX_DIST) * speed; - nextX = Math.max(20, Math.min(MAP_SIZE - 20, nextX)); nextY = Math.max(20, Math.min(MAP_SIZE - 20, nextY)); myPosRef.current = { x: nextX, y: nextY }; - - // サーバーへ送信 socketClient.sendMove(nextX, nextY); - // ローカルの画面も更新 setPlayers((prev) => { if (!prev[myId]) return prev; return { ...prev, [myId]: { ...prev[myId], x: nextX, y: nextY } }; }); } - - // 次のフレームでもう一度 tick を呼ぶ(これがループの正体) animationFrameId = requestAnimationFrame(tick); }; - // ループ開始 animationFrameId = requestAnimationFrame(tick); - - // コンポーネントが消える時にループを止める return () => cancelAnimationFrame(animationFrameId); - }, [myId]); // 自分のIDが決まったらループ開始 + }, [myId, gameState]); - // 👇 以下は通信処理と描画処理(変更なし) useEffect(() => { socketClient.onConnect((id) => setMyId(id)); + socketClient.onRoomUpdate((updatedRoom) => { + setRoom(updatedRoom); + setGameState("lobby"); + }); + + socketClient.onGameStart(() => { + setGameState("playing"); + }); + socketClient.onCurrentPlayers((serverPlayers: any) => { const pMap: Record = {}; - if (Array.isArray(serverPlayers)) - serverPlayers.forEach((p) => (pMap[p.id] = p)); + if (Array.isArray(serverPlayers)) serverPlayers.forEach((p) => (pMap[p.id] = p)); else Object.assign(pMap, serverPlayers); setPlayers(pMap); }); - - socketClient.onNewPlayer((p: Player) => { - setPlayers((prev) => ({ ...prev, [p.id]: p })); - }); - + socketClient.onNewPlayer((p: Player) => setPlayers((prev) => ({ ...prev, [p.id]: p }))); socketClient.onUpdatePlayer((data: any) => { if (data.id === socketClient.socket.id) return; setPlayers((prev) => { @@ -93,7 +85,6 @@ return { ...prev, [data.id]: { ...prev[data.id], ...data } }; }); }); - socketClient.onRemovePlayer((id: string) => { setPlayers((prev) => { const next = { ...prev }; @@ -104,60 +95,34 @@ const handleResize = () => setViewport({ w: window.innerWidth, h: window.innerHeight }); window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize); }, []); + if (gameState === "title") { + return socketClient.joinRoom(roomId, playerName)} />; + } + + if (gameState === "lobby") { + return socketClient.startGame()} />; + } + const me = myId ? players[myId] : null; const camX = (me?.x || MAP_SIZE / 2) - viewport.w / 2; const camY = (me?.y || MAP_SIZE / 2) - viewport.h / 2; return ( -
+
- + {Object.values(players).map((p) => ( - + ))}
- - -
- ● Online: {Object.keys(players).length} -
); } \ No newline at end of file diff --git a/apps/client/src/network/SocketClient.ts b/apps/client/src/network/SocketClient.ts index 21493e3..c4fb5bf 100644 --- a/apps/client/src/network/SocketClient.ts +++ b/apps/client/src/network/SocketClient.ts @@ -48,11 +48,27 @@ this.socket.emit("move", { x, y }); } - // 切断処理 - disconnect() { - this.socket.disconnect(); + // 👇==== ここからロビー・ルーム用の機能を追加 ====👇 + + // 7. ルーム入室 + joinRoom(roomId: string, playerName: string) { + this.socket.emit("join-room", { roomId, playerName }); + } + + // 8. ルーム情報が更新されたとき + onRoomUpdate(callback: (room: any) => void) { + this.socket.on("room-update", callback); + } + + // 9. ゲーム開始の合図を受け取ったとき + onGameStart(callback: () => void) { + this.socket.on("game-start", callback); + } + + // 10. ゲーム開始をサーバーにリクエストする + startGame() { + this.socket.emit("start-game"); } } -// どこからでも同じ接続を使えるようにインスタンス化してエクスポート export const socketClient = new SocketClient(); \ No newline at end of file diff --git a/apps/client/src/scenes/LobbyScene.tsx b/apps/client/src/scenes/LobbyScene.tsx new file mode 100644 index 0000000..9dee113 --- /dev/null +++ b/apps/client/src/scenes/LobbyScene.tsx @@ -0,0 +1,48 @@ +import type { Room } from "../../../../packages/shared/src/types/room"; // パスは適宜調整してください + +type Props = { + room: Room | null; + myId: string | null; + onStart: () => void; +}; + +export const LobbyScene = ({ room, myId, onStart }: Props) => { + if (!room) return
読み込み中...
; + + const isMeOwner = room.ownerId === myId; + + return ( +
+

ルーム: {room.roomId} (待機中)

+ +
+

+ 参加プレイヤー ({room.players.length}/{room.maxPlayers}) +

+
    + {room.players.map((p) => ( +
  • + {p.id === myId ? "🟢" : "⚪"} + {p.name} + {p.isOwner && 👑} + {p.isReady && } +
  • + ))} +
+
+ +
+ {isMeOwner ? ( + + ) : ( +

オーナーがゲームを開始するのを待っています...

+ )} +
+
+ ); +}; \ No newline at end of file diff --git a/apps/client/src/scenes/TitleScene.tsx b/apps/client/src/scenes/TitleScene.tsx new file mode 100644 index 0000000..a559685 --- /dev/null +++ b/apps/client/src/scenes/TitleScene.tsx @@ -0,0 +1,49 @@ +import { useState } from "react"; + +type Props = { + onJoin: (roomId: string, playerName: string) => void; +}; + +export const TitleScene = ({ onJoin }: Props) => { + const [playerName, setPlayerName] = useState(""); + const [roomIdInput, setRoomIdInput] = useState(""); + + return ( +
+

Pixel Paint War

+ +
+ setPlayerName(e.target.value)} + style={{ padding: "12px", fontSize: "1.2rem", borderRadius: "5px", border: "none" }} + /> + setRoomIdInput(e.target.value)} + style={{ padding: "12px", fontSize: "1.2rem", borderRadius: "5px", border: "none" }} + /> +
+ + +
+ ); +}; \ No newline at end of file diff --git a/apps/server/src/network/SocketManager.ts b/apps/server/src/network/SocketManager.ts index 6536984..0608eee 100644 --- a/apps/server/src/network/SocketManager.ts +++ b/apps/server/src/network/SocketManager.ts @@ -1,10 +1,16 @@ // src/network/SocketManager.ts import { Server, Socket } from "socket.io"; import { GameManager } from "../managers/GameManager.js"; +// shared側の型をインポート(※パスは実際の環境に合わせて修正してください) +import type { Room } from "/home/ryuryu/lab/SkillSemiWebGame/packages/shared/src/types/room"; +type RoomPlayer = Room["players"][0]; export class SocketManager { private io: Server; private gameManager: GameManager; + + // 🌟 追加: サーバーのメモリ上でルーム情報を管理 + private rooms: Map = new Map(); constructor(io: Server, gameManager: GameManager) { this.io = io; @@ -15,40 +21,120 @@ this.io.on("connection", (socket: Socket) => { console.log(`✅ User connected: ${socket.id}`); - // 1. ゲームにプレイヤーを追加 - const player = this.gameManager.addPlayer(socket.id); + // ========================================== + // 🚪 ロビー・ルーム関連のイベント + // ========================================== - // 2. 参加した本人に「現在の全プレイヤー」を教える - socket.emit("current_players", this.gameManager.getAllPlayers()); + socket.on("join-room", (data: { roomId: string; playerName: string }) => { + const { roomId, playerName } = data; + + // socket.io の機能でグループ(ルーム)に参加 + socket.join(roomId); - // 3. 他のみんなに「新しい人が来たよ」と教える - socket.broadcast.emit("new_player", player); + // ルームが存在しない場合は作成 + let room = this.rooms.get(roomId); + if (!room) { + room = { + roomId: roomId, + ownerId: socket.id, // 最初に作った人がオーナー + players: [], + status: 'waiting', + maxPlayers: 4 + }; + this.rooms.set(roomId, room); + } - // --- イベント受信 --- + // プレイヤー情報を追加 + const newPlayer: RoomPlayer = { + id: socket.id, + name: playerName, + isOwner: room.ownerId === socket.id, + isReady: false + }; + room.players.push(newPlayer); - // 移動データが来た時 + // ルームの全員に最新情報を送信 + this.io.to(roomId).emit("room-update", room); + }); + + // 🚀 ゲーム開始イベント + socket.on("start-game", () => { + // 自分がオーナーのルームを探す + for (const [roomId, room] of this.rooms.entries()) { + if (room.ownerId === socket.id) { + room.status = 'playing'; + + // 📢 全員に「ゲーム画面に切り替えて!」と指示 + this.io.to(roomId).emit("game-start"); + + // 🎮 ここで初めて全員を GameManager (PixiJSの世界) に追加する + room.players.forEach(p => { + this.gameManager.addPlayer(p.id); + }); + + // ゲーム用の初期データを送信 (参加した全員分の座標など) + // getAllPlayers() は Map を配列に変換して返すメソッドと仮定 + const allPlayers = Array.from(this.gameManager['players'].values()); + this.io.to(roomId).emit("current_players", allPlayers); + break; + } + } + }); + + // ========================================== + // 🎮 ゲームプレイ中のイベント + // ========================================== + socket.on("move", (data: { x: number; y: number }) => { // マネージャーの状態を更新 this.gameManager.movePlayer(socket.id, data.x, data.y); - // 全員に位置情報を配信 - // (人数が増えたらここを最適化しますが、まずはこれでOK) const updatedPlayer = this.gameManager.getPlayer(socket.id); if (updatedPlayer) { - this.io.emit("update_player", { - id: socket.id, - x: updatedPlayer.x, - y: updatedPlayer.y - }); + // 自分のいるルームを取得して、同じルームの人にだけ座標を送る + const myRooms = Array.from(socket.rooms).filter(r => r !== socket.id); + const targetRoom = myRooms.length > 0 ? myRooms[0] : null; + + if (targetRoom) { + this.io.to(targetRoom).emit("update_player", { + id: socket.id, + x: updatedPlayer.x, + y: updatedPlayer.y + }); + } } }); - // 切断した時 + // ========================================== + // ❌ 切断時のイベント + // ========================================== + socket.on("disconnect", () => { console.log(`❌ User disconnected: ${socket.id}`); this.gameManager.removePlayer(socket.id); this.io.emit("remove_player", socket.id); + + // ルームからも削除する + for (const [roomId, room] of this.rooms.entries()) { + const playerIndex = room.players.findIndex(p => p.id === socket.id); + if (playerIndex !== -1) { + room.players.splice(playerIndex, 1); + + if (room.players.length === 0) { + // 誰もいなくなったらルームごと削除 + this.rooms.delete(roomId); + } else { + // オーナーが抜けた場合は次の人をオーナーにする + if (room.ownerId === socket.id) { + room.ownerId = room.players[0].id; + room.players[0].isOwner = true; + } + this.io.to(roomId).emit("room-update", room); + } + } + } }); + }); } } \ No newline at end of file diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json index 1ed84b5..26213dc 100644 --- a/apps/server/tsconfig.json +++ b/apps/server/tsconfig.json @@ -6,7 +6,7 @@ "lib": ["ES2022"], "strict": true, "outDir": "./dist", - "rootDir": "./src", + //"rootDir": "./src", "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true,