Newer
Older
PixelPaintWar / apps / client / src / entities / Player.ts
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();
  }
}