/**
* BombPlacementService
* 爆弾設置要求の生成と描画ペイロード化を担う
* クールダウン判定とチーム情報解決を提供する
*/
import { config } from "@client/config";
import { LocalPlayerController } from "@client/scenes/game/entities/player/PlayerController";
import { AppearanceResolver } from "@client/scenes/game/application/AppearanceResolver";
import type { GamePlayers } from "@client/scenes/game/application/game.types";
import type { PlaceBombPayload } from "@repo/shared";
import { config as sharedConfig } from "@repo/shared";
import { BombIdRegistry } from "@client/scenes/game/entities/bomb/BombIdRegistry";
import type { BombRenderPayload } from "@client/scenes/game/entities/bomb/runtime/BombRepository";
/** BombPlacementService の初期化入力 */
export type BombPlacementServiceOptions = {
players: GamePlayers;
myId: string;
getElapsedMs: () => number;
appearanceResolver: AppearanceResolver;
bombIdRegistry: BombIdRegistry;
};
/** ローカル設置時の生成結果型 */
export type OwnBombPlacement = {
tempBombId: string;
payload: PlaceBombPayload;
renderPayload: BombRenderPayload;
};
/** 爆弾設置要求の生成と描画情報変換を担う */
export class BombPlacementService {
private readonly players: GamePlayers;
private readonly myId: string;
private readonly getElapsedMs: () => number;
private readonly appearanceResolver: AppearanceResolver;
private readonly bombIdRegistry: BombIdRegistry;
private lastBombPlacedElapsedMs = Number.NEGATIVE_INFINITY;
constructor({
players,
myId,
getElapsedMs,
appearanceResolver,
bombIdRegistry,
}: BombPlacementServiceOptions) {
this.players = players;
this.myId = myId;
this.getElapsedMs = getElapsedMs;
this.appearanceResolver = appearanceResolver;
this.bombIdRegistry = bombIdRegistry;
}
/** 自プレイヤー位置に爆弾を仮IDで設置し,設置要求を返す */
public placeOwnBomb(): OwnBombPlacement | null {
const me = this.players[this.myId];
if (!me || !(me instanceof LocalPlayerController)) {
return null;
}
const elapsedMs = this.getElapsedMs();
const {
BOMB_COOLDOWN_MS,
BOMB_NORMAL_COOLDOWN_MS,
BOMB_FEVER_COOLDOWN_MS,
BOMB_FEVER_START_REMAINING_SEC,
BOMB_FUSE_MS,
GAME_DURATION_SEC,
} = config.GAME_CONFIG;
const remainingSec = Math.max(0, GAME_DURATION_SEC - elapsedMs / 1000);
const isFeverTime = remainingSec <= BOMB_FEVER_START_REMAINING_SEC;
const cooldownMs = isFeverTime
? BOMB_FEVER_COOLDOWN_MS
: (BOMB_NORMAL_COOLDOWN_MS ?? BOMB_COOLDOWN_MS);
if (elapsedMs - this.lastBombPlacedElapsedMs < cooldownMs) {
return null;
}
const position = me.getPosition();
const { requestId, tempBombId } =
this.bombIdRegistry.issuePendingOwnBombId();
const payload: PlaceBombPayload = {
requestId,
x: position.x,
y: position.y,
explodeAtElapsedMs: elapsedMs + BOMB_FUSE_MS,
};
const ownTeamId = this.resolveTeamIdBySocketId(this.myId);
this.lastBombPlacedElapsedMs = elapsedMs;
return {
tempBombId,
payload,
renderPayload: this.toRenderPayload(payload, ownTeamId),
};
}
/** 指定オーナー情報から描画ペイロードを生成する */
public createRenderPayload(
payload: { x: number; y: number; explodeAtElapsedMs: number },
ownerSocketId: string,
): BombRenderPayload {
const ownerTeamId = this.resolveTeamIdBySocketId(ownerSocketId);
return this.toRenderPayload(payload, ownerTeamId);
}
private toRenderPayload(
payload: { x: number; y: number; explodeAtElapsedMs: number },
teamId: number,
): BombRenderPayload {
return {
x: payload.x,
y: payload.y,
explodeAtElapsedMs: payload.explodeAtElapsedMs,
radiusGrid: config.GAME_CONFIG.BOMB_RADIUS_GRID,
teamId,
color: this.appearanceResolver.resolveTeamColor(teamId),
};
}
private resolveTeamIdBySocketId(socketId: string): number {
const playerController = this.players[socketId];
if (!playerController) {
return sharedConfig.UNKNOWN_TEAM_ID;
}
return playerController.getSnapshot().teamId;
}
}