Change => notation in urls to ; so HTML can validate, adds more documentation

Chris Pollett [2023-01-13 17:Jan:th]
Change => notation in urls to ; so HTML can validate, adds more documentation
Filename
game.js
diff --git a/game.js b/game.js
index ce8be7c..85acdec 100644
--- a/game.js
+++ b/game.js
@@ -6,11 +6,46 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  */
 /**
- * Frise Game Engine
+ * FRISE (FRee Interactive Story Engine)
  *
  * A light-weight engine for writing interactive fiction and games.
  *
  */
+/**
+ * Global Variables
+ */
+/**
+ * Contains {locale => {to_translate_id => translation}} for each
+ * item in the game engine that needs to be localized.
+ * There are not too many strings to translate. For a paricular game you
+ * could tack on to this tl variables after you load game.js in a script
+ * tag before you call initGame.
+ * @type {Object}
+ */
+var tl = {
+    "en": {
+        "init_slot_states_load" : "Load",
+        "init_slot_states_save" : "Save",
+        "restore_state_invalid_game" : "Does not appear to be a valid game save"
+    }
+};
+/**
+ * Locale to use when looking up strings to be output in a particular language.
+ * We set the default here, but a particular game can override this in
+ * its script tag before calling initGame()
+ * @type {string}
+ */
+var locale = "en";
+/**
+ * Global game object used to track one interactive story fiction game
+ * @type {Game}
+ */
+var game;
+/**
+ * Flag used to tell if current user is interacting on a mobile device or not
+ * @type {boolean}
+ */
+var is_mobile = window.matchMedia("(max-width: 1000px)").matches;
 /**
  * Common Global Functions
  */
@@ -27,6 +62,7 @@ function elt(id)
 /**
  * Returns a collection of objects for Element's that match the provided Element
  * name in the current document.
+ *
  * @param {string} name of HTML/XML Element that want from current document
  * @return {HTMLCollection} of matching Element's
  */
@@ -36,6 +72,7 @@ function tag(name)
 }
 /**
  * Returns a list of Node's which match the CSS selector string provided.
+ *
  * @param {string} a CSS selector string to match against current document
  * @return {NodeList} of matching Element's
  */
@@ -69,6 +106,7 @@ function xsel(selector)
 /**
  * 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
  */
