Newer
Older
PixelPaintWar / apps / client / src / scenes / game / application / loopSteps / SimulationStep.ts
/**
 * SimulationStep
 * ゲームループのシミュレーション段を担う
 * ローカル更新とリモート補間更新を順に実行する
 */
import { config } from "@client/config";
import { LocalPlayerController, RemotePlayerController } from "@client/scenes/game/entities/player/PlayerController";
import type { PlayerRepository } from "@client/scenes/game/entities/player/PlayerRepository";
import type { MoveSender } from "@client/scenes/game/application/network/PlayerMoveSender";
import type { LoopFrameContext, LoopFrameEffects, LoopStep } from "./LoopStep";

/** SimulationStep の初期化入力 */
type SimulationStepOptions = {
  moveSender: MoveSender;
  nowMsProvider?: () => number;
};

type SimulationStepParams = {
  me: LocalPlayerController;
  playerRepository: PlayerRepository;
  deltaSeconds: number;
  isMoving: boolean;
};

/** シミュレーション段の更新処理を担うステップ */
export class SimulationStep implements LoopStep {
  private readonly moveSender: MoveSender;
  private readonly nowMsProvider: () => number;
  private lastPositionSentTime = 0;
  private wasMoving = false;

  constructor({ moveSender, nowMsProvider = () => performance.now() }: SimulationStepOptions) {
    this.moveSender = moveSender;
    this.nowMsProvider = nowMsProvider;
  }

  /** ローカル更新とリモート補間更新を実行する */
  public run(
    context: Readonly<LoopFrameContext>,
    _effects: LoopFrameEffects,
  ): void {
    const params: SimulationStepParams = {
      me: context.me,
      playerRepository: context.playerRepository,
      deltaSeconds: context.deltaSeconds,
      isMoving: context.isMoving,
    };

    this.runLocalSimulation({ me: params.me, isMoving: params.isMoving });
    this.runRemoteSimulation({
      playerRepository: params.playerRepository,
      deltaSeconds: params.deltaSeconds,
    });
  }

  private runLocalSimulation({ me, isMoving }: Pick<SimulationStepParams, "me" | "isMoving">) {
    if (isMoving) {
      me.tick();

      const now = this.nowMsProvider();
      if (now - this.lastPositionSentTime >= config.GAME_CONFIG.PLAYER_POSITION_UPDATE_MS) {
        const position = me.getPosition();
        this.moveSender.sendMove(position.x, position.y);
        this.lastPositionSentTime = now;
      }
    } else if (this.wasMoving) {
      me.tick();
      const position = me.getPosition();
      this.moveSender.sendMove(position.x, position.y);
    } else {
      me.tick();
    }

    this.wasMoving = isMoving;
  }

  private runRemoteSimulation({ playerRepository, deltaSeconds }: Pick<SimulationStepParams, "playerRepository" | "deltaSeconds">) {
    playerRepository.values().forEach((player) => {
      if (player instanceof RemotePlayerController) {
        player.tick(deltaSeconds);
      }
    });
  }
}