Newer
Older
PixelPaintWar / apps / server / src / domains / game / loop / HurricaneSystem.ts
/**
 * HurricaneSystem
 * ハリケーンの生成と各サービス呼び出しを管理する
 * GameLoop からハリケーン専用責務を分離する
 */
import { config } from "@server/config";
import { Player } from "../entities/player/Player.js";
import { HurricaneMotionService } from "./hurricane/HurricaneMotionService.js";
import { HurricaneSyncService } from "./hurricane/HurricaneSyncService.js";
import { HurricaneHitService } from "./hurricane/HurricaneHitService.js";
import type {
  HurricaneState,
  HurricaneSyncOutputs,
  MapGridSize,
} from "./hurricane/hurricaneTypes.js";

/** 1ティック分のハリケーン同期出力 */
export type { HurricaneSyncOutputs } from "./hurricane/hurricaneTypes.js";

/** ハリケーン状態の生成更新と被弾判定を管理する */
export class HurricaneSystem {
  private readonly mapSize: MapGridSize;
  private hasSpawned = false;
  private hurricanes: HurricaneState[] = [];
  private readonly motionService: HurricaneMotionService;
  private readonly syncService: HurricaneSyncService;
  private readonly hitService: HurricaneHitService;

  constructor(mapSize: MapGridSize) {
    this.mapSize = mapSize;
    this.motionService = new HurricaneMotionService(mapSize);
    this.syncService = new HurricaneSyncService();
    this.hitService = new HurricaneHitService();
  }

  /** 残り時間しきい値到達時にハリケーンを一度だけ生成する */
  public ensureSpawned(elapsedMs: number): void {
    if (!config.GAME_CONFIG.HURRICANE_ENABLED || this.hasSpawned) {
      return;
    }

    const remainingSec =
      config.GAME_CONFIG.GAME_DURATION_SEC - elapsedMs / 1000;
    if (remainingSec > config.GAME_CONFIG.HURRICANE_SPAWN_REMAINING_SEC) {
      return;
    }

    this.hasSpawned = true;
    this.syncService.markInitialSyncPending();
    this.hurricanes = Array.from(
      { length: config.GAME_CONFIG.HURRICANE_COUNT },
      (_, index) => this.createHurricane(index),
    );
  }

  /** 1ティック分のハリケーン同期出力をまとめて返す */
  public consumeSyncOutputs(elapsedMs: number): HurricaneSyncOutputs {
    return this.syncService.consumeSyncOutputs(elapsedMs, this.hurricanes);
  }

  /** ハリケーンを直線移動させ,境界で反射させる */
  public update(deltaSec: number): void {
    this.motionService.update(this.hurricanes, deltaSec);
  }

  /** クールダウン付きで被弾プレイヤーID配列を返す */
  public collectHitPlayerIds(
    players: Map<string, Player>,
    nowMs: number,
  ): string[] {
    return this.hitService.collectHitPlayerIds(this.hurricanes, players, nowMs);
  }

  /** 状態を初期化する */
  public clear(): void {
    this.hasSpawned = false;
    this.hurricanes = [];
    this.syncService.clear();
    this.hitService.clear();
  }

  /** ハリケーン初期状態を生成する */
  private createHurricane(index: number): HurricaneState {
    const radius = config.GAME_CONFIG.HURRICANE_DIAMETER_GRID / 2;
    const x = this.randomInRange(radius, this.mapSize.gridCols - radius);
    const y = this.randomInRange(radius, this.mapSize.gridRows - radius);
    const directionRad = this.randomInRange(0, Math.PI * 2);
    const speed = config.GAME_CONFIG.HURRICANE_MOVE_SPEED;

    return {
      id: `hurricane-${index + 1}`,
      x,
      y,
      vx: Math.cos(directionRad) * speed,
      vy: Math.sin(directionRad) * speed,
      radius,
      rotationRad: directionRad,
    };
  }

  private randomInRange(min: number, max: number): number {
    return min + Math.random() * Math.max(0, max - min);
  }
}