// --------------------------------------------------------------------------- // UNDUM game library. This file needs to be supplemented with a game // file (conventionally called "your-game-name.game.js" which will // define the content of the game. // --------------------------------------------------------------------------- (function() { // ----------------------------------------------------------------------- // Internal Infrastructure Implementations [NB: These have to be // at the top, because we use them below, but you can safely // ignore them and skip down to the next section.] // ----------------------------------------------------------------------- /* Crockford's inherit function */ Function.prototype.inherits = function(Parent) { var d = {}, p = (this.prototype = new Parent()); this.prototype.uber = function(name) { if (!(name in d)) d[name] = 0; var f, r, t = d[name], v = Parent.prototype; if (t) { while (t) { v = v.constructor.prototype; t -= 1; } f = v[name]; } else { f = p[name]; if (f == this[name]) { f = v[name]; } } d[name] += 1; r = f.apply(this, Array.prototype.slice.apply(arguments, [1])); d[name] -= 1; return r; }; return this; }; // Feature detection var hasLocalStorage = function() { var hasStorage = false; try { hasStorage = ('localStorage' in window) && window.localStorage !== null && window.localStorage !== undefined; } catch (err) { // Firefox with the "Always Ask" cookie accept setting // will throw an error when attempting to access localStorage hasStorage = false; } return hasStorage; }; var isMobileDevice = function() { return (navigator.userAgent.toLowerCase().search( /iphone|ipad|palm|blackberry|android/ ) >= 0 || $("html").width() <= 640); }; // Assertion var AssertionError = function(message) { this.message = message; this.name = AssertionError; }; AssertionError.inherits(Error); var assert = function(expression, message) { if (!expression) { throw new AssertionError(message); } }; // ----------------------------------------------------------------------- // Types for Author Use // ----------------------------------------------------------------------- /* The game is split into situations, which respond to user * choices. Situation is the base type. It has three methods: * enter, act and exit, which you implement to perform any * processing and output any content. The default implementations * do nothing. * * You can either create your own type of Situation, and add * enter, act and/or exit functions to the prototype (see * SimpleSituation in this file for an example of that), or you * can give those functions in the opts parameter. The opts * parameter is an object. So you could write: * * var situation = Situation({ * enter: function(character, system, from) { * ... your implementation ... * } * }); * * If you pass in enter, act and/or exit through these options, * then they should have the same function signature as the full * function definitions, below. * * Note that SimpleSituation, a derived type of Situation, calls * passed in enter, act and exit functions AS WELL AS their normal * action. This is most often what you want: the normal behavior * plus a little extra custom behavior. If you want to override * the behavior of a SimpleSituation, you'll have to create a * derived type and set the enter, act and/or exit function on * their prototypes. In most cases, however, if you want to do * something completely different, it is better to derive your * type from this type: Situation, rather than one of its * children. * * In addition to enter, exit and act, the following options * related to implicit situation selection are available: * * optionText: a string or a function(character, system, * situation) which should return the label to put in an * option block where a link to this situation can be * chosen. The situation passed in is the situation where the * option block is being displayed. * * canView: a function(character, system, situation) which should * return true if this situation should be visible in an * option block in the given situation. * * canChoose: a function(character, system, situation) which should * return true if this situation should appear clickable in an * option block. Returning false allows you to present the * option but prevent it being selected. You may want to * indicate to the player that they need to collect some * important object before the option is available, for * example. * * tags: a list of tags for this situation, which can be used for * implicit situation selection. The tags can also be given as * space, tab or comma separated tags in a string. Note that, * when calling getSituationIdChoices, tags are prefixed with * a hash, but that should not be the case here. Just use the * plain tag name. * * priority: a numeric priority value (default = 1). When * selecting situations implicitly, higher priority situations * are considered first. * * frequency: a numeric relative frequency (default = 1), so 100 * would be 100 times more frequent. When there are more * options that can be displayed, situations will be selected * randomly based on their frequency. * * displayOrder: a numeric ordering value (default = 1). When * situations are selected implicitly, the results are ordered * by increasing displayOrder. */ var Situation = function(opts) { if (opts) { if (opts.enter) this._enter = opts.enter; if (opts.act) this._act = opts.act; if (opts.exit) this._exit = opts.exit; // Options related to this situation being automatically // selected and displayed in a list of options. this._optionText = opts.optionText; this._canView = opts.canView || true; this._canChoose = opts.canChoose || true; this._priority = (opts.priority !== undefined) ? opts.priority : 1; this._frequency = (opts.frequency !== undefined) ? opts.frequency : 1; this._displayOrder = (opts.displayOrder != undefined) ? opts.displayOrder : 1; // Tag are not stored with an underscore, because they are // accessed directy. They should not be context sensitive // (use the canView function to do context sensitive // manipulation). if (opts.tags !== undefined) { if ($.isArray(opts.tags)) { this.tags = opts.tags } else { this.tags = opts.tags.split(/[ \t,]+/); } } else { this.tags = []; } } else { this._canView = true; this._canChoose = true; this._priority = 1; this._frequency = 1; this._displayOrder = 1; this.tags = []; } }; /* A function that takes action when we enter a situation. The * last parameter indicates the situation we have just left: it * may be null if this is the starting situation. Unlike the * exit() method, this method cannot prevent the transition * happening: its return value is ignored. */ Situation.prototype.enter = function(character, system, from) { if (this._enter) this._enter(character, system, from); }; /* A function that takes action when we carry out some action in a * situation that isn't intended to lead to a new situation. */ Situation.prototype.act = function(character, system, action) { if (this._act) this._act(character, system, action); }; /* A function that takes action when we exit a situation. The last * parameter indicates the situation we are going to. */ Situation.prototype.exit = function(character, system, to) { if (this._exit) this._exit(character, system, to); }; /* Determines whether this situation should be contained within a * list of options generated automatically by the given * situation. */ Situation.prototype.canView = function(character, system, situation) { if ($.isFunction(this._canView)) { return this._canView(character, system, situation); } else { return this._canView; } }; /* Determines whether this situation should be clickable within a * list of options generated automatically by the given situation. */ Situation.prototype.canChoose = function(character, system, situation) { if ($.isFunction(this._canChoose)) { return this._canChoose(character, system, situation); } else { return this._canChoose; } }; /* Returns the text that should be used to display this situation * in an automatically generated list of choices. */ Situation.prototype.optionText = function(character, system, situation) { if ($.isFunction(this._optionText)) { return this._optionText(character, system, situation); } else { return this._optionText; } }; /* Returns the priority, frequency and displayOrder for this situation, * when being selected using system.getSituationIdChoices. */ Situation.prototype.choiceData = function(character, system, situation) { return { priority: this._priority, frequency: this._frequency, displayOrder: this._displayOrder } }; /* A simple situation has a block of content that it displays when * the situation is entered. The content must be valid "Display * Content" (see `System.prototype.write` for a definition). This * constructor has options that control its behavior: * * heading: The optional `heading` will be used as a section title * before the content is displayed. The heading can be any * HTML string, it doesn't need to be "Display Content". If * the heading is not given, no heading will be displayed. If * a heading is given, and no optionText is specified (see * `Situation` for more information on `optionText`), then the * heading will also be used for the situation's option text. * * actions: This should be an object mapping action Ids to a * response. The response should either be "Display Content" * to display if this action is carried out, or it should be a * function(character, system, action) that will process the * action. * * choices: A list of situation ids and tags that, if given, will * be used to compile an implicit option block using * getSituationIdChoices (see that function for more details * of how this works). Tags in this list should be prefixed * with a hash # symbol, to distinguish them from situation * ids. * * minChoices: If choices is given, and an implicit choice block * should be compiled, set this option to require at least * this number of options to be displayed. See * getSituationIdChoices for a description of the algorithm by * which this happens. If you do not specify the `choices` * option, then this option will be ignored. * * maxChoices: If choices is given, and an implicit choice block * should be compiled, set this option to require no more than * this number of options to be displayed. See * getSituationIdChoices for a description of the algorithm by * which this happens. If you do not specify the `choices` * option, then this option will be ignored. * * The remaining options in the opts parameter are the same as for * the base Situation. */ var SimpleSituation = function(content, opts) { Situation.call(this, opts); this.content = content; this.heading = opts && opts.heading; this.actions = opts && opts.actions; this.choices = opts && opts.choices; this.minChoices = opts && opts.minChoices; this.maxChoices = opts && opts.maxChoices; }; SimpleSituation.inherits(Situation); SimpleSituation.prototype.enter = function(character, system, from) { if (this.heading) { if ($.isFunction(this.heading)) { system.writeHeading(this.heading()); } else { system.writeHeading(this.heading); } } if (this.content) { if ($.isFunction(this.content)) { system.write(this.content()); } else { system.write(this.content); } } if (this.choices) { var choices = system.getSituationIdChoices(this.choices, this.minChoices, this.maxChoices); system.writeChoices(choices); } if (this._enter) this._enter(character, system, from); }; SimpleSituation.prototype.act = function(character, system, action) { var response = this.actions[action]; try { response(character, system, action); } catch (err) { if (response) system.write(response); } if (this._act) this._act(character, system, action); }; SimpleSituation.prototype.optionText = function(character, system, sitn) { var parentResult = Situation.prototype.optionText.call(this, character, system, sitn); if (parentResult === undefined) { return this.heading; } else { return parentResult; } }; /* Instances of this class define the qualities that characters * may possess. The title should be a string, and can contain * HTML. Options are passed in in the opts parameter. The * following options are available. * * priority - A string used to sort qualities within their * groups. When the system displays a list of qualities they * will be sorted by this string. If you don't give a * priority, then the title will be used, so you'll get * alphabetic order. Normally you either don't give a * priority, or else use a priority string containing 0-padded * numbers (e.g. "00001"). * * group - The Id of a group in which to display this * parameter. The corresponding group must be defined in * your `undum.game.qualityGroups` property. * * extraClasses - These classes will be attached to the
of the first paragraph, and ends with the
of * the last. So "Foo
" is valid, but "foo" is not. * * The content goes to the end of the page, unless you supply the * optional selector argument. If you do, the content appears * after the element that matches that selector. */ System.prototype.write = function(content, elementSelector) { doWrite(content, elementSelector, 'append', 'after'); }; /* Outputs the given content in a heading on the page. The content * supplied must be valid "Display Content". * * The content goes to the end of the page, unless you supply the * optional selector argument. If you do, the content appears * after the element that matches that selector. */ System.prototype.writeHeading = function(headingContent, elementSelector) { var heading = $(""+"no_local_storage".l()+"
"); startGame(); } // Display the "click to begin" message. (We do this in code // so that, if Javascript is off, it doesn't happen.) $(".click_message").show(); // Show the game when we click on the title. $("#title").one('click', function() { $("#content_wrapper, #legal").fadeIn(500); $("#tools_wrapper").fadeIn(2000); $("#title").css("cursor", "default"); $("#title .click_message").fadeOut(250); if (mobile) { $("#toolbar").slideDown(500); $("#menu").show(); } }); // Any point that an option list appears, its options are its // first links. $("ul.options li, #menu li").on('click', function(event) { // Make option clicks pass through to their first link. var link = $("a", this); if (link.length > 0) { $(link.get(0)).click(); } }); // Switch between the two UIs as we resize. var resize = function() { // Work out if we're mobile or not. var wasMobile = mobile; mobile = isMobileDevice(); if (wasMobile != mobile) { var showing = !$(".click_message").is(":visible"); if (mobile) { var menu = $("#menu") if (showing) { $("#toolbar").show(); menu.show(); } menu.css('top', -menu.height()-52); // Go to the story view. $("#character_panel, #info_panel").hide(); } else { // Use the full width version $("#toolbar").hide(); $("#menu").hide(); if (showing) { // Display the side bars $("#tools_wrapper").show(); } $("#character_panel, #info_panel").show(); } $("#title").show(); if (showing) $("#content_wrapper").show(); } }; $(window).bind('resize', resize); resize(); // Handle display of the menu and resizing: used on mobile // devices and an small screens. initMenu(); }); var initMenu = function() { var menu = $("#menu"); var menuVisible = false; var open = function() { menu.animate({top:48}, 500); menuVisible = true; }; var close = function() { menu.animate({top:-menu.height()-52}, 250); menuVisible = false; }; menu.css('top', -menu.height()-52); // Slide up and down on clicks from the main button. $("#menu-button").click(function(event) { event.preventDefault(); event.stopPropagation(); if (menuVisible) { close(); } else { open(); } return false; }); // Register for clicks on the individual menu items: show the // relevant item. $("#menu a").click(function(event) { event.preventDefault(); event.stopPropagation(); var target = $($(this).attr('href')); if (!target.is(":visible")) { // Fade out those we don't want. $("#menu a").each(function() { var href = $(this).attr('href'); if (href != target) { $(href).fadeOut(250); } }); // Fade in our target setTimeout(function() { target.fadeIn(500); }, 250); } close(); return false; }); }; // ----------------------------------------------------------------------- // Contributed Code // ----------------------------------------------------------------------- // Internationalization support based on the code provided by Oreolek. (function() { var codesToTry = {}; /* Compiles a list of fallback languages to try if the given code * doesn't have the message we need. Caches it for future use. */ var getCodesToTry = function(languageCode) { var codeArray; if (codeArray = codesToTry[languageCode]) return codeArray; codeArray = []; if (languageCode in undum.language) { codeArray.push(languageCode); } var elements = languageCode.split('-'); for (var i = elements.length-2; i > 0; i--) { var thisCode = elements.slice(0, i).join('-'); if (thisCode in undum.language) { codeArray.push(thisCode); } } codeArray.push(""); codesToTry[languageCode] = codeArray; return codeArray; }; var lookup = function(languageCode, message) { var languageData = undum.language[languageCode]; if (!languageData) return null; return languageData[message]; }; var localize = function(languageCode, message) { var localized, thisCode; var languageCodes = getCodesToTry(languageCode); for (var i = 0; i < languageCodes.length; i++) { thisCode = languageCodes[i]; if (localized = lookup(thisCode, message)) return localized; } return message; }; // API String.prototype.l = function(args) { // Get lang attribute from html tag. var lang = $("html").attr("lang") || ""; // Find the localized form. var localized = localize(lang, this); // Merge in any replacement content. if (args) { for (var name in args) { localized = localized.replace( new RegExp("\\{"+name+"\\}"), args[name] ); } } return localized; }; })(); // Random Number generation based on seedrandom.js code by David Bau. // Copyright 2010 David Bau, all rights reserved. // // Redistribution and use in source and binary forms, with or // without modification, are permitted provided that the following // conditions are met: // // 1. Redistributions of source code must retain the above // copyright notice, this list of conditions and the // following disclaimer. // // 2. Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the // following disclaimer in the documentation and/or other // materials provided with the distribution. // // 3. Neither the name of this module nor the names of its // contributors may be used to endorse or promote products // derived from this software without specific prior written // permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND // CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, // INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) // HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR // OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, // EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. var Random = (function() { // Within this closure function the code is basically // David's. Undum's custom extensions are added to the // prototype outside of this function. var width = 256; var chunks = 6; var significanceExponent = 52; var startdenom = Math.pow(width, chunks); var significance = Math.pow(2, significanceExponent); var overflow = significance * 2; var Random = function(seed) { this.random = null; if (!seed) throw { name: "RandomSeedError", message: "random_seed_error".l() }; var key = []; mixkey(seed, key); var arc4 = new ARC4(key); this.random = function() { var n = arc4.g(chunks); var d = startdenom; var x = 0; while (n < significance) { n = (n + x) * width; d *= width; x = arc4.g(1); } while (n >= overflow) { n /= 2; d /= 2; x >>>= 1; } return (n + x) / d; }; }; // Helper type. var ARC4 = function(key) { var t, u, me = this, keylen = key.length; var i = 0, j = me.i = me.j = me.m = 0; me.S = []; me.c = []; if (!keylen) { key = [keylen++]; } while (i < width) { me.S[i] = i++; } for (i = 0; i < width; i++) { t = me.S[i]; j = lowbits(j + t + key[i % keylen]); u = me.S[j]; me.S[i] = u; me.S[j] = t; } me.g = function getnext(count) { var s = me.S; var i = lowbits(me.i + 1); var t = s[i]; var j = lowbits(me.j + t); var u = s[j]; s[i] = u; s[j] = t; var r = s[lowbits(t + u)]; while (--count) { i = lowbits(i + 1); t = s[i]; j = lowbits(j + t); u = s[j]; s[i] = u; s[j] = t; r = r * width + s[lowbits(t + u)]; } me.i = i; me.j = j; return r; }; me.g(width); }; // Helper functions. var mixkey = function(seed, key) { seed += ''; var smear = 0; for (var j = 0; j < seed.length; j++) { var lb = lowbits(j); smear ^= key[lb]; key[lb] = lowbits(smear*19 + seed.charCodeAt(j)); } seed = ''; for (j in key) { seed += String.fromCharCode(key[j]); } return seed; }; var lowbits = function(n) { return n & (width - 1); }; return Random; })(); /* Returns a random floating point number between zero and * one. NB: The prototype implementation below just throws an * error, it will be overridden in each Random object when the * seed has been correctly configured. */ Random.prototype.random = function() { throw new { name:"RandomError", message: "random_error".l() }; }; /* Returns an integer between the given min and max values, * inclusive. */ Random.prototype.randomInt = function(min, max) { return min + Math.floor((max-min+1)*this.random()); }; /* Returns the result of rolling n dice with dx sides, and adding * plus. */ Random.prototype.dice = function(n, dx, plus) { var result = 0; for (var i = 0; i < n; i++) { result += this.randomInt(1, dx); } if (plus) result += plus; return result; }; /* Returns the result of rolling n averaging dice (i.e. 6 sided dice * with sides 2,3,3,4,4,5). And adding plus. */ Random.prototype.aveDice = (function() { var mapping = [2,3,3,4,4,5]; return function(n, plus) { var result = 0; for (var i = 0; i < n; i++) { result += mapping[this.randomInt(0, 5)]; } if (plus) result += plus; return result; }; })(); /* Returns a dice-roll result from the given string dice * specification. The specification should be of the form xdy+z, * where the x component and z component are optional. This rolls * x dice of with y sides, and adds z to the result, the z * component can also be negative: xdy-z. The y component can be * either a number of sides, or can be the special values 'F', for * a fudge die (with 3 sides, +,0,-), '%' for a 100 sided die, or * 'A' for an averaging die (with sides 2,3,3,4,4,5). */ Random.prototype.diceString = (function() { var diceRe = /^([1-9][0-9]*)?d([%FA]|[1-9][0-9]*)([-+][1-9][0-9]*)?$/; return function(def) { var match = def.match(diceRe); if (!match) { throw new Error( "dice_string_error".l({string:def}) ); } var num = match[1]?parseInt(match[1], 10):1; var sides; var bonus = match[3]?parseInt(match[3], 10):0; switch (match[2]) { case 'A': return this.aveDice(num, bonus); case 'F': sides = 3; bonus -= num*2; break; case '%': sides = 100; default: sides = parseInt(match[2], 10); break; } return this.dice(num, sides, bonus); }; })(); // ----------------------------------------------------------------------- // Default Messages // ----------------------------------------------------------------------- var en = { terrible: "terrible", poor: "poor", mediocre: "mediocre", fair: "fair", good: "good", great: "great", superb: "superb", yes: "yes", no: "no", choice: "Choice {number}", no_group_definition: "Couldn't find a group definition for {id}.", link_not_valid: "The link '{link}' doesn't appear to be valid.", link_no_action: "A link with a situation of '.', must have an action.", unknown_situation: "You can't move to an unknown situation: {id}.", existing_situation: "You can't override situation {id} in HTML.", erase_message: "This will permanently delete this character and immediately return you to the start of the game. Are you sure?", no_current_situation: "I can't display, because we don't have a current situation.", no_local_storage: "No local storage available.", random_seed_error: "You must provide a valid random seed.", random_error: "Initialize the Random with a non-empty seed before use.", dice_string_error: "Couldn't interpret your dice string: '{string}'." }; // Set this data as both the default fallback language, and the english // preferred language. undum.language[""] = en; undum.language["en"] = en; })();