diff --git a/rettangoli.config.yaml b/rettangoli.config.yaml index f44fe4fa..bdc03823 100644 --- a/rettangoli.config.yaml +++ b/rettangoli.config.yaml @@ -36,5 +36,7 @@ vt: files: "bgm" - title: "SFX" files: "sfx" + - title: "Save" + files: "save" - title: "Keyboard" - files: "keyboard" \ No newline at end of file + files: "keyboard" diff --git a/spec/system/actions/loadSaveSlot.yaml b/spec/system/actions/loadSaveSlot.yaml new file mode 100644 index 00000000..cf9ef762 --- /dev/null +++ b/spec/system/actions/loadSaveSlot.yaml @@ -0,0 +1,87 @@ +file: "../../../src/stores/system.store.js" +group: systemStore.loadSaveSlot +suites: [loadSaveSlot] +--- +suite: loadSaveSlot +exportName: loadSaveSlot +--- +case: load from existing slot +in: + - state: + global: + saveSlots: + "1": + slotKey: "1" + date: 1704110400 + image: "save.png" + state: + projectData: { story: { initialSceneId: "loaded" } } + global: + currentLocalizationPackageId: "en" + contexts: + - pointers: + read: { sectionId: "sec2", lineId: "line2" } + pendingEffects: [] + - slot: 1 +out: + global: + pendingEffects: + - name: "loadGame" + options: + initialState: + projectData: { story: { initialSceneId: "loaded" } } +--- +case: load from non-existent slot +in: + - state: + global: + saveSlots: {} + pendingEffects: [] + - slot: 99 +out: + global: + pendingEffects: [] +--- +case: load from slot 2 +in: + - state: + global: + saveSlots: + "2": + slotKey: "2" + date: 1704110500 + image: "save2.png" + state: + level: 10 + pendingEffects: [] + - slot: 2 +out: + global: + pendingEffects: + - name: "loadGame" + options: + initialState: + level: 10 +--- +case: load with existing pending effects +in: + - state: + global: + saveSlots: + "1": + slotKey: "1" + date: 1704110400 + image: "save.png" + state: + level: 5 + pendingEffects: + - name: "existingEffect" + - slot: 1 +out: + global: + pendingEffects: + - name: "existingEffect" + - name: "loadGame" + options: + initialState: + level: 5 diff --git a/spec/system/actions/replaceSaveSlot.spec.yaml b/spec/system/actions/replaceSaveSlot.spec.yaml deleted file mode 100644 index 00958c06..00000000 --- a/spec/system/actions/replaceSaveSlot.spec.yaml +++ /dev/null @@ -1,281 +0,0 @@ -file: "../../../src/stores/system.store.js" -group: systemStore.replaceSaveSlot -suites: [replaceSaveSlot] ---- -suite: replaceSaveSlot -exportName: replaceSaveSlot ---- -case: replace save slot in empty registry -in: - - state: - global: - saveSlots: {} - pendingEffects: [] - - slotKey: "1" - date: 1704110400 - image: "iVBORw0KGgoAAAANSUhEUgAA..." - state: - level: 1 - score: 0 - playerName: "Hero" -out: - global: - saveSlots: - "1": - slotKey: "1" - date: 1704110400 - image: "iVBORw0KGgoAAAANSUhEUgAA..." - state: - level: 1 - score: 0 - playerName: "Hero" - pendingEffects: - - name: "render" ---- -case: replace existing save slot -in: - - state: - global: - saveSlots: - "1": - slotKey: "1" - date: 1704110400 - image: "old_image.png" - state: - level: 5 - score: 1000 - pendingEffects: [] - - slotKey: "1" - date: 1704110500 - image: "new_image.png" - state: - level: 10 - score: 2500 - playerName: "Hero" -out: - global: - saveSlots: - "1": - slotKey: "1" - date: 1704110500 - image: "new_image.png" - state: - level: 10 - score: 2500 - playerName: "Hero" - pendingEffects: - - name: "render" ---- -case: replace save slot with string slotKey -in: - - state: - global: - saveSlots: - "1": - slotKey: "1" - date: 1704110400 - image: "slot1.png" - state: - progress: 25 - "autosave": - slotKey: "autosave" - date: 1704110300 - image: "auto_old.png" - state: - auto: true - pendingEffects: [] - - slotKey: "autosave" - date: 1704110600 - image: "auto_new.png" - state: - auto: true - lastAction: "dialogue" - checkpoint: "chapter_3" -out: - global: - saveSlots: - "1": - slotKey: "1" - date: 1704110400 - image: "slot1.png" - state: - progress: 25 - "autosave": - slotKey: "autosave" - date: 1704110600 - image: "auto_new.png" - state: - auto: true - lastAction: "dialogue" - checkpoint: "chapter_3" - pendingEffects: - - name: "render" ---- -case: replace save slot with existing pending effects -in: - - state: - global: - saveSlots: - "2": - slotKey: "2" - date: 1704110400 - image: "save2.png" - state: - completed: false - pendingEffects: - - name: "existingEffect" - data: "test" - - slotKey: "2" - date: 1704110700 - image: "save2_updated.png" - state: - completed: true - endTime: 1704110800 -out: - global: - saveSlots: - "2": - slotKey: "2" - date: 1704110700 - image: "save2_updated.png" - state: - completed: true - endTime: 1704110800 - pendingEffects: - - name: "existingEffect" - data: "test" - - name: "render" ---- -case: replace save slot with complex state object -in: - - state: - global: - saveSlots: - "3": - slotKey: "3" - date: 1704110400 - image: "old_save.png" - state: - player: - name: "Alice" - class: "Warrior" - level: 15 - inventory: - - id: "sword_001" - name: "Iron Sword" - - id: "potion_001" - name: "Health Potion" - questLog: - active: - - id: "quest_main" - title: "Save the Kingdom" - progress: 75 - pendingEffects: [] - - slotKey: "3" - date: 1704110800 - image: "new_save.png" - state: - player: - name: "Alice" - class: "Warrior" - level: 20 - stats: - hp: 150 - mp: 50 - strength: 25 - inventory: - - id: "sword_001" - name: "Iron Sword" - - id: "sword_002" - name: "Steel Sword" - - id: "potion_001" - name: "Health Potion" - questLog: - active: - - id: "quest_main" - title: "Save the Kingdom" - progress: 100 - completed: - - id: "quest_side1" - title: "Help the Villagers" -out: - global: - saveSlots: - "3": - slotKey: "3" - date: 1704110800 - image: "new_save.png" - state: - player: - name: "Alice" - class: "Warrior" - level: 20 - stats: - hp: 150 - mp: 50 - strength: 25 - inventory: - - id: "sword_001" - name: "Iron Sword" - - id: "sword_002" - name: "Steel Sword" - - id: "potion_001" - name: "Health Potion" - questLog: - active: - - id: "quest_main" - title: "Save the Kingdom" - progress: 100 - completed: - - id: "quest_side1" - title: "Help the Villagers" - pendingEffects: - - name: "render" ---- -case: replace save slot with other global properties unchanged -in: - - state: - global: - saveSlots: - "1": - slotKey: "1" - date: 1704110400 - image: "old.png" - state: - level: 1 - autoMode: true - skipMode: false - dialogueUIHidden: false - currentLocalizationPackageId: "en" - nextLineConfig: - manual: - enabled: true - requireComplete: false - pendingEffects: [] - - slotKey: "1" - date: 1704110900 - image: "updated.png" - state: - level: 15 - newFeature: true -out: - global: - saveSlots: - "1": - slotKey: "1" - date: 1704110900 - image: "updated.png" - state: - level: 15 - newFeature: true - autoMode: true - skipMode: false - dialogueUIHidden: false - currentLocalizationPackageId: "en" - nextLineConfig: - manual: - enabled: true - requireComplete: false - pendingEffects: - - name: "render" - diff --git a/spec/system/actions/saveSaveSlot.yaml b/spec/system/actions/saveSaveSlot.yaml new file mode 100644 index 00000000..b7c9c718 --- /dev/null +++ b/spec/system/actions/saveSaveSlot.yaml @@ -0,0 +1,146 @@ +file: "../../../src/stores/system.store.js" +group: systemStore.saveSaveSlot +suites: [saveSaveSlot] +--- +suite: saveSaveSlot +exportName: saveSaveSlot +--- +case: save to slot 1 +in: + - state: + global: + saveSlots: {} + pendingEffects: [] + projectData: + story: { initialSceneId: "test" } + contexts: + - pointers: + read: { sectionId: "sec1", lineId: "line1" } + - slot: 1 +out: + global: + saveSlots: + "1": + slotKey: "1" + date: null + image: null + state: + projectData: + story: { initialSceneId: "test" } + global: + pendingEffects: [] + contexts: + - pointers: + read: { sectionId: "sec1", lineId: "line1" } + pendingEffects: + - name: "render" + contexts: + - pointers: + read: { sectionId: "sec1", lineId: "line1" } +--- +case: save to slot 2 +in: + - state: + global: + saveSlots: {} + pendingEffects: [] + projectData: + story: { initialSceneId: "test" } + contexts: + - pointers: + read: { sectionId: "sec1", lineId: "line1" } + - slot: 2 +out: + global: + saveSlots: + "2": + slotKey: "2" + date: null + image: null + state: + projectData: + story: { initialSceneId: "test" } + global: + pendingEffects: [] + contexts: + - pointers: + read: { sectionId: "sec1", lineId: "line1" } + pendingEffects: + - name: "render" + contexts: + - pointers: + read: { sectionId: "sec1", lineId: "line1" } +--- +case: overwrite existing save slot +in: + - state: + global: + saveSlots: + "1": + slotKey: "1" + date: 1704110400 + image: "old_image.png" + state: + level: 5 + pendingEffects: [] + projectData: + story: { initialSceneId: "test" } + contexts: + - pointers: + read: { sectionId: "sec1", lineId: "line1" } + - slot: 1 +out: + global: + saveSlots: + "1": + slotKey: "1" + date: null + image: null + state: + projectData: + story: { initialSceneId: "test" } + global: + pendingEffects: [] + contexts: + - pointers: + read: { sectionId: "sec1", lineId: "line1" } + pendingEffects: + - name: "render" + contexts: + - pointers: + read: { sectionId: "sec1", lineId: "line1" } +--- +case: save to slot with existing pending effects +in: + - state: + global: + saveSlots: {} + pendingEffects: + - name: "existingEffect" + projectData: + story: { initialSceneId: "test" } + contexts: + - pointers: + read: { sectionId: "sec1", lineId: "line1" } + - slot: 1 +out: + global: + saveSlots: + "1": + slotKey: "1" + date: null + image: null + state: + projectData: + story: { initialSceneId: "test" } + global: + pendingEffects: [] + contexts: + - pointers: + read: { sectionId: "sec1", lineId: "line1" } + pendingEffects: + - name: "existingEffect" + - name: "render" + contexts: + - pointers: + read: { sectionId: "sec1", lineId: "line1" } diff --git a/src/RouteEngine.js b/src/RouteEngine.js index c421ec7c..a1c7a839 100644 --- a/src/RouteEngine.js +++ b/src/RouteEngine.js @@ -23,6 +23,10 @@ export default function createRouteEngine(options) { return _systemStore.selectRenderState(); }; + const selectSaveSlots = () => { + return _systemStore.selectSaveSlots(); + }; + const handleAction = (actionType, payload) => { if (!_systemStore[actionType]) { return; @@ -51,6 +55,7 @@ export default function createRouteEngine(options) { handleActions, selectRenderState, selectPresentationState, + selectSaveSlots, handleLineActions, }; } diff --git a/src/createEffectsHandler.js b/src/createEffectsHandler.js index e7e18dc2..7f0565ec 100644 --- a/src/createEffectsHandler.js +++ b/src/createEffectsHandler.js @@ -102,8 +102,13 @@ const clearSkipNextTimer = ({ ticker, skipTimer }, payload) => { skipTimer.setElapsed(0); }; +const saveSlots = ({}, payload) => { + localStorage.setItem("saveSlots", JSON.stringify(payload.saveSlots)); +}; + const effects = { render, + saveSlots, handleLineActions, startAutoNextTimer, clearAutoNextTimer, diff --git a/src/schemas/systemActions.yaml b/src/schemas/systemActions.yaml index f15de9ae..c3d23ac9 100644 --- a/src/schemas/systemActions.yaml +++ b/src/schemas/systemActions.yaml @@ -159,7 +159,7 @@ properties: # Save and Load ############ # TODO: finalize naming - globalSaveStory: + saveSaveSlot: type: object description: Save current story state. See the side effect for all the data it stores properties: @@ -171,7 +171,7 @@ properties: additionalProperties: false # TODO: finalize naming - globalLoadStory: + loadSaveSlot: type: object description: Load game state from a save slot properties: diff --git a/src/schemas/systemState/effects.yaml b/src/schemas/systemState/effects.yaml index de6e393b..c0af51bf 100644 --- a/src/schemas/systemState/effects.yaml +++ b/src/schemas/systemState/effects.yaml @@ -16,24 +16,37 @@ oneOf: additionalProperties: false - title: Save VN data effect - description: Saves visual novel data to a slot + description: Saves visual novel data to localStorage saveSlots type: object properties: - type: + name: const: saveVnData - saveData: + options: type: object - description: Save game data for saving - additionalProperties: true - slotIndex: - type: integer - description: Save slot index - minimum: 0 + properties: + saveData: + type: object + description: Save game data + properties: + date: + type: integer + description: Save timestamp + screenshot: + type: string + description: Screenshot in base64 format + state: + type: object + description: Game state including sectionId, lineId, history, and settings + additionalProperties: true + slotIndex: + type: integer + description: Save slot index + minimum: 0 priority: type: integer description: Priority of the effect (lower numbers execute first) default: 0 - required: [type, saveData, slotIndex] + required: [name] additionalProperties: false - title: Save variables effect diff --git a/src/schemas/systemState/systemState.yaml b/src/schemas/systemState/systemState.yaml index 83fbb28b..6d22c07d 100644 --- a/src/schemas/systemState/systemState.yaml +++ b/src/schemas/systemState/systemState.yaml @@ -45,22 +45,25 @@ properties: additionalProperties: true default: {} - saveData: + saveSlots: type: object - description: Additional save game data + description: Save game slots indexed by slotKey properties: - "^d+$": # slot number + "^.+$": # slot key (string like "1", "autosave", etc.) + type: object properties: - date: + slotKey: type: string - description: date + date: + type: integer + description: Unix timestamp image: type: string - description: base64 representation of the image + description: Base64 screenshot (null if none) state: type: object - description: full state - additionalProperties: true + description: Full system state + required: [slotKey, date, state] default: {} # TODO: don't remember why we need this for diff --git a/src/stores/system.store.js b/src/stores/system.store.js index e43ffff8..ecfa0713 100644 --- a/src/stores/system.store.js +++ b/src/stores/system.store.js @@ -4,7 +4,7 @@ import { constructRenderState } from "./constructRenderState.js"; export const createInitialState = (payload) => { const { - global: { currentLocalizationPackageId }, + global: { currentLocalizationPackageId, saveSlots }, // initialPointer, projectData, } = payload; @@ -47,7 +47,7 @@ export const createInitialState = (payload) => { //delay: 1000, }, }, - saveSlots: {}, + saveSlots, layeredViews: [], }, contexts: [ @@ -75,6 +75,7 @@ export const createInitialState = (payload) => { }, ], }; + console.log("Initial State:", state); return state; }; @@ -681,28 +682,59 @@ export const setAutoplayDelay = ({ state }, { delay }) => { }; /** - * Replaces a save slot with new data + * Saves current game state to a slot * @param {Object} state - Current state object - * @param {Object} initialState - Action payload - * @param {string} initialState.slotKey - The key identifying the save slot - * @param {number} initialState.date - Unix timestamp for when the save was created - * @param {string} initialState.image - Base64 encoded save image/screenshot - * @param {Object} initialState.state - The game state to be saved + * @param {Object} payload - Action payload + * @param {number} payload.slot - Save slot number + * @param {string} payload.thumbnailImage - Base64 thumbnail image * @returns {Object} Updated state object */ -export const replaceSaveSlot = ({ state }, payload) => { - const { slotKey, date, image, state: slotState } = payload; +export const saveSaveSlot = ({ state }, payload) => { + const { slot, thumbnailImage } = payload; + const slotKey = String(slot); + + const currentState = { + contexts: [...state.contexts], + viewedRegistry: state.global.viewedRegistry, + }; - state.global.saveSlots[slotKey] = { + const saveData = { slotKey, - date, - image, - state: slotState, + date: Date.now(), + image: thumbnailImage, + state: currentState, }; - state.global.pendingEffects.push({ - name: "render", - }); + state.global.saveSlots[slotKey] = saveData; + + state.global.pendingEffects.push( + { + name: "saveSlots", + payload: { + saveSlots: { ...state.global.saveSlots }, + }, + }, + { name: "render" }, + ); + return state; +}; + +/** + * Loads game state from a save slot + * @param {Object} state - Current state object + * @param {Object} payload - Action payload + * @param {number} payload.slot - Save slot number + * @returns {Object} Updated state object + */ +export const loadSaveSlot = ({ state }, payload) => { + const { slot } = payload; + const slotKey = String(slot); + const slotData = state.global.saveSlots[slotKey]; + if (slotData) { + state.global.viewedRegistry = slotData.state.viewedRegistry; + state.contexts = slotData.state.contexts; + state.global.pendingEffects.push({ name: "render" }); + } return state; }; @@ -1144,7 +1176,8 @@ export const createSystemStore = (initialState) => { addViewedLine, addViewedResource, setNextLineConfig, - replaceSaveSlot, + saveSaveSlot, + loadSaveSlot, setAutoplayDelay, updateProjectData, sectionTransition, diff --git a/src/util.js b/src/util.js index 97814883..f1794de3 100644 --- a/src/util.js +++ b/src/util.js @@ -291,15 +291,3 @@ export const createSelectiveActionsExecutor = ( }); }; }; - -export const base64ToArrayBuffer = (base64) => { - const binaryString = window.atob( - base64.replace(/^data:image\/[a-z]+;base64,/, ""), - ); - const len = binaryString.length; - const bytes = new Uint8Array(len); - for (let i = 0; i < len; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - return bytes.buffer; -}; diff --git a/vt/specs/save/basic.yaml b/vt/specs/save/basic.yaml new file mode 100644 index 00000000..146ac2e7 --- /dev/null +++ b/vt/specs/save/basic.yaml @@ -0,0 +1,452 @@ +--- +title: "Save/Load Test" +--- +l10n: + packages: + eklekfjwalefj: + label: English + lang: en + keys: + line1: "Line 1: Welcome to Save/Load test" + line2: "Line 2: Click Save in top right to save your game" + line3: "Line 3: Choose a slot, then click X to close" + line4: "Line 4: Click Load to load a saved game" + line5: "Line 5: Try saving now, then continue to next section" + line6: "Line 6: This is section 2 - loading returns you to section 1" + saveButton: "Save" + loadButton: "Load" + saveTitle: "Save Game" + loadTitle: "Load Game" + closeHint: "Click X to close" + +screen: + width: 1920 + height: 1080 + backgroundColor: "#000000" + +resources: + characters: + narrator: + name: "" + + layouts: + baseLayout: + elements: + - id: background + type: rect + x: 0 + y: 0 + width: 1920 + height: 1080 + fill: "#000000" + click: + actionPayload: + actions: + nextLine: {} + + dialogueLayout: + name: Dialogue Screen + mode: adv + elements: + - id: dialogue-box + type: rect + x: 100 + y: 800 + width: 1720 + height: 200 + fill: "#333333" + + - id: dialogue-text + type: text + x: 150 + y: 850 + content: ${dialogue.content[0].text} + textStyle: + fontSize: 32 + fill: "white" + + - id: save-button + type: rect + x: 1780 + y: 20 + width: 100 + height: 50 + fill: "#4CAF50" + hover: + fill: "#66BB6A" + click: + actionPayload: + actions: + pushLayeredView: + resourceId: saveMenuLayout + resourceType: layout + + - id: save-button-text + type: text + x: 1810 + y: 35 + content: ${l10n.keys.saveButton} + textStyle: + fontSize: 24 + fill: "white" + + - id: load-button + type: rect + x: 1660 + y: 20 + width: 100 + height: 50 + fill: "#2196F3" + hover: + fill: "#42A5F5" + click: + actionPayload: + actions: + pushLayeredView: + resourceId: loadMenuLayout + resourceType: layout + + - id: load-button-text + type: text + x: 1690 + y: 35 + content: ${l10n.keys.loadButton} + textStyle: + fontSize: 24 + fill: "white" + + saveMenuLayout: + elements: + - id: save-menu-overlay + type: rect + fill: "rgba(0,0,0,0.7)" + width: 1920 + height: 1080 + x: 0 + y: 0 + + - id: save-menu-bg + type: rect + fill: "#1a1a2e" + width: 800 + height: 500 + x: 560 + y: 290 + + - id: save-menu-title + type: text + content: ${l10n.keys.saveTitle} + x: 900 + y: 340 + textStyle: + fontSize: 48 + fill: "#ffffff" + + - id: close-btn + type: rect + fill: "#F44336" + width: 50 + height: 50 + x: 1290 + y: 300 + hover: + fill: "#EF5350" + click: + actionPayload: + actions: + clearLayeredViews: {} + + - id: close-text + type: text + content: "X" + x: 1305 + y: 310 + textStyle: + fontSize: 32 + fill: "#ffffff" + + - id: save-slot-1-btn + type: rect + fill: "#4CAF50" + width: 600 + height: 70 + x: 660 + y: 400 + hover: + fill: "#66BB6A" + click: + actionPayload: + actions: + saveSaveSlot: + slot: 1 + + - id: save-slot-1-text + type: text + content: "Slot 1" + x: 920 + y: 420 + textStyle: + fontSize: 32 + fill: "#ffffff" + + - id: save-slot-2-btn + type: rect + fill: "#2196F3" + width: 600 + height: 70 + x: 660 + y: 490 + hover: + fill: "#42A5F5" + click: + actionPayload: + actions: + saveSaveSlot: + slot: 2 + + - id: save-slot-2-text + type: text + content: "Slot 2" + x: 920 + y: 510 + textStyle: + fontSize: 32 + fill: "#ffffff" + + - id: save-slot-3-btn + type: rect + fill: "#FF9800" + width: 600 + height: 70 + x: 660 + y: 580 + hover: + fill: "#FFB74D" + click: + actionPayload: + actions: + saveSaveSlot: + slot: 3 + + - id: save-slot-3-text + type: text + content: "Slot 3" + x: 920 + y: 600 + textStyle: + fontSize: 32 + fill: "#ffffff" + + - id: save-menu-info + type: text + content: ${l10n.keys.closeHint} + x: 880 + y: 730 + textStyle: + fontSize: 20 + fill: "#aaaaaa" + + loadMenuLayout: + elements: + - id: load-menu-overlay + type: rect + fill: "rgba(0,0,0,0.7)" + width: 1920 + height: 1080 + x: 0 + y: 0 + + - id: load-menu-bg + type: rect + fill: "#1a1a2e" + width: 800 + height: 500 + x: 560 + y: 290 + + - id: load-menu-title + type: text + content: ${l10n.keys.loadTitle} + x: 900 + y: 340 + textStyle: + fontSize: 48 + fill: "#ffffff" + + - id: load-close-btn + type: rect + fill: "#F44336" + width: 50 + height: 50 + x: 1290 + y: 300 + hover: + fill: "#EF5350" + click: + actionPayload: + actions: + clearLayeredViews: {} + + - id: load-close-text + type: text + content: "X" + x: 1305 + y: 310 + textStyle: + fontSize: 32 + fill: "#ffffff" + + - id: load-slot-1-btn + type: rect + fill: "#9C27B0" + width: 600 + height: 70 + x: 660 + y: 400 + hover: + fill: "#AB47BC" + click: + actionPayload: + actions: + loadSaveSlot: + slot: 1 + + - id: load-slot-1-text + type: text + content: "Slot 1" + x: 920 + y: 420 + textStyle: + fontSize: 32 + fill: "#ffffff" + + - id: load-slot-2-btn + type: rect + fill: "#E91E63" + width: 600 + height: 70 + x: 660 + y: 490 + hover: + fill: "#EC407A" + click: + actionPayload: + actions: + loadSaveSlot: + slot: 2 + + - id: load-slot-2-text + type: text + content: "Slot 2" + x: 920 + y: 510 + textStyle: + fontSize: 32 + fill: "#ffffff" + + - id: load-slot-3-btn + type: rect + fill: "#673AB7" + width: 600 + height: 70 + x: 660 + y: 580 + hover: + fill: "#7E57C2" + click: + actionPayload: + actions: + loadSaveSlot: + slot: 3 + + - id: load-slot-3-text + type: text + content: "Slot 3" + x: 920 + y: 600 + textStyle: + fontSize: 32 + fill: "#ffffff" + + - id: load-menu-info + type: text + content: ${l10n.keys.closeHint} + x: 880 + y: 730 + textStyle: + fontSize: 20 + fill: "#aaaaaa" + +story: + initialSceneId: test_scene + scenes: + test_scene: + initialSectionId: test_section + sections: + test_section: + lines: + - id: line1 + actions: + base: + resourceId: baseLayout + dialogue: + mode: adv + gui: + resourceId: dialogueLayout + content: + - text: ${l10n.keys.line1} + characterId: narrator + + - id: line2 + actions: + dialogue: + content: + - text: ${l10n.keys.line2} + characterId: narrator + + - id: line3 + actions: + dialogue: + content: + - text: ${l10n.keys.line3} + characterId: narrator + + - id: line4 + actions: + dialogue: + content: + - text: ${l10n.keys.line4} + characterId: narrator + + - id: line5 + actions: + dialogue: + content: + - text: ${l10n.keys.line5} + characterId: narrator + + - id: line6 + actions: + sectionTransition: + sectionId: section2 + + section2: + lines: + - id: line1 + actions: + dialogue: + content: + - text: ${l10n.keys.line6} + characterId: narrator + + - id: line2 + actions: + dialogue: + content: + - text: ${l10n.keys.line1} + characterId: narrator + + - id: line3 + actions: + sectionTransition: + sectionId: test_section \ No newline at end of file diff --git a/vt/static/main.js b/vt/static/main.js index fa76fc46..85e2e2de 100644 --- a/vt/static/main.js +++ b/vt/static/main.js @@ -12,8 +12,7 @@ import createRouteGraphics, { textRevealingPlugin, tweenPlugin, soundPlugin, - videoPlugin, -} from "https://cdn.jsdelivr.net/npm/route-graphics@0.0.16/+esm" +} from "https://cdn.jsdelivr.net/npm/route-graphics@0.0.19/+esm" const projectData = parse(window.yamlContent); @@ -135,11 +134,24 @@ const init = async () => { const ticker = new Ticker(); ticker.start(); + const base64ToArrayBuffer = (base64) => { + const binaryString = window.atob( + base64.replace(/^data:image\/[a-z]+;base64,/, ""), + ); + const len = binaryString.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes.buffer; + }; + + await routeGraphics.init({ width: 1920, height: 1080, plugins, - eventHandler: (eventName, payload) => { + eventHandler: async (eventName, payload) => { if (eventName === 'renderComplete') { if (count >= 2) { return; @@ -149,6 +161,17 @@ const init = async () => { // engine.handleLineActions(); } else { if (payload.actions) { + if(payload.actions.saveSaveSlot){ + const url = await routeGraphics.extractBase64("story"); + const assets = { + [`saveThumbnailImage:${payload.actions.saveSaveSlot.slot}`]: { + buffer: base64ToArrayBuffer(url), + type: "image/png", + }, + }; + await routeGraphics.loadAssets(assets); + payload.actions.saveSaveSlot.thumbnailImage = url; + } engine.handleActions(payload.actions); } } @@ -163,11 +186,13 @@ const init = async () => { const effectsHandler = createEffectsHandler({ getEngine: () => engine, routeGraphics, ticker }); const engine = createRouteEngine({ handlePendingEffects: effectsHandler }); + const saveSlots = JSON.parse(localStorage.getItem("saveSlots")) || {}; engine.init({ initialState: { global: { - currentLocalizationPackageId: 'eklekfjwalefj' + currentLocalizationPackageId: 'eklekfjwalefj', + saveSlots }, projectData }