Newer
Older
PixelPaintWar / apps / client / src / app.tsx
import { useEffect, useState, useRef } from "react";
import { Application, Container } from "pixi.js";

// ネットワーク・入力
import { socketClient, type PlayerData } from "./network/SocketClient";
import { VirtualJoystick, MAX_DIST } from "./input/VirtualJoystick";

// シーン(画面)コンポーネント
import { TitleScene } from "./scenes/TitleScene";
import { LobbyScene } from "./scenes/LobbyScene";

// ゲームオブジェクト(純粋なPixiJSクラス)
import { GameMap } from "./entities/GameMap";
import { Player } from "./entities/Player";

import type { Room } from "@repo/shared/src/types/room";

export default function App() {
  const [gameState, setGameState] = useState<"title" | "lobby" | "playing">("title");
  const [room, setRoom] = useState<Room | null>(null);
  const [myId, setMyId] = useState<string | null>(null);

  const pixiContainerRef = useRef<HTMLDivElement>(null);
  const joystickInputRef = useRef({ x: 0, y: 0 });
  const playersRef = useRef<Record<string, Player>>({});

  useEffect(() => {
    socketClient.onConnect((id) => setMyId(id));
    socketClient.onRoomUpdate((updatedRoom) => {
      setRoom(updatedRoom);
      setGameState("lobby");
    });
    socketClient.onGameStart(() => setGameState("playing"));
  }, []);

  /**
   * ゲーム本体(PixiJSの実行環境)
   */
  useEffect(() => {
    if (gameState !== "playing" || !pixiContainerRef.current) return;

    let isCancelled = false;
    const app = new Application();
    const worldContainer = new Container();

    const initPixi = async () => {
      // 🌟 【修正ポイント】マップを「一番最初」にコンテナに追加する!
      // これでマップが一番下のレイヤー(背景)として確定します。
      const gameMap = new GameMap();
      worldContainer.addChild(gameMap);

      // マップを敷いた後で、ソケットの受信設定をする
      socketClient.onCurrentPlayers((serverPlayers: PlayerData[] | Record<string, PlayerData>) => {
        console.log("🔥 プレイヤー一覧を受信:", serverPlayers); // 確認用のログ
        const playersArray = (Array.isArray(serverPlayers) ? serverPlayers : Object.values(serverPlayers)) as PlayerData[];
        playersArray.forEach((p) => {
          const isMe = p.id === myId;
          const playerSprite = new Player(p.color, isMe);
          playerSprite.position.set(p.x, p.y);
          worldContainer.addChild(playerSprite);
          playersRef.current[p.id] = playerSprite;
        });
      });

      socketClient.onNewPlayer((p: PlayerData) => {
        console.log("🔥 新規プレイヤー参加:", p);
        const playerSprite = new Player(p.color, false);
        playerSprite.position.set(p.x, p.y);
        worldContainer.addChild(playerSprite);
        playersRef.current[p.id] = playerSprite;
      });

      socketClient.onUpdatePlayer((data: Partial<PlayerData> & { id: string }) => {
        if (data.id === myId) return;
        const target = playersRef.current[data.id];
        if (target) {
          if (data.x !== undefined) target.x = data.x;
          if (data.y !== undefined) target.y = data.y;
        }
      });

      socketClient.onRemovePlayer((id: string) => {
        const target = playersRef.current[id];
        if (target) {
          worldContainer.removeChild(target);
          target.destroy();
          delete playersRef.current[id];
        }
      });

      // データの受け取り口を作った後で、ゆっくりPixiを初期化する
      await app.init({ resizeTo: window, backgroundColor: 0x111111, antialias: true });
      if (isCancelled) return;
      
      pixiContainerRef.current?.appendChild(app.canvas);
      app.stage.addChild(worldContainer);

      // 🌟 【新規追加】 画面も受信設定もすべて完了したので、サーバーに「データちょうだい!」と要求する
      socketClient.readyForGame();

      // ゲームループ(Ticker)
      app.ticker.add((ticker) => {
        if (!myId) return;
        const me = playersRef.current[myId];
        if (!me) return; // 👈 自分のデータがない場合はここで処理が止まっていた

        const { x: dx, y: dy } = joystickInputRef.current;
        
        if (dx !== 0 || dy !== 0) {
          me.move(dx / MAX_DIST, dy / MAX_DIST, ticker.deltaTime);
          socketClient.sendMove(me.x, me.y);
        }

        // カメラ追従
        worldContainer.position.set(
          -(me.x - window.innerWidth / 2),
          -(me.y - window.innerHeight / 2)
        );
      });
    };

    initPixi();

    // 🚨 【修正ポイント2】 クリーンアップ処理を追加
    return () => {
      isCancelled = true;
      app.destroy(true, { children: true });
      playersRef.current = {};
      
      // コンポーネントが破棄される時に、ソケットの受信設定も解除する(重複防止)
      socketClient.socket.off("current_players");
      socketClient.socket.off("new_player");
      socketClient.socket.off("update_player");
      socketClient.socket.off("remove_player");
    };
  }, [gameState, myId]);

  // レンダリング分岐(省略:TitleScene / LobbyScene)
  if (gameState === "title") return <TitleScene onJoin={(roomId, playerName) => socketClient.joinRoom(roomId, playerName)} />;
  if (gameState === "lobby") return <LobbyScene room={room} myId={myId} onStart={() => socketClient.startGame()} />;

  return (
    <div style={{ width: "100vw", height: "100vh", overflow: "hidden", position: "relative", backgroundColor: "#000" }}>
      <div ref={pixiContainerRef} style={{ position: "absolute", top: 0, left: 0, zIndex: 1 }} />
      <div style={{ position: "absolute", zIndex: 2, width: "100%", height: "100%" }}>
        {/* ジョイスティックはReact UIとして管理 */}
        <VirtualJoystick onMove={(x, y) => { joystickInputRef.current = { x, y }; }} />
      </div>
    </div>
  );
}