Github
This repository
This repository
All repositories
Fetching contributors…

Octocat-spinner-32-eaf2f5

Cannot retrieve contributors at this time

file 1818 lines (1651 sloc) 68.659 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817
// ---------------------------------------------------------------------------
// 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 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 <div> 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 <p> of the first paragraph, and ends with the </p> of
* the last. So "<p>Foo</p><img src='bar'>" is valid, but "foo<img
* src='bar'>" 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($("<span>").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);
        }
    };

    /* 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 = {
        // The name of this save, if we have one.
        name: "",
        // 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
    };

    /* This is a list of all the saved progress sets, with the current
* progress as the first. */
    var savedData = [
    ];

    /* 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 = $('<div>').addClass('highlight');
            domElement.append(highlight);
        }
        highlight.fadeIn(250);
        setTimeout(function() {
            highlight.fadeOut(1000);
        }, 2000);
    };

    /* Finds the correct location and inserts a particular DOM element
* fits into an existing list of DOM elements. This is done by
* priority order, so all elements (existing and new) must have
* their data-priority attribute set. */
    var insertAtCorrectPosition = function(parent, newItem) {
        var newPriority = newItem.attr('data-priority');
        var children = parent.children();
        for (var i = 0; i < children.size(); i++) {
            var child = children.eq(i);
            if (newPriority < child.attr('data-priority')) {
                child.before(newItem);
                return;
            }
        }
        parent.append(newItem);
    };

    /* Adds a new group to the correct location in the quality list. */
    var addGroupBlock = function(groupId) {
        var groupDefinition = game.qualityGroups[groupId];

        // Build the group div with appropriate heading.
        var groupBlock = $("#ui_library #quality_group").clone();
        groupBlock.attr("id", "g_"+groupId);
        groupBlock.attr("data-priority", groupDefinition.priority);

        var titleElement = groupBlock.find("[data-attr='title']");
        if (groupDefinition.title) {
            titleElement.html(groupDefinition.title);
        } else {
            titleElement.remove();
        }

        if (groupDefinition.extraClasses) {
            for (var i = 0; i < groupDefinition.extraClasses.length; i++) {
                groupBlock.addClass(groupDefinition.extraClasses[i]);
            }
        }

        // Add the block to the correct place.
        var qualities = $("#qualities");
        insertAtCorrectPosition(qualities, groupBlock);
        return groupBlock;
    };

    /* Adds a new quality to the correct location in the quality list. */
    var addQualityBlock = function(qualityId) {
        // Make sure we want to display this quality.
        var qualityDefinition = game.qualities[qualityId];
        if (!qualityDefinition) return null;

        // Work out how the value should be displayed.
        var name = qualityDefinition.title;
        var val = qualityDefinition.format(
            character, character.qualities[qualityId]
        );
        if (val === null) return;

        // Create the quality output.
        var qualityBlock = $("#ui_library #quality").clone();
        qualityBlock.attr("id", "q_"+qualityId);
        qualityBlock.attr("data-priority", qualityDefinition.priority);
        qualityBlock.find("[data-attr='name']").html(name);
        qualityBlock.find("[data-attr='value']").html(val);
        if (qualityDefinition.extraClasses) {
            for (var i = 0; i < qualityDefinition.extraClasses.length; i++) {
                qualityBlock.addClass(qualityDefinition.extraClasses[i]);
            }
        }

        // Find or create the group block.
        var groupBlock;
        var groupId = qualityDefinition.group;
        if (groupId) {
            var group = game.qualityGroups[groupId];
            assert(group, "no_group_definition".l({id: groupId}));
            groupBlock = $("#g_"+groupId);
            if (groupBlock.size() <= 0) {
                groupBlock = addGroupBlock(groupId);
            }
        }

        // Position it correctly.
        var groupQualityList = groupBlock.find(".qualities_in_group");
        insertAtCorrectPosition(groupQualityList, qualityBlock);
        return qualityBlock;
    };

    /* Output events are tracked, so we can make sure we scroll
* correctly. We do this in a stack because one click might cause
* a chain reaction. Of output events, only when we return to the
* top level will we do the scroll.
*
* However, that leaves the question of where to scroll *to*.
* (Remember that elements could be inserted anywhere in the
* document.) Whenever we do a write(), we'll have to update the
* top (last) stack element to that position.
*/
    var scrollStack = [];
    var pendingFirstWrite = false;
    var startOutputTransaction = function() {
        if (scrollStack.length == 0) {
            pendingFirstWrite = true;
        }
        // The default is "all the way down".
        scrollStack.push(
            $("#content").height() + $("#title").height() + 60
        );
    };
    var continueOutputTransaction = function() {
        if (pendingFirstWrite) {
            pendingFirstWrite = false;
            var separator = $("#ui_library #turn_separator").clone();
            separator.removeAttr("id");
            $("#content").append(separator);
        }
    };
    var endOutputTransaction = function() {
        var scrollPoint = scrollStack.pop();
        if (scrollStack.length == 0 && scrollPoint != null) {
            if (interactive && !mobile) {
                $("body, html").animate({scrollTop: scrollPoint}, 500);
            }
            scrollPoint = null;
        }
    };

    /* This gets called when a link needs to be followed, regardless
* of whether it was user action that initiated it. */
    var linkRe = /^([-a-z0-9]+|\.)(\/([-0-9a-z]+))?$/;
    var processLink = function(code) {
        // Check if we should do this now, or if processing is already
        // underway.
        if (linkStack !== null) {
            linkStack.push(code);
            return;
        }

        // Track where we're about to add new content.
        startOutputTransaction();

        // We're processing, so make the stack available.
        linkStack = [];

        // Handle each link in turn.
        processOneLink(code);
        while (linkStack.length > 0) {
            code = linkStack.shift();
            processOneLink(code);
        }

        // We're done, so remove the stack to prevent future pushes.
        linkStack = null;

        // Scroll to the top of the new content.
        endOutputTransaction();

        // Save the game
        if (hasLocalStorage()) saveGame();
    };

    /* This gets called to actually do the work of processing a code.
* When one doLink is called (or a link is clicked), this may set call
* code that further calls doLink, and so on. This method processes
* each one, and processLink manages this.
*/
    var processOneLink = function(code) {
        var match = code.match(linkRe);
        assert(match, "link_not_valid".l({link:code}));

        var situation = match[1];
        var action = match[3];

        // Change the situation
        if (situation !== '.') {
            if (situation !== current) {
                doTransitionTo(situation);
            }
        } else {
            // We should have an action if we have no situation change.
            assert(
                action, "link_no_action".l()
            );
        }

        // Carry out the action
        if (action) {
            situation = getCurrentSituation();
            if (situation) {
                if (game.beforeAction) {
                    // Try the global act handler, and see if we need
                    // to notify the situation.
                    var consumed = game.beforeAction(
                        character, system, current, action
                    );
                    if (consumed !== true) {
                        situation.act(character, system, action);
                    }
                } else {
                    // We have no global act handler, always notify
                    // the situation.
                    situation.act(character, system, action);
                }
                if (game.afterAction) {
                    game.afterAction(character, system, current, action);
                }
            }
        }
    };

    /* This gets called when the user clicks a link to carry out an
* action. */
    var processClick = function(code) {
        var now = (new Date()).getTime() * 0.001;
        system.time = now - startTime;
        progress.sequence.push({link:code, when:system.time});
        processLink(code);
    };

    /* Transitions between situations. */
    var doTransitionTo = function(newSituationId) {
        var oldSituationId = current;
        var oldSituation = getCurrentSituation();
        var newSituation = game.situations[newSituationId];

        assert(newSituation, "unknown_situation".l({id:newSituationId}));

        // We might not have an old situation if this is the start of
        // the game.
        if (oldSituation) {
            // Notify the exiting situation.
            oldSituation.exit(character, system, newSituationId);
            if (game.exit) {
                game.exit(character, system, oldSituationId, newSituationId);
            }

            // Remove links and transient sections.
            $('#content a').each(function(index, element) {
                var a = $(element);
                if (a.hasClass('sticky')) return;
                a.replaceWith($("<span>").addClass("ex_link").html(a.html()));
            });
            if (interactive) {
                if (mobile) {
                    $('#content .transient, #content ul.options').
                        fadeOut(2000);
                } else {
                    $('#content .transient, #content ul.options').
                        animate({opacity: 0}, 1500).
                        slideUp(500, function() {
                            $(this).remove();
                        });
                }
            } else {
                $('#content .transient, #content ul.options').remove();
            }
        }

        // Move the character.
        current = newSituationId;

        // Notify the incoming situation.
        if (game.enter) {
            game.enter(character, system, oldSituationId, newSituationId);
        }
        newSituation.enter(character, system, oldSituationId);

        // additional hook for when the situation text has already been printed
        if( game.afterEnter ) {
            game.afterEnter(character, system, oldSituationId, newSituationId);
        }
    };

    /* Returns HTML from the given content with the non-raw links
* wired up. */
    var augmentLinks = function(content) {
        var output = $(content);

        // Wire up the links for regular <a> tags.
        output.find("a").each(function(index, element) {
            var a = $(element);
            if (!a.hasClass("raw")) {
                var href = a.attr('href');
                if (href.match(linkRe)) {
                    a.click(function(event) {
                        event.preventDefault();

                        // If we're a once-click, remove all matching
                        // links.
                        if (a.hasClass("once")) {
                            system.clearLinks(href);
                        }

                        processClick(href);
                        return false;
                    });
                } else {
                    a.addClass("raw");
                }
            }
        });

        return output;
    };

    /* Set up the screen from scratch to reflect the current game
* state. */
    var initGameDisplay = function() {
        // Transition into the first situation,
        $("#content").empty();

        var situation = getCurrentSituation();
        assert(
            situation, "no_current_situation".l()
        );

        showQualities();
    };

    /* Clear the current game output and start again. */
    var startNewGame = function(name) {
        progress = {
            name: name,
            seed: new Date().toString(),
            saveTime: null
        };
        character = new Character();
        system.rnd = new Random(progress.seed);
        progress.sequence = [{link:game.start, when:0}];

        // Empty the display
        $("#content").empty();

        // Store this as the current save.
        savedData.unshift(progress);

        // Start the game
        startTime = new Date().getTime() * 0.001;
        system.time = 0;
        if (game.init) game.init(character, system);
        showQualities();

        // Do the first state.
        doTransitionTo(game.start);
    };

    /* Saves the character to local storage. */
    var saveGame = function() {
        // Store when we're saving the game, to avoid exploits where a
        // player loads their file to gain extra time.
        var now = (new Date()).getTime() * 0.001;
        progress.saveTime = now - startTime;

        // Save the game.
        var saveString = JSON.stringify(savedData);
        localStorage[getSaveId()] = saveString;
    };

    /* Loads the game from the given index in the savedData array */
    var loadGame = function(i) {
        // Get the progress and move it to the start.
        progress = savedData[i];
        if (i > 0) {
            savedData.splice(i, 1);
            savedData.unshift(progress);
        }

        character = new Character();
        system.rnd = new Random(progress.seed);

        // Empty the display
        $("#content").empty();
        showQualities();

        // Now play through the actions so far:
        if (game.init) game.init(character, system);

        // Run through all the player's history.
        interactive = false;
        for (var i = 0; i < progress.sequence.length; i++) {
            var step = progress.sequence[i];
            // The action must be done at the recorded time.
            system.time = step.when;
            processLink(step.link);
        }
        interactive = true;

        // Reverse engineer the start time.
        var now = new Date().getTime() * 0.001;
        startTime = now - progress.saveTime;

        // Because we did the run through non-interactively, now we
        // need to update the UI.
        showQualities();
    };

    // -----------------------------------------------------------------------
    // Setup
    // -----------------------------------------------------------------------

    /* Export our API. */
    window.undum = {
        Situation: Situation,
        SimpleSituation: SimpleSituation,

        QualityDefinition: QualityDefinition,
        IntegerQuality: IntegerQuality,
        NonZeroIntegerQuality: NonZeroIntegerQuality,
        NumericQuality: NumericQuality,
        WordScaleQuality: WordScaleQuality,
        FudgeAdjectivesQuality: FudgeAdjectivesQuality,
        OnOffQuality: OnOffQuality,
        YesNoQuality: YesNoQuality,

        QualityGroup: QualityGroup,

        game: game,

        isInteractive: function() { return interactive; },

        // The undum set of translated strings.
        language: {}
    };

    /* Set up the game when everything is loaded. */
    $(function() {
        // Handle storage.
        if (hasLocalStorage()) {
            initSavePanel();
        } else {
            $("#save_panel").remove();
            $(".buttons").html("<p>"+"no_local_storage".l()+"</p>");
            startNewGame("");
        }

        // 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() {
            if (hasLocalStorage()) {
                $("#save_panel, #legal").fadeIn(500);
            } else {
                $("#content_wrapper, #legal").fadeIn(500);
                $("#tools_wrapper").fadeIn(2000);
                if (mobile) {
                    $("#toolbar").slideDown(500);
                    $("#menu").show();
                }
            }
            $("#title").css("cursor", "default");
            $("#title .click_message").fadeOut(250);
        });

        // Any point that an option list appears, its options are its
        // first links.
        $("ul.options li, #menu li").live('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 initSavePanel = function() {
        var panel = $("#save_panel");

        // Load the stored data.
        savedData = localStorage[getSaveId()];
        if (savedData !== undefined) savedData = JSON.parse(savedData);

        // Populate the panel with previous saves.
        if (savedData === undefined || savedData.length === 0) {
            // We have no previous saves.
            savedData = [];
            $("#load_from_save", panel).hide();
        } else {
            var $ul = $("ul", panel);
            for (var i = 0; i < savedData.length; ++i) {
                var save = savedData[i];
                var $a = $("<a>").html(save.name).attr("href", "#").click(
                    (function(i) {
                        return function(event) {
                            event.preventDefault();
                            event.stopPropagation();
                            loadGame(i);
                            transitionFromSaveToContent();
                        }
                    })(i)
                );
                var $li = $("<li>").html($a);
                $ul.append($li);
            };
        }

        // Register to hear about creating a new game.
        $("form", panel).submit(createNewSave);
    };

    var createNewSave = function(event) {
        event.preventDefault();
        event.stopPropagation();

        var saveName = $("#new_save input[name=name]").val();
        if (!saveName || saveName.length === 0) return;

        startNewGame(saveName);
        transitionFromSaveToContent();
    };

    var transitionFromSaveToContent = function() {
        $("#save_panel").slideUp();
        $("#content_wrapper").fadeIn(500);
        $("#tools_wrapper").fadeIn(2000);
        if (mobile) {
            $("#toolbar").slideDown(500);
            $("#menu").show();
        }
    };

    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",
        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}.",
        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;
})();
Something went wrong with that request. Please try again.