From 81a2f4d06379332cc746891386fba438bc5946e1 Mon Sep 17 00:00:00 2001 From: Tobias Berger Date: Thu, 30 Sep 2021 13:37:20 +0200 Subject: [PATCH] Saving --- src/components/Header.svelte | 8 +- src/components/Log.svelte | 7 +- src/components/Modals/SettingsModal.svelte | 2 +- src/components/TabSelector.svelte | 6 +- src/components/Wrapper.svelte | 14 +- src/shark/Log.ts | 6 +- src/shark/Message.ts | 2 +- src/shark/SaveHandler.ts | 111 +++++++ src/shark/Settings.ts | 330 +++++++++++---------- src/shark/SharkGame.ts | 12 +- src/shark/{Tabs.ts => TabHandler.ts} | 6 +- 11 files changed, 328 insertions(+), 176 deletions(-) create mode 100644 src/shark/SaveHandler.ts rename src/shark/{Tabs.ts => TabHandler.ts} (57%) diff --git a/src/components/Header.svelte b/src/components/Header.svelte index 14400b3..6ca3334 100644 --- a/src/components/Header.svelte +++ b/src/components/Header.svelte @@ -11,10 +11,14 @@ const mainHeaderButtons = { save() { - console.log("save"); + game.SaveHandler.save(game); }, settings() { - openModal(SettingsModal, { settings: game.Settings }, { replace: true }); + openModal( + SettingsModal, + { settings: game.Settings.settings }, + { replace: true } + ); }, help() { console.log("help"); diff --git a/src/components/Log.svelte b/src/components/Log.svelte index 06a5d3b..6a9c764 100644 --- a/src/components/Log.svelte +++ b/src/components/Log.svelte @@ -25,8 +25,8 @@ function addMessage(message: string, messageType?: MessageType) { dispatch("addMessage", { message, messageType }); } - function resetMessages() { - dispatch("resetLog", { reset: true }); + function resetMessages(notify = true) { + dispatch("resetLog", { notify }); } $: displayMessages = $messages.slice(-logLength).reverse(); @@ -48,7 +48,8 @@ addMessage("error" + $messages.length, MessageType.error); }}>Add Error - + +
    {#each displayMessages as message, i (message)} {#if i < logLength} diff --git a/src/components/Modals/SettingsModal.svelte b/src/components/Modals/SettingsModal.svelte index 5b3538a..0b8357b 100644 --- a/src/components/Modals/SettingsModal.svelte +++ b/src/components/Modals/SettingsModal.svelte @@ -5,7 +5,7 @@ export let isOpen: boolean; - export let settings: typeof Settings; + export let settings: typeof Settings["settings"]; diff --git a/src/components/TabSelector.svelte b/src/components/TabSelector.svelte index ee5e65d..76ac0da 100644 --- a/src/components/TabSelector.svelte +++ b/src/components/TabSelector.svelte @@ -1,8 +1,8 @@ diff --git a/src/components/Wrapper.svelte b/src/components/Wrapper.svelte index 403664f..9686bbc 100644 --- a/src/components/Wrapper.svelte +++ b/src/components/Wrapper.svelte @@ -1,5 +1,5 @@ @@ -57,13 +57,13 @@
    { + Settings.settings.subscribe((settings) => { Log.#maxLogLength = Math.max(...settings.layout.logLength.options); })(); } @@ -25,9 +25,9 @@ export class Log extends StaticClass { ); } - static reset(): void { + static reset(notify = true): void { Message.even = true; this.messages.set([]); - this.addMessage("Log cleared"); + if (notify) this.addMessage("Log cleared"); } } diff --git a/src/shark/Message.ts b/src/shark/Message.ts index be6e161..2679302 100644 --- a/src/shark/Message.ts +++ b/src/shark/Message.ts @@ -1,5 +1,5 @@ export type AddMessageEvent = { message: string; messageType?: MessageType }; -export type ResetMessagesEvent = { reset: true }; +export type ResetMessagesEvent = { notify?: boolean }; export enum MessageType { message = "message", diff --git a/src/shark/SaveHandler.ts b/src/shark/SaveHandler.ts new file mode 100644 index 0000000..9041a72 --- /dev/null +++ b/src/shark/SaveHandler.ts @@ -0,0 +1,111 @@ +import type { Message, MessageType } from "./Message"; +import type { Settings } from "./Settings"; +import type { SharkGame } from "./SharkGame"; +import { StaticClass } from "./StaticClass"; +import type { TabHandler } from "./TabHandler"; + +const __EMPTY_OBJECT = {}; +type Version0Save = typeof __EMPTY_OBJECT; +type Version1Save = Version0Save & { + version: 1; + messages: { + message: string; + type: MessageType; + }[]; + selectedTab: keyof typeof TabHandler["AllTabs"]; + settings: ReturnType; +}; + +type CurrentVersionSave = Version1Save; +type Save = Version0Save | Version1Save; + +export class SaveHandler extends StaticClass { + static readonly saveName = "sharg-save"; + + static async save(game: typeof SharkGame): Promise { + const messages = await new Promise((resolve) => { + game.Log.messages.subscribe((messages) => { + resolve(messages); + })(); + }); + + const allTabsEntries = Object.entries( + game.TabHandler.AllTabs + ) as unknown as [ + keyof typeof TabHandler["AllTabs"], + typeof TabHandler["AllTabs"][keyof typeof TabHandler["AllTabs"]] + ][]; + const selectedTabIndex = allTabsEntries.findIndex( + ([, tab]) => tab === game.TabHandler.currentTab + ); + const selectedTabName = allTabsEntries[selectedTabIndex][0]; + + const save: CurrentVersionSave = { + version: 1, + selectedTab: selectedTabName, + settings: game.Settings.getCurrent(), + messages: messages.map((message) => { + return { + message: message.message, + type: message.type, + }; + }), + }; + const encodedSave = JSON.stringify(save); + + localStorage.setItem(SaveHandler.saveName, encodedSave); + } + + static async load(game: typeof SharkGame): Promise { + const localSave = localStorage.getItem(SaveHandler.saveName); + const loadedSave = JSON.parse(localSave ?? "{}"); + const saveVersion = loadedSave.version ?? 0; + + const migrators = SaveHandler.migrators.slice(saveVersion); + + let save = loadedSave; + for (let i = 0; i < migrators.length; i++) { + console.debug( + `Executing save migrator ${i + 1} / ${migrators.length} (${ + saveVersion + i + 1 + })` + ); + save = migrators[i](save); + } + + const fullSave = save as CurrentVersionSave; + + game.Settings.settings.update((settings) => { + Object.entries(settings).forEach(([categoryName, categorySettings]) => { + Object.entries(categorySettings).forEach(([settingName, setting]) => { + setting.current = + fullSave.settings[`${categoryName};${settingName}`] ?? + setting.default; + }); + }); + return settings; + }); + + game.Log.reset(false); + + fullSave.messages.forEach((message) => { + game.Log.addMessage(message.message, message.type); + }); + game.TabHandler.currentTab = game.TabHandler.AllTabs[fullSave.selectedTab]; + + return game; + } + + static migrators: ((save: Save) => Save)[] = [ + (): Version1Save => { + const newSave = { + version: 1 as const, + messages: [], + selectedTab: "Home" as const, + settings: {}, + }; + + return newSave; + }, + ]; +} diff --git a/src/shark/Settings.ts b/src/shark/Settings.ts index bdcd40e..ac8b8a2 100644 --- a/src/shark/Settings.ts +++ b/src/shark/Settings.ts @@ -1,156 +1,188 @@ -// How to make current just be undefined here, but keep the type of defaultValue? +// How to make current just be undefined here, but keep the type of default? import { writable } from "svelte/store"; +import type { Writable } from "svelte/store"; +import { StaticClass } from "./StaticClass"; -export const Settings = writable({ - layout: { - logLength: { - current: 30, - defaultValue: 30 as const, - name: "Log Messages" as const, - description: "The number of messages shown in the log" as const, - options: [5, 10, 15, 20, 30, 60] as const, +export class Settings extends StaticClass { + static readonly #settings = { + layout: { + logLength: { + current: 30, + default: 30 as const, + name: "Log Messages" as const, + description: "The number of messages shown in the log" as const, + options: [5, 10, 15, 20, 30, 60] as const, + }, }, - }, - appearance: { - enableThemes: { - current: true, - defaultValue: true as const, - name: "Enable Planet-dependent Styles" as const, - description: - "Whether page colors should change for different planets" as const, - options: [true, false] as const, + appearance: { + enableThemes: { + current: true, + default: true as const, + name: "Enable Planet-dependent Styles" as const, + description: + "Whether page colors should change for different planets" as const, + options: [true, false] as const, + }, + theme: { + current: "marine", + default: "marine" as const, + name: "Currently enabled theme" as const, + description: + "Only applied if planet-dependent styles are enabled" as const, + options: [ + "abandoned", + "chaotic", + "frigid", + "haven", + "marine", + "shrouded", + "tempestuous", + "violent", + ], + }, }, - theme: { - current: "marine", - defaultValue: "marine" as const, - name: "Currently enabled theme" as const, - description: - "Only applied if planet-dependent styles are enabled" as const, - options: [ - "abandoned", - "chaotic", - "frigid", - "haven", - "marine", - "shrouded", - "tempestuous", - "violent", - ], + other: { + updateCheck: { + current: true, + default: true as const, + name: "Check for updates" as const, + description: "Whether to notify you of new updates" as const, + options: [true, false] as const, + }, }, - }, - other: { - updateCheck: { - current: true, - defaultValue: true as const, - name: "Check for updates" as const, - description: "Whether to notify you of new updates" as const, - options: [true, false] as const, + // To test scrolling of the settings modal (gonna use them to test saving later, too) + nil: { + nil1: { + default: true as const, + name: "placeholder" as const, + description: "placeholder." as const, + options: [true, false] as const, + }, + nil2: { + default: true as const, + name: "placeholder" as const, + description: "placeholder." as const, + options: [true, false] as const, + }, + nil3: { + default: true as const, + name: "placeholder" as const, + description: "placeholder." as const, + options: [true, false] as const, + }, + nil4: { + default: true as const, + name: "placeholder" as const, + description: "placeholder." as const, + options: [true, false] as const, + }, + nil5: { + default: true as const, + name: "placeholder" as const, + description: "placeholder." as const, + options: [true, false] as const, + }, + nil6: { + default: true as const, + name: "placeholder" as const, + description: "placeholder." as const, + options: [true, false] as const, + }, + nil7: { + default: true as const, + name: "placeholder" as const, + description: "placeholder." as const, + options: [true, false] as const, + }, + nil8: { + default: true as const, + name: "placeholder" as const, + description: "placeholder." as const, + options: [true, false] as const, + }, + nil9: { + default: true as const, + name: "placeholder" as const, + description: "placeholder." as const, + options: [true, false] as const, + }, + nil10: { + default: true as const, + name: "placeholder" as const, + description: "placeholder." as const, + options: [true, false] as const, + }, + nil11: { + default: true as const, + name: "placeholder" as const, + description: "placeholder." as const, + options: [true, false] as const, + }, + nil12: { + default: true as const, + name: "placeholder" as const, + description: "placeholder." as const, + options: [true, false] as const, + }, + nil13: { + default: true as const, + name: "placeholder" as const, + description: "placeholder." as const, + options: [true, false] as const, + }, + nil14: { + default: true as const, + name: "placeholder" as const, + description: "placeholder." as const, + options: [true, false] as const, + }, + nil15: { + default: true as const, + name: "placeholder" as const, + description: "placeholder." as const, + options: [true, false] as const, + }, + nil16: { + default: true as const, + name: "placeholder" as const, + description: "placeholder." as const, + options: [true, false] as const, + }, + nil17: { + default: true as const, + name: "placeholder" as const, + description: "placeholder." as const, + options: [true, false] as const, + }, }, - }, - // To test scrolling of the settings modal (gonna use them to test saving later, too) - nil: { - nil1: { - defaultValue: true as const, - name: "placeholder" as const, - description: "placeholder." as const, - options: [true, false] as const, - }, - nil2: { - defaultValue: true as const, - name: "placeholder" as const, - description: "placeholder." as const, - options: [true, false] as const, - }, - nil3: { - defaultValue: true as const, - name: "placeholder" as const, - description: "placeholder." as const, - options: [true, false] as const, - }, - nil4: { - defaultValue: true as const, - name: "placeholder" as const, - description: "placeholder." as const, - options: [true, false] as const, - }, - nil5: { - defaultValue: true as const, - name: "placeholder" as const, - description: "placeholder." as const, - options: [true, false] as const, - }, - nil6: { - defaultValue: true as const, - name: "placeholder" as const, - description: "placeholder." as const, - options: [true, false] as const, - }, - nil7: { - defaultValue: true as const, - name: "placeholder" as const, - description: "placeholder." as const, - options: [true, false] as const, - }, - nil8: { - defaultValue: true as const, - name: "placeholder" as const, - description: "placeholder." as const, - options: [true, false] as const, - }, - nil9: { - defaultValue: true as const, - name: "placeholder" as const, - description: "placeholder." as const, - options: [true, false] as const, - }, - nil10: { - defaultValue: true as const, - name: "placeholder" as const, - description: "placeholder." as const, - options: [true, false] as const, - }, - nil11: { - defaultValue: true as const, - name: "placeholder" as const, - description: "placeholder." as const, - options: [true, false] as const, - }, - nil12: { - defaultValue: true as const, - name: "placeholder" as const, - description: "placeholder." as const, - options: [true, false] as const, - }, - nil13: { - defaultValue: true as const, - name: "placeholder" as const, - description: "placeholder." as const, - options: [true, false] as const, - }, - nil14: { - defaultValue: true as const, - name: "placeholder" as const, - description: "placeholder." as const, - options: [true, false] as const, - }, - nil15: { - defaultValue: true as const, - name: "placeholder" as const, - description: "placeholder." as const, - options: [true, false] as const, - }, - nil16: { - defaultValue: true as const, - name: "placeholder" as const, - description: "placeholder." as const, - options: [true, false] as const, - }, - nil17: { - defaultValue: true as const, - name: "placeholder" as const, - description: "placeholder." as const, - options: [true, false] as const, - }, - }, -}); + }; + static readonly settings = writable(Settings.#settings); + + static getSettings(): SettingsRecord { + return Object.seal(Object.assign({}, Settings.#settings)); + } + static getCurrent(): Record<`${string};${string}`, Setting> { + const current = this.getSettings(); + const result: Record<`${string};${string}`, Setting> = {}; + for (const [categoryName, category] of Object.entries(current)) { + for (const [settingName, setting] of Object.entries(category)) { + result[`${categoryName};${settingName}`] = setting.current; + } + } + return result; + } +} + +export type Setting = { + current: unknown; + readonly default: unknown; + readonly name: unknown; + readonly description: unknown; + readonly options: readonly unknown[]; +}; +export type SettingsRecord = typeof Settings["settings"] extends Writable< + infer X +> + ? X + : never; diff --git a/src/shark/SharkGame.ts b/src/shark/SharkGame.ts index 8825c08..1476904 100644 --- a/src/shark/SharkGame.ts +++ b/src/shark/SharkGame.ts @@ -1,8 +1,9 @@ import { Resources } from "./data/Resources"; import { Log } from "./Log"; +import { SaveHandler } from "./SaveHandler"; import { Settings } from "./Settings"; import { StaticClass } from "./StaticClass"; -import { Tabs } from "./Tabs"; +import { TabHandler } from "./TabHandler"; export class SharkGame extends StaticClass { static readonly #GAME_NAMES = [ @@ -58,19 +59,22 @@ export class SharkGame extends StaticClass { static readonly Settings = Settings; static readonly Log = Log; static readonly Resources = Resources; - static readonly Tabs = Tabs; + static readonly TabHandler = TabHandler; + static readonly SaveHandler = SaveHandler; static init(): void { - Settings.update((settings) => { + Settings.settings.update((settings) => { for (const settingsCategory of Object.values(settings)) { for (const setting of Object.values(settingsCategory)) { - setting.current = setting.defaultValue; + setting.current = setting.default; } } return settings; }); Log.init(); + SaveHandler.load(this); + SharkGame.title = SharkGame.#GAME_NAMES[ Math.floor(Math.random() * SharkGame.#GAME_NAMES.length) diff --git a/src/shark/Tabs.ts b/src/shark/TabHandler.ts similarity index 57% rename from src/shark/Tabs.ts rename to src/shark/TabHandler.ts index 508b138..0fc0bb0 100644 --- a/src/shark/Tabs.ts +++ b/src/shark/TabHandler.ts @@ -2,11 +2,11 @@ import Home from "../components/Tabs/Home.svelte"; import Lab from "../components/Tabs/Lab.svelte"; import { StaticClass } from "./StaticClass"; -export class Tabs extends StaticClass { +export class TabHandler extends StaticClass { static readonly AllTabs = { Home, Lab, } as const; - static currentTab: typeof Tabs.AllTabs[keyof typeof Tabs.AllTabs] = - Tabs.AllTabs.Home; + static currentTab: typeof TabHandler.AllTabs[keyof typeof TabHandler.AllTabs] = + TabHandler.AllTabs.Home; }