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 = [];
}