/**
* Конечный автомат: состояния с enter/exit/update и переходы по событиям.
* Используется для AI врагов, состояний игрока (idle/run/attack), экранов меню и т.п.
*/
export interface StateDef {
/** Вызывается при входе в состояние. */
enter?: () => void;
exit?: () => void;
/** dt в секундах, фиксированный шаг. */
update?: (dt: number) => void;
}
export class StateMachine {
private states = new Map<string, StateDef>();
/** (состояние, событие) -> состояние-цель. */
private transitions = new Map<string, string>();
private currentName: string | null = null;
private timeInState = 0;
constructor(
/** Неизвестное событие в текущем состоянии (по умолчанию — просто игнор). */
private onUnhandled?: (state: string, event: string) => void
) {}
add(name: string, def: StateDef): this {
this.states.set(name, def);
return this;
}
/** Переход из состояния `from` по событию `event` в состояние `to`. */
transition(from: string, event: string, to: string): this {
if (!this.states.has(to)) {
throw new Error(`Переход в неизвестное состояние: ${to}`);
}
this.transitions.set(`${from}::${event}`, to);
return this;
}
/** Перейти в состояние (exit текущего -> enter нового). Повторный вход в то же состояние игнорируется. */
change(name: string): void {
if (!this.states.has(name)) {
throw new Error(`Неизвестное состояние: ${name}`);
}
if (this.currentName === name) return;
this.currentName ? this.states.get(this.currentName)!.exit?.() : undefined;
this.currentName = name;
this.timeInState = 0;
this.states.get(name)!.enter?.();
}
/** Обработать событие: если из текущего состояния есть переход — выполнить его. */
handleEvent(event: string): void {
if (!this.currentName) return;
const to = this.transitions.get(`${this.currentName}::${event}`);
if (to) {
this.change(to);
} else {
this.onUnhandled?.(this.currentName, event);
}
}
get current(): string | null {
return this.currentName;
}
/** Секунд с момента входа в текущее состояние. */
get time(): number {
return this.timeInState;
}
update(dt: number): void {
if (!this.currentName) return;
this.timeInState += dt;
this.states.get(this.currentName)!.update?.(dt);
}
reset(): void {
this.currentName = null;
this.timeInState = 0;
}
}