Adding query params support to Profectus

This is a patch you can apply to Profectus to add support for registering query parameters that will either call a function according to their value, or automatically update a ref (so long as that ref is a string, number, or boolean). It also registers /new as a path to automatically create a new save.

An example use case of this is Chromatic Lattice, a multiplayer incremental game where I want users to be able to link others to their board, so I can register a query param by doing this:

const currentBoard = ref<string | undefined>();
reqisterQueryParam("board", currentBoard);
watch(currentBoard, username => {
    if (username == null) return;

    // Make a network request to retrieve the board data for the given username
});

Here’s the patch to apply to get this feature:

patch
From 0cdc33d0bec6b2248f00e8bb62abc4b11625a448 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sun, 11 Feb 2024 13:40:10 -0600
Subject: [PATCH 1/2] Added /new and query param handlers

---
 src/game/routing.ts | 68 +++++++++++++++++++++++++++++++++++++++++++++
 src/main.ts         | 15 ++++++++--
 src/util/save.ts    |  5 +---
 3 files changed, 82 insertions(+), 6 deletions(-)
 create mode 100644 src/game/routing.ts

diff --git a/src/game/routing.ts b/src/game/routing.ts
new file mode 100644
index 0000000..693c1d6
--- /dev/null
+++ b/src/game/routing.ts
@@ -0,0 +1,68 @@
+import { globalBus } from "game/events";
+import { DecimalSource } from "util/bignum";
+import { Ref } from "vue";
+import player from "./player";
+
+// https://stackoverflow.com/questions/2090551/parse-query-string-in-javascript
+function parseQuery(queryString = window.location.search) {
+    const query: Record<string, string> = {};
+    const pairs = (queryString[0] === "?" ? queryString.substring(1) : queryString).split("&");
+    for (let i = 0; i < pairs.length; i++) {
+        const pair = pairs[i].split("=");
+        query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || "");
+    }
+    return query;
+}
+const params = parseQuery();
+
+/**
+ * Register a handler to be called when creating new saves based on a query param
+ * @param key The query param to regster
+ * @param handler The callback function when the query param is present
+ * @param newSavesOnly If set to true, only call the handler on the /new path
+ */
+export function registerQueryParam(
+    key: string,
+    handler: (value: string) => void,
+    newSavesOnly?: boolean
+): void;
+/**
+ * Register a ref to have its value set based on a query param
+ * @param key The query param to regster
+ * @param ref The ref to set the value of
+ * @param newSavesOnly If set to true, only overwrite values on the /new path
+ * @see {@link numberHandler}.
+ */
+export function registerQueryParam<T extends string | boolean | DecimalSource>(
+    key: string,
+    ref: Ref<T>,
+    newSavesOnly?: boolean
+): void;
+export function registerQueryParam<T extends string | boolean | DecimalSource>(
+    key: string,
+    handlerOrRef: ((value: string) => void) | Ref<T>,
+    newSavesOnly = false
+) {
+    globalBus.on("onLoad", () => {
+        if (newSavesOnly && player.timePlayed > 0) {
+            return;
+        }
+        if (key in params) {
+            if (typeof handlerOrRef === "function") {
+                handlerOrRef(params[key]);
+            } else {
+                if (typeof handlerOrRef.value === "boolean") {
+                    (handlerOrRef.value as boolean) = params[key].toLowerCase() === "true";
+                } else {
+                    (handlerOrRef.value as string | DecimalSource) = params[key];
+                }
+            }
+        }
+    });
+}
+
+export function numberHandler(ref: Ref<number>) {
+    return function (value: string) {
+        ref.value = parseFloat(value);
+    };
+}
diff --git a/src/main.ts b/src/main.ts
index 3b5de9f..f19eea7 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -3,7 +3,8 @@ import App from "App.vue";
 import projInfo from "data/projInfo.json";
 import "game/notifications";
 import state from "game/state";
-import { load } from "util/save";
+import { loadSettings } from "game/settings";
+import { load, loadSave, newSave } from "util/save";
 import { useRegisterSW } from "virtual:pwa-register/vue";
 import type { App as VueApp } from "vue";
 import { createApp, nextTick } from "vue";
@@ -60,7 +61,17 @@ requestAnimationFrame(async () => {
         "font-weight: bold; font-size: 24px; color: #A3BE8C; background: #2E3440; padding: 4px 8px; border-radius: 8px;",
         "padding: 4px;"
     );
-    await load();
+
+    // Load global settings
+    loadSettings();
+
+    if (window.location.pathname === "/new") {
+        await loadSave(newSave());
+    } else {
+        await load();
+    }
+    window.history.replaceState({}, document.title, "/");
+
     const { globalBus } = await import("./game/events");
     const { startGameLoop } = await import("./game/gameLoop");
 
diff --git a/src/util/save.ts b/src/util/save.ts
index 54e0e9b..7e8f78c 100644
--- a/src/util/save.ts
+++ b/src/util/save.ts
@@ -3,7 +3,7 @@ import projInfo from "data/projInfo.json";
 import { globalBus } from "game/events";
 import type { Player } from "game/player";
 import player, { stringifySave } from "game/player";
-import settings, { loadSettings } from "game/settings";
+import settings from "game/settings";
 import LZString from "lz-string";
 import { ref, shallowReactive } from "vue";
 
@@ -34,9 +34,6 @@ export function save(playerData?: Player): string {
 }
 
 export async function load(): Promise<void> {
-    // Load global settings
-    loadSettings();
-
     try {
         let save = localStorage.getItem(settings.active);
         if (save == null) {
-- 
2.45.2


From c21e949722a63b91018dbffda4d135806552d88c Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@incremental.social>
Date: Thu, 26 Dec 2024 16:38:45 +0000
Subject: [PATCH 2/2] Fix typo

---
 src/game/routing.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/game/routing.ts b/src/game/routing.ts
index 693c1d6..0d27c8b 100644
--- a/src/game/routing.ts
+++ b/src/game/routing.ts
@@ -28,7 +28,7 @@ export function registerQueryParam(
 ): void;
 /**
  * Register a ref to have its value set based on a query param
- * @param key The query param to regster
+ * @param key The query param to register
  * @param ref The ref to set the value of
  * @param newSavesOnly If set to true, only overwrite values on the /new path
  * @see {@link numberHandler}.
-- 
2.45.2