diff --git a/apps/client/package.json b/apps/client/package.json index a2efbe5..d7b20a2 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -9,18 +9,19 @@ "preview": "vite preview" }, "dependencies": { + "@pixi/react": "^7.1.2", "@repo/shared": "workspace:*", - "@types/react": "^19.2.13", - "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.3", - "pixi.js": "^8.16.0", - "react": "^19.2.4", - "react-dom": "^19.2.4", + "pixi.js": "^7.4.3", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-joystick-component": "^6.2.1", "socket.io-client": "^4.8.3" }, "devDependencies": { "@types/node": "^24.10.1", + "@types/react": "^18.3.28", + "@types/react-dom": "^18.3.7", "typescript": "~5.9.3", "vite": "^7.2.4" } diff --git a/apps/client/src/app.tsx b/apps/client/src/app.tsx index b0b5d8a..4bcc634 100644 --- a/apps/client/src/app.tsx +++ b/apps/client/src/app.tsx @@ -1,10 +1,10 @@ -import { useEffect, useState, useRef } from "react"; +import { useEffect, useState, useRef, useCallback } from "react"; import { io, Socket } from "socket.io-client"; +// ✅ v7 (安定版) の正しいインポート +import { Stage, Container, Graphics } from "@pixi/react"; -// 設定:マップの広さ(全員共通) const MAP_SIZE = 2000; -// ジョイスティックの設定(Joystick.ts のロジックを参考) -const MAX_DIST = 60; +const MAX_DIST = 60; // ジョイスティックの可動範囲 type Player = { id: string; @@ -13,146 +13,176 @@ color: string; }; -function App() { +// 🎨 プレイヤー描画(v7対応) +const PlayerCharacter = ({ + x, + y, + color, + isMe, +}: { + x: number; + y: number; + color: string; + isMe: boolean; +}) => { + const draw = useCallback( + (g: any) => { + g.clear(); + const hexColor = parseInt(color.replace("#", "0x"), 16) || 0xff0000; + + // v7の書き方: beginFill を使う + g.beginFill(hexColor); + g.drawCircle(0, 0, 10); + g.endFill(); + + if (isMe) { + g.lineStyle(3, 0xffff00); + g.drawCircle(0, 0, 10); + } + }, + [color, isMe], + ); + + return ; +}; + +// 🗺️ 背景マップ描画(v7対応) +const GameMap = () => { + const draw = useCallback((g: any) => { + g.clear(); + g.beginFill(0x111111); + g.drawRect(0, 0, MAP_SIZE, MAP_SIZE); + g.endFill(); + + // グリッド線 + g.lineStyle(1, 0x333333); + for (let i = 0; i <= MAP_SIZE; i += 100) { + g.moveTo(i, 0).lineTo(i, MAP_SIZE); + g.moveTo(0, i).lineTo(MAP_SIZE, i); + } + + // 外枠 + g.lineStyle(5, 0xff4444); + g.drawRect(0, 0, MAP_SIZE, MAP_SIZE); + }, []); + + return ; +}; + +export default function App() { 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 [isMoving, setIsMoving] = useState(false); - const [basePos, setBasePos] = useState({ x: 0, y: 0 }); // タッチを開始した中心点 - const [stickPos, setStickPos] = useState({ x: 0, y: 0 }); // スティックの相対移動量 - - // 自分の位置(計算用) + const [basePos, setBasePos] = useState({ x: 0, y: 0 }); + const [stickPos, setStickPos] = useState({ x: 0, y: 0 }); + const myPosRef = useRef({ x: MAP_SIZE / 2, y: MAP_SIZE / 2 }); const socketRef = useRef(null); - const initializedRef = useRef(false); - // 1. タッチ/クリック開始:ジョイスティックの拠点を決める + // 🕹️ 操作開始 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; - + 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); }; - // 2. 移動中:スティックを動かし、プレイヤーを移動させる + // 🕹️ 操作中 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 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); - - // スティックが外枠からはみ出さないように制限 (Joystick.ts のロジック) + const limitedDist = Math.min(dist, MAX_DIST); const moveX = Math.cos(angle) * limitedDist; const moveY = Math.sin(angle) * limitedDist; setStickPos({ x: moveX, y: moveY }); - // 入力値の正規化 (-1.0 ~ 1.0) - const inputX = moveX / MAX_DIST; - const inputY = moveY / MAX_DIST; + if (socketRef.current && myId) { + const speed = 5.0; + let nextX = myPosRef.current.x + (moveX / MAX_DIST) * speed; + let nextY = myPosRef.current.y + (moveY / MAX_DIST) * speed; - if (!socketRef.current || !myId) return; + nextX = Math.max(20, Math.min(MAP_SIZE - 20, nextX)); + nextY = Math.max(20, Math.min(MAP_SIZE - 20, nextY)); - // 移動の実行 - const speed = 5.0; - let nextX = myPosRef.current.x + (inputX * speed); - let nextY = myPosRef.current.y + (inputY * speed); + myPosRef.current = { x: nextX, y: nextY }; - // 壁判定 - if (nextX < 20) nextX = 20; - if (nextX > MAP_SIZE - 20) nextX = MAP_SIZE - 20; - if (nextY < 20) nextY = 20; - if (nextY > MAP_SIZE - 20) nextY = MAP_SIZE - 20; - - myPosRef.current = { x: nextX, y: nextY }; - - // 自分の画面を即座に更新 - setPlayers(prev => { - if (!prev[myId]) return prev; - return { ...prev, [myId]: { ...prev[myId], x: nextX, y: nextY } }; - }); - - // サーバーへ送信 - socketRef.current.emit("move", { x: nextX, y: nextY }); + socketRef.current.emit("move", { x: nextX, y: nextY }); + setPlayers((prev) => { + if (!prev[myId]) return prev; + return { ...prev, [myId]: { ...prev[myId], x: nextX, y: nextY } }; + }); + } }; - // 3. 終了:ジョイスティックをリセット const handleEnd = () => { setIsMoving(false); setStickPos({ x: 0, y: 0 }); }; - // 初期位置同期 useEffect(() => { - if (!myId || initializedRef.current) return; - const me = players[myId]; - if (me) { - myPosRef.current = { x: me.x, y: me.y }; - initializedRef.current = true; - } - }, [players, myId]); - - // Socket通信設定 - useEffect(() => { - const handleResize = () => setViewport({ w: window.innerWidth, h: window.innerHeight }); - window.addEventListener("resize", handleResize); - - socketRef.current = io(); - const socket = socketRef.current; + const socket = io(); + socketRef.current = socket; socket.on("connect", () => setMyId(socket.id || null)); - socket.on("current_players", (serverPlayers: Player[]) => { + socket.on("current_players", (serverPlayers: any) => { const pMap: Record = {}; - 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); }); - socket.on("new_player", (p: Player) => setPlayers(prev => ({ ...prev, [p.id]: p }))); - socket.on("update_player", (d: { id: string; x: number; y: number }) => { - if (d.id === socket.id) return; - setPlayers(prev => { - const target = prev[d.id]; - if (!target) return prev; - return { ...prev, [d.id]: { ...target, x: d.x, y: d.y } }; + socket.on("new_player", (p: Player) => + setPlayers((prev) => ({ ...prev, [p.id]: p })), + ); + socket.on("update_player", (data: any) => { + if (data.id === socket.id) return; + setPlayers((prev) => { + if (!prev[data.id]) return prev; + return { ...prev, [data.id]: { ...prev[data.id], ...data } }; }); }); socket.on("remove_player", (id: string) => { - setPlayers(prev => { + setPlayers((prev) => { const next = { ...prev }; delete next[id]; return next; }); }); + const handleResize = () => + setViewport({ w: window.innerWidth, h: window.innerHeight }); + window.addEventListener("resize", handleResize); return () => { socket.disconnect(); window.removeEventListener("resize", handleResize); }; }, []); - // カメラ計算 - let cameraX = 0; - let cameraY = 0; - if (myId && players[myId]) { - const me = players[myId]; - cameraX = me.x - viewport.w / 2; - cameraY = me.y - viewport.h / 2; - } + 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 ( -
- - {/* UIレイヤー */} -
-
● Online: {Object.keys(players).length}
-
ID: {myId?.slice(0, 4)}
+ {/* 🚀 Layer 1: PixiJS描画 (v7) */} +
+ + + + {Object.values(players).map((p) => ( + + ))} + +
- {/* ゲームワールド */} -
- {Object.values(players).map((p) => ( + {/* 🕹️ Layer 2: ジョイスティックUI */} + {isMoving && ( +
-
- {p.id.slice(0,4)} -
-
- ))} -
- - {/* 自作ジョイスティックのUI */} - {isMoving && ( -
- {/* 内側の動くスティック部分 */} -
+ />
)} +
+ ● Online: {Object.keys(players).length} +
); } - -export default App; \ No newline at end of file