diff --git a/README.md b/README.md index 8f4cb84..a0ed3ea 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Salet A general client-side framework for cybertext interactive fiction games. -**Salet** is based upon the ideas of [Undum,](https://github.com/idmillington/undum) but is not a direct follower. -It also uses some code from [Raconteur,](https://github.com/sequitur/raconteur) rewritten in CoffeeScript and altered to its own needs. +**Salet** is based upon [Undum,](https://github.com/idmillington/undum) rewritten in CoffeeScript and altered to its own needs. +It also uses some code from [Raconteur,](https://github.com/sequitur/raconteur) same deal. ## License diff --git a/game/begin.coffee b/game/begin.coffee index 5454b96..565776d 100644 --- a/game/begin.coffee +++ b/game/begin.coffee @@ -2,10 +2,11 @@ markdown = require('../../lib/markdown.coffee') room = require("../../lib/room.coffee") obj = require('../../lib/obj.coffee') dialogue = require('../../lib/dialogue.coffee') -undum = require('undum-commonjs') oneOf = require('../../lib/oneOf.coffee') -$ = require("jquery") require('../../lib/interface.coffee') +languages = require('../../lib/localize.coffee') +undum = require('../../lib/undum.js') +undum.language = languages undum.game.id = "your-game-id-here" undum.game.version = "1.0" diff --git a/html/index.html b/html/index.html index 35ee6ee..66bced9 100644 --- a/html/index.html +++ b/html/index.html @@ -89,7 +89,7 @@ - + diff --git a/lib/interface.coffee b/lib/interface.coffee index bc58d84..60b85b5 100644 --- a/lib/interface.coffee +++ b/lib/interface.coffee @@ -1,8 +1,7 @@ ### Salet interface configuration. ### -$ = require("jquery") - +undum = require('./undum.js') $(document).ready(() -> $("#ways").on("click", "a", (event) -> event.preventDefault() diff --git a/lib/localize.coffee b/lib/localize.coffee new file mode 100644 index 0000000..945bc89 --- /dev/null +++ b/lib/localize.coffee @@ -0,0 +1,60 @@ +# Internationalization support + +languages = {} + +# Default Messages + +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. +languages[""] = en +languages["en"] = en + +languageCodes = Object.keys(languages) + +localize = (languageCode, message) -> + for thisCode in languageCodes + localized = languages[languageCode] + if localized + return localized + return message + +# API +String.prototype.l = (args) -> + # Get lang attribute from html tag. + lang = document.getElementsByTagName("html")[0].getAttribute("lang") || "" + + # Find the localized form. + localized = localize(lang, this) + + # Merge in any replacement content. + if args + for name in args + localized = localized.replace( + new RegExp("\\{"+name+"\\}"), args[name] + ) + return localized + +module.exports = languages; diff --git a/lib/obj.coffee b/lib/obj.coffee index febdb05..748ffe0 100644 --- a/lib/obj.coffee +++ b/lib/obj.coffee @@ -1,6 +1,5 @@ markdown = require('./markdown.coffee') -undum = require('undum-commonjs') -$ = require("jquery") +undum = require('./undum.js') objlink = (content, ref) -> return "#{content}" diff --git a/lib/random.js b/lib/random.js new file mode 100644 index 0000000..48b00a7 --- /dev/null +++ b/lib/random.js @@ -0,0 +1,207 @@ +// 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 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 () { + 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 { + 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; + break; + default: + sides = parseInt(match[2], 10); + break; + } + return this.dice(num, sides, bonus); +}; +})(); + +module.exports = Random; diff --git a/lib/room.coffee b/lib/room.coffee index ddf9bba..7423cbd 100644 --- a/lib/room.coffee +++ b/lib/room.coffee @@ -1,10 +1,9 @@ # I confess that this world model heavily borrows from INSTEAD engine. - A.Y. -undum = require('undum-commonjs') +undum = require('./undum.js') RaconteurSituation = require('./situation.coffee') obj = require('./obj.coffee') markdown = require('./markdown.coffee') -$ = require("jquery") way_to = (content, ref) -> return "#{content}" diff --git a/lib/situation.coffee b/lib/situation.coffee index ea18c59..567fc89 100644 --- a/lib/situation.coffee +++ b/lib/situation.coffee @@ -14,7 +14,7 @@ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. ### -undum = require('undum-commonjs') +undum = require('./undum.js') markdown = require('./markdown.coffee') ### diff --git a/lib/undum.js b/lib/undum.js new file mode 100644 index 0000000..c92b36f --- /dev/null +++ b/lib/undum.js @@ -0,0 +1,1833 @@ +// --------------------------------------------------------------------------- +// UNDUM game library. Games and other modules using it should require it: +// var undum = require('undum'); +// +// Version: 3.2.0-oreolek +// --------------------------------------------------------------------------- + +var Random = require('./random.js') + +// --------------------------------------------------------------------------- +// Infrastructure implementations + +/* Crockford's inherit function */ +/* (Exported globally) */ + +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() { + /* User agent detection is unreliable and evil. If your screen is less + than 700 pixels wide, we'll give you the mobile interface. This may be + less than ideal with Retina displays, however, so FIXME? */ + return $("html").width() <= 700; +}; + +// 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. If just a single tag or id is needed, it can be passed + * in as a string without wrapping into a list. + * + * 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._enter) this._enter(character, system, from); + 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); + } +}; +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
tag + * that surrounds the quality when it is displayed. A common + * use for this is to add icons representing the quality. In + * your CSS define a class for each icon, then pass those + * classes into the appropriate quality definitions. + * + * One key purpose of QualityDefinition is to format the quality + * value for display. Quality values are always stored as numeric + * values, but may be displayed in words or symbols. A number of + * sub-types of QualityDefinition are given that format their + * values in different ways. + */ +var QualityDefinition = function(title, opts) { + var myOpts = $.extend({ + priority: title, + group: null, + extraClasses: null + }, opts); + this.title = title; + this.priority = myOpts.priority; + this.group = myOpts.group; + this.extraClasses = myOpts.extraClasses; +}; +/* Formats the value (which is always numeric) into the value to + * be displayed. The result should be HTML (but no tags are + * needed). If null is returned, then the quality definition will + * not be displayed, so if you want an empty value return an empty + * string. */ +QualityDefinition.prototype.format = function(character, value) { + return value.toString(); +}; + +/* A quality that is always displayed as the nearest integer of + * the current value, rounded down. Options (in the opts + * parameter) are the same as for QualityDefinition. */ +var IntegerQuality = function(title, opts) { + QualityDefinition.call(this, title, opts); +}; +IntegerQuality.inherits(QualityDefinition); +IntegerQuality.prototype.format = function(character, value) { + return Math.floor(value).toString(); +}; + +/* A quality that displays as an IntegerQuality, unless it is + * zero, when it is omitted. Options (in the opts * parameter) are + * the same as for QualityDefinition. */ +var NonZeroIntegerQuality = function(title, opts) { + IntegerQuality.call(this, title, opts); +}; +NonZeroIntegerQuality.inherits(IntegerQuality); +NonZeroIntegerQuality.prototype.format = function(character, value) { + if (value === 0) { + return null; + } else { + return IntegerQuality.prototype.format.call( + this, character, value + ); + } +}; + +/* A quality that displays its full numeric value, including + * decimal component. This is actually a trivial wrapper around + * the QualityDefinition class, which formats in the same + * way. Options (in the opts parameter) are the same as for + * QualityDefinition. */ +var NumericQuality = function(title, opts) { + QualityDefinition.call(this, title, opts); +}; +NumericQuality.inherits(QualityDefinition); + +/* A quality that displays its values as one of a set of + * words. The quality value is first rounded down to the nearest + * integer, then this value is used to select a word to + * display. The offset parameter (optionally passed in as part of + * the opts object) controls what number maps to what word. + * + * The following options (in the opts parameter) are available: + * + * offset - With offset=0 (the default), the quantity value of 0 + * will map to the first word, and so on. If offset is + * non-zero then the value given will correspond to the first + * word in the list. So if offset=4, then the first word in + * the list will be used for value=4. + * + * useBonuses - If this is true (the default), then values outside + * the range of words will be construced from the word and a + * numeric bonus. So with offset=0 and five words, the last of + * which is 'amazing', a score of six would give 'amazing+1'. + * if this is false, then the bonus would be omitted, so + * anything beyond 'amazing' is still 'amazing'. + * + * Other options are the same as for QualityDefinition. + */ +var WordScaleQuality = function(title, values, opts) { + var myOpts = $.extend({ + offset: null, + useBonuses: true + }, opts); + QualityDefinition.call(this, title, opts); + this.values = values; + this.offset = myOpts.offset; + this.useBonuses = myOpts.useBonuses; +}; +WordScaleQuality.inherits(QualityDefinition); +WordScaleQuality.prototype.format = function(character, value) { + var val = Math.floor(value - this.offset); + var mod = ""; + if (val < 0) { + mod = val.toString(); + val = 0; + } else if (val >= this.values.length) { + mod = "+" + (val - this.values.length + 1).toString(); + val = this.values.length - 1; + } + if (!this.useBonuses) mod = ""; + if (this.values[val] === null) return null; + return this.values[val] + mod; // Type coercion +}; + +/* A specialization of WordScaleQuality that uses the FUDGE RPG's + * adjective scale (from 'terrible' at -3 to 'superb' at +3). The + * options are as for WordScaleQuality. In particular you can use + * the offset option to control where the scale starts. So you + * could model a quality that everyone starts off as 'terrible' + * (such as Nuclear Physics) with an offset of 0, while another that + * is more common (such as Health) could have an offset of -5 so + * everyone starts with 'great'. + */ +var FudgeAdjectivesQuality = function(title, opts) { + WordScaleQuality.call(this, title, [ + "terrible".l(), "poor".l(), "mediocre".l(), + "fair".l(), "good".l(), "great".l(), "superb".l() + ], opts); + if (!('offset' in opts)) this.offset = -3; +}; +FudgeAdjectivesQuality.inherits(WordScaleQuality); + +/* An boolean quality that removes itself from the quality list if + * it has a zero value. If it has a non-zero value, its value + * field is usually left empty, but you can specify your own + * string to display as the `onDisplay` parameter of the opts + * object. Other options (in the opts parameter) are the same as + * for QualityDefinition. */ +var OnOffQuality = function(title, opts) { + var myOpts = $.extend({ + onDisplay: "" + }, opts); + QualityDefinition.call(this, title, opts); + this.onDisplay = myOpts.onDisplay; +}; +OnOffQuality.inherits(QualityDefinition); +OnOffQuality.prototype.format = function(character, value) { + if (value) return this.onDisplay; + else return null; +}; + +/* A boolean quality that has different output text for zero or + * non-zero quality values. Unlike OnOffQuality, this definition + * doesn't remove itself from the list when it is 0. The options + * are as for QualityDefinition, with the addition of options + * 'yesDisplay' and 'noDisplay', which contain the HTML fragments + * used to display true and false values. If not given, these + * default to 'yes' and 'no'. + */ +var YesNoQuality = function(title, opts) { + var myOpts = $.extend({ + yesDisplay: "yes".l(), + noDisplay: "no".l() + }, opts); + QualityDefinition.call(this, title, opts); + this.yesDisplay = myOpts.yesDisplay; + this.noDisplay = myOpts.noDisplay; +}; +YesNoQuality.inherits(QualityDefinition); +YesNoQuality.prototype.format = function(character, value) { + if (value) return this.yesDisplay; + else return this.noDisplay; +}; + +/* Defines a group of qualities that should be displayed together, + * under the given optional title. These should be defined in the + * `undum.game.qualityGroups` parameter. */ +var QualityGroup = function(title, opts) { + var myOpts = $.extend({ + priority: title, + extraClasses: null + }, opts); + this.title = title; + this.priority = myOpts.priority; + this.extraClasses = myOpts.extraClasses; +}; + + +// ----------------------------------------------------------------------- +// Types Passed to Situations +// ----------------------------------------------------------------------- + +/* A system object is passed into the enter, act and exit + * functions of each situation. It is used to interact with the + * UI. + */ +var System = function() { + this.rnd = null; + this.time = 0; +}; + +/* Removes all content from the page, clearing the main content area. + * + * If an elementSelector is given, then only that selector will be + * cleared. Note that all content from the cleared element is removed, + * but the element itself remains, ready to be filled again using + * System.write. + */ +System.prototype.clearContent = function(elementSelector) { + var $element; + if (elementSelector) $element = $(elementSelector); + if (!$element) $element = $("#content"); + $element.empty(); +}; + +/* Outputs regular content to the page. The content supplied must + * be valid "Display Content". + * + * "Display Content" is any HTML string that begins with a HTML + * start tag, ends with either an end or a closed tag, and is a + * valid and self-contained snippet of HTML. Note that the string + * doesn't have to consist of only one HTML tag. You could have + * several paragraphs, for example, as long as the content starts + * with 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 = $("

").html(headingContent); + doWrite(heading, elementSelector, 'append', 'after'); +}; + +/* Outputs regular content to the page. The content supplied must + * be valid "Display Content". + * + * The content goes to the beginning 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.writeBefore = function(content, elementSelector) { + doWrite(content, elementSelector, 'prepend', 'before'); +}; + +/* Outputs regular content to the page. The content supplied must + * be valid "Display Content". + * + * When a selector is not specified, this behaves identically to + * System.prototype.write. If you supply a selector, the content + * appears as a child node at the end of the content of the + * element that matches that selector. + */ + +System.prototype.writeInto = function(content, elementSelector) { + doWrite(content, elementSelector, 'append', 'append'); +}; + +/* Replaces content with the content supplied, which must be valid + * "Display Content". + * + * When a selector is not specified, this replaces the entire + * content of the page. Otherwise, it replaces the element matched + * with the selector. This replaces the entire element, including + * the matched tags, so ideally the content supplied should fit + * in its place in the DOM with the same kind of display element. + */ + +System.prototype.replaceWith = function(content, elementSelector) { + doWrite(content, elementSelector, 'replaceWith', 'replaceWith'); +}; + +/* Carries out the given situation change or action, as if it were + * in a link that has been clicked. This allows you to do + * procedural transitions. You might have an action that builds up + * the character's strength, and depletes their magic. When the + * magic is all gone, you can force a situation change by calling + * this method. */ +System.prototype.doLink = function(code) { + processLink(code); +}; + +/* Turns any links that target the given href into plain + * text. This can be used to remove action options when an action + * is no longer available. It is used automatically when you give + * a link the 'once' class. */ +System.prototype.clearLinks = function(code) { + $("a[href='" + code + "']").each(function(index, element) { + var a = $(element); + a.replaceWith($("").addClass("ex_link").html(a.html())); + }); +}; + +/* Given a list of situation ids, this outputs a standard option + * block with the situation choices in the given order. + * + * The contents of each choice will be a link to the situation, + * the text of the link will be given by the situation's + * outputText property. Note that the canChoose function is + * called, and if it returns false, then the text will appear, but + * the link will not be clickable. + * + * Although canChoose is honored, canView and displayOrder are + * not. If you need to honor these, you should either do so + * manually, ot else use the `getSituationIdChoices` method to + * return an ordered list of valid viewable situation ids. + */ +System.prototype.writeChoices = function(listOfIds, elementSelector) { + if (listOfIds.length === 0) return; + + var currentSituation = getCurrentSituation(); + var $options = $("