@@ -91,25 +129,40 @@ function sleep(time) {
   return new Promise(resolve => setTimeout(resolve, time));
 }
 /**
- *
+ * Add a link with link text message, which when clicked will allow the
+ * rendering of a Location to continue.
+ * @param {string} message link text for the link that is pausing the rendering
+ *  of the current location.
+ * @return {Promise} which will be resolved which the link is clicked.
  */
 function proceedClick(message)
 {
     let game_content = elt("game-content");
+    game.tick++;
     game_content.innerHTML +=
-        `<a id='proceed-click' href=''>${message}</a>`;
-    return new Promise(resolve => elt('proceed-click').addEventListener(
-        "click", resolve));
+        `<a id='proceed-click${game.tick}' href=''>${message}</a>`;
+    return new Promise(resolve =>
+        elt(`proceed-click${game.tick}`).addEventListener("click", resolve));
 }
 /**
+ * Creates from an HTMLCollection or Nodelist of x-object or x-location
+ * elements an array of Location's or Object's suitable for a FRISE Game
+ * object. Elements of the collection whose name isn't of the form x-
+ * or which don't have id's are ignored. Otherwise, the id field of
+ * the output Cbject or Location is set to the value of the x-object or
+ * x-location's id. Details about how a sutiable FRISE Game object is created
+ * can be found @see makeGameObject()
  *
+ * @param {HTMLCollection|Nodelist} tag_objects collection to be converted into
+ *  an array FRISE game objects or location objects.
+ * @return {Array} an array of FRISE game or location objects.
  */
 function makeGameObjects(tag_objects)
 {
     let game_objects = {};
     for (const tag_object of tag_objects) {
         let game_object = makeGameObject(tag_object);
-        if (game_object) {
+        if (game_object && game_object.id) {
             game_objects[game_object.id] = game_object;
         }
     }
@@ -135,7 +188,18 @@ function upperFirst(str)
  */
 var object_counter = 0;
 /**
+ * Used to convert a DOM Element dom_object to an Object or Location sutiable
+ * for a FRISE game. dom_object's whose tagName does not begin with x-
+ * will result in null being returned. If the tagName is x-location, a
+ * Location object will be returned otherwise a Javascript Object is returned.
+ * The innerHTML of any subtag of an x-object or an
+ * x-location beginning x- become the value of a field in the resulting
+ * object with name the name of of the tag less x-. For example,
+ * <x-object>
+ *
+ * </x-object>
  * @param {Element}
+ * @return {Object?|Location?}
  */
 function makeGameObject(dom_object)
 {
@@ -188,7 +252,13 @@ function makeGameObject(dom_object)
     return game_object;
 }
 /**
+ * Used to change the display css property of the element of the provided id
+ * to display_type if it doesn't already have that value, if it does,
+ * then the display property is set to none.. If display_type is not provided
+ * then its value is assumed to be block.
  *
+ * @param {string} id of HTML Element to toggle display property of
+ * @param {string} display_type value to toggle between it and none.
  */
 function toggleDisplay(id, display_type)
 {
@@ -236,7 +306,13 @@ function toggleOptions(elt_id, stop_pos)
     }
 }
 /**
+ * Adds click event listeners to all anchor objects in a list
+ * such objects that have href targets beginning with #. Such a target
+ * is to a location within the current game, so the click event callback
+ * then calls game.takeTurn based on this url.
  *
+ * @param {NodeList|HTMLCollection} anchors to add click listeners for
+ *  game take turn callbacks.
  */
 function addListenersAnchors(anchors)
 {
@@ -265,7 +341,16 @@ function addListenersAnchors(anchors)
     }
 }
 /**
- *
+ * Used to disable any links to save or inventory pages
+ * in either the main-nav panel/main-nav bar or in the
+ * game content area. The current place where this is used
+ * is at the start of rendering a location. A location
+ * might have several x-present tags, some of which could
+ * be drawn after a proceedClick, or a delay. As these
+ * steps in rendering are not good "save places", at the
+ * start of rendering a location all save are disabled.
+ * Only after the last drawable x-present for a location
+ * has completed at they renabled.
  */
 function disableSavesAndInventory()
 {
@@ -279,7 +364,11 @@ function disableSavesAndInventory()
     }
 }
 /**
- *
+ * Used to re-enable any disabled links to save or inventory pages
+ * in either the main-nav panel/main-nav bar or in the
+ * game content area. The current place where this is used
+ * is at the end of rendering a location. For why this is called,
+ * @see disableSavesAndInventory()
  */
 function enableSavesAndInventory()
 {
@@ -340,9 +429,8 @@ class Location
      * 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
+     * have x-speaker subtags. These llow one to present text from a speaker
+     * in bubbles. An x-present tag may also involve x-input tags to
      * receive/update values for x-objects or x-locations.
      */
     async renderPresentation()
@@ -370,7 +458,7 @@ class Location
             }
         }
         game_content.innerHTML += "<div class='footer-space'></div>";
-        this.prepareXInputs();
+        this.prepareControls();
         let anchors = sel("#game-content a, #game-content x-button");
         addListenersAnchors(anchors);
         enableSavesAndInventory();
@@ -378,14 +466,13 @@ class Location
     /**
      *
      */
-    prepareXInputs()
+    prepareControls()
     {
         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 input_fields = game_content.querySelectorAll("input");
+        for (const input_field of input_fields) {
             let target_object = null;
-            let target_name = x_input.getAttribute("for");
+            let target_name = input_field.getAttribute("data-for");
             if (typeof target_name != "undefined") {
                 if (game.objects[target_name]) {
                     target_object = game.objects[target_name];
@@ -393,12 +480,12 @@ class Location
                     target_object = game.locations[target_name];
                 }
                 if (target_object) {
-                    let target_field = x_input.getAttribute("field");
+                    let target_field = input_field.getAttribute("name");
                     if (target_field) {
-                        x_input.innerHTML = target_object[target_field];
-                        x_input.addEventListener("input", (evt) =>
+                        input_field.value = target_object[target_field];
+                        input_field.addEventListener("input", (evt) =>
                         {
-                            target_object[target_field] = x_input.innerHTML;
+                            target_object[target_field] = input_field.value;
                         });
                     }
                 }
@@ -445,7 +532,15 @@ class Location
         return [check_result, proceed, pause];
     }
     /**
+     * A given Location contains one or more x-present tags which are
+     * used when rendering that location to the game-content area. This
+     * method takes the text from one such tag, interpolates any game
+     * variables into it, and adds to any x-speaker tags in it the HTML
+     * code to render that speaker bubble (adds img tag for speaker icon, etc).
      *
+     * @param {string} section text to process before putting into the
+     *  game content area.
+     * @return {string} resulting HTML text after interpolation and processing
      */
     prepareSection(section)
     {
@@ -455,14 +550,23 @@ class Location
         while (section != old_section) {
             old_section = section;
             let speaker_pattern = new RegExp(
-                "\<x-(?:speaker|thought)[^\>]+name\s*\=\s*("+quote+")[^\>]*>",
+                "\<x-speaker([^\>]*)name\s*\=\s*("+quote+")([^\>]*)>",
                 'i');
+            let expression_pattern = new RegExp(
+                "expression\s*\=\s*("+quote+")", 'i');
             let speaker_match = section.match(speaker_pattern);
             if (speaker_match) {
-                let speaker_id = speaker_match[3];
+                let speaker_id = speaker_match[4];
+                let pre_name = (speaker_match[3]) ? speaker_match[3] : "";
+                let post_name = (speaker_match[5]) ? speaker_match[5] : "";
+                let rest_of_tag = pre_name + " " + post_name;
+                let expression_match = rest_of_tag.match(expression_pattern);
                 if (speaker_id && game.objects[speaker_id]) {
                     let speaker = game.objects[speaker_id];
                     let name = speaker.name;
+                    if (expression_match && expression_match[3]) {
+                        name += " <b>(" + expression_match[3] + ")</b>";
+                    }
                     let icon = speaker.icon;
                     let html_fragment = speaker_match[0];
                     let html_fragment_mod = html_fragment.replace(/name\s*\=/,
@@ -494,6 +598,10 @@ class Game
      * @type {number}
      */
     timestamp;
+    /**
+     * @type {number}
+     */
+    tick = 0;
     /**
      *@type {Array<Object>}
      */
@@ -603,7 +711,11 @@ class Game
         }
     }
     /**
+     * Create a JSON encoded string representing the current state of
+     * the game (all object and location states and where the main character
+     * is).
      *
+     * @return {string} JSON encoded current state of game.
      */
     captureState()
     {
@@ -620,14 +732,18 @@ class Game
         });
     }
     /**
+     * Sets the current state of the game (current settings for all objects,
+     * locations, and main character position), based on the state given in
+     * a JSON encode string representing a game state.
      *
+     * @param {string} gave_save a JSON encoded state of the a FRISE game.
      */
     restoreState(game_save)
     {
         let game_state = JSON.parse(game_save);
         if (!game_state || !game_state.timestamp ||
             !game_state.objects || !game_state.locations) {
-            alert("Does not appear to be a valid game save");
+            alert(tl[locale]['restore_state_invalid_game']);
             return false;
         }
         this.timestamp = game_state.timestamp;
@@ -738,17 +854,18 @@ class Game
             let slot_matches = field.match(/^slot(\d+)/);
             if (slot_matches && slot_matches[1]) {
                 let slot_number = parseInt(slot_matches[1]);
-                console.log("slot" + slot_number);
                 let game_save = sessionStorage.getItem("slot" + slot_number);
                 if (game_save) {
                     let game_state = JSON.parse(game_save);
-                    saves_location["slot" + slot_number] = 'Load';
+                    saves_location["slot" + slot_number] =
+                        tl[locale]['init_slot_states_load'];
                     saves_location["delete" + slot_number] = "";
                     saves_location["filled" + slot_number] = 'filled';
                     saves_location["filename" + slot_number] =
                         game_state.timestamp;
                 } else {
-                    saves_location["slot" + slot_number] = 'Save';
+                    saves_location["slot" + slot_number] =
+                        tl[locale]['init_slot_states_save'];
                     saves_location["filled" + slot_number] = 'not-filled';
                     saves_location["delete" + slot_number] = "disabled";
                     saves_location["filename" + slot_number] = '...';
@@ -757,7 +874,13 @@ class Game
         }
     }
     /**
+     * Saves the current state to a sessionStorage save slot if that
+     * slot if empty; otherwise, if the slot has data in it, then sets
+     * the current game state to the state stored at that slot. When saving
+     * this method also records the timestamp of the save time to the
+     * game's saves location.
      *
+     * @param {number} slot_number
      */
     saveLoadSlot(slot_number)
     {
@@ -779,7 +902,13 @@ class Game
         }
     }
     /**
+     * Deletes any game data from sessionStorage at location
+     * "slot" + slot_number, updates the game's save location to reflect the
+     * change.
      *
+     * @param {number} slot_number which save game to delete. Games are stored
+     *   at a sessionStorage  field "slot" + slot_number where it is intended
+     *   (but not enforced) that slot_number be an integer.
      */
     deleteSlot(slot_number)
     {
@@ -858,10 +987,7 @@ class Game
             new_game_state = sessionStorage.current;
         }
         if (!this.moveMainCharacter(hash)) {
-            console.log("exiting");
             return;
-        } else {
-            console.log("continuing");
         }
         this.future_history = [];
         elt('next-history').disabled = true;
@@ -921,7 +1047,7 @@ class Game
             return true;
         }
         hash = hash.substring(1);
-        let hash_parts = hash.split("=>");
+        let hash_parts = hash.split(";");
         let destination = hash_parts.pop();
         for (const hash_part of hash_parts) {
             let hash_matches = hash_part.match(/([^\(]+)(\(([^\)]+)\))?\s*/);
@@ -953,7 +1079,14 @@ class Game
         return true;
     }
     /**
+     * Given a string holding pre-Javascript code from an x-action tag,
+     * converts any loc(id) or obj(id) subtrings into game.location['id'],
+     * game.object['id'] then evaluates the code. If this function
+     * is passed additional arguments then an args array is set up
+     * that can be used as closure variable for this eval call.
      *
+     * @param {string} code pre-Javascript code, after the loc(id), and
+     *  obj(id) replacements mentioned above should be valid Javascript.
      */
     evaluateAction(code)
     {
@@ -980,21 +1113,10 @@ class Game
     {
         let main_character = this.objects['main-character'];
         let position = main_character.position;
-        console.log(position);
         let location = this.locations[position];
         location.renderPresentation();
     }
 }
-/**
- * Global game object used to track one interactive story fiction game
- * @type {Game}
- */
-var game;
-/**
- * Flag used to tell if current user is interacting on a mobile device or not
- * @type {boolean}
- */
-var is_mobile = window.matchMedia("(max-width: 1000px)").matches;
 /**
  * Module initialization function, used to set up the game object corresponding
  * to the current HTML document.
@@ -1002,7 +1124,6 @@ var is_mobile = window.matchMedia("(max-width: 1000px)").matches;
 async function initGame()
 {
     game = new Game();
-    console.log(game.locations);
     if (sessionStorage.current) {
         game.restoreState(sessionStorage.current);
     }
ViewGit