import { playSfx } from '../data/sfxSpecs';
import {
MenuList,
MenuSceneBase,
PixelText,
type Invariant,
type JsonValue,
type SnapshotLayer
} from '@rpg/engine';
import type { Game } from '../Game';
import { areaOf } from '../data/locations';
import { LocationScene } from './LocationScene';
import { SaveSlotsScene } from './SaveSlotsScene';
import { SettingsScene } from './SettingsScene';
import { normalizeSave, type SaveData } from './saveData';
export { SAVE_VERSION, normalizeSave, type ReturnToData, type SaveData } from './saveData';
/**
* Главное меню: название, «новая игра», «продолжить» (если есть сейв).
* Список на движковом MenuList: мышь и клавиатура/геймпад.
*/
export class MenuScene extends MenuSceneBase {
constructor(private game: Game) {
super(
{ input: game.engine.input, inputBlocked: () => game.scenes.transitioning },
{ up: 'up', down: 'down', confirm: 'advance', cancel: 'menu' }
);
}
protected build(): void {
const title = new PixelText({
text: 'ПЕПЕЛЬНЫЕ ЛУГА',
size: 28,
color: 0xd8c79a
});
title.anchor.set(0.5);
title.position.set(240, 66);
this.view.addChild(title);
const subtitle = new PixelText({
text: 'пепел всё ещё дышит',
size: 11,
color: 0x8a8a9a
});
subtitle.anchor.set(0.5);
subtitle.position.set(240, 94);
this.view.addChild(subtitle);
const items = [
{
label: 'Новая игра',
// Звук не блокирует навигацию: в suspended-контексте play может не дойти.
onSelect: () => {
playSfx(this.game.audio, 'sfx/ui_click');
this.startNewGame();
}
},
{
label: 'Сейвы',
onSelect: () => {
playSfx(this.game.audio, 'sfx/ui_click');
void this.game.scenes.push(
new SaveSlotsScene(
this.game,
(save) => void this.game.scenes.pop().then(() => this.start(save)),
() => void this.game.scenes.pop()
)
);
}
},
{
label: 'Настройки',
onSelect: () => {
playSfx(this.game.audio, 'sfx/ui_click');
void this.game.scenes.push(
new SettingsScene(this.game, () => void this.game.scenes.pop())
);
}
}
];
if (this.game.saves.has('autosave')) {
items.unshift({
label: 'Продолжить',
onSelect: () => {
playSfx(this.game.audio, 'sfx/ui_click');
const save = this.game.saves.load<SaveData>('autosave');
this.start(save);
}
});
}
this.menu = new MenuList({ width: 120, height: 16, gap: 3 });
this.menu.setItems(items);
this.menu.position.set(180, 130);
this.view.addChild(this.menu);
const hint = new PixelText({
text: 'клик по тайлу — идти · клик по NPC — говорить\nEsc в игре — меню с сохранением',
size: 9,
color: 0x666677,
align: 'center',
lineHeight: 13,
wordWrapWidth: 400
});
hint.anchor.set(0.5);
hint.position.set(240, 234);
this.view.addChild(hint);
this.game.renderer.uiRoot.addChild(this.view);
}
/** Новая игра: сброс прогресса и сумки, старт на лугах. */
private startNewGame(): void {
this.game.state.reset();
this.game.inventory.clear();
this.start(null);
}
// --- агентный мост: сцена отдаёт снапшот и принимает команды ---
agentSnapshot(): SnapshotLayer {
return { scene: 'menu' };
}
agentInvariants(): Invariant[] {
return [];
}
agentCommand(name: string): JsonValue {
if (name === 'menu:newGame') {
this.startNewGame();
return true;
}
return null;
}
private start(save: SaveData | null): void {
if (save) {
const norm = normalizeSave(save);
this.game.inventory.load({ items: norm.items });
void this.game.scenes.replace(
new LocationScene(this.game, norm, areaOf(norm.area), undefined, norm.returnTo ?? undefined),
{ duration: 0.4 }
);
} else {
this.game.inventory.clear();
void this.game.scenes.replace(new LocationScene(this.game, null, areaOf(undefined)), {
duration: 0.4
});
}
}
}