Browse Source

Allow loading the editor's config in the game directly

Fixes https://github.com/thatsmydoing/typingfreaks/issues/20
Thomas Dy 2 years ago
parent
commit
a9df1d4def
6 changed files with 95 additions and 9 deletions
  1. 6 0
      README.md
  2. 2 2
      src/game.ts
  3. 28 6
      src/game/loading.ts
  4. 5 1
      src/index.ts
  5. 9 0
      src/level.ts
  6. 45 0
      src/util.ts

+ 6 - 0
README.md

@@ -89,6 +89,11 @@ incompatibilities.
 There is an [online editor][3] available. It lets you create a full config from
 scratch and then download the JSON file.
 
+For fast testing, it's also possible to go to try out the editor's config in the
+game by appending `?from=editor` to the game URL. For the online editor, that
+would be [here][4]. Note that this is all in browser, sharing the link with
+someone else will simply error out.
+
 ## Build Instructions
 
 The project is a vanilla typescript project. Simply run:
@@ -101,3 +106,4 @@ npm run build
 [1]: https://github.com/innocenat/typingmania
 [2]: https://github.com/thatsmydoing/typingfreaks/releases/latest
 [3]: https://typingfreaks.pleasantprogrammer.com/editor.html
+[4]: https://typingfreaks.pleasantprogrammer.com/?from=editor

+ 2 - 2
src/game.ts

@@ -9,7 +9,7 @@ import { LoadingScreen } from './game/loading';
 export class MainController extends ScreenManager {
   loadingScreen: Screen;
 
-  constructor(container: HTMLElement, configUrl: string) {
+  constructor(container: HTMLElement, configUrl: string, fromEditor: boolean) {
     super(container);
     container.appendChild(util.loadTemplate(container, 'base'));
 
@@ -27,7 +27,7 @@ export class MainController extends ScreenManager {
       },
     };
 
-    this.loadingScreen = new LoadingScreen(gameContext, configUrl);
+    this.loadingScreen = new LoadingScreen(gameContext, configUrl, fromEditor);
 
     document.addEventListener('keydown', (event) => {
       if (event.key === 'Tab') {

+ 28 - 6
src/game/loading.ts

@@ -7,20 +7,42 @@ import { SelectScreen } from './select';
 export class LoadingScreen implements Screen {
   readonly name: string = 'loading';
 
-  constructor(private context: GameContext, private configUrl: string) {}
+  constructor(
+    private context: GameContext,
+    private configUrl: string,
+    private fromEditor: boolean
+  ) {}
 
   enter(): void {
     console.log('Loading assets...');
-    let configPromise;
+    let configPromise: Promise<level.Config>;
     if (this.configUrl.endsWith('.json')) {
       configPromise = level.loadFromJson(this.configUrl);
     } else {
       configPromise = level.loadFromTM(this.configUrl);
     }
-    configPromise.then((config) => {
-      this.context.config = config;
-      this.loadAssets();
-    });
+    let editorConfigPromise: Promise<level.Config | null>;
+    if (this.fromEditor) {
+      editorConfigPromise = level.loadFromLocalStorage();
+    } else {
+      editorConfigPromise = Promise.resolve(null);
+    }
+
+    Promise.all([configPromise, editorConfigPromise]).then(
+      ([config, editorConfig]) => {
+        if (editorConfig !== null) {
+          console.log('Using editor levels');
+          const [result, context] = util.deepEqual(config, editorConfig);
+          if (!result) {
+            console.log(`Editor levels differ: ${context}`);
+          }
+          this.context.config = editorConfig;
+        } else {
+          this.context.config = config;
+        }
+        this.loadAssets();
+      }
+    );
   }
 
   loadAssets(): void {

+ 5 - 1
src/index.ts

@@ -1,6 +1,10 @@
 import { MainController } from './game';
 
+const query = new URLSearchParams(location.search);
+const fromEditor = query.get('from') === 'editor';
+
 new MainController(
   document.querySelector('#container')!,
-  document.querySelector('#levels')?.textContent!
+  document.querySelector('#levels')?.textContent!,
+  fromEditor
 ).start();

+ 9 - 0
src/level.ts

@@ -40,6 +40,15 @@ export interface Config {
   levelSets: LevelSet[];
 }
 
+export async function loadFromLocalStorage(): Promise<Config> {
+  const text = localStorage.getItem('LEVELS_JSON');
+  if (text === null) {
+    throw new Error('No LEVELS_JSON in local storage');
+  } else {
+    return JSON.parse(text);
+  }
+}
+
 export async function loadFromJson(url: string): Promise<Config> {
   const response = await window.fetch(url);
   return await response.json();

+ 45 - 0
src/util.ts

@@ -139,3 +139,48 @@ export function makeDeferred(): Deferred {
     resolve: resolve!,
   };
 }
+
+export function deepEqual(
+  a: unknown,
+  b: unknown,
+  context: string = ''
+): [boolean, string] {
+  if (a === b) {
+    return [true, context];
+  }
+  if (a === null || b === null || a === undefined || b === undefined) {
+    return [false, context];
+  }
+  if (typeof a !== typeof b) {
+    return [false, context];
+  }
+  if (Array.isArray(a) && Array.isArray(b)) {
+    if (a.length !== b.length) {
+      return [false, context];
+    }
+    for (let i = 0; i < a.length; ++i) {
+      const result = deepEqual(a[i], b[i], `${context}/${i}`);
+      if (!result[0]) {
+        return result;
+      }
+    }
+    return [true, context];
+  }
+  if (typeof a === 'object' && typeof b === 'object') {
+    const keys = Object.keys(a!);
+    for (const key of keys) {
+      // @ts-ignore
+      const result = deepEqual(a[key], b[key], `${context}/${key}`);
+      if (!result[0]) {
+        return result;
+      }
+    }
+    for (const key of Object.keys(b!)) {
+      if (!keys.includes(key)) {
+        return [false, `${context}/${key}`];
+      }
+    }
+    return [true, context];
+  }
+  return [a === b, context];
+}