diff --git a/docs/ResetStoryAtSection.md b/docs/ResetStoryAtSection.md new file mode 100644 index 00000000..374252e7 --- /dev/null +++ b/docs/ResetStoryAtSection.md @@ -0,0 +1,174 @@ +# Reset Story At Section + +This document defines the public behavior of `resetStoryAtSection`. + +Status: implemented + +## Goal + +Support common destructive restart flows like: + +- title -> start game +- in-game -> return to title + +without overloading `sectionTransition`. + +## Public Action + +```yaml +resetStoryAtSection: + sectionId: gameStart +``` + +### Schema Shape + +```yaml +resetStoryAtSection: + type: object + description: Reset story-local state and enter the first line of a section + properties: + sectionId: + type: string + required: [sectionId] + additionalProperties: false +``` + +## Core Semantics + +`resetStoryAtSection` is destructive navigation. + +It: + +1. resets story-local runtime state +2. moves the read pointer to the first line of `sectionId` +3. treats that destination line as the new rollback root + +The destination line then runs as normal through queued `handleLineActions`. + +## What It Preserves + +- `projectData` +- `global.saveSlots` +- `global.variables` for `device` and `account` scopes + +## What It Resets + +- current context variables: + - replaced with project defaults for `scope: context` +- current context pointer mode: + - forced to `read` +- current context history pointer: + - cleared +- current context rollback: + - replaced with a single checkpoint anchored at the destination line +- viewed state: + - `viewedRegistry.sections = []` + - `viewedRegistry.resources = []` +- transient runtime globals: + - `autoMode = false` + - `skipMode = false` + - `dialogueUIHidden = false` + - `confirmDialog = null` + - `layeredViews = []` + - `nextLineConfig = DEFAULT_NEXT_LINE_CONFIG` + +After the destination line is entered, normal line-entry behavior applies, so +`isLineCompleted` becomes `false` and `handleLineActions` is queued. + +## Effects + +`resetStoryAtSection` appends effects. It does not replace the pending queue. + +It enqueues: + +- `clearAutoNextTimer` +- `clearSkipNextTimer` +- `clearNextLineConfigTimer` +- `render` +- `handleLineActions` + +## Relation To `sectionTransition` + +Use `sectionTransition` for non-destructive movement within the current story +state: + +```yaml +sectionTransition: + sectionId: chapter2 +``` + +Use `resetStoryAtSection` when the destination must start from fresh +story-local state: + +```yaml +resetStoryAtSection: + sectionId: title +``` + +The difference is intentional: + +- `sectionTransition` preserves rollback/viewed state/context variables +- `resetStoryAtSection` clears them + +## Examples + +Start game from title: + +```yaml +resetStoryAtSection: + sectionId: gameStart +``` + +Return to title from gameplay: + +```yaml +resetStoryAtSection: + sectionId: title +``` + +## Rollback Behavior + +`resetStoryAtSection` creates a new rollback timeline anchored at the +destination: + +```js +{ + currentIndex: 0, + isRestoring: false, + replayStartIndex: 0, + timeline: [ + { + sectionId: "destination", + lineId: "firstLine", + rollbackPolicy: "free", + }, + ], +} +``` + +That means: + +- `Back` after a restart stays inside the new story run +- the player cannot roll back into the pre-reset title/gameplay state + +## RouteEngine Interface + +```js +engine.handleAction("resetStoryAtSection", { + sectionId: "title", +}); +``` + +## Rationale + +Why not overload `sectionTransition`: + +- destructive fresh-start behavior is too easy to miss behind a flag +- normal navigation and destructive restart have different review/debug risk +- common destructive behavior deserves a first-class authored verb + +Why not keep a public reset-only action: + +- the primary product need is destructive restart to a destination section +- a combined action is simpler to audit in authored data +- it avoids reset-only intermediate renders during multi-action batches diff --git a/docs/RouteEngine.md b/docs/RouteEngine.md index 6b328df9..975d10c4 100644 --- a/docs/RouteEngine.md +++ b/docs/RouteEngine.md @@ -395,7 +395,7 @@ Playback timing semantics: | ------------------- | -------------------- | ------------------------------- | | `setNextLineConfig` | `{ manual?, auto? }` | Configure line advancement | | `updateProjectData` | `{ projectData }` | Replace project data | -| `resetStorySession` | - | Reset story-local session state | +| `resetStoryAtSection` | `{ sectionId }` | Reset story-local state and enter a section | ### Registry Actions @@ -419,7 +419,7 @@ Seen-line semantics: | `loadSlot` | `{ slotId }` | Load game from a slot | Save/load design, requirements, and storage boundaries are documented in [SaveLoad.md](./SaveLoad.md). -Story-session reset semantics are documented in [StorySessionReset.md](./StorySessionReset.md). +Destructive fresh-start navigation semantics are documented in [ResetStoryAtSection.md](./ResetStoryAtSection.md). Notes: diff --git a/docs/StorySessionReset.md b/docs/StorySessionReset.md deleted file mode 100644 index 30dff4a2..00000000 --- a/docs/StorySessionReset.md +++ /dev/null @@ -1,250 +0,0 @@ -# Story Session Reset Proposal - -This document proposes a new public system action for resetting story-local -session state without changing the current read pointer directly. - -Status: implemented - -## Goal - -Support flows like: - -- title -> start game -- in-game -> exit to title - -where the engine should: - -- reset context-scoped variables to project defaults -- clear rollback/session-local history -- clear seen/viewed state -- clear transient runtime UI/playback state -- preserve `device` and `account` variables - -without introducing dedicated `startGame` / `exitGame` engine actions. - -## Proposed Public Action - -```yaml -resetStorySession: {} -``` - -### Proposed Schema Shape - -```yaml -resetStorySession: - type: object - description: Reset story-local session state around the current read pointer - properties: {} - additionalProperties: false -``` - -## Core Semantics - -`resetStorySession` resets story-local runtime state while preserving the -current context read pointer. - -It does not choose a destination section itself. - -That means: - -- `pointers.read.sectionId` is preserved -- `pointers.read.lineId` is preserved -- `sectionTransition` remains the action used for navigation - -## State Changes - -When `resetStorySession` runs, the engine should: - -### Preserve - -- current context read pointer -- `global.variables` for `device` and `account` -- `projectData` -- `global.saveSlots` - -### Reset - -- current context variables: - - replace with project-defined defaults for variables with `scope: context` -- current context pointer mode: - - set `currentPointerMode` to `read` -- current context history pointer: - - set `pointers.history` to an empty pointer -- current context rollback state: - - replace with a minimal rollback timeline anchored at the preserved read - pointer -- global viewed state: - - clear `viewedRegistry.sections` - - clear `viewedRegistry.resources` -- transient runtime globals: - - `autoMode = false` - - `skipMode = false` - - `dialogueUIHidden = false` - - `confirmDialog = null` - - `layeredViews = []` - - `nextLineConfig = DEFAULT_NEXT_LINE_CONFIG` - - `isLineCompleted = true` - -### Effects - -The action should append reset effects without replacing already queued effects. - -It should enqueue: - -- `clearAutoNextTimer` -- `clearSkipNextTimer` -- `clearNextLineConfigTimer` -- `render` - -It must not replace `pendingEffects`, because the action may be used inside a -larger authored batch. - -## Non-Goals - -`resetStorySession` should not: - -- change the read pointer directly -- pick a title section or gameplay start section -- mutate persistent global variables -- remove save slots -- replay line actions automatically -- load a slot - -## Composition With `sectionTransition` - -This action is intended to compose with `sectionTransition`. - -### Recommended Batch Order - -If the caller wants to move to a new section and start a fresh story session at -that destination, author the batch in this order: - -```yaml -resetStorySession: {} -sectionTransition: - sectionId: gameStart -``` - -Why this order: - -- `resetStorySession` clears story-local state before the destination line runs -- `sectionTransition` then moves to the destination pointer -- inside the same action batch, the engine treats the transition as the new - rollback anchor -- the destination line's actions run against fresh session state - -This avoids both: - -- carrying the old title/gameplay pointer as the new rollback anchor -- running destination line actions before the reset occurs - -### Example: Start Game From Title - -```yaml -resetStorySession: {} -sectionTransition: - sectionId: gameStart -``` - -Final intended result: - -- pointer lands at `gameStart` -- context variables are reset to defaults -- viewed state is empty -- rollback starts at `gameStart` -- persistent globals are unchanged - -### Example: Exit To Title - -```yaml -resetStorySession: {} -sectionTransition: - sectionId: title -``` - -Final intended result: - -- pointer lands at `title` -- context variables are reset to defaults -- viewed state is empty -- rollback starts at `title` -- persistent globals are unchanged - -## Rollback Behavior - -`resetStorySession` is a session boundary action. - -It should not be recorded as a normal rollbackable story mutation. Instead, it -replaces the active rollback state with a new minimal timeline: - -```js -{ - currentIndex: 0, - isRestoring: false, - replayStartIndex: 0, - timeline: [ - { - sectionId: currentReadPointer.sectionId, - lineId: currentReadPointer.lineId, - rollbackPolicy: "free", - }, - ], -} -``` - -## Rationale - -Why not `startGame` / `exitGame`: - -- they encode product flows, not engine primitives -- they would overlap with `sectionTransition` -- they do not generalize beyond title/gameplay - -Why not only `resetContextVariables`: - -- the requirement is broader than variables -- rollback, viewed state, and transient runtime state also need reset - -Why not overload `sectionTransition`: - -- `sectionTransition` is navigation within the current session -- session reset has different semantics and different failure/debug surface - -## Proposed RouteEngine Interface - -```js -engine.handleAction("resetStorySession"); -``` - -Inside authored action batches: - -```yaml -actions: - sectionTransition: - sectionId: title - resetStorySession: {} -``` - -## Test Cases To Add On Implementation - -- resets context-scoped variables to project defaults -- preserves current read pointer -- clears history pointer and forces `currentPointerMode = "read"` -- resets rollback to a single checkpoint anchored at the current read pointer -- clears `viewedRegistry.sections` -- clears `viewedRegistry.resources` -- preserves `device` and `account` variables -- clears transient globals and queues timer-clear effects -- appends effects instead of replacing the pending queue -- when ordered after `sectionTransition` in the same batch, anchors rollback at - the destination pointer - -## Open Questions - -Current proposal: - -- `isLineCompleted` resets to `true` to avoid resuming stale reveal/timer state -- the action does not automatically invoke `handleLineActions` - -That keeps the action side-effect-safe when used before navigation inside a -single authored batch. diff --git a/package.json b/package.json index fde53caf..c9ca7928 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "route-engine-js", - "version": "1.0.3", + "version": "1.1.0", "description": "A lightweight Visual Novel engine built in JavaScript for creating interactive narrative games with branching storylines", "repository": { "type": "git", diff --git a/spec/RouteEngine.systemState.test.js b/spec/RouteEngine.systemState.test.js index 78f1dd8b..25e3bda3 100644 --- a/spec/RouteEngine.systemState.test.js +++ b/spec/RouteEngine.systemState.test.js @@ -91,7 +91,7 @@ const createRollbackChoiceProjectData = () => ({ }, }); -const createSessionResetProjectData = () => ({ +const createResetStoryAtSectionProjectData = () => ({ screen: { width: 1920, height: 1080, @@ -302,22 +302,19 @@ describe("RouteEngine selectSystemState", () => { ]); }); - it("anchors a reset+transition batch at the destination section with fresh story-local state", () => { + it("resets story-local state and enters the destination section", () => { const engine = createRouteEngineWithInlineEffects(); engine.init({ initialState: { - projectData: createSessionResetProjectData(), + projectData: createResetStoryAtSectionProjectData(), }, }); expect(engine.selectSystemState().contexts[0].variables.score).toBe(7); - engine.handleActions({ - resetStorySession: {}, - sectionTransition: { - sectionId: "gameStart", - }, + engine.handleAction("resetStoryAtSection", { + sectionId: "gameStart", }); const state = engine.selectSystemState(); @@ -359,4 +356,48 @@ describe("RouteEngine selectSystemState", () => { ], }); }); + + it("leaves the current story state untouched when resetStoryAtSection targets a missing section", () => { + const engine = createRouteEngineWithInlineEffects(); + + engine.init({ + initialState: { + projectData: createResetStoryAtSectionProjectData(), + }, + }); + + engine.handleAction("resetStoryAtSection", { + sectionId: "missing", + }); + + const state = engine.selectSystemState(); + + expect(state.contexts[0].pointers.read).toMatchObject({ + sectionId: "title", + lineId: "titleLine", + }); + expect(state.contexts[0].variables.score).toBe(7); + expect(state.contexts[0].rollback.timeline).toEqual([ + { + sectionId: "title", + lineId: "titleLine", + rollbackPolicy: "free", + executedActions: [ + { + type: "updateVariable", + payload: { + id: "seedTitleScore", + operations: [ + { + variableId: "score", + op: "set", + value: 7, + }, + ], + }, + }, + ], + }, + ]); + }); }); diff --git a/spec/system/actions/resetStorySession.spec.yaml b/spec/system/actions/resetStoryAtSection.spec.yaml similarity index 64% rename from spec/system/actions/resetStorySession.spec.yaml rename to spec/system/actions/resetStoryAtSection.spec.yaml index a84f542c..6f834b98 100644 --- a/spec/system/actions/resetStorySession.spec.yaml +++ b/spec/system/actions/resetStoryAtSection.spec.yaml @@ -1,11 +1,11 @@ file: "../../../src/stores/system.store.js" group: systemStore.actions -suites: [resetStorySession] +suites: [resetStoryAtSection] --- -suite: resetStorySession -exportName: resetStorySession +suite: resetStoryAtSection +exportName: resetStoryAtSection --- -case: reset story session preserves pointer and clears story-local session state +case: reset story at section clears story-local state and anchors rollback at the destination in: - state: projectData: @@ -42,7 +42,9 @@ in: gameStart: lines: - id: gameLine - actions: {} + actions: + background: + resourceId: bg2 global: autoMode: true skipMode: true @@ -117,7 +119,7 @@ in: - sectionId: title lineId: titleLine rollbackPolicy: free - - {} + - sectionId: gameStart out: projectData: resources: @@ -153,7 +155,9 @@ out: gameStart: lines: - id: gameLine - actions: {} + actions: + background: + resourceId: bg2 global: autoMode: false skipMode: false @@ -170,7 +174,7 @@ out: enabled: false applyMode: persistent layeredViews: [] - isLineCompleted: true + isLineCompleted: false variables: volume: 15 unlockedEnding: true @@ -192,12 +196,13 @@ out: - name: clearSkipNextTimer - name: clearNextLineConfigTimer - name: render + - name: handleLineActions contexts: - currentPointerMode: read pointers: read: - sectionId: title - lineId: titleLine + sectionId: gameStart + lineId: gameLine history: sectionId: __undefined__ lineId: __undefined__ @@ -213,6 +218,114 @@ out: isRestoring: false replayStartIndex: 0 timeline: - - sectionId: title - lineId: titleLine + - sectionId: gameStart + lineId: gameLine rollbackPolicy: free +--- +case: section not found leaves state unchanged +in: + - state: + projectData: + story: + initialSceneId: scene1 + scenes: + scene1: + initialSectionId: title + sections: + title: + lines: + - id: titleLine + actions: {} + global: + pendingEffects: [] + autoMode: true + skipMode: true + contexts: + - currentPointerMode: read + pointers: + read: + sectionId: title + lineId: titleLine + variables: + score: 4 + - sectionId: missing +out: + projectData: + story: + initialSceneId: scene1 + scenes: + scene1: + initialSectionId: title + sections: + title: + lines: + - id: titleLine + actions: {} + global: + pendingEffects: [] + autoMode: true + skipMode: true + contexts: + - currentPointerMode: read + pointers: + read: + sectionId: title + lineId: titleLine + variables: + score: 4 +--- +case: empty destination section leaves state unchanged +in: + - state: + projectData: + story: + initialSceneId: scene1 + scenes: + scene1: + initialSectionId: title + sections: + title: + lines: + - id: titleLine + actions: {} + empty: + lines: [] + global: + pendingEffects: [] + autoMode: true + skipMode: true + contexts: + - currentPointerMode: read + pointers: + read: + sectionId: title + lineId: titleLine + variables: + score: 4 + - sectionId: empty +out: + projectData: + story: + initialSceneId: scene1 + scenes: + scene1: + initialSectionId: title + sections: + title: + lines: + - id: titleLine + actions: {} + empty: + lines: [] + global: + pendingEffects: [] + autoMode: true + skipMode: true + contexts: + - currentPointerMode: read + pointers: + read: + sectionId: title + lineId: titleLine + variables: + score: 4 diff --git a/src/schemas/systemActions.yaml b/src/schemas/systemActions.yaml index a9d7d36c..5e2dcb59 100644 --- a/src/schemas/systemActions.yaml +++ b/src/schemas/systemActions.yaml @@ -272,10 +272,14 @@ properties: required: [slotId] additionalProperties: false - resetStorySession: + resetStoryAtSection: type: object - description: Reset story-local session state while preserving the current read pointer - properties: {} + description: Reset story-local state and enter the first line of a section + properties: + sectionId: + type: string + description: Target section ID + required: [sectionId] additionalProperties: false sectionTransition: diff --git a/src/stores/system.store.js b/src/stores/system.store.js index e7df23ab..f67af12c 100644 --- a/src/stores/system.store.js +++ b/src/stores/system.store.js @@ -630,33 +630,6 @@ const clearConfirmDialog = (state) => { const rollbackActionBatchStack = []; -const getActiveRollbackActionBatch = () => - rollbackActionBatchStack[rollbackActionBatchStack.length - 1] ?? null; - -const setActiveRollbackBatchCheckpoint = (checkpointIndex) => { - const activeBatch = getActiveRollbackActionBatch(); - if (activeBatch) { - activeBatch.checkpointIndex = checkpointIndex; - } -}; - -const markPendingStorySessionReset = () => { - const activeBatch = getActiveRollbackActionBatch(); - if (activeBatch) { - activeBatch.pendingStorySessionReset = true; - } -}; - -const consumePendingStorySessionReset = () => { - const activeBatch = getActiveRollbackActionBatch(); - if (!activeBatch?.pendingStorySessionReset) { - return false; - } - - delete activeBatch.pendingStorySessionReset; - return true; -}; - const createRollbackCheckpoint = ({ sectionId, lineId, rollbackPolicy }) => ({ sectionId, lineId, @@ -841,7 +814,26 @@ const createDefaultContextState = ({ pointer, projectData }) => ({ }), }); -const resetStorySessionTransientGlobals = (state) => { +const resetCurrentStoryState = (state) => { + const lastContext = state.contexts?.[state.contexts.length - 1]; + const readPointer = lastContext?.pointers?.read; + + if (!lastContext || !readPointer?.sectionId || !readPointer?.lineId) { + return null; + } + + const resetContext = createDefaultContextState({ + pointer: readPointer, + projectData: state.projectData, + }); + + state.contexts[state.contexts.length - 1] = resetContext; + resetStoryStateTransientGlobals(state); + + return resetContext; +}; + +const resetStoryStateTransientGlobals = (state) => { state.global.autoMode = false; state.global.skipMode = false; state.global.dialogueUIHidden = false; @@ -2146,25 +2138,65 @@ export const appendPendingEffect = ({ state }, payload) => { return state; }; -export const resetStorySession = ({ state }) => { - const lastContext = state.contexts?.[state.contexts.length - 1]; - const readPointer = lastContext?.pointers?.read; +const transitionToSection = (state, { sectionId, resetStoryState = false }) => { + const targetSection = selectSection({ state }, { sectionId }); + if (!targetSection) { + console.warn(`Section not found: ${sectionId}`); + return state; + } - if (!lastContext || !readPointer?.sectionId || !readPointer?.lineId) { + const firstLine = targetSection.lines?.[0]; + if (!firstLine) { + console.warn(`Section ${sectionId} has no lines`); return state; } - state.contexts[state.contexts.length - 1] = createDefaultContextState({ - pointer: readPointer, - projectData: state.projectData, - }); - resetStorySessionTransientGlobals(state); - markPendingStorySessionReset(); - setActiveRollbackBatchCheckpoint(0); + if (resetStoryState && !resetCurrentStoryState(state)) { + return state; + } + + if (state.global.autoMode) { + stopAutoMode({ state }); + } + if (state.global.skipMode) { + stopSkipMode({ state }); + } + + const lastContext = state.contexts?.[state.contexts.length - 1]; + if (lastContext) { + if (!lastContext.rollback) { + ensureRollbackState(lastContext, { compatibilityAnchor: true }); + } + + lastContext.pointers.read = { + sectionId, + lineId: firstLine.id, + }; + + if (resetStoryState) { + lastContext.rollback = createRollbackState({ + pointer: lastContext.pointers.read, + }); + } else { + appendRollbackCheckpoint(state, { + sectionId, + lineId: firstLine.id, + }); + } + } + + queueEnteredLineEffects(state, lastContext?.pointers?.read); return state; }; +export const resetStoryAtSection = ({ state }, payload) => { + return transitionToSection(state, { + sectionId: payload?.sectionId, + resetStoryState: true, + }); +}; + const recordViewedLine = (state, { sectionId, lineId }) => { if (!state.global.viewedRegistry) { state.global.viewedRegistry = {}; @@ -2497,13 +2529,6 @@ export const jumpToLine = ({ state }, payload) => { lineId: lineId, }; - if (consumePendingStorySessionReset()) { - lastContext.rollback = createRollbackState({ - pointer: lastContext.pointers.read, - }); - setActiveRollbackBatchCheckpoint(lastContext.rollback.currentIndex); - } - queueEnteredLineEffects(state, lastContext.pointers.read); return state; @@ -2803,61 +2828,9 @@ export const prevLine = ({ state }, payload) => { * - Logs warnings if section or lines not found */ export const sectionTransition = ({ state }, payload) => { - const { sectionId } = payload; - - // if (state.global.nextLineConfig.manual.enabled === false) { - // return state; - // } - // Validate section exists - const targetSection = selectSection({ state }, { sectionId }); - if (!targetSection) { - console.warn(`Section not found: ${sectionId}`); - return state; - } - - // Get first line of target section - const firstLine = targetSection.lines?.[0]; - if (!firstLine) { - console.warn(`Section ${sectionId} has no lines`); - return state; - } - - // Stop auto/skip modes on section transition - if (state.global.autoMode) { - stopAutoMode({ state }); - } - if (state.global.skipMode) { - stopSkipMode({ state }); - } - - // Update current pointer to new section's first line - const lastContext = state.contexts[state.contexts.length - 1]; - if (lastContext) { - if (!lastContext.rollback) { - ensureRollbackState(lastContext, { compatibilityAnchor: true }); - } - - lastContext.pointers.read = { - sectionId, - lineId: firstLine.id, - }; - - if (consumePendingStorySessionReset()) { - lastContext.rollback = createRollbackState({ - pointer: lastContext.pointers.read, - }); - setActiveRollbackBatchCheckpoint(lastContext.rollback.currentIndex); - } else { - appendRollbackCheckpoint(state, { - sectionId, - lineId: firstLine.id, - }); - } - } - - queueEnteredLineEffects(state, lastContext?.pointers?.read); - - return state; + return transitionToSection(state, { + sectionId: payload?.sectionId, + }); }; export const nextLineFromSystem = ({ state }) => { @@ -3246,7 +3219,7 @@ export const createSystemStore = (initialState) => { setMenuEntryPoint, showConfirmDialog, hideConfirmDialog, - resetStorySession, + resetStoryAtSection, clearPendingEffects, appendPendingEffect, beginRollbackActionBatch, diff --git a/vt/reference/sectionTransition/reset-story-at-section--capture-01.webp b/vt/reference/sectionTransition/reset-story-at-section--capture-01.webp new file mode 100644 index 00000000..da78fc3d --- /dev/null +++ b/vt/reference/sectionTransition/reset-story-at-section--capture-01.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16c3e848f7b3074f55fbb98bdee93ebc49006b9abc0107b0bce0a69f65c39731 +size 2492 diff --git a/vt/reference/sectionTransition/reset-story-at-section--capture-02.webp b/vt/reference/sectionTransition/reset-story-at-section--capture-02.webp new file mode 100644 index 00000000..5388442d --- /dev/null +++ b/vt/reference/sectionTransition/reset-story-at-section--capture-02.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4deb96521314cfc73497c325f47e5efd0617ea37cbeedc65a29b926373c3d6a +size 2090 diff --git a/vt/specs/sectionTransition/reset-story-at-section.yaml b/vt/specs/sectionTransition/reset-story-at-section.yaml new file mode 100644 index 00000000..3c3464c5 --- /dev/null +++ b/vt/specs/sectionTransition/reset-story-at-section.yaml @@ -0,0 +1,248 @@ +--- +title: Reset Story At Section +description: "resetStoryAtSection should start fresh gameplay and fresh title runs without leaving rollback access to the previous section." +specs: + - the title screen seeds a dirty context score before Start is pressed + - Start uses resetStoryAtSection and the game section begins from score 0 + its own line action + - rollback on the first gameplay checkpoint does not return to the title section + - returning to title with the same option also clears rollback to gameplay +skipInitialScreenshot: true +viewport: + id: capture + width: 960 + height: 540 +steps: + - action: click + x: 480 + y: 302 + - action: wait + ms: 150 + - action: click + x: 170 + y: 460 + - action: wait + ms: 150 + - action: screenshot + - action: click + x: 480 + y: 302 + - action: wait + ms: 150 + - action: click + x: 170 + y: 460 + - action: wait + ms: 150 + - action: screenshot +--- +screen: + width: 960 + height: 540 + backgroundColor: "#000000" +resources: + variables: + score: + type: number + scope: context + default: 0 + layouts: + titleLayout: + elements: + - id: title-bg + type: rect + width: 960 + height: 540 + colorId: bg + - id: title-card + type: rect + x: 180 + y: 80 + width: 600 + height: 150 + colorId: panel + - id: title-heading + type: text + x: 480 + y: 128 + anchorX: 0.5 + content: TITLE SCREEN + textStyleId: heading + - id: title-score + type: text + x: 480 + y: 184 + anchorX: 0.5 + content: "Score: ${variables.score}" + textStyleId: body + - id: start-button + type: rect + x: 300 + y: 260 + width: 360 + height: 84 + colorId: button + click: + payload: + actions: + resetStoryAtSection: + sectionId: game + - id: start-label + type: text + x: 480 + y: 289 + anchorX: 0.5 + content: START + textStyleId: buttonText + - id: title-rollback-button + type: rect + x: 60 + y: 430 + width: 220 + height: 60 + colorId: buttonMuted + click: + payload: + actions: + rollbackByOffset: + offset: -1 + - id: title-rollback-label + type: text + x: 170 + y: 450 + anchorX: 0.5 + content: ROLLBACK + textStyleId: body + gameLayout: + elements: + - id: game-bg + type: rect + width: 960 + height: 540 + colorId: bg + - id: game-card + type: rect + x: 180 + y: 80 + width: 600 + height: 150 + colorId: panel + - id: game-heading + type: text + x: 480 + y: 128 + anchorX: 0.5 + content: GAME SCREEN + textStyleId: heading + - id: game-score + type: text + x: 480 + y: 184 + anchorX: 0.5 + content: "Score: ${variables.score}" + textStyleId: body + - id: return-button + type: rect + x: 300 + y: 260 + width: 360 + height: 84 + colorId: button + click: + payload: + actions: + resetStoryAtSection: + sectionId: title + - id: return-label + type: text + x: 480 + y: 289 + anchorX: 0.5 + content: RETURN TO TITLE + textStyleId: buttonText + - id: game-rollback-button + type: rect + x: 60 + y: 430 + width: 220 + height: 60 + colorId: buttonMuted + click: + payload: + actions: + rollbackByOffset: + offset: -1 + - id: game-rollback-label + type: text + x: 170 + y: 450 + anchorX: 0.5 + content: ROLLBACK + textStyleId: body + fonts: + fontDefault: + fileId: Arial + colors: + bg: + hex: "#000000" + panel: + hex: "#4D4D4D" + button: + hex: "#737373" + buttonMuted: + hex: "#5A5A5A" + fg: + hex: "#FFFFFF" + fgMuted: + hex: "#D9D9D9" + textStyles: + heading: + fontId: fontDefault + colorId: fg + fontSize: 34 + fontWeight: bold + fontStyle: normal + lineHeight: 1.2 + body: + fontId: fontDefault + colorId: fgMuted + fontSize: 28 + fontWeight: "400" + fontStyle: normal + lineHeight: 1.2 + buttonText: + fontId: fontDefault + colorId: fg + fontSize: 28 + fontWeight: bold + fontStyle: normal + lineHeight: 1.2 +story: + initialSceneId: main + scenes: + main: + initialSectionId: title + sections: + title: + lines: + - id: titleLine + actions: + updateVariable: + id: seedTitleScore + operations: + - variableId: score + op: set + value: 7 + background: + resourceId: titleLayout + game: + lines: + - id: gameLine + actions: + updateVariable: + id: seedGameScore + operations: + - variableId: score + op: increment + value: 1 + background: + resourceId: gameLayout