I made a grave mistake in not committing some of these things separately

But now we both have to live with this,
because I can't really be bothered to fix it

Summary:

+Save resources
*Restructure Handlers
-Remove StaticClass
+Add Resource data files
~Change loading
This commit is contained in:
Tobias Berger 2021-11-03 12:42:37 +01:00
parent 69932a28f7
commit 0c107bc849
25 changed files with 368 additions and 302 deletions

View file

@ -20,4 +20,7 @@ module.exports = {
es2021: true,
node: false,
},
rules: {
"@typescript-eslint/no-unused-vars": ["warn", { args: "all", argsIgnorePattern: "^_", ignoreRestSiblings: false }],
},
};

View file

@ -14,7 +14,7 @@
onMount(() => {
root = document.documentElement;
SharkGame.Settings.settings.subscribe((settings) => {
SharkGame.SettingsHandler.settings.subscribe((settings) => {
root.classList.toggle("no-theme", !settings.appearance.enableThemes.current);
settings.appearance.theme.options.forEach((theme) => {
root.classList.toggle(theme, theme === settings.appearance.theme.current);
@ -33,12 +33,6 @@
}
function handleBeforeUnload(event: BeforeUnloadEvent) {
console.debug(
"beforeUnload",
Date.now() - SharkGame.SaveHandler.lastSaved >= 60 * 1000,
Date.now() - SharkGame.SaveHandler.lastSaved,
60 * 1000
);
// If last save is over a minute old
if (Date.now() - SharkGame.SaveHandler.lastSaved >= 60 * 1000) {
// Annotyingly, the standardized way isn't supported, so both outdated ones will have to suffice

View file

@ -16,7 +16,7 @@
return game.SaveHandler.save(game);
},
settings() {
openModal(SettingsModal, { settings: game.Settings.settings }, { replace: true });
openModal(SettingsModal, { settings: game.SettingsHandler.settings }, { replace: true });
},
help() {
openModal(HelpModal, { discordLink }, { replace: true });

View file

@ -6,8 +6,8 @@
import type { Writable } from "svelte/store";
import { slide } from "svelte/transition";
import type { AddMessageEvent, ResetLogEvent, Message } from "../shark/Message";
import { MessageType } from "../shark/Message";
import type { AddMessageEvent, ResetLogEvent, Message } from "../shark/helperTypes/Message";
import { MessageType } from "../shark/helperTypes/Message";
export let messages: Writable<Message[]>;
export let logLength: number;

View file

@ -12,7 +12,7 @@
<ConfirmModal
{isOpen}
confirm={() => {
game.SaveHandler.reset();
game.SaveHandler.resetSave();
}}
deny={closeAllModals}>Do you want to reset your save?</ConfirmModal
>

View file

@ -1,11 +1,11 @@
<script lang="ts">
import type { Settings } from "../../shark/Settings";
import type { SettingsHandler } from "../../shark/handlers/SettingsHandler";
import BaseModal from "./Base/Modal.svelte";
export let isOpen: boolean;
export let settings: typeof Settings["settings"];
export let settings: typeof SettingsHandler["settings"];
</script>
<BaseModal {isOpen}>

View file

@ -1,11 +1,11 @@
<svelte:options immutable />
<script lang="ts">
import type { Resource } from "../../shark/data/Resources";
import type { Resource } from "../../shark/helperTypes/Resource";
export let categoryName: string;
export let collapsed: boolean;
export let resources: [string, Resource][];
export let resources: Resource[];
</script>
<tr on:click class="subhead" tabindex="0" role="button">
@ -13,9 +13,9 @@
<td colspan="3"><h4>{categoryName}</h4> </td>
</tr>
{#if !collapsed}
{#each resources as [resourceName, resource], index}
<tr class:even={index % 2 === 0} class:odd={index % 2 === 1}>
<td colspan="2">{resourceName}</td>
{#each resources as resource, index}
<tr style="--resource-color: {resource.color}" class:even={index % 2 === 0} class:odd={index % 2 === 1}>
<td colspan="2">{resource.humanName}</td>
<td>{resource.amount}</td>
<td>{Math.round(100 * (resource.change + Number.EPSILON)) / 100}/s</td>
</tr>
@ -30,8 +30,12 @@
background-color: var(--color-lighter);
}
text-shadow: -1px -1px 2px var(--color-med), 1px -1px 2px var(--color-med), -1px 1px 2px var(--color-med),
1px 1px 2px var(--color-med);
> td:first-child {
text-align: left;
color: var(--resource-color);
}
> td {
padding: 2px 5px;

View file

@ -3,7 +3,7 @@
<script lang="ts">
import type { Writable } from "svelte/store";
import type { Resource } from "../../shark/data/Resources";
import type { Resource } from "../../shark/helperTypes/Resource";
import ResourceGroup from "./ResourceGroup.svelte";
let collapsed: string[] = [];
@ -15,16 +15,14 @@
});
function resourceGroups(resources: Record<string, Resource>) {
return Object.entries(
Object.entries(resources).reduce((reduced, [resourceName, resource]) => {
if (reduced[resource.category] === undefined) {
reduced[resource.category] = [];
const result: Record<string, Resource[]> = {};
for (const resource of Object.values(resources)) {
if (result[resource.category] === undefined) {
result[resource.category] = [];
}
reduced[resource.category].push([resourceName, resource]);
return reduced;
}, {} as Record<string, [string, Resource][]>)
);
result[resource.category].push(resource);
}
return Object.entries(result);
}
function toggleCollapsed(categoryName: string) {

View file

@ -2,24 +2,34 @@
import { createEventDispatcher } from "svelte";
import type { HomeAction, HomeActions } from "../../shark/data/HomeActions";
import type { AddMessageEvent } from "../../shark/Message";
import { Resources } from "../../shark/data/Resources";
import type { ResourceHandler } from "../../shark/handlers/ResourceHandler";
import { AddMessageEvent, MessageType } from "../../shark/helperTypes/Message";
export let homeActions: ReturnType<typeof HomeActions["getActionTable"]>;
export let resourceHandler: typeof ResourceHandler;
export let showIcons: boolean;
const dispatch = createEventDispatcher<{ addMessage: AddMessageEvent }>();
function homeActionClick(homeAction: HomeAction) {
if (homeAction.effect.resource) {
const resources = Resources.getResources(Object.keys(homeAction.effect.resource));
if (resources !== null) {
for (const [resourceName, resource] of Object.entries(resources)) {
resource.amount += homeAction.effect.resource[resourceName];
if (homeAction.effect.addResources !== undefined) {
const addResources = homeAction.effect.addResources;
// Don't use ResourceHandlers's increaseResource method
// it would call update once per resource instead of once total
resourceHandler.allResources.update((allResources) => {
for (const [resourceId, delta] of Object.entries(addResources)) {
if (resourceId in addResources) {
if (!(resourceId in allResources)) {
dispatch("addMessage", {
message: `Unknown resourceId '${resourceId}'`,
messageType: MessageType.error,
});
}
allResources[resourceId].amount += delta;
}
}
return allResources;
});
}
if (homeAction.outcomes) {

View file

@ -1,5 +1,5 @@
<script lang="ts">
import type { AddMessageEvent } from "../shark/Message";
import type { AddMessageEvent } from "../shark/helperTypes/Message";
import type { SharkGame } from "../shark/SharkGame";
import Log from "./Log.svelte";
import ResourceTable from "./ResourceTable/ResourceTable.svelte";
@ -21,7 +21,7 @@
let logLength: number;
let showIcons: boolean;
game.Settings.settings.subscribe((settings) => {
game.SettingsHandler.settings.subscribe((settings) => {
logLength = settings.layout.logLength.current;
showIcons = settings.appearance.showIcons.current;
});
@ -29,7 +29,7 @@
<main>
<div id="left-column" class:expanded={sidebarExpanded} class:collapsed={!sidebarExpanded}>
<ResourceTable resources={game.Resources.Resources} />
<ResourceTable resources={game.ResourceHandler.allResources} />
<Log
{logLength}
messages={game.MessageHandler.messages}
@ -63,6 +63,7 @@
on:addMessage={handleAddMessage}
{showIcons}
homeActions={game.Data.HomeActions.getActionTable()}
resourceHandler={game.ResourceHandler}
/>
{:else if game.TabHandler.currentTab === game.TabHandler.AllTabs.Lab}
<svelte:component this={game.TabHandler.AllTabs.Lab} />

View file

@ -1,11 +1,9 @@
import { StaticClass } from "./StaticClass";
export const LZString = new (class LZString {
readonly #KeyStrBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
export class LZString extends StaticClass {
static readonly #KeyStrBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
readonly #KeyStrBase64Dict = this.#createBaseDict(this.#KeyStrBase64);
static readonly #KeyStrBase64Dict = LZString.#createBaseDict(LZString.#KeyStrBase64);
static #createBaseDict(alphabet: string): Record<string, number> {
#createBaseDict(alphabet: string): Record<string, number> {
const dict: Record<string, number> = {};
for (let i = 0; i < alphabet.length; i++) {
dict[alphabet[i]] = i;
@ -13,21 +11,21 @@ export class LZString extends StaticClass {
return dict;
}
static compressToBase64(input: string): string {
const result = LZString.#compress(input, 6, (a) => LZString.#KeyStrBase64.charAt(a));
compressToBase64(input: string): string {
const result = this.#compress(input, 6, (a) => this.#KeyStrBase64.charAt(a));
return result + "=".repeat((4 - (result.length % 4)) % 4);
}
static decompressFromBase64(input: string | null): string | null {
decompressFromBase64(input: string | null): string | null {
if (input === null || input === "") return null;
return LZString.#decompress(input.length, 32, (index) => LZString.#KeyStrBase64Dict[input.charAt(index)]);
return this.#decompress(input.length, 32, (index) => this.#KeyStrBase64Dict[input.charAt(index)]);
}
static compress(uncompressed: string): string {
return LZString.#compress(uncompressed);
compress(uncompressed: string): string {
return this.#compress(uncompressed);
}
static #compress(uncompressed: string): string;
static #compress(uncompressed: string, bitsPerChar: number, getCharFromInt: (index: number) => string): string;
static #compress(
#compress(uncompressed: string): string;
#compress(uncompressed: string, bitsPerChar: number, getCharFromInt: (index: number) => string): string;
#compress(
uncompressed: string,
bitsPerChar = 16,
getCharFromNumber = (code: number) => String.fromCharCode(code)
@ -240,11 +238,11 @@ export class LZString extends StaticClass {
return contextData;
}
static decompress(compressed: string | null): string | null {
decompress(compressed: string | null): string | null {
if (compressed === null || compressed === "") return null;
return LZString.#decompress(compressed.length, 32768, (index) => compressed.charCodeAt(index));
return this.#decompress(compressed.length, 32768, (index) => compressed.charCodeAt(index));
}
static #decompress(length: number, resetValue: number, getNextValue: (index: number) => number): string | null {
#decompress(length: number, resetValue: number, getNextValue: (index: number) => number): string | null {
const dictionary: (number | string)[] = [];
let enlargeIn = 4;
@ -411,6 +409,6 @@ export class LZString extends StaticClass {
}
}
}
}
})();
window.LZString = LZString;

View file

@ -1,65 +0,0 @@
// 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 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,
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"] as const,
},
showIcons: {
current: true,
default: true as const,
name: "Show icons" as const,
description: "Whether to show decorative icons in various places" as const,
options: [true, false] as const,
},
},
behavior: {
autoSave: {
current: 1 as "Off" | number,
default: 1 as const,
name: "Autosave" as const,
description: "How many seconds to wait between each autosave" as const,
options: ["Off", 1, 5, 10, 15, 60] as const,
},
},
};
static readonly settings = writable(Settings.#settings);
static getSettings(): SettingsRecord {
return Object.seal(Object.assign({}, Settings.#settings));
}
}
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;

View file

@ -1,14 +1,15 @@
import { Resources } from "./data/Resources";
import { ResourceHandler } from "./handlers/ResourceHandler";
import { MessageHandler } from "./handlers/MessageHandler";
import { SaveHandler } from "./handlers/SaveHandler";
import { Settings } from "./Settings";
import { StaticClass } from "./StaticClass";
import { TabHandler } from "./handlers/TabHandler";
import { SettingsHandler } from "./handlers/SettingsHandler";
import { HomeActions } from "./data/HomeActions";
import { ResourceData } from "./data/Resources";
import { SettingsData } from "./data/Settings";
export class SharkGame extends StaticClass {
static readonly #GAME_NAMES = [
export const SharkGame = new (class SharkGame {
readonly #GAME_NAMES = [
"Five Seconds A Shark",
"Next Shark Game",
"Next Shark Game: Barkfest",
@ -57,28 +58,35 @@ export class SharkGame extends StaticClass {
"what the crab doin",
] as const;
static title: string;
static readonly Settings = Settings;
static readonly MessageHandler = MessageHandler;
static readonly Resources = Resources;
static readonly TabHandler = TabHandler;
static readonly SaveHandler = SaveHandler;
static readonly Data = Object.seal({
title: string = this.#GAME_NAMES[Math.floor(Math.random() * this.#GAME_NAMES.length)];
readonly SettingsHandler = SettingsHandler;
readonly MessageHandler = MessageHandler;
readonly ResourceHandler = ResourceHandler;
readonly TabHandler = TabHandler;
readonly SaveHandler = SaveHandler;
readonly Data = Object.seal({
HomeActions,
ResourceData,
SettingsData,
});
static async initialize(): Promise<void> {
async initialize(): Promise<void> {
MessageHandler.initialize(this);
SaveHandler.reset();
TabHandler.reset();
ResourceHandler.reset();
SettingsHandler.reset();
MessageHandler.reset();
await SaveHandler.load(this);
SharkGame.title = SharkGame.#GAME_NAMES[Math.floor(Math.random() * SharkGame.#GAME_NAMES.length)];
MessageHandler.init();
SaveHandler.init(this);
SaveHandler.initialize(this);
}
static save(): Promise<void> {
save(): Promise<void> {
return SaveHandler.save(this);
}
}
})();
window.SharkGame = SharkGame;

View file

@ -1,7 +0,0 @@
export class StaticClass {
constructor() {
if (this instanceof StaticClass) {
throw new Error("A static class cannot be instantiated");
}
}
}

View file

@ -1,16 +1,14 @@
import { StaticClass } from "../StaticClass";
export type HomeAction = {
name: string;
effect: {
resource: Record<string, number>;
};
effect: Partial<{
addResources: Record<string, number>;
}>;
outcomes: string[];
multiOutcomes?: string[];
image?: string;
};
export class HomeActions extends StaticClass {
export class HomeActions {
static getActionTable(): Record<string, HomeAction> {
return HomeActions.actionTable;
}
@ -20,7 +18,7 @@ export class HomeActions extends StaticClass {
image: "catchFish.webp",
name: "Steal fish",
effect: {
resource: {
addResources: {
fish: 1,
},
},
@ -71,7 +69,7 @@ export class HomeActions extends StaticClass {
image: "getShark.webp",
name: "Kidnap shark",
effect: {
resource: {
addResources: {
shark: 1,
},
},
@ -81,7 +79,7 @@ export class HomeActions extends StaticClass {
image: "getManta.webp",
name: "Summon ray",
effect: {
resource: {
addResources: {
get ray() {
return Math.random() < 0.01 ? 1e9 : 1;
},

View file

@ -1,68 +1,23 @@
import { writable } from "svelte/store";
import { StaticClass } from "../StaticClass";
export class Resource {
export type ResourceRaw = {
color: string;
humanName: string;
category: string;
amount: number;
change: number;
};
constructor(category: string) {
this.category = category;
this.amount = 0;
this.change = 0;
}
}
export class Resources extends StaticClass {
static readonly Resources = writable<Record<string, Resource>>({
shark: new Resource("Frenzy"),
ray: new Resource("Frenzy"),
fish: new Resource("Animals"),
});
static createResource(name: string, category: string): void {
Resources.Resources.update((res) => {
res[name] = new Resource(category);
return res;
});
}
static setResource(resourceName: string, amount: number): void {
Resources.Resources.update((res) => {
res[resourceName].amount = amount;
return res;
});
}
static getResource(resourceName: string): Resource | null {
let result: null | Resource = null;
Resources.Resources.update((res) => {
if (Object.hasOwnProperty.call(res, resourceName)) result = res[resourceName];
return res;
});
return result;
}
static getResources(resourceNames: string[]): Record<string, Resource> | null;
static getResources(...resourceNames: string[]): Record<string, Resource> | null;
static getResources(...resourceNames: string[] | string[][]): Record<string, Resource> | null {
if (Array.isArray(resourceNames[0])) {
resourceNames = resourceNames[0];
}
let result: null | [string, Resource][] = null;
Resources.Resources.update((res) => {
for (const resourceName of resourceNames as string[]) {
if (Object.hasOwnProperty.call(res, resourceName)) {
if (result === null) {
result = [];
}
result.push([resourceName, res[resourceName]]);
}
}
return res;
});
return result === null ? null : Object.fromEntries(result);
}
}
export const ResourceData = {
fish: {
color: "yellow",
humanName: "fish",
category: "animals",
},
shark: {
color: "#92C1E0",
humanName: "shark",
category: "frenzy",
},
ray: {
color: "#8080FF",
humanName: "ray",
category: "frenzy",
},
} as const; // as Record<string, ResourceRaw>

View file

@ -0,0 +1,53 @@
// How to make current just be undefined here, but keep the type of default?
export type Setting = {
current: unknown;
readonly default: unknown;
readonly name: unknown;
readonly description: unknown;
readonly options: readonly unknown[];
};
export const SettingsData = {
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,
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"] as const,
},
showIcons: {
current: true,
default: true as const,
name: "Show icons" as const,
description: "Whether to show decorative icons in various places" as const,
options: [true, false] as const,
},
},
behavior: {
autoSave: {
current: 1 as "Off" | 1 | 5 | 10 | 15 | 60,
default: 1 as const,
name: "Autosave" as const,
description: "How many seconds to wait between each autosave" as const,
options: ["Off", 1, 5, 10, 15, 60] as const,
},
},
};

View file

@ -0,0 +1,3 @@
export interface BaseHandler {
reset(): void;
}

View file

@ -1,27 +1,25 @@
import { writable } from "svelte/store";
import { get, writable } from "svelte/store";
import { Message, MessageType } from "../Message";
import { Settings } from "../Settings";
import { StaticClass } from "../StaticClass";
import { Message, MessageType } from "../helperTypes/Message";
import type { SharkGame } from "../SharkGame";
import type { BaseHandler } from "./BaseHandler";
export class MessageHandler extends StaticClass {
static messages = writable<Message[]>([]);
static #maxLogLength: number;
export const MessageHandler = new (class MessageHandler implements BaseHandler {
readonly messages = writable<Message[]>([]);
#maxLogLength = Infinity;
static init(): void {
Settings.settings.subscribe((settings) => {
MessageHandler.#maxLogLength = Math.max(...settings.layout.logLength.options);
})();
initialize(game: typeof SharkGame): void {
this.#maxLogLength = Math.max(...get(game.SettingsHandler.settings).layout.logLength.options);
}
static addMessage(message: string, messageType: MessageType = MessageType.message): void {
addMessage(message: string, messageType: MessageType = MessageType.message): void {
this.messages.update((oldMessages) =>
[...oldMessages, new Message(message, messageType)].slice(-MessageHandler.#maxLogLength)
[...oldMessages, new Message(message, messageType)].slice(-this.#maxLogLength)
);
}
static reset(): void {
reset(): void {
Message.even = true;
this.messages.set([]);
}
}
})();

View file

@ -0,0 +1,32 @@
import { writable } from "svelte/store";
import { ResourceData } from "../data/Resources";
import { Resource } from "../helperTypes/Resource";
import type { BaseHandler } from "./BaseHandler";
type ResourceMap = Record<string, Resource>;
export const ResourceHandler = new (class ResourceHandler implements BaseHandler {
allResources = writable<ResourceMap>({});
reset() {
this.allResources.update((allResources) => {
for (const [resourceId, resourceRaw] of Object.entries(ResourceData)) {
allResources[resourceId] = new Resource(resourceRaw);
}
return allResources;
});
}
increaseResource(resourceId: string, amount: number) {
this.allResources.update((resources) => {
if (!(resourceId in resources)) {
throw new Error(`Unknown resourceId '${resourceId}'`);
}
resources[resourceId].amount += amount;
return resources;
});
}
})();

View file

@ -1,9 +1,18 @@
import type { Message, MessageType } from "../Message";
import type { SharkGame } from "../SharkGame";
import { StaticClass } from "../StaticClass";
import type { TabHandler } from "./TabHandler";
import type { BaseHandler } from "./BaseHandler";
import { SettingsHandler } from "./SettingsHandler";
import { get } from "svelte/store";
import { Resource } from "../helperTypes/Resource";
import { Message, MessageType } from "../helperTypes/Message";
import { ResourceData } from "../data/Resources";
import { LZString } from "../LZString";
import { Settings } from "../Settings";
import type { SettingsData } from "../data/Settings";
const __EMPTY_OBJECT = {};
type Version0Save = typeof __EMPTY_OBJECT;
@ -15,46 +24,50 @@ type Version1Save = Version0Save & {
}[];
selectedTab: keyof typeof TabHandler["AllTabs"];
settings: Record<string, Record<string, unknown>>;
resources: Record<string, { amount: number; total: number }>;
};
type CurrentVersionSave = Version1Save;
type Save = Version0Save | Version1Save;
export class SaveHandler extends StaticClass {
static readonly saveName = "sharg-save";
static #lastSaved = Date.now();
static get lastSaved(): number {
return SaveHandler.#lastSaved;
export const SaveHandler = new (class SaveHandler implements BaseHandler {
readonly saveName = "sharg-save";
#lastSaved = Date.now();
get lastSaved(): number {
return this.#lastSaved;
}
private static set lastSaved(val) {
SaveHandler.#lastSaved = val;
private set lastSaved(val) {
this.#lastSaved = val;
}
static #saveInterval: ReturnType<typeof setInterval> | undefined = undefined;
#saveInterval: ReturnType<typeof setInterval> | undefined = undefined;
static init(game: typeof SharkGame): void {
Settings.settings.subscribe((settings) => {
if (SaveHandler.#saveInterval !== undefined) {
clearInterval(SaveHandler.#saveInterval);
}
initialize(game: typeof SharkGame) {
let previousSetting: typeof SettingsData["behavior"]["autoSave"]["options"][number];
SettingsHandler.settings.subscribe((settings) => {
const currentSetting = settings.behavior.autoSave.current;
if (previousSetting !== currentSetting) {
this.reset();
if (settings.behavior.autoSave.current !== "Off") {
SaveHandler.#saveInterval = setInterval(
() => SaveHandler.save(game),
settings.behavior.autoSave.current * 1000
);
this.#saveInterval = setInterval(() => this.save(game), settings.behavior.autoSave.current * 1000);
}
}
previousSetting = currentSetting;
});
}
static async save(game: typeof SharkGame): Promise<void> {
reset(): void {
if (this.#saveInterval !== undefined) {
clearInterval(this.#saveInterval);
}
}
async save(game: typeof SharkGame): Promise<void> {
console.debug("Saving");
console.time("Done saving");
const messages = await new Promise<Message[]>((resolve) => {
game.MessageHandler.messages.subscribe((messages) => {
resolve(messages);
})();
});
const messages = get(game.MessageHandler.messages);
const resources = get(game.ResourceHandler.allResources);
// Save tab
const allTabsEntries = Object.entries(game.TabHandler.AllTabs) as unknown as [
keyof typeof TabHandler["AllTabs"],
typeof TabHandler["AllTabs"][keyof typeof TabHandler["AllTabs"]]
@ -62,7 +75,8 @@ export class SaveHandler extends StaticClass {
const selectedTabIndex = allTabsEntries.findIndex(([, tab]) => tab === game.TabHandler.currentTab);
const selectedTabName = allTabsEntries[selectedTabIndex][0];
const currentSettings = game.Settings.getSettings();
// Save settings
const currentSettings = get(game.SettingsHandler.settings);
const saveSettings: CurrentVersionSave["settings"] = {};
for (const [categoryId, categorySettings] of Object.entries(currentSettings)) {
const categorySave: CurrentVersionSave["settings"][string] = {};
@ -76,16 +90,25 @@ export class SaveHandler extends StaticClass {
}
}
const save: CurrentVersionSave = {
version: 1,
selectedTab: selectedTabName,
settings: saveSettings,
messages: messages.map((message) => {
return {
// Save resources
const saveResources: CurrentVersionSave["resources"] = {};
for (const [resourceId, resource] of Object.entries(resources)) {
if (resource.amount > 0 || resource.total > 0)
saveResources[resourceId] = { amount: resource.amount, total: resource.total };
}
// Save messages
const saveMessages: CurrentVersionSave["messages"] = messages.map((message) => ({
content: message.content,
type: message.type,
};
}),
}));
const save: CurrentVersionSave = {
version: 1,
messages: saveMessages,
selectedTab: selectedTabName,
settings: saveSettings,
resources: saveResources,
};
const stringifiedSave = JSON.stringify(save);
const encodedSave = LZString.compressToBase64(stringifiedSave);
@ -95,22 +118,22 @@ export class SaveHandler extends StaticClass {
}% size`
);
localStorage.setItem(SaveHandler.saveName, encodedSave);
SaveHandler.#lastSaved = Date.now();
localStorage.setItem(this.saveName, encodedSave);
this.#lastSaved = Date.now();
console.timeEnd("Done saving");
}
static async load(game: typeof SharkGame): Promise<void> {
async load(game: typeof SharkGame): Promise<void> {
console.debug("Loading");
console.time("Done loading");
const localSave = LZString.decompressFromBase64(localStorage.getItem(SaveHandler.saveName));
const localSave = LZString.decompressFromBase64(localStorage.getItem(this.saveName));
const loadedSave = JSON.parse(localSave ?? "{}");
const saveVersion = loadedSave.version ?? 0;
const migrators = SaveHandler.migrators.slice(saveVersion);
const migrators = this.migrators.slice(saveVersion);
if (migrators.length > 0 && localSave !== null) {
localStorage.setItem(SaveHandler.saveName + "-backup", localSave);
localStorage.setItem(this.saveName + "-backup", localSave);
}
let save = loadedSave;
@ -121,44 +144,64 @@ export class SaveHandler extends StaticClass {
const fullSave = save as CurrentVersionSave;
game.Settings.settings.update((settings) => {
Object.entries(settings).forEach(([categoryName, categorySettings]) => {
Object.entries(categorySettings).forEach(([settingId, setting]) => {
// Load settings
game.SettingsHandler.reset();
game.SettingsHandler.settings.update((settings) => {
for (const [categoryName, categorySettings] of Object.entries(settings)) {
for (const [settingId, setting] of Object.entries(categorySettings)) {
setting.current = fullSave.settings[categoryName]?.[settingId] ?? setting.default;
});
});
}
}
return settings;
});
game.MessageHandler.reset();
// Load resources
game.ResourceHandler.reset();
game.ResourceHandler.allResources.update((allResources) => {
for (const [resourceId, resourceInfo] of Object.entries(fullSave.resources)) {
if (resourceId in ResourceData) {
const resource = new Resource(ResourceData[resourceId as keyof typeof ResourceData]);
fullSave.messages.forEach((message) => {
game.MessageHandler.addMessage(message.content, message.type);
resource.amount = resourceInfo.amount;
resource.total = resourceInfo.total;
allResources[resourceId] = resource;
}
}
return allResources;
});
// Load messages
game.MessageHandler.reset();
game.MessageHandler.messages.set(
fullSave.messages.map((saveMessage) => new Message(saveMessage.content, saveMessage.type))
);
game.TabHandler.currentTab = game.TabHandler.AllTabs[fullSave.selectedTab];
console.timeEnd("Done loading");
}
static reset(): void {
const localSave = localStorage.getItem(SaveHandler.saveName);
resetSave(): void {
const localSave = localStorage.getItem(this.saveName);
if (localSave !== null) {
localStorage.setItem(SaveHandler.saveName + "-backup", localSave);
localStorage.setItem(this.saveName + "-backup", localSave);
}
localStorage.removeItem(SaveHandler.saveName);
localStorage.removeItem(this.saveName);
window.location.reload();
}
static migrators: ((save: Save) => Save)[] = [
migrators: ((save: Save) => Save)[] = [
(): Version1Save => {
const newSave = {
version: 1 as const,
messages: [],
selectedTab: "Home" as const,
settings: {},
resources: {},
};
return newSave;
},
];
}
})();

View file

@ -0,0 +1,19 @@
import { writable } from "svelte/store";
import type { Writable } from "svelte/store";
import type { BaseHandler } from "./BaseHandler";
import { SettingsData } from "../data/Settings";
export const SettingsHandler = new (class Settings implements BaseHandler {
readonly settings = writable(SettingsData);
reset() {
for (const category of Object.values(SettingsData)) {
for (const setting of Object.values(category)) {
setting.current = setting.default;
}
}
this.settings.set(SettingsData);
}
})();
export type SettingsRecord = typeof SettingsHandler["settings"] extends Writable<infer X> ? X : never;

View file

@ -1,11 +1,15 @@
import { StaticClass } from "../StaticClass";
import Home from "../../components/Tabs/Home.svelte";
import Lab from "../../components/Tabs/Lab.svelte";
import type { BaseHandler } from "./BaseHandler";
export class TabHandler extends StaticClass {
static readonly AllTabs = {
export const TabHandler = new (class TabHandler implements BaseHandler {
reset() {
this.currentTab = this.AllTabs.Home;
}
readonly AllTabs = {
Home,
Lab,
} as const;
static currentTab: typeof TabHandler.AllTabs[keyof typeof TabHandler.AllTabs] = TabHandler.AllTabs.Home;
}
currentTab: typeof this.AllTabs[keyof typeof this.AllTabs] = this.AllTabs.Home;
})();

View file

@ -0,0 +1,17 @@
import type { ResourceRaw } from "../data/Resources";
export class Resource implements ResourceRaw {
humanName: string;
category: string;
color: string;
amount = 0;
total = 0;
change = 0;
constructor(raw: ResourceRaw) {
this.humanName = raw.humanName;
this.category = raw.category;
this.color = raw.color;
}
}