/**
* Сцены (перенос схемы v1, без привязки к бэкенду): сцена — экран игры
* (меню, локация, бой); менеджер держит стек, чаще всего достаточно
* push/pop/replace. Затемнение при смене — инъекция FadeOverlay (в демке
* это DOM-слой; в тестах — заглушка): ядро о бэкенде не знает (урок №3).
* Параллельные переходы глотаются; enter новой сцены не ожидается —
* проявление идёт параллельно с её инициализацией.
*/
/** Сцена — экран игры. update — фиксированный шаг dt в секундах. */
export interface Scene {
/** Имя для моста/отладки (необязательно). */
name?: string;
/** Вызывается один раз при входе (может быть асинхронным). */
enter(): void | Promise<void>;
exit(): void | Promise<void>;
update(dt: number): void;
render(): void;
}
/** Затемнение экрана при смене сцены (цвет/стиль — на игре). */
export interface FadeOverlay {
/** Прозрачность 0..1 (0 — не рисовать). */
setAlpha(alpha: number): void;
}
/** Параметры перехода: длительность затухание+проявление, секунды. */
export interface SceneTransition {
duration?: number;
}
type TransitionKind = 'push' | 'pop' | 'replace';
interface PendingTransition {
kind: TransitionKind;
scene?: Scene;
duration: number;
phase: 'out' | 'in';
/** Прошедшее время внутри фазы. */
t: number;
}
const DEFAULT_DURATION = 0.5;
export class SceneManager {
private stack: Scene[] = [];
private pending: PendingTransition | null = null;
constructor(private readonly overlay?: FadeOverlay) {}
/** Положить сцену на стек (с переходом — по завершении затухания). */
push(scene: Scene, transition?: SceneTransition): void {
if (transition) {
this.begin('push', scene, transition);
return;
}
this.stack.push(scene);
void scene.enter();
}
/** Снять верхнюю сцену. */
pop(transition?: SceneTransition): void {
if (transition) {
this.begin('pop', undefined, transition);
return;
}
this.stack.pop()?.exit();
}
/** Заменить верхнюю сцену (например меню → локация). */
replace(scene: Scene, transition?: SceneTransition): void {
if (transition) {
this.begin('replace', scene, transition);
return;
}
this.stack.pop()?.exit();
this.stack.push(scene);
void scene.enter();
}
get current(): Scene | undefined {
return this.stack[this.stack.length - 1];
}
/** Идёт ли сейчас переход (для блокировки ввода). */
get transitioning(): boolean {
return this.pending !== null;
}
update(dt: number): void {
if (this.pending) this.stepTransition(dt);
this.current?.update(dt);
}
/** Рисует весь стек: нижние сцены видны под верхними. */
render(): void {
for (const scene of this.stack) scene.render();
}
private begin(kind: TransitionKind, scene: Scene | undefined, tr: SceneTransition): void {
if (this.pending) return; // параллельные переходы запрещены
this.pending = {
kind,
scene,
duration: tr.duration && tr.duration > 0 ? tr.duration : DEFAULT_DURATION,
phase: 'out',
t: 0,
};
}
/** Фазы: затухание → смена сцены → проявление. */
private stepTransition(dt: number): void {
const p = this.pending!;
p.t += dt;
if (p.phase === 'out') {
const a = Math.min(p.t / p.duration, 1);
this.overlay?.setAlpha(a);
if (a >= 1) {
this.performSwap(p);
p.phase = 'in';
p.t = 0;
}
return;
}
const a = Math.max(1 - p.t / p.duration, 0);
this.overlay?.setAlpha(a);
if (p.t >= p.duration) {
this.pending = null;
this.overlay?.setAlpha(0);
}
}
private performSwap(p: PendingTransition): void {
switch (p.kind) {
case 'push':
if (p.scene) this.stack.push(p.scene);
break;
case 'pop':
this.stack.pop()?.exit();
break;
case 'replace':
this.stack.pop()?.exit();
if (p.scene) this.stack.push(p.scene);
break;
}
void this.stack[this.stack.length - 1]?.enter();
}
}