Make reload capture changes to html presentation while still restoring state, adds more documentation

Chris Pollett [2023-01-11 01:Jan:th]
Make reload capture changes to html presentation while still restoring state, adds more documentation
Filename
game.js
diff --git a/game.js b/game.js
index 31c5361..ce8be7c 100644
--- a/game.js
+++ b/game.js
@@ -1,5 +1,5 @@
 /**
- * Copyright 2022 Christopher Pollett
+ * Copyright 2022-2023 Christopher Pollett
  *
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -67,7 +67,10 @@ function xsel(selector)
     return makeGameObjects(tag_objects);
 }
 /**
- *
+ * Returns an array of game objects based on the elements in the current
+ * document of the given tag name.
+ * @param {string} a name of a tag such as x-location or x-object.
+ * @return {Array} of game objects based on the tags that matched the selector
  */
 function xtag(name)
 {
@@ -75,10 +78,17 @@ function xtag(name)
     return makeGameObjects(tag_objects);
 }
 /**
+ * Sleep time many milliseconds before continuing to execute whatever code was
+ * executing. This function returns a Promise so needs to be executed with await
+ * so that the code following the sleep statement will be used to resolve the
+ * Promise
  *
+ * @param {number} time time in milliseconds to sleep for
+ * @return {Promise} a promise whose resolve callback is to be executed after
+ *  that many milliseconds.
  */
-function sleep(ms) {
-  return new Promise(resolve => setTimeout(resolve, ms));
+function sleep(time) {
+  return new Promise(resolve => setTimeout(resolve, time));
 }
 /**
  *
@@ -315,13 +325,25 @@ function interpolateVariables(text)
     return text;
 }
 /**
- *
+ * Encapsulates one place that objects can be in a Game.
  */
 class Location
 {
     present = [];
     /**
-     *
+     * Used to display a description of a location to the game content
+     * area. This description is based on the x-present tags that were
+     * in the x-location tag from which the Location was parse. For
+     * each such x-present tag in the order it was in the original HTML,
+     * the ck condition is first evaluated (this may contain a delay
+     * or clickProceed call or a boolean expression), once/if condition is
+     * satisfied then the HTML contents of the tag are shown. If the case,
+     * where the ck evaluates to false that x-present tags contents are
+     * omitted. In addition to usual HTML tags, a x-present tag can
+     * have x-conversation subtags. These in turn can have x-speaker
+     * sub-tags to allow one to present conversation between two or more
+     * speakers in bubbles. An x-present tag may also involve x-input tags to
+     * receive/update values for x-objects or x-locations.
      */
     async renderPresentation()
     {
@@ -348,10 +370,41 @@ class Location
             }
         }
         game_content.innerHTML += "<div class='footer-space'></div>";
+        this.prepareXInputs();
         let anchors = sel("#game-content a, #game-content x-button");
         addListenersAnchors(anchors);
         enableSavesAndInventory();
     }
+    /**
+     *
+     */
+    prepareXInputs()
+    {
+        let game_content = elt("game-content");
+        let x_inputs = game_content.querySelectorAll("x-input");
+        for (const x_input of x_inputs) {
+            x_input.setAttribute("contenteditable", "true");
+            let target_object = null;
+            let target_name = x_input.getAttribute("for");
+            if (typeof target_name != "undefined") {
+                if (game.objects[target_name]) {
+                    target_object = game.objects[target_name];
+                } else if (game.locations[target_name]) {
+                    target_object = game.locations[target_name];
+                }
+                if (target_object) {
+                    let target_field = x_input.getAttribute("field");
+                    if (target_field) {
+                        x_input.innerHTML = target_object[target_field];
+                        x_input.addEventListener("input", (evt) =>
+                        {
+                            target_object[target_field] = x_input.innerHTML;
+                        });
+                    }
+                }
+            }
+        }
+    }
     /**
      *
      */
@@ -402,7 +455,8 @@ class Location
         while (section != old_section) {
             old_section = section;
             let speaker_pattern = new RegExp(
-                "\<x-speaker[^\>]+name\s*\=\s*("+quote+")[^\>]*>", 'i');
+                "\<x-(?:speaker|thought)[^\>]+name\s*\=\s*("+quote+")[^\>]*>",
+                'i');
             let speaker_match = section.match(speaker_pattern);
             if (speaker_match) {
                 let speaker_id = speaker_match[3];
@@ -437,15 +491,15 @@ class Location
 class Game
 {
     /**
-     *
+     * @type {number}
      */
     timestamp;
     /**
-     *
+     *@type {Array<Object>}
      */
     objects;
     /**
-     *
+     *@type {Array<Object>}
      */
     locations;
     /**
@@ -577,14 +631,52 @@ class Game
             return false;
         }
         this.timestamp = game_state.timestamp;
+        /*
+          during development, changing an object or location's text might
+          not be viewable on a reload unless we copy some fields of the
+          reparsed html file into a save game object.
+         */
+        let old_objects = this.objects;
         this.objects = game_state.objects;
+        for (const field in old_objects) {
+            if (typeof this.objects[field] === 'undefined') {
+                /* we assume our game never deletes objects or locations, so
+                   if we find an object in old_objects (presumably coming
+                   from a more recently parse HTML file) that was not
+                   in the saved state, we copy it over.
+                 */
+                this.objects[field] = old_objects[field];
+            } else {
+                if (typeof old_objects['action'] !== 'undefined') {
+                    this.objects['action'] = old_objects['action'];
+                } else if (typeof this.objects['action'] !== 'undefined') {
+                    delete this.objects['action'];
+                }
+            }
+        }
+        let old_locations = this.locations;
         let locations = game_state.locations;
+        let location;
         this.locations = {};
-        for (const location_name in locations) {
-            let location_object = locations[location_name];
-            let location = new Location();
-            for (const field in location_object) {
-                location[field] = location_object[field];
+        for (const location_name in old_locations) {
+            if (typeof locations[location_name] === 'undefined') {
+                location = old_locations[location_name];
+            } else {
+                let location_object = locations[location_name];
+                location = new Location();
+                for (const field in location_object) {
+                    location[field] = location_object[field];
+                    if (field == 'present' || field == 'action' ||
+                        field == 'default-action') {
+                        if (typeof old_locations[location_name][field] ===
+                            'undefined') {
+                            delete location[field];
+                        } else {
+                            location[field] =
+                                old_locations[location_name][field];
+                        }
+                    }
+                }
             }
             this.locations[location_name] = location;
         }
@@ -882,13 +974,13 @@ class Game
         eval(code);
     }
     /**
-     *
+     * Used to present the location the Main Character is currently at.
      */
     describeMainCharacterLocation()
     {
         let main_character = this.objects['main-character'];
         let position = main_character.position;
-                console.log(position);
+        console.log(position);
         let location = this.locations[position];
         location.renderPresentation();
     }
@@ -904,12 +996,13 @@ var game;
  */
 var is_mobile = window.matchMedia("(max-width: 1000px)").matches;
 /**
- * Module initialization function, used to set up the game object corrsponding
+ * Module initialization function, used to set up the game object corresponding
  * to the current HTML document.
  */
 async function initGame()
 {
     game = new Game();
+    console.log(game.locations);
     if (sessionStorage.current) {
         game.restoreState(sessionStorage.current);
     }
ViewGit