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);
}