diff --git a/frontend/src/player/engine.ts b/frontend/src/player/engine.ts index 8f08d80..98d3215 100644 --- a/frontend/src/player/engine.ts +++ b/frontend/src/player/engine.ts @@ -48,6 +48,9 @@ function fontGroup() { return group; } +/** How many interactions Rollback can step back through. */ +const ROLLBACK_LIMIT = 50; + export class Engine { private liveImages?: LiveImages; private imageScales = new Map(); @@ -83,6 +86,8 @@ export class Engine { private windowShown = true; private lastSay: { name: string; text: string; color?: string; char?: CharacterDef } | null = null; private history: { name: string; text: string }[] = []; + /** One state per interaction, newest last: what Rollback steps back through. */ + private rollbackLog: { snap: SaveData; history: { name: string; text: string }[] }[] = []; private nvlLines: HTMLElement[] = []; private interactions = 0; @@ -1061,6 +1066,7 @@ export class Engine { quit: () => this.onExit(), mainMenu: () => this.restart(() => { this.stack = []; this.cur = { label: "", path: [0] }; }), openMenu: () => this.openGameMenu(), + rollback: () => this.rollback(), openUrl: (url) => { if (/^https?:\/\//i.test(url)) window.open(url, "_blank", "noopener,noreferrer"); }, @@ -1531,11 +1537,24 @@ export class Engine { private interactionStart() { this.renderer.commit(); + this.rollbackLog.push({ snap: this.snapshotState(), history: this.history.slice() }); + if (this.rollbackLog.length > ROLLBACK_LIMIT) this.rollbackLog.shift(); this.interactions++; if (this.interactions % 15 === 0) void this.save("auto", true); this.preloadAhead(); } + /** Back to the previous interaction: the one being shown is dropped, the one before it is shown again. */ + private rollback() { + if (!this.running || this.rollbackLog.length < 2) return; + this.rollbackLog.pop(); + const entry = this.rollbackLog.pop()!; + this.restart(() => { + this.applySave(entry.snap); + this.history = entry.history; + }); + } + private preloadAhead() { const block = this.labels.has(this.cur.label) ? this.blockFor(this.cur) : []; const start = this.cur.path[this.cur.path.length - 1]; @@ -1878,6 +1897,7 @@ export class Engine { this.newStore(); this.stack = []; this.history = []; + this.rollbackLog = []; this.lastSay = null; this.textbox.classList.add("pg-hidden"); this.cur = at ?? { label: this.game.entry, path: [0] }; @@ -1973,6 +1993,7 @@ export class Engine { } if (!data.revision && !(await this.confirmAsk("Старое сохранение не содержит ревизию игры. После изменения скрипта его позиции могут быть неверны. Попробовать загрузить?", "Загрузить"))) return; this.replayStash = null; + this.rollbackLog = []; this.restart(() => this.applySave(data)); } @@ -2123,7 +2144,7 @@ export class Engine { this.advanceInput(); }); stage.addEventListener("wheel", (ev) => { - if (ev.deltaY < 0 && this.screen.classList.contains("pg-hidden") && this.running) this.openHistory(); + if (ev.deltaY < 0 && this.screen.classList.contains("pg-hidden") && this.running) this.rollback(); }, { passive: true }); } @@ -2203,6 +2224,9 @@ export class Engine { this.setSkip(!this.skip); } else if (ev.key === "a" || ev.key === "ф") { this.setAuto(!this.auto); + } else if (ev.key === "PageUp") { + ev.preventDefault(); + this.rollback(); } else if (ev.key === "h" || ev.key === "р") { this.textbox.classList.toggle("pg-ui-hidden"); } diff --git a/frontend/src/player/screens.ts b/frontend/src/player/screens.ts index 633eef2..7454663 100644 --- a/frontend/src/player/screens.ts +++ b/frontend/src/player/screens.ts @@ -43,6 +43,8 @@ export interface ScreenHost { quit(): void; mainMenu(): void; openMenu(): void; + /** The Rollback action: back to the previous interaction. */ + rollback?(): void; openUrl(url: string): void; quickSave(): void; quickLoad(): void; @@ -1797,7 +1799,7 @@ export class ScreenManager { case "UploadSync": case "DownloadSync": this.host.notify("Синхронизация недоступна в браузере"); return; case "Skip": this.host.setSkipping?.(true); return; - case "Rollback": return; // there is no rollback in the browser player + case "Rollback": this.host.rollback?.(); return; case "ToggleLocalVariable": { const now = owner?.vars.get(a); owner?.vars.set(a, !truthy(now));