import { Container, MenuList, PixelText, type GameStateData, type Scene } from '@rpg/engine';
import type { Game } from '../Game';
import { locationOf } from '../data/locations';
import { LocationScene } from './LocationScene';
import { SaveSlotsScene } from './SaveSlotsScene';
import { SettingsScene } from './SettingsScene';
/**
* Главное меню: название, «новая игра», «продолжить» (если есть сейв).
* Список на движковом MenuList: мышь и клавиатура/геймпад.
*/
export class MenuScene implements Scene {
private view = new Container();
private menu: MenuList | null = null;
constructor(private game: Game) {}
enter(): 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: 10,
color: 0x8a8a9a
});
subtitle.anchor.set(0.5);
subtitle.position.set(240, 94);
this.view.addChild(subtitle);
const items = [
{
label: 'Новая игра',
// Звук не блокирует навигацию: в suspended-контексте play может не дойти.
onSelect: () => {
void this.game.audio.play('sfx/ui_click');
this.game.state.reset();
this.start(null);
}
},
{
label: 'Сейвы',
onSelect: () => {
void this.game.audio.play('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: () => {
void this.game.audio.play('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: () => {
void this.game.audio.play('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: 8,
color: 0x666677,
align: 'center',
lineHeight: 12,
wordWrapWidth: 400
});
hint.anchor.set(0.5);
hint.position.set(240, 234);
this.view.addChild(hint);
this.game.renderer.uiRoot.addChild(this.view);
}
private start(save: SaveData | null): void {
void this.game.scenes.replace(new LocationScene(this.game, save, locationOf(save?.location)), {
duration: 0.4
});
}
update(_dt: number): void {
if (!this.menu || this.game.scenes.transitioning) return;
const input = this.game.engine.input;
if (input.isActionJustPressed('up')) this.menu.moveCursor(-1);
if (input.isActionJustPressed('down')) this.menu.moveCursor(1);
if (input.isActionJustPressed('advance')) this.menu.activate();
}
render(): void {}
exit(): void {
this.view.destroy({ children: true });
}
}
/** Формат сейва: локация, позиция героя и сериализованное состояние прохождения. */
export interface SaveData {
/** В какой локации герой (фолбэк 'meadows' для старых сейвов). */
location: string;
pos: { x: number; y: number };
state: GameStateData;
savedAt: number;
}