import { playSfx } from '../data/sfxSpecs';
import { MenuList, MenuSceneBase, Panel, PixelText } from '@rpg/engine';
import { Game } from '../Game';
import { SAVE_SLOTS, canDelete, slotTitle } from '../data/saves';
import type { SaveData } from './saveData';
/**
* Меню сейвов: autosave (только загрузка) + 3 именованных слота
* (загрузка/удаление). Панель поверх меню (push/pop). Подписи — из меты
* слота; у старых сейвов без меты — полная загрузка (как раньше).
*/
export class SaveSlotsScene extends MenuSceneBase {
constructor(
private game: Game,
private onPick: (save: SaveData) => void,
private onBack: () => void
) {
super(
{ input: game.engine.input, inputBlocked: () => game.scenes.transitioning },
{ up: 'up', down: 'down', confirm: 'advance', cancel: 'menu', extra: ['attack'] }
);
}
protected build(): void {
const panel = new Panel({ width: 240, height: 150 });
panel.position.set((Game.VIRTUAL_W - 240) / 2, (Game.VIRTUAL_H - 150) / 2);
const title = new PixelText({ text: 'СЕЙВЫ', size: 14, color: 0xd8c79a });
title.anchor.set(0.5);
title.position.set(120, 14);
panel.addChild(title);
this.menu = new MenuList({ width: 200, height: 14, gap: 3, size: 10 });
this.menu.position.set(20, 36);
panel.addChild(this.menu);
this.view.addChild(panel);
this.game.renderer.uiRoot.addChild(this.view);
this.rebuild();
}
/** Delete на выбранном именованном слоте — стереть. */
protected onAction(action: string, index: number): void {
if (action === 'attack') this.deleteAt(index);
}
protected onCancel(): void {
this.back();
}
/** Пересобрать список слотов из SaveManager. */
private rebuild(): void {
const items: { label: string; disabled?: boolean; onSelect?: () => void }[] = [];
const slots = this.game.saves.listSlots();
for (const slot of SAVE_SLOTS) {
const meta = this.game.saves.slotMeta(slot);
const save = meta
? null
: slots.includes(slot)
? this.game.saves.load<SaveData>(slot)!
: null;
if (!meta && !save) {
items.push({ label: `${slotTitle(slot)} — пусто —`, disabled: true });
continue;
}
const label = meta
? this.metaLabel(slot, meta)
: this.saveLabel(slot, save!);
items.push({
label,
onSelect: () => {
playSfx(this.game.audio, 'sfx/ui_click');
const picked = save ?? this.game.saves.load<SaveData>(slot)!;
this.onPick(picked);
}
});
}
items.push({ label: 'Назад', onSelect: () => this.back() });
this.menu!.setItems(items);
}
/** Подпись из меты: локация из extras, время из savedAt. */
private metaLabel(slot: string, meta: { savedAt?: number; extras?: Record<string, string | number | boolean> }): string {
const area = typeof meta.extras?.area === 'string' ? meta.extras.area : undefined;
const when = meta.savedAt ? this.formatTime(meta.savedAt) : '--:--';
return `${slotTitle(slot)} ${this.areaName(area)} ${when}`;
}
/** Подпись из полного сейва (старые сейвы без меты). */
private saveLabel(slot: string, save: SaveData): string {
const areaId = save.area ?? (save as { location?: string }).location;
return `${slotTitle(slot)} ${this.areaName(areaId)} ${this.formatTime(save.savedAt)}`;
}
private formatTime(ts: number): string {
const date = new Date(ts);
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
}
private areaName(areaId: string | undefined): string {
return areaId === 'ponds' ? 'пруды' : areaId === 'zvenets' ? 'Звенец' : 'луга';
}
/** Удалить сейв в выбранной строке (именованные слоты; autosave защищён). */
private deleteAt(index: number): void {
const slot = SAVE_SLOTS[index];
if (!slot || !canDelete(slot) || !this.game.saves.listSlots().includes(slot)) return;
this.game.saves.delete(slot);
playSfx(this.game.audio, 'sfx/ui_click');
this.rebuild();
}
private back(): void {
playSfx(this.game.audio, 'sfx/ui_click');
this.onBack();
}
}