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

// ネットワーク・入力関連
import { socketClient } from "../network/SocketClient";
import { VirtualJoystick, MAX_DIST } from "../input/VirtualJoystick";
import { GAME_CONFIG } from "@repo/shared/src/config/gameConfig";
import { type PlayerData } from "@repo/shared/src/types/player";

// ゲーム描画オブジェクト
import { GameMap } from "../entities/GameMap";
// 変更点: BasePlayer, LocalPlayer, RemotePlayer をインポート
import { BasePlayer, LocalPlayer, RemotePlayer } from "../entities/Player";

interface GameSceneProps {
  myId: string | null;
}

/**
 * メインゲーム画面コンポーネント
 * PixiJS 初期化・エンティティ管理・ソケット同期処理
 */
export function GameScene({ myId }: GameSceneProps) {
  const pixiContainerRef = useRef<HTMLDivElement>(null);
  // ジョイスティック入力値
  const joystickInputRef = useRef({ x: 0, y: 0 });
  
  // 変更点: 型を共通基底クラス BasePlayer に変更
  const playersRef = useRef<Record<string, BasePlayer>>({});
  
  // 位置送信間隔制御用タイムスタンプ
  const lastPositionSentTimeRef = useRef<number>(0);
  // 前フレーム移動状態
  const wasMovingRef = useRef<boolean>(false);

  // 💡 削除: targetPositionsRef は RemotePlayer 内にカプセル化されたため不要になりました!

  useEffect(() => {
    if (!pixiContainerRef.current) return;

    let isCancelled = false;
    let isInitialized = 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>) => {
        const playersArray = (Array.isArray(serverPlayers) ? serverPlayers : Object.values(serverPlayers)) as PlayerData[];
        playersArray.forEach((p) => {
          // 変更点: 自身か他プレイヤーかで生成するインスタンスを切り替え
          const playerSprite = p.id === myId 
            ? new LocalPlayer(p) 
            : new RemotePlayer(p);
            
          worldContainer.addChild(playerSprite);
          playersRef.current[p.id] = playerSprite;
        });
      });

      // 新規参加プレイヤー追加処理
      socketClient.onNewPlayer((p: PlayerData) => {
        // 新規参加は必ず他人なので RemotePlayer を生成
        const playerSprite = new RemotePlayer(p);
        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];
        // 変更点: RemotePlayer のメソッドを呼び出して目標座標をセットするだけ
        if (target && target instanceof RemotePlayer) {
          target.setTargetPosition(data.x, data.y);
        }
      });

      // 退出プレイヤー参照削除とオブジェクト破棄
      socketClient.onRemovePlayer((id: string) => {
        const target = playersRef.current[id];
        if (target) {
          worldContainer.removeChild(target);
          target.destroy();
          delete playersRef.current[id];
        }
      });

      // --- PixiJS 本体初期化 ---
      await app.init({ resizeTo: window, backgroundColor: 0x111111, antialias: true });
      isInitialized = true;
      
      if (isCancelled) {
        app.destroy(true, { children: true });
        return;
      }
      
      pixiContainerRef.current?.appendChild(app.canvas);
      app.stage.addChild(worldContainer);

      socketClient.readyForGame();

      // --- メインゲームループ ---
      app.ticker.add((ticker) => {
        if (!myId) return;
        const me = playersRef.current[myId];
        // 自身が LocalPlayer であることを担保
        if (!me || !(me instanceof LocalPlayer)) return; 

        // 自プレイヤー移動と送信処理
        const { x: dx, y: dy } = joystickInputRef.current;
        const isMoving = dx !== 0 || dy !== 0;
        
        if (isMoving) {
          me.move(dx / MAX_DIST, dy / MAX_DIST, ticker.deltaTime);
          
          const now = performance.now();
          if (now - lastPositionSentTimeRef.current >= GAME_CONFIG.PLAYER_POSITION_UPDATE_MS) {
            socketClient.sendMove(me.x, me.y);
            lastPositionSentTimeRef.current = now;
          }
        } else if (wasMovingRef.current) {
          socketClient.sendMove(me.x, me.y);
        }
        wasMovingRef.current = isMoving;

        // 変更点: ループ内の補間処理がなくなり、一律に update を呼ぶだけになりました!(ポリモーフィズム)
        Object.values(playersRef.current).forEach((player) => {
          player.update(ticker.deltaTime);
        });

        // 自プレイヤー中心表示向けワールド逆方向オフセット(カメラ追従)
        worldContainer.position.set(
          -(me.x - app.screen.width / 2),
          -(me.y - app.screen.height / 2)
        );
      });
    };

    initPixi();

    return () => {
      isCancelled = true;
      if (isInitialized) {
        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");
    };
  }, [myId]);

  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%" }}>
        <VirtualJoystick onMove={(x, y) => { joystickInputRef.current = { x, y }; }} />
      </div>
    </div>
  );
}