import { Graphics } from 'pixi.js';
/**
* Сцена — экран игры (меню, локация, бой). Менеджер держит стек,
* но в 2D-RPG чаще всего достаточно push/pop/replace.
*/
export interface Scene {
/** Вызывается один раз при входе. */
enter(): void | Promise<void>;
exit(): void | Promise<void>;
/** dt в секундах, фиксированный шаг. */
update(dt: number): void;
render(): void;
}
/** Затемнение экрана при смене сцены. */
export interface SceneTransition {
/** Общая длительность (затухание + проявление), секунды. По умолчанию 0.5. */
duration?: number;
/** Цвет затемнения. По умолчанию чёрный. */
color?: number;
}
type TransitionKind = 'push' | 'pop' | 'replace';
interface PendingTransition {
kind: TransitionKind;
scene?: Scene;
duration: number;
color: number;
phase: 'out' | 'in';
t: number;
}
export class SceneManager {
private stack: Scene[] = [];
private overlay: Graphics | null = null;
private overlayWidth = 0;
private overlayHeight = 0;
private pending: PendingTransition | null = null;
/** Движок подключает оверлей затемнения (верхний слой uiRoot). */
setOverlay(g: Graphics, width: number, height: number): void {
this.overlay = g;
this.overlayWidth = width;
this.overlayHeight = height;
}
async push(scene: Scene, transition?: SceneTransition): Promise<void> {
if (transition) {
this.begin('push', scene, transition);
return;
}
this.stack.push(scene);
await scene.enter();
}
async pop(transition?: SceneTransition): Promise<void> {
if (transition) {
this.begin('pop', undefined, transition);
return;
}
const scene = this.stack.pop();
if (scene) {
await scene.exit();
}
}
/** Заменить верхнюю сцену (например меню -> локация). */
async replace(scene: Scene, transition?: SceneTransition): Promise<void> {
if (transition) {
this.begin('replace', scene, transition);
return;
}
await this.pop();
await this.push(scene);
}
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 ?? 0.5,
color: tr.color ?? 0x000000,
phase: 'out',
t: 0
};
}
private stepTransition(dt: number): void {
const p = this.pending!;
p.t += dt;
if (p.phase === 'out') {
this.drawOverlay(p.color, Math.min(1, p.t / p.duration));
if (p.t < p.duration) return;
// экран скрыт — меняем сцену
this.performSwap(p);
p.phase = 'in';
p.t -= p.duration;
}
// фаза 'in': проявление
this.drawOverlay(p.color, Math.max(0, 1 - p.t / p.duration));
if (p.t >= p.duration) {
this.pending = null;
this.overlay?.clear();
}
}
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;
}
// enter может быть асинхронным — переход не ждёт его,
// новая сцена проявляется параллельно со своей инициализацией.
void this.stack[this.stack.length - 1]?.enter();
}
private drawOverlay(color: number, alpha: number): void {
if (!this.overlay) return;
const g = this.overlay;
g.clear();
if (alpha > 0) {
g.rect(0, 0, this.overlayWidth, this.overlayHeight).fill({ color, alpha });
}
}
}