// ---------------------------------------------------------------------------
// 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() {
return ('localStorage' in window) && window.localStorage !== null;
};
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 the derived types of Situation (current
* SimpleSituation), call passed in 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.
*/
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;
}
};
/* 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);
};
/* 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.
*
* 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.
*
* 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;
};
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._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);
};
/* 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 = "";
return this.values[val] + mod;
};
/* 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;
};
/* 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 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');
};
/* 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()));
});
};
/* Call this to change the character text: the text in the right
* toolbar before the qualities list. This text is designed to be
* a short description of the current state of your character. The
* content you give should be "Display Content" (see
* `System.prototype.write` for the definition).
*/
System.prototype.setCharacterText = function(content) {
var block = $("#character_text_content");
var oldContent = block.html();
var newContent = augmentLinks(content);
if (interactive && block.is(':visible')) {
block.fadeOut(250, function() {
block.html(newContent);
block.fadeIn(750);
});
showHighlight($("#character_text"));
} else {
block.html(newContent);
}
};
/* The function for adding sound) */
System.prototype.writeSound = function(character, audioName) {
this.write("
");
if (character.sandbox.soundon==0){
$('.sound').remove();
}
};
/* Call this to change the value of a character quality. Don't
* directly change quality values, because that will not update
* the UI. (You can change any data in the character's sandbox
* directly, however, since that isn't displayed). */
System.prototype.setQuality = function(quality, newValue) {
var oldValue = character.qualities[quality];
character.qualities[quality] = newValue;
if (!interactive) return;
// Work out how to display the values.
var newDisplay = newValue.toString();
var qualityDefinition = game.qualities[quality];
if (qualityDefinition) {
newDisplay = qualityDefinition.format(character, newValue);
}
// Add the data block, if we need it.
var qualityBlock = $("#q_"+quality);
if (qualityBlock.size() <= 0) {
if (newDisplay === null) return;
qualityBlock = addQualityBlock(quality).hide().fadeIn(500);
} else {
// Do nothing if there's nothing to do.
if (oldValue == newValue) return;
// Change the value.
if (newDisplay === null) {
// Remove the block, and possibly the whole group, if
// it is the last quality in the group.
var toRemove = null;
var groupBlock = qualityBlock.parents('.quality_group');
if (groupBlock.find('.quality').size() <= 1) {
toRemove = groupBlock;
} else {
toRemove = qualityBlock;
}
toRemove.fadeOut(1000, function() {
toRemove.remove();
});
} else {
var valBlock = qualityBlock.find("[data-attr='value']");
valBlock.fadeOut(250, function() {
valBlock.html(newDisplay);
valBlock.fadeIn(750);
});
}
}
showHighlight(qualityBlock);
};
/* Changes a quality to a new value, but also shows a progress bar
* animation of the change. This probably only makes sense for
* qualities that are numeric, especially ones that the player is
* grinding to increase. The quality and newValue parameters are
* as for setQuality. The progress bar is controlled by the
* following options in the opts parameter:
*
* from - The proportion along the progress bar where the
* animation starts. Defaults to 0, valid range is 0-1.
*
* to - The proportion along the progress bar where the
* animation ends. Defaults to 1, valid range is 0-1.
*
* showValue - If true (the default) then the new value of the
* quality is displayed above the progress bar.
*
* displayValue - If this is given, and showValue is true, then
* the displayValue is used above the progress bar. If this
* isn't given, and showValue is true, then the display value
* will be calculated from the QualityDefinition, as
* normal. This option is useful for qualities that don't have
* a definition, because they don't normally appear in the UI.
*
* title - The title of the progress bar. If this is not given,
* then the title of the quality is used. As for displayValue
* this is primarily used when the progress bar doesn't have a
* QualityDefinition, and therefore doesn't have a title.
*
* leftLabel, rightLabel - Underneath the progress bar you can
* place two labels at the left and right extent of the
* track. These can help to give scale to the bar. So if the
* bar signifies going from 10.2 to 10.5, you might label the
* left and right extents with "10" and "11" respectively. If
* these are not given, then the labels will be omitted.
*/
System.prototype.animateQuality = function(quality, newValue, opts) {
var currentValue = character.qualities[quality];
if (!currentValue) currentValue = 0;
// Change the base UI.
this.setQuality(quality, newValue);
if (!interactive) return;
// Overload default options.
var myOpts = {
from: 0,
to: 1,
title: null,
showValue: true,
displayValue: null,
leftLabel: null,
rightLabel: null
};
if (newValue < currentValue) {
myOpts.from = 1;
myOpts.to = 0;
}
$.extend(myOpts, opts);
// Run through the quality definition.
var qualityDefinition = game.qualities[quality];
if (qualityDefinition) {
// Work out how to display the value
if (myOpts.displayValue === null) {
myOpts.displayValue = qualityDefinition.format(
character, newValue
);
}
// Use the title.
if (myOpts.title === null) {
myOpts.title = qualityDefinition.title;
}
}
// Create the animated bar.
var totalWidth = 496;
var bar = $("#ui_library #progress_bar").clone();
bar.removeAttr("id");
var widthElement = bar.find("[data-attr='width']");
widthElement.css('width', myOpts.from*totalWidth);
// Configure its labels
var titleLabel = bar.find("[data-attr='name']");
var valueLabel = bar.find("[data-attr='value']");
var leftLabel = bar.find("[data-attr='left_label']");
var rightLabel = bar.find("[data-attr='right_label']");
if (myOpts.title) {
titleLabel.html(myOpts.title);
} else {
titleLabel.remove();
}
if (myOpts.showValue && myOpts.displayValue !== null) {
valueLabel.html(myOpts.displayValue);
} else {
valueLabel.remove();
}
if (myOpts.leftLabel) {
leftLabel.html(myOpts.leftLabel);
} else {
leftLabel.remove();
}
if (myOpts.rightLabel) {
rightLabel.html(myOpts.rightLabel);
} else {
rightLabel.remove();
}
$('#content').append(bar);
// Start the animation
setTimeout(function() {
widthElement.animate(
{'width': myOpts.to*totalWidth}, 1000,
function() {
// After a moment to allow the bar to be read, we can
// remove it.
setTimeout(function() {
if (mobile) {
bar.fadeOut(1500, function() {$(this).remove();});
} else {
bar.animate({opacity: 0}, 1500).
slideUp(500, function() {
$(this).remove();
});
}
}, 2000);
}
);
}, 500);
};
/* The character that is passed into each situation is of this
* form.
*
* The `qualities` data member maps the Ids of each quality to its
* current value. When implementing enter, act or exit functions,
* you should consider this to be read-only. Make all
* modifications through `System.prototype.setQuality`, or
* `System.prototype.animateQuality`. In your `init` function, you
* can set these values directly.
*
* The `sandbox` data member is designed to allow your code to
* track any data it needs to. The only proviso is that the data
* structure should be serializable into JSON (this means it must
* only consist of primitive types [objects, arrays, numbers,
* booleans, strings], and it must not contain circular series of
* references). The data in the sandbox is not displayed in the
* UI, although you are free to use it to create suitable output
* for the player..
*/
var Character = function() {
this.qualities = {};
this.sandbox = {};
};
/* The data structure holding the content for the game. By default
* this holds nothing. It is this data structure that is populated
* in the `.game.js` file. Each element in the structure is
* commented, below.
*
* This should be static data that never changes through the
* course of the game. It is never saved, so anything that might
* change should be stored in the character.
*/
var game = {
// Situations
/* An object mapping from the unique id of each situation, to
* the situation object itself. This is the heart of the game
* specification. */
situations: {},
/* The unique id of the situation to enter at the start of a
* new game. */
start: "start",
// Quality display definitions
/* An object mapping the unique id of each quality to its
* QualityDefinition. You don't need definitions for every
* quality, but only qualities in this mapping will be
* displayed in the character box of the UI. */
qualities: {},
/* Qualities can have an optional group Id. This maps those
* Ids to the group definitions that says how to format its
* qualities.
*/
qualityGroups: {},
// Hooks
/* This function is called at the start of the game. It is
* normally overridden to provide initial character creation
* (setting initial quality values, setting the
* character-text. This is optional, however, as set-up
* processing could also be done by the first situation's
* enter function. If this function is given it should have
* the signature function(character, system).
*/
init: null,
/* This function is called before entering any new
* situation. It is called before the corresponding situation
* has its `enter` method called. It can be used to implement
* timed triggers, but is totally optional. If this function
* is given it should have the signature:
*
* function(character, system, oldSituationId, newSituationId);
*/
enter: null,
/* Hook for when the situation has already been carried out and printed.
* The signature is
*
* function( character, system, oldSituationId, newSituationId );
*/
afterEnter: null,
/* This function is called before carrying out any action in
* any situation. It is called before the corresponding
* situation has its `act` method called. If this optional
* function is given it should have the signature:
*
* function(character, system, situationId, actionId);
*
* If the function returns true, then it is indicating that it
* has consumed the action, and the action will not be passed
* on to the situation. Note that this is the only one of
* these global handlers that can consume the event.
*/
beforeAction: null,
/* This function is called after carrying out any action in
* any situation. It is called after the corresponding
* situation has its `act` method called. If this optional
* function is given it should have the signature:
*
* function(character, system, situationId, actionId);
*/
afterAction: null,
/* This function is called after leaving any situation. It is
* called after the corresponding situation has its `exit`
* method called. If this optional function is given it should
* have the signature:
*
* function(character, system, oldSituationId, newSituationId);
*/
exit: null
};
// =======================================================================
// Code below doesn't form part of the public API for UNDUM, so
// you shouldn't find you need to use it.
// -----------------------------------------------------------------------
// Internal Data
// -----------------------------------------------------------------------
/* The global system object. */
var system = new System();
/* This is the data on the player's progress that gets saved. */
var progress = {
// A random seed string, used internally to make random
// sequences predictable.
seed: null,
// Keeps track of the links clicked, and when.
sequence: [],
// The time when the progress was saved.
saveTime: null
};
/* The Id of the current situation the player is in. */
var current = null;
/* This is the current character. It should be reconstructable
* from the above progress data. */
var character = null;
/* Tracks whether we're in interactive mode or batch mode. */
var interactive = true;
/* Tracks whether we're mobile or not. */
var mobile = isMobileDevice();
/* The system time when the game was initialized. */
var startTime;
/* The stack of links, resulting from the last action, still be to
* resolved. */
var linkStack = null
// -----------------------------------------------------------------------
// Utility Functions
// -----------------------------------------------------------------------
var getCurrentSituation = function() {
if (current) {
return game.situations[current];
} else {
return null;
}
};
/* Outputs regular content to the page. Used by write and
* writeBefore, the last two arguments control what jQuery methods
* are used to add the content.
*/
var doWrite = function(content, selector, addMethod, appendMethod) {
continueOutputTransaction();
var output = augmentLinks(content);
var element;
if (selector) element = $(selector);
if (!element) {
$('#content')[addMethod](output);
}
else {
element[appendMethod](output);
}
/* We want to scroll this new element to the bottom of the screen.
* while still being visible. The easiest way is to find the
* top edge of the *following* element and move that exactly
* to the bottom (while still ensuring that this element is fully
* visible.) */
var nextel = output.last().next();
var scrollPoint;
if (!nextel.length)
scrollPoint = $("#content").height() + $("#title").height() + 60;
else
scrollPoint = nextel.offset().top - $(window).height();
if (scrollPoint > output.offset().top)
scrollPoint = output.offset().top;
scrollStack[scrollStack.length-1] = scrollPoint;
};
/* Gets the unique id used to identify saved games. */
var getSaveId = function() {
return 'undum_'+game.id+"_"+game.version;
}
/* Adds the quality blocks to the character tools. */
var showQualities = function() {
$("#qualities").empty();
for (var qualityId in character.qualities) {
addQualityBlock(qualityId);
}
};
/* Fades in and out a highlight on the given element. */
var showHighlight = function(domElement) {
var highlight = domElement.find(".highlight");
if (highlight.size() <= 0) {
highlight = $('