Finish first pass at all the code documentation

Chris Pollett [2023-01-17 22:Jan:th]
Finish first pass at all the code documentation
Filename
css/game.css
js/game.js
diff --git a/css/game.css b/css/game.css
index 2535d3a..2aa15f1 100644
--- a/css/game.css
+++ b/css/game.css
@@ -78,10 +78,20 @@ x-action
     padding-top: 23px;
     width: calc(100% - 300px);
 }
+.rtl
+{
+    direction:rtl;
+}
+.rtl #game-content
+{
+    left: unset;
+    right: 300px;
+    transition: right .25s ease-in;
+}
 #main-nav,
 #main-bar
 {
-    background: linear-gradient(.25turn, white, 97%, lightgray);
+    background: linear-gradient(to right, white, 97%, lightgray);
     height: 100%;
     left: 0;
     overflow-y: scroll;
@@ -92,6 +102,14 @@ x-action
     width: 240px;
     z-index: 0;
 }
+.rtl #main-nav,
+.rtl #main-bar
+{
+    background: linear-gradient(to left, white, 97%, lightgray);
+    left: unset;
+    right: 0;
+    transition: right .25s ease-in;
+}
 #main-bar
 {
     background: white;
@@ -105,11 +123,14 @@ x-action
 {
     background: white;
     height: 50px;
-    text-align: left;
     top: 0;
     width: 50px;
     z-index: 2;
 }
+.rtl #main-bar
+{
+    text-align: right;
+}
 #main-nav button,
 #main-bar button
 {
@@ -138,11 +159,26 @@ x-action
 }
 #main-nav .nav-label
 {
-    font-size: 16pt;
+    font-size: 18pt;
     margin: auto;
-    text-align:left;
+    text-align: left;
+    width: 170px;
+}
+.rtl #main-nav .nav-label
+{
+    text-align: right;
+}
+#main-nav .nav-info
+{
+    font-size: 18pt;
+    margin: 0px auto 15px auto;
+    text-align: left;
     width: 170px;
 }
+.rtl #main-nav .nav-info
+{
+    text-align: right;
+}
 .filled
 {
     background-color: blue;
@@ -268,6 +304,10 @@ x-speaker figure:first-of-type
     margin: 0;
     width: 120px;
 }
+.rtl x-speaker figure:first-of-type
+{
+    float: right;
+}
 .mobile x-speaker figure:first-of-type
 {
     width: 80px;
diff --git a/js/game.js b/js/game.js
index 12d65ea..9aa62fe 100644
--- a/js/game.js
+++ b/js/game.js
@@ -20,7 +20,7 @@
 /**
  * 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
+ * There are not too many strings to translate. For a particular game you
  * could tack on to this tl variables after you load game.js in a script
  * tag before you call initGame.
  * @type {Object}
@@ -39,6 +39,12 @@ var tl = {
  * @type {string}
  */
 var locale = "en";
+/**
+ * Boolean flag to set the text direction of the game to right-to-left.
+ * (if false, the text direction is left-to-right)
+ * @type {boolean}
+ */
+var is_right_to_left = false;
 /**
  * Global game object used to track one interactive story fiction game
  * @type {Game}
@@ -301,28 +307,42 @@ function toggleMainNav()
     let nav_obj = elt('main-nav');
     if ((!nav_obj.style.left && !nav_obj.style.right) ||
         nav_obj.style.left == '0px' || nav_obj.style.right == '0px') {
-        game_content.style.left = "55px";
         game_content.style.width = "calc(100% - 40px)";
-        nav_obj.style.left = '-300px';
+        if (is_right_to_left) {
+            game_content.style.right = "55px";
+            nav_obj.style.right = '-300px';
+        } else {
+            game_content.style.left = "55px";
+            nav_obj.style.left = '-300px';
+        }
         if (is_mobile) {
             nav_obj.style.width = "240px";
             game_content.style.width = "calc(100% - 70px)";
         }
         nav_obj.style.backgroundColor = 'white';
     } else {
-        nav_obj.style.left = '0px';
-        game_content.style.left = "300px";
+        if (is_right_to_left) {
+            nav_obj.style.right = '0px';
+            game_content.style.right = "300px";
+        } else {
+            nav_obj.style.left = '0px';
+            game_content.style.left = "300px";
+        }
         game_content.style.width = "calc(100% - 480px)";
         if (is_mobile) {
             nav_obj.style.width = "100%";
-            game_content.style.left = "100%";
+            if (is_right_to_left) {
+                game_content.style.right = "100%";
+            } else {
+                game_content.style.left = "100%";
+            }
         }
         nav_obj.style.backgroundColor = 'lightgray';
     }
 }
 /**
  * Adds click event listeners to all anchor objects in a list
- * such objects that have href targets beginning with #. Such a target
+ * such 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.
  *
@@ -397,7 +417,16 @@ function enableSavesAndInventory()
     }
 }
 /**
- *
+ * Given a string which may contain Javascript string interpolation expressions,
+ * i.e., ${some_expression}, produces a string where those expressions have
+ * been replaces with their values. This method performs a replacement for
+ * loc(location_id) and obj(object_id) with the action Javascript objects
+ * game.locations['location_id'] and game.objects['object_id'] before evaluating
+ * expression.
+ *
+ * @param {string} text to have Javascript interpolation expressions replaced
+ *  values
+ * @return {string} result of carrying out the replacement
  */
 function interpolateVariables(text)
 {
@@ -433,6 +462,11 @@ function interpolateVariables(text)
  */
 class Location
 {
+    /**
+     * An array of [check_condition, text_to_present] pairs typically
+     * coming from the x-present-tag's of a in the HTML of a Location.
+     * @type {Array}
+     */
     present = [];
     /**
      * Used to display a description of a location to the game content
@@ -481,13 +515,29 @@ class Location
         enableSavesAndInventory();
     }
     /**
-     *
+     * Prepares input tags in the game so that they can bind to
+     * game Object or Location fields by adding various Javascript
+     * Event handlers. A input tag like:
+     * <input data-for="bob" name="name" >
+     * Binds with the name field of the bob game object (i.e., obj(bob).name).
+     * In the case above, as the default type of an input tag is text, this
+     * would produce a text field whose initial value is the current value
+     * obj(bob).name. If the user changes the field, the value of the obj
+     * changes with it. This set up binds input tags regardless of type,
+     * can use with it with other types such as range, email, color, etc.
      */
     prepareControls()
     {
         const content_areas = ["main-nav", "game-content"];
         for (const content_area of content_areas) {
             let content = elt(content_area);
+            if (content_area == 'main-nav') {
+                if (typeof (content.originalHTML) === 'undefined') {
+                    content.originalHTML = content.innerHTML;
+                }
+                content.innerHTML = interpolateVariables(content.originalHTML);
+                game.initializeGameNavListeners();
+            }
             let input_fields = content.querySelectorAll("input");
             for (const input_field of input_fields) {
                 let target_object = null;
@@ -516,7 +566,19 @@ class Location
         }
     }
     /**
+     * Evaluates the condition from a ck attribute ofan x-present tag.
      *
+     * @param {string} condition contents from a ck attribute.
+     *  Conditions can be boolean conditions on game variable, delay conditions,
+     *  or proceedClick condition.
+     * @return {Array} [check_result, proceed, pause] if the condition involved
+     *  a boolean expression, then check_result will hold the result of
+     *  the expression (so the caller then could prevent the the display of
+     *  an x-present tag if false), proceed is the link text (if any) for a
+     *  link if the condition involved a proceedClick (which is supposed to
+     *  delay the presentation of the x-present tag until after the user
+     *  clicks the link), pause (if non zero) is the number of miliseconds
+     *  to sleep before presenting the x-pressent tag according to the condition
      */
     evaluateCheckCondition(condition)
     {
@@ -618,73 +680,136 @@ class Location
 class Game
 {
     /**
+     * Current date followed by a space followedby the current time of
+     * the most recent game capture. Used in providing a description of
+     * game saves.
      * @type {number}
      */
     timestamp;
     /**
+     * A counter that is incremented each time Javascript draws a new
+     * proceedClick a tag. Each such tag is given a id, tick is ensure these
+     * id's are unique.
      * @type {number}
      */
     tick = 0;
     /**
-     *@type {Array<Object>}
+     * The list of all Game Object's managed by the FRISE script. An object
+     * can be used to represent a thing such as a person, tool, piece of
+     * clothing, letter, etc. In an HTML document an object is defined using
+     * an x-object tag.
+     * @type {Array<Object>}
      */
     objects;
     /**
-     *@type {Array<Object>}
+     * The list of all Game Location's managed by the FRISE script. A Location
+     * can be used to represent a place the main character can go. This
+     * could be standard locations in the game, as wells as Lcoations
+     * like a Save page, Inventory page, Status page, etc.
+     * In an HTML document a Location is defined using an x-location tag.
+     * @type {Array<Location>}
      */
     locations;
     /**
-     *
+     * Used to maintain a stack (using Array push/pop) of Game State Objects
+     * based on the turns the user has taken (top of stack corresponds
+     * to the previous turn). A Game State Object is a serialized string:
+     * {
+     *  timestamp: capture_time,
+     *  objects: array_of_game_objects_at_capture_time,
+     *  locations: array_of_game_locations_at_capture_time,
+     *  }
+     * @type {Array}
      */
     history;
     /**
-     *
+     * Used to maintain a stack (using Array push/pop) of Game State Objects
+     * based on the the number previous turn clicks the user has done.
+     * I.e.,when a user clicks previous turn, the current state is pushed on
+     * to this array so that it the user then clicks next turn the current
+     * state canbe restored.
+     * A Game State Object is a serialized string:
+     * {
+     *  timestamp: capture_time,
+     *  objects: array_of_game_objects_at_capture_time,
+     *  locations: array_of_game_locations_at_capture_time,
+     *  }
+     * @type {Array}
      */
     future_history;
     /**
-     *
+     * Sets up game object with empty history, an initialized main navigation,
+     * and with objects and locations parsed out of the current HTML file
      */
     constructor()
     {
         this.history = [];
         this.future_history = [];
-        this.addMainNav();
+        this.initializeMainNavGameContentArea();
         this.initializeObjectsLocations();
     }
     /**
-     *
+     * Sets up the main navigation bar and menu on the side of the screen
+     * determined  by the is_right_to_left variable, sets up an initially empty
+     * game content area which can be written to by calling a Location
+     * object's renderPresentation. The main navigation consists of hamburger
+     * menu toggle button for the navigation as well as previous,
+     * and next history arrows at the top of screen. The rest of the main
+     * navigation content is determined by the contentsof the x-main-nav
+     * tag in the HTML file for the game. If this tag is not present, the
+     * game will not have a main navigation bar and menu.
      */
-    addMainNav()
+    initializesMainNavGameContentArea()
     {
         let body_objs = tag("body");
         if (body_objs[0] === undefined) {
             return;
         }
         let body_obj = body_objs[0];
-        let main_nav_objs = tag("x-main-nav");
-        if (main_nav_objs[0] === undefined) {
-            return;
-        }
-        let main_nav_obj = main_nav_objs[0];
         let game_screen = elt('game-screen');
         if (!game_screen) {
             body_obj.innerHTML = '<div id="game-screen"></div>' +
                 body_obj.innerHTML;
             game_screen = elt('game-screen');
         }
+        let main_nav_objs = tag("x-main-nav");
+        if (typeof main_nav_objs[0] === "undefined") {
+            game_screen.innerHTML = `<div id="game-content"></div>`;
+            return;
+        }
+        let main_nav_obj = main_nav_objs[0];
+        let history_buttons;
+        if (is_right_to_left) {
+            history_buttons =
+                `<button id="previous-history">→</button>
+                 <button id="next-history">←</button>`;
+        } else {
+            history_buttons =
+                `<button id="previous-history">←</button>
+                 <button id="next-history">→</button>`;
+        }
         game_screen.innerHTML = `
             <div id="main-bar">
             <button id="main-toggle"
             class="float-left"><span class="main-close">≡</button>
             </div>
             <div id="main-nav">
-            <button id="previous-history">←</button>
-            <button id="next-history">→</button>
+            ${history_buttons}
             <div id="game-nav">
             ${main_nav_obj.innerHTML}
             </div>
             </div>
             <div id="game-content"></div>`;
+        this.initializeGameNavListeners();
+    }
+    /**
+     * Used to initialize the event listerns for the next/previous history
+     * buttons. It also adds listeners to all the a tag and x-button tags
+     * to process the href before following any links to the target.
+     * @see addListenersAnchors
+     */
+    initializeGameNavListeners()
+    {
         elt('main-toggle').onclick = (evt) => {
             toggleMainNav('main-nav', 0);
         };
@@ -698,14 +823,20 @@ class Game
         addListenersAnchors(anchors, is_mobile);
     }
     /**
-     *
+     * Checks if the game is being played on mobile device. If not, this
+     * method does nothing; if it is, sets up the viewport so HTML will
+     * display properly. Also, in the case of playing on a mobile device,
+     * sets it so the main nav bar on the side of the screen is closed.
      */
     initializeScreen()
     {
+        let html = tag("html")[0];
+        if (is_right_to_left) {
+            html.classList.add("rtl");
+        }
         if(!is_mobile) {
             return;
         }
-        let html = tag("html")[0];
         html.classList.add("mobile");
         let head = tag("head")[0];
         head.innerHTML += `<meta name="viewport" `+
@@ -713,7 +844,9 @@ class Game
         toggleMainNav();
     }
     /**
-     *
+     * For each object, if object.position is defined, then add the object
+     * to the location.item array of the Location who id is
+     * given by object.position .
      */
     initializeObjectsLocations()
     {
@@ -721,9 +854,9 @@ class Game
         this.locations = xtag("x-location");
         for (const oid in this.objects) {
             let object = this.objects[oid];
-            if (object.position) {
+            if (typeof object.position !== 'undefined') {
                 let location_name = object.position;
-                if (this.locations[location_name]) {
+                if (typeof this.locations[location_name] !== 'undefined') {
                     let location = this.locations[location_name]
                     if (typeof location.items == "undefined") {
                         location.items = [];
@@ -822,7 +955,8 @@ class Game
         return true;
     }
     /**
-     *
+     * Deletes the game state capture history for the game. After this
+     * is games the next and previous arrow buttons won't do anything.
      */
     clearHistory()
     {
@@ -830,7 +964,11 @@ class Game
         this.future_history = [];
     }
     /**
-     *
+     * Function called when the left arrow button on the main nav page is
+     * clicked to go back one turn in the game. Pushes the current game state
+     * to the future_history game state history array, then pops the most
+     * recent game state from the history game state array and sets it as
+     * the current state.
      */
     previousHistory()
     {
@@ -849,7 +987,11 @@ class Game
         elt('next-history').disabled = false;
     }
     /**
-     *
+     * Function called when the right arrow button on the main nav page is
+     * clicked to go forawrd one turn in the game (assume the user had
+     * clicked previous at least once). Pushes the current game state
+     * to the history game state array, then pops the game state from the
+     * future_history game state array and sets it as the current state.
      */
     nextHistory()
     {
@@ -868,7 +1010,10 @@ class Game
         elt('previous-history').disabled = false;
     }
     /**
-     *
+     * Initializes the save slots for saves location page of a game.
+     * This involves looking at session storage and determine which slots
+     * have games alread save to them and for those slots determining also
+     * what time the game was saved.
      */
     initSlotStates()
     {
@@ -956,7 +1101,9 @@ class Game
         }
     }
     /**
-     *
+     * Launches a file picker to allowthe user to select a file
+     * containing a saved game state, then tries to loads the current game
+     * from this file.
      */
     load()
     {
@@ -982,7 +1129,7 @@ class Game
         file_load.click();
     }
     /**
-     *
+     * Creates a downloadable save file for the current game state.
      */
     save()
     {
@@ -994,7 +1141,29 @@ class Game
         link.click();
     }
     /**
-     *
+     * Computes one turn of the current game based on the provided url hash
+     * fragment. A url hash fragment is the part of the url after a # symbol.
+     * In non-game HTML, #fragment is traditionally used to indicate the browser
+     * should show the page as if it had been scrolled to where the element
+     * with id attribute fragment is. In the FRISE game a fragment has
+     * the form #action_1_name;action_2_name;...;action_n_name;next_location_id
+     * Such a fragment when process by takeTurn will cause the Javascript in
+     * x-action tags with id's action_1_name, action_2_name,...,action_n_name
+     * to be invoked in turn. Then the main-character object is moved to
+     * location next_location_id.  If the fragment, only consists of
+     * 1 item, i.e., is of the form, #next_location_id, then this method
+     * just moves the main-character to next_location_id.
+     * After carrying out the action and move the main-character,
+     * takeTurn updates the game state history and future_history
+     * accordingly. Then for each object and each location,
+     * if the object/location, has a x-default-action tag, this default action
+     * is executed. Finally, the Location of the main-character is presented
+     * (its renderPresentation is called).
+     * takeTurn supports two special case action #previous and #next
+     * which move one step back or forward (if possible) in the Game state
+     * history.
+     * @param {string} hash url fragment ot use when computing one turn of the
+     *  current game.
      */
     takeTurn(hash)
     {
@@ -1028,7 +1197,11 @@ class Game
         }
     }
     /**
+     * For each game Object and each game Location in x_entities evaluate the
+     * Javascript (if exists) of its default action (from it x-default-action
+     * tag).
      *
+     * @param {Array} of game Object's or Location's
      */
     evaluateDefaultActions(x_entities)
     {
@@ -1040,21 +1213,27 @@ class Game
         }
     }
     /**
+     * Moves a game Object to a new game Location. If the object had
+     * previous location, then also deletes the object from there.
      *
+     * @param {string} object_id of game Object to move
+     * @param {string} destination_id of game Location to move it to
      */
-    moveObject(object_id, destination)
+    moveObject(object_id, destination_id)
     {
         let move_object = this.objects[object_id];
-        if (!move_object || !this.locations[destination]) {
+        if (!move_object || !this.locations[destination_id]) {
             return false;
         }
-        let old_position = move_object.position;
-        let old_location = this.locations[old_position];
-        old_location.items.filter((value) => {
-            value != object_id;
-        });
-        move_object.position = destination;
-        let new_location = this.locations[destination];
+        if (typeof move_object.position !== 'undefined') {
+            let old_position = move_object.position;
+            let old_location = this.locations[old_position];
+            old_location.items.filter((value) => {
+                value != object_id;
+            });
+        }
+        move_object.position = destination_id;
+        let new_location = this.locations[destination_id];
         if (!new_location.items) {
             new_location.items = [];
         }
ViewGit