Initial import.
authorJoe Wreschnig <joe.wreschnig@gmail.com>
Sun, 24 Aug 2014 19:01:05 +0000 (21:01 +0200)
committerJoe Wreschnig <joe.wreschnig@gmail.com>
Sun, 24 Aug 2014 19:01:05 +0000 (21:01 +0200)
22 files changed:
.gitignore [new file with mode: 0644]
Cardo-Bold.woff [new file with mode: 0644]
Cardo-Italic.woff [new file with mode: 0644]
Cardo-Regular.woff [new file with mode: 0644]
Makefile [new file with mode: 0644]
Oranienbaum-Regular.woff [new file with mode: 0644]
README.md [new file with mode: 0644]
TODO.org [new file with mode: 0644]
abilities.html [new file with mode: 0644]
fastclick.js [new file with mode: 0644]
favicon_128.png [new file with mode: 0644]
favicon_192.png [new file with mode: 0644]
favicon_256.png [new file with mode: 0644]
favicon_32.png [new file with mode: 0644]
heroik.appcache.in [new file with mode: 0644]
heroik.css [new file with mode: 0644]
heroik.html [new file with mode: 0644]
heroik.js [new file with mode: 0644]
names.js [new file with mode: 0644]
scenario.html [new file with mode: 0644]
scenarios.js [new file with mode: 0644]
variants.html [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..8cefe1e
--- /dev/null
@@ -0,0 +1 @@
+heroik.appcache
diff --git a/Cardo-Bold.woff b/Cardo-Bold.woff
new file mode 100644 (file)
index 0000000..6be8496
Binary files /dev/null and b/Cardo-Bold.woff differ
diff --git a/Cardo-Italic.woff b/Cardo-Italic.woff
new file mode 100644 (file)
index 0000000..32fa235
Binary files /dev/null and b/Cardo-Italic.woff differ
diff --git a/Cardo-Regular.woff b/Cardo-Regular.woff
new file mode 100644 (file)
index 0000000..4fff607
Binary files /dev/null and b/Cardo-Regular.woff differ
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..736c55e
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,13 @@
+appcache>contents = $(shell grep -vFx -e "*" -e "CACHE MANIFEST" -e "CACHE:" -e "NETWORK:" -e "FALLBACK:" $(1) | grep -v -e "^\#" )
+
+.PHONY: all clean
+
+.SECONDEXPANSION:
+%.appcache: %.appcache.in $$(call appcache>contents,$$*.appcache.in)
+       sed "s/<Last-Updated>$$/Last-Updated: `date -u +'%Y-%m-%dT%H:%M:%SZ'`/" $< > $@
+
+all: heroik.appcache
+
+clean:
+       $(RM) heroik.appcache
+
diff --git a/Oranienbaum-Regular.woff b/Oranienbaum-Regular.woff
new file mode 100644 (file)
index 0000000..47d77d2
Binary files /dev/null and b/Oranienbaum-Regular.woff differ
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..5859820
--- /dev/null
+++ b/README.md
@@ -0,0 +1,6 @@
+# Scenario Generator & Variant Rules for Hero: Immortal King
+
+This tool generates random scenarios for the _Hero: Immortal King_
+dungeon crawl card game.
+
+Target browsers are iOS 6+ Safari, Android 4.x Chrome, desktop.
diff --git a/TODO.org b/TODO.org
new file mode 100644 (file)
index 0000000..3c8e31a
--- /dev/null
+++ b/TODO.org
@@ -0,0 +1,8 @@
+* TODO Access to ability reference from other pages
+  Between completely broken history in Mobile Safari in iOS 7 and
+  inconsistent history/caching behavior across all browsers, there's
+  no way to do this safely now. Accessing it would clear the current
+  scenario state.
+
+  If browser caches ever start working on iOS again, this might be
+  possible.
diff --git a/abilities.html b/abilities.html
new file mode 100644 (file)
index 0000000..3a80734
--- /dev/null
@@ -0,0 +1,221 @@
+<!DOCTYPE html>
+<html manifest=heroik.appcache>
+  <!--
+  The person who associated a work with this deed has dedicated the work
+  to the public domain by waiving all of his or her rights to the work
+  worldwide under copyright law, including all related and neighboring
+  rights, to the extent allowed by law.
+
+  You can copy, modify, distribute and perform the work, even for
+  commercial purposes, all without asking permission.
+
+  See https://creativecommons.org/publicdomain/zero/1.0/ for details.
+  -->
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
+    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
+    <meta name="apple-mobile-web-app-capable" content="yes">
+    <meta name="apple-mobile-web-app-title" content="Hero: IK">
+    <meta name="mobile-web-app-capable" content="yes">
+    <link rel="apple-touch-icon" href="favicon_192.png">
+    <link rel=icon type="image/png" sizes="32x32" href="favicon_32.png">
+    <link rel=icon type="image/png" sizes="192x192" href="favicon_192.png">
+    <link rel=icon type="image/png" sizes="256x256" href="favicon_256.png">
+    <link rel=stylesheet href="heroik.css" type="text/css">
+    <script src="fastclick.js" type="text/javascript"></script>
+    <script src="heroik.js" type="text/javascript"></script>
+    <title>Special Abilities - Hero: Immortal King</title>
+  </head>
+  <body>
+    <div id=statusbar></div>
+    <main>
+    <a class="button no-print" href="heroik.html">
+      ◀ Return
+    </a>
+    <h1>Dungeon Card Special Abilities</h1>
+    <table id=abilities>
+      <tbody>
+        <tr>
+          <td>1d6</td>
+          <td>
+            the Strength of this card is determined by using a 6-sided
+            die. Each time the card is attacked, roll the die again to
+            determine its current Strength.
+          </td>
+        </tr>
+        <tr>
+          <td>1d8</td>
+          <td>
+            the Strength of this card is determined by using an
+            8-sided die. Each time the card is attacked, roll the die
+            again to determine its current Strength.
+          </td>
+        </tr>
+        <tr>
+          <td>1d10</td>
+          <td>
+            the Strength of this card is determined by using a
+            10-sided die. Each time the card is attacked, roll the die
+            again to determine its current Strength.
+          </td>
+        </tr>
+        <tr>
+          <td>A</td>
+          <td>
+            adventurers: When this letter is associated with a card's
+            Strength, it is equal to the number of adventurers in the
+            group.
+          </td>
+        </tr>
+        <tr>
+          <td>Bodyguard</td>
+          <td>
+            when this dungeon card is defeated, the dungeon master may
+            immediately place a Tenacity token on the face-up monster
+            card of her choice (never on a trap).
+            <br><br>
+            In the solitaire game, place a Tenacity token on the
+            monster that has the lowest Strength of those in play that
+            does not have one. In case of a tie, randomly determine
+            which among the lowest.
+          </td>
+        </tr>
+        <tr>
+          <td id=Bubbleyes>Bubbleyes +X</td>
+          <td>
+            all dungeon cards of the bubbleyes family have their
+            Strength increased by X when this card is face-up.
+          </td>
+        </tr>
+        <tr>
+          <td id=C>C</td>
+          <td>
+            courage: When this letter is associated with a card's
+            Strength, it is equal to the number of Courage tokens the
+            adventurer player has at the beginning of the battle.
+          </td>
+        </tr>
+        <tr>
+          <td>Dragons +X</td>
+          <td>
+            all dungeon cards of the dragon family have their Strength
+            increased by X when this card is face-up.
+          </td>
+        </tr>
+        <tr>
+          <td>F</td>
+          <td>
+            fear: When this letter is associated with a card's
+            Strength, it is equal to the number of Fear tokens the
+            dungeon master has at the beginning of the battle.
+          </td>
+        </tr>
+        <tr>
+          <td id=Fierce>Fierce</td>
+          <td>
+            when this dungeon card has a Tenacity token, it earns a
+            bonus of 2 Strength (instead of 1).
+            <br><br>
+            In the solitaire game, this card automatically receives a
+            Tenacity token.
+          </td>
+        </tr>
+        <tr>
+          <td>Final monster</td>
+          <td>
+            ultimate powers and quests will not work on a final
+            monster.
+          </td>
+        </tr>
+        <tr>
+          <td>Immunity X</td>
+          <td>
+            this dungeon card cannot be defeated using a die roll that
+            shows a result of X.
+          </td>
+        </tr>
+        <tr>
+          <td>M</td>
+          <td>
+            mana: When this letter is associated with a card's
+            Strength, it is equal to the number of Mana tokens the
+            adventurer player has at the beginning of the battle.
+          </td>
+        </tr>
+        <tr>
+          <td id=Nemesis>Nemesis X</td>
+          <td>
+            this card has two Strength values. If the group of
+            adventurers has characters of type X, use the value on the
+            right. Otherwise, use the value on the left.
+          </td>
+        </tr>
+        <tr>
+          <td>Nemesis +X</td>
+          <td>
+            all dungeon cards with the special ability
+            <a href="#Nemesis">Nemesis</a>
+            have their Strength increased by X when this card is
+            face-up.
+          </td>
+        </tr>
+        <tr>
+          <td>Pain</td>
+          <td>
+            if the adventurers fail to defeat this dungeon card, the
+            adventurer player loses 2 Courage tokens instead of 1.
+          </td>
+        </tr>
+        <tr>
+          <td>Psi</td>
+          <td>
+            if this card is defeated during the combat phase, the
+            dungeon master may discard a Mana token from the
+            adventurer of her choice.
+            <br><br>
+            In the solitaire game, the player chooses.
+          </td>
+        </tr>
+        <tr>
+          <td id=Supremacy>Supremacy</td>
+          <td>
+            if the dungeon master has more Fear tokens than the
+            adventurer player has Courage tokens, this card receives a
+            bonus of +2 Strength.
+          </td>
+        </tr>
+        <tr>
+          <td>Traps +X</td>
+          <td>
+            all dungeon cards of the trap family have their Strength
+            increased by X when this card is face-up.
+          </td>
+        </tr>
+        <tr>
+          <td>Treasure</td>
+          <td>
+            once this card is defeated, it counts as a "wildcard". It
+            can therefore be used as any one color symbol when paying
+            the cost for an ultimate power, quest, or object.
+          </td>
+        </tr>
+        <tr>
+          <td>Undead +X</td>
+          <td>
+            all dungeon cards of the undead family have their Strength
+            increased by X when this card is face-up.
+          </td>
+        </tr>
+        <tr>
+          <td id=Unique>Unique</td>
+          <td>
+            dungeon decks and adventurer groups can only have one copy
+            of this card. All adventurers are Unique.
+          </td>
+        </tr>
+      </tbody>
+    </table>
+    </main>
+  </body>
+</html>
diff --git a/fastclick.js b/fastclick.js
new file mode 100644 (file)
index 0000000..2fda2a9
--- /dev/null
@@ -0,0 +1,818 @@
+/**
+ * @preserve FastClick: polyfill to remove click delays on browsers with touch UIs.
+ *
+ * @version 1.0.2
+ * @codingstandard ftlabs-jsv2
+ * @copyright The Financial Times Limited [All Rights Reserved]
+ * @license MIT License (see LICENSE.txt)
+ */
+
+/*jslint browser:true, node:true*/
+/*global define, Event, Node*/
+
+
+/**
+ * Instantiate fast-clicking listeners on the specified layer.
+ *
+ * @constructor
+ * @param {Element} layer The layer to listen on
+ * @param {Object} options The options to override the defaults
+ */
+function FastClick(layer, options) {
+       'use strict';
+       var oldOnClick;
+
+       options = options || {};
+
+       /**
+        * Whether a click is currently being tracked.
+        *
+        * @type boolean
+        */
+       this.trackingClick = false;
+
+
+       /**
+        * Timestamp for when click tracking started.
+        *
+        * @type number
+        */
+       this.trackingClickStart = 0;
+
+
+       /**
+        * The element being tracked for a click.
+        *
+        * @type EventTarget
+        */
+       this.targetElement = null;
+
+
+       /**
+        * X-coordinate of touch start event.
+        *
+        * @type number
+        */
+       this.touchStartX = 0;
+
+
+       /**
+        * Y-coordinate of touch start event.
+        *
+        * @type number
+        */
+       this.touchStartY = 0;
+
+
+       /**
+        * ID of the last touch, retrieved from Touch.identifier.
+        *
+        * @type number
+        */
+       this.lastTouchIdentifier = 0;
+
+
+       /**
+        * Touchmove boundary, beyond which a click will be cancelled.
+        *
+        * @type number
+        */
+       this.touchBoundary = options.touchBoundary || 10;
+
+
+       /**
+        * The FastClick layer.
+        *
+        * @type Element
+        */
+       this.layer = layer;
+
+       /**
+        * The minimum time between tap(touchstart and touchend) events
+        *
+        * @type number
+        */
+       this.tapDelay = options.tapDelay || 200;
+
+       if (FastClick.notNeeded(layer)) {
+               return;
+       }
+
+       // Some old versions of Android don't have Function.prototype.bind
+       function bind(method, context) {
+               return function() { return method.apply(context, arguments); };
+       }
+
+
+       var methods = ['onMouse', 'onClick', 'onTouchStart', 'onTouchMove', 'onTouchEnd', 'onTouchCancel'];
+       var context = this;
+       for (var i = 0, l = methods.length; i < l; i++) {
+               context[methods[i]] = bind(context[methods[i]], context);
+       }
+
+       // Set up event handlers as required
+       if (deviceIsAndroid) {
+               layer.addEventListener('mouseover', this.onMouse, true);
+               layer.addEventListener('mousedown', this.onMouse, true);
+               layer.addEventListener('mouseup', this.onMouse, true);
+       }
+
+       layer.addEventListener('click', this.onClick, true);
+       layer.addEventListener('touchstart', this.onTouchStart, false);
+       layer.addEventListener('touchmove', this.onTouchMove, false);
+       layer.addEventListener('touchend', this.onTouchEnd, false);
+       layer.addEventListener('touchcancel', this.onTouchCancel, false);
+
+       // Hack is required for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
+       // which is how FastClick normally stops click events bubbling to callbacks registered on the FastClick
+       // layer when they are cancelled.
+       if (!Event.prototype.stopImmediatePropagation) {
+               layer.removeEventListener = function(type, callback, capture) {
+                       var rmv = Node.prototype.removeEventListener;
+                       if (type === 'click') {
+                               rmv.call(layer, type, callback.hijacked || callback, capture);
+                       } else {
+                               rmv.call(layer, type, callback, capture);
+                       }
+               };
+
+               layer.addEventListener = function(type, callback, capture) {
+                       var adv = Node.prototype.addEventListener;
+                       if (type === 'click') {
+                               adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
+                                       if (!event.propagationStopped) {
+                                               callback(event);
+                                       }
+                               }), capture);
+                       } else {
+                               adv.call(layer, type, callback, capture);
+                       }
+               };
+       }
+
+       // If a handler is already declared in the element's onclick attribute, it will be fired before
+       // FastClick's onClick handler. Fix this by pulling out the user-defined handler function and
+       // adding it as listener.
+       if (typeof layer.onclick === 'function') {
+
+               // Android browser on at least 3.2 requires a new reference to the function in layer.onclick
+               // - the old one won't work if passed to addEventListener directly.
+               oldOnClick = layer.onclick;
+               layer.addEventListener('click', function(event) {
+                       oldOnClick(event);
+               }, false);
+               layer.onclick = null;
+       }
+}
+
+
+/**
+ * Android requires exceptions.
+ *
+ * @type boolean
+ */
+var deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0;
+
+
+/**
+ * iOS requires exceptions.
+ *
+ * @type boolean
+ */
+var deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent);
+
+
+/**
+ * iOS 4 requires an exception for select elements.
+ *
+ * @type boolean
+ */
+var deviceIsIOS4 = deviceIsIOS && (/OS 4_\d(_\d)?/).test(navigator.userAgent);
+
+
+/**
+ * iOS 6.0(+?) requires the target element to be manually derived
+ *
+ * @type boolean
+ */
+var deviceIsIOSWithBadTarget = deviceIsIOS && (/OS ([6-9]|\d{2})_\d/).test(navigator.userAgent);
+
+/**
+ * BlackBerry requires exceptions.
+ *
+ * @type boolean
+ */
+var deviceIsBlackBerry10 = navigator.userAgent.indexOf('BB10') > 0;
+
+/**
+ * Determine whether a given element requires a native click.
+ *
+ * @param {EventTarget|Element} target Target DOM element
+ * @returns {boolean} Returns true if the element needs a native click
+ */
+FastClick.prototype.needsClick = function(target) {
+       'use strict';
+       switch (target.nodeName.toLowerCase()) {
+
+       // Don't send a synthetic click to disabled inputs (issue #62)
+       case 'button':
+       case 'select':
+       case 'textarea':
+               if (target.disabled) {
+                       return true;
+               }
+
+               break;
+       case 'input':
+
+               // File inputs need real clicks on iOS 6 due to a browser bug (issue #68)
+               if ((deviceIsIOS && target.type === 'file') || target.disabled) {
+                       return true;
+               }
+
+               break;
+       case 'label':
+       case 'video':
+               return true;
+       }
+
+       return (/\bneedsclick\b/).test(target.className);
+};
+
+
+/**
+ * Determine whether a given element requires a call to focus to simulate click into element.
+ *
+ * @param {EventTarget|Element} target Target DOM element
+ * @returns {boolean} Returns true if the element requires a call to focus to simulate native click.
+ */
+FastClick.prototype.needsFocus = function(target) {
+       'use strict';
+       switch (target.nodeName.toLowerCase()) {
+       case 'textarea':
+               return true;
+       case 'select':
+               return !deviceIsAndroid;
+       case 'input':
+               switch (target.type) {
+               case 'button':
+               case 'checkbox':
+               case 'file':
+               case 'image':
+               case 'radio':
+               case 'submit':
+                       return false;
+               }
+
+               // No point in attempting to focus disabled inputs
+               return !target.disabled && !target.readOnly;
+       default:
+               return (/\bneedsfocus\b/).test(target.className);
+       }
+};
+
+
+/**
+ * Send a click event to the specified element.
+ *
+ * @param {EventTarget|Element} targetElement
+ * @param {Event} event
+ */
+FastClick.prototype.sendClick = function(targetElement, event) {
+       'use strict';
+       var clickEvent, touch;
+
+       // On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24)
+       if (document.activeElement && document.activeElement !== targetElement) {
+               document.activeElement.blur();
+       }
+
+       touch = event.changedTouches[0];
+
+       // Synthesise a click event, with an extra attribute so it can be tracked
+       clickEvent = document.createEvent('MouseEvents');
+       clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
+       clickEvent.forwardedTouchEvent = true;
+       targetElement.dispatchEvent(clickEvent);
+};
+
+FastClick.prototype.determineEventType = function(targetElement) {
+       'use strict';
+
+       //Issue #159: Android Chrome Select Box does not open with a synthetic click event
+       if (deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select') {
+               return 'mousedown';
+       }
+
+       return 'click';
+};
+
+
+/**
+ * @param {EventTarget|Element} targetElement
+ */
+FastClick.prototype.focus = function(targetElement) {
+       'use strict';
+       var length;
+
+       // Issue #160: on iOS 7, some input elements (e.g. date datetime) throw a vague TypeError on setSelectionRange. These elements don't have an integer value for the selectionStart and selectionEnd properties, but unfortunately that can't be used for detection because accessing the properties also throws a TypeError. Just check the type instead. Filed as Apple bug #15122724.
+       if (deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time') {
+               length = targetElement.value.length;
+               targetElement.setSelectionRange(length, length);
+       } else {
+               targetElement.focus();
+       }
+};
+
+
+/**
+ * Check whether the given target element is a child of a scrollable layer and if so, set a flag on it.
+ *
+ * @param {EventTarget|Element} targetElement
+ */
+FastClick.prototype.updateScrollParent = function(targetElement) {
+       'use strict';
+       var scrollParent, parentElement;
+
+       scrollParent = targetElement.fastClickScrollParent;
+
+       // Attempt to discover whether the target element is contained within a scrollable layer. Re-check if the
+       // target element was moved to another parent.
+       if (!scrollParent || !scrollParent.contains(targetElement)) {
+               parentElement = targetElement;
+               do {
+                       if (parentElement.scrollHeight > parentElement.offsetHeight) {
+                               scrollParent = parentElement;
+                               targetElement.fastClickScrollParent = parentElement;
+                               break;
+                       }
+
+                       parentElement = parentElement.parentElement;
+               } while (parentElement);
+       }
+
+       // Always update the scroll top tracker if possible.
+       if (scrollParent) {
+               scrollParent.fastClickLastScrollTop = scrollParent.scrollTop;
+       }
+};
+
+
+/**
+ * @param {EventTarget} targetElement
+ * @returns {Element|EventTarget}
+ */
+FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) {
+       'use strict';
+
+       // On some older browsers (notably Safari on iOS 4.1 - see issue #56) the event target may be a text node.
+       if (eventTarget.nodeType === Node.TEXT_NODE) {
+               return eventTarget.parentNode;
+       }
+
+       return eventTarget;
+};
+
+
+/**
+ * On touch start, record the position and scroll offset.
+ *
+ * @param {Event} event
+ * @returns {boolean}
+ */
+FastClick.prototype.onTouchStart = function(event) {
+       'use strict';
+       var targetElement, touch, selection;
+
+       // Ignore multiple touches, otherwise pinch-to-zoom is prevented if both fingers are on the FastClick element (issue #111).
+       if (event.targetTouches.length > 1) {
+               return true;
+       }
+
+       targetElement = this.getTargetElementFromEventTarget(event.target);
+       touch = event.targetTouches[0];
+
+       if (deviceIsIOS) {
+
+               // Only trusted events will deselect text on iOS (issue #49)
+               selection = window.getSelection();
+               if (selection.rangeCount && !selection.isCollapsed) {
+                       return true;
+               }
+
+               if (!deviceIsIOS4) {
+
+                       // Weird things happen on iOS when an alert or confirm dialog is opened from a click event callback (issue #23):
+                       // when the user next taps anywhere else on the page, new touchstart and touchend events are dispatched
+                       // with the same identifier as the touch event that previously triggered the click that triggered the alert.
+                       // Sadly, there is an issue on iOS 4 that causes some normal touch events to have the same identifier as an
+                       // immediately preceeding touch event (issue #52), so this fix is unavailable on that platform.
+                       if (touch.identifier === this.lastTouchIdentifier) {
+                               event.preventDefault();
+                               return false;
+                       }
+
+                       this.lastTouchIdentifier = touch.identifier;
+
+                       // If the target element is a child of a scrollable layer (using -webkit-overflow-scrolling: touch) and:
+                       // 1) the user does a fling scroll on the scrollable layer
+                       // 2) the user stops the fling scroll with another tap
+                       // then the event.target of the last 'touchend' event will be the element that was under the user's finger
+                       // when the fling scroll was started, causing FastClick to send a click event to that layer - unless a check
+                       // is made to ensure that a parent layer was not scrolled before sending a synthetic click (issue #42).
+                       this.updateScrollParent(targetElement);
+               }
+       }
+
+       this.trackingClick = true;
+       this.trackingClickStart = event.timeStamp;
+       this.targetElement = targetElement;
+
+       this.touchStartX = touch.pageX;
+       this.touchStartY = touch.pageY;
+
+       // Prevent phantom clicks on fast double-tap (issue #36)
+       if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
+               event.preventDefault();
+       }
+
+       return true;
+};
+
+
+/**
+ * Based on a touchmove event object, check whether the touch has moved past a boundary since it started.
+ *
+ * @param {Event} event
+ * @returns {boolean}
+ */
+FastClick.prototype.touchHasMoved = function(event) {
+       'use strict';
+       var touch = event.changedTouches[0], boundary = this.touchBoundary;
+
+       if (Math.abs(touch.pageX - this.touchStartX) > boundary || Math.abs(touch.pageY - this.touchStartY) > boundary) {
+               return true;
+       }
+
+       return false;
+};
+
+
+/**
+ * Update the last position.
+ *
+ * @param {Event} event
+ * @returns {boolean}
+ */
+FastClick.prototype.onTouchMove = function(event) {
+       'use strict';
+       if (!this.trackingClick) {
+               return true;
+       }
+
+       // If the touch has moved, cancel the click tracking
+       if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) {
+               this.trackingClick = false;
+               this.targetElement = null;
+       }
+
+       return true;
+};
+
+
+/**
+ * Attempt to find the labelled control for the given label element.
+ *
+ * @param {EventTarget|HTMLLabelElement} labelElement
+ * @returns {Element|null}
+ */
+FastClick.prototype.findControl = function(labelElement) {
+       'use strict';
+
+       // Fast path for newer browsers supporting the HTML5 control attribute
+       if (labelElement.control !== undefined) {
+               return labelElement.control;
+       }
+
+       // All browsers under test that support touch events also support the HTML5 htmlFor attribute
+       if (labelElement.htmlFor) {
+               return document.getElementById(labelElement.htmlFor);
+       }
+
+       // If no for attribute exists, attempt to retrieve the first labellable descendant element
+       // the list of which is defined here: http://www.w3.org/TR/html5/forms.html#category-label
+       return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea');
+};
+
+
+/**
+ * On touch end, determine whether to send a click event at once.
+ *
+ * @param {Event} event
+ * @returns {boolean}
+ */
+FastClick.prototype.onTouchEnd = function(event) {
+       'use strict';
+       var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;
+
+       if (!this.trackingClick) {
+               return true;
+       }
+
+       // Prevent phantom clicks on fast double-tap (issue #36)
+       if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
+               this.cancelNextClick = true;
+               return true;
+       }
+
+       // Reset to prevent wrong click cancel on input (issue #156).
+       this.cancelNextClick = false;
+
+       this.lastClickTime = event.timeStamp;
+
+       trackingClickStart = this.trackingClickStart;
+       this.trackingClick = false;
+       this.trackingClickStart = 0;
+
+       // On some iOS devices, the targetElement supplied with the event is invalid if the layer
+       // is performing a transition or scroll, and has to be re-detected manually. Note that
+       // for this to function correctly, it must be called *after* the event target is checked!
+       // See issue #57; also filed as rdar://13048589 .
+       if (deviceIsIOSWithBadTarget) {
+               touch = event.changedTouches[0];
+
+               // In certain cases arguments of elementFromPoint can be negative, so prevent setting targetElement to null
+               targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement;
+               targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent;
+       }
+
+       targetTagName = targetElement.tagName.toLowerCase();
+       if (targetTagName === 'label') {
+               forElement = this.findControl(targetElement);
+               if (forElement) {
+                       this.focus(targetElement);
+                       if (deviceIsAndroid) {
+                               return false;
+                       }
+
+                       targetElement = forElement;
+               }
+       } else if (this.needsFocus(targetElement)) {
+
+               // Case 1: If the touch started a while ago (best guess is 100ms based on tests for issue #36) then focus will be triggered anyway. Return early and unset the target element reference so that the subsequent click will be allowed through.
+               // Case 2: Without this exception for input elements tapped when the document is contained in an iframe, then any inputted text won't be visible even though the value attribute is updated as the user types (issue #37).
+               if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) {
+                       this.targetElement = null;
+                       return false;
+               }
+
+               this.focus(targetElement);
+               this.sendClick(targetElement, event);
+
+               // Select elements need the event to go through on iOS 4, otherwise the selector menu won't open.
+               // Also this breaks opening selects when VoiceOver is active on iOS6, iOS7 (and possibly others)
+               if (!deviceIsIOS || targetTagName !== 'select') {
+                       this.targetElement = null;
+                       event.preventDefault();
+               }
+
+               return false;
+       }
+
+       if (deviceIsIOS && !deviceIsIOS4) {
+
+               // Don't send a synthetic click event if the target element is contained within a parent layer that was scrolled
+               // and this tap is being used to stop the scrolling (usually initiated by a fling - issue #42).
+               scrollParent = targetElement.fastClickScrollParent;
+               if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {
+                       return true;
+               }
+       }
+
+       // Prevent the actual click from going though - unless the target node is marked as requiring
+       // real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted.
+       if (!this.needsClick(targetElement)) {
+               event.preventDefault();
+               this.sendClick(targetElement, event);
+       }
+
+       return false;
+};
+
+
+/**
+ * On touch cancel, stop tracking the click.
+ *
+ * @returns {void}
+ */
+FastClick.prototype.onTouchCancel = function() {
+       'use strict';
+       this.trackingClick = false;
+       this.targetElement = null;
+};
+
+
+/**
+ * Determine mouse events which should be permitted.
+ *
+ * @param {Event} event
+ * @returns {boolean}
+ */
+FastClick.prototype.onMouse = function(event) {
+       'use strict';
+
+       // If a target element was never set (because a touch event was never fired) allow the event
+       if (!this.targetElement) {
+               return true;
+       }
+
+       if (event.forwardedTouchEvent) {
+               return true;
+       }
+
+       // Programmatically generated events targeting a specific element should be permitted
+       if (!event.cancelable) {
+               return true;
+       }
+
+       // Derive and check the target element to see whether the mouse event needs to be permitted;
+       // unless explicitly enabled, prevent non-touch click events from triggering actions,
+       // to prevent ghost/doubleclicks.
+       if (!this.needsClick(this.targetElement) || this.cancelNextClick) {
+
+               // Prevent any user-added listeners declared on FastClick element from being fired.
+               if (event.stopImmediatePropagation) {
+                       event.stopImmediatePropagation();
+               } else {
+
+                       // Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
+                       event.propagationStopped = true;
+               }
+
+               // Cancel the event
+               event.stopPropagation();
+               event.preventDefault();
+
+               return false;
+       }
+
+       // If the mouse event is permitted, return true for the action to go through.
+       return true;
+};
+
+
+/**
+ * On actual clicks, determine whether this is a touch-generated click, a click action occurring
+ * naturally after a delay after a touch (which needs to be cancelled to avoid duplication), or
+ * an actual click which should be permitted.
+ *
+ * @param {Event} event
+ * @returns {boolean}
+ */
+FastClick.prototype.onClick = function(event) {
+       'use strict';
+       var permitted;
+
+       // It's possible for another FastClick-like library delivered with third-party code to fire a click event before FastClick does (issue #44). In that case, set the click-tracking flag back to false and return early. This will cause onTouchEnd to return early.
+       if (this.trackingClick) {
+               this.targetElement = null;
+               this.trackingClick = false;
+               return true;
+       }
+
+       // Very odd behaviour on iOS (issue #18): if a submit element is present inside a form and the user hits enter in the iOS simulator or clicks the Go button on the pop-up OS keyboard the a kind of 'fake' click event will be triggered with the submit-type input element as the target.
+       if (event.target.type === 'submit' && event.detail === 0) {
+               return true;
+       }
+
+       permitted = this.onMouse(event);
+
+       // Only unset targetElement if the click is not permitted. This will ensure that the check for !targetElement in onMouse fails and the browser's click doesn't go through.
+       if (!permitted) {
+               this.targetElement = null;
+       }
+
+       // If clicks are permitted, return true for the action to go through.
+       return permitted;
+};
+
+
+/**
+ * Remove all FastClick's event listeners.
+ *
+ * @returns {void}
+ */
+FastClick.prototype.destroy = function() {
+       'use strict';
+       var layer = this.layer;
+
+       if (deviceIsAndroid) {
+               layer.removeEventListener('mouseover', this.onMouse, true);
+               layer.removeEventListener('mousedown', this.onMouse, true);
+               layer.removeEventListener('mouseup', this.onMouse, true);
+       }
+
+       layer.removeEventListener('click', this.onClick, true);
+       layer.removeEventListener('touchstart', this.onTouchStart, false);
+       layer.removeEventListener('touchmove', this.onTouchMove, false);
+       layer.removeEventListener('touchend', this.onTouchEnd, false);
+       layer.removeEventListener('touchcancel', this.onTouchCancel, false);
+};
+
+
+/**
+ * Check whether FastClick is needed.
+ *
+ * @param {Element} layer The layer to listen on
+ */
+FastClick.notNeeded = function(layer) {
+       'use strict';
+       var metaViewport;
+       var chromeVersion;
+       var blackberryVersion;
+
+       // Devices that don't support touch don't need FastClick
+       if (typeof window.ontouchstart === 'undefined') {
+               return true;
+       }
+
+       // Chrome version - zero for other browsers
+       chromeVersion = +(/Chrome\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1];
+
+       if (chromeVersion) {
+
+               if (deviceIsAndroid) {
+                       metaViewport = document.querySelector('meta[name=viewport]');
+
+                       if (metaViewport) {
+                               // Chrome on Android with user-scalable="no" doesn't need FastClick (issue #89)
+                               if (metaViewport.content.indexOf('user-scalable=no') !== -1) {
+                                       return true;
+                               }
+                               // Chrome 32 and above with width=device-width or less don't need FastClick
+                               if (chromeVersion > 31 && document.documentElement.scrollWidth <= window.outerWidth) {
+                                       return true;
+                               }
+                       }
+
+               // Chrome desktop doesn't need FastClick (issue #15)
+               } else {
+                       return true;
+               }
+       }
+
+       if (deviceIsBlackBerry10) {
+               blackberryVersion = navigator.userAgent.match(/Version\/([0-9]*)\.([0-9]*)/);
+               
+               // BlackBerry 10.3+ does not require Fastclick library.
+               // https://github.com/ftlabs/fastclick/issues/251
+               if (blackberryVersion[1] >= 10 && blackberryVersion[2] >= 3) {
+                       metaViewport = document.querySelector('meta[name=viewport]');
+
+                       if (metaViewport) {
+                               // user-scalable=no eliminates click delay.
+                               if (metaViewport.content.indexOf('user-scalable=no') !== -1) {
+                                       return true;
+                               }
+                               // width=device-width (or less than device-width) eliminates click delay.
+                               if (document.documentElement.scrollWidth <= window.outerWidth) {
+                                       return true;
+                               }
+                       }
+               }
+       }
+       
+       // IE10 with -ms-touch-action: none, which disables double-tap-to-zoom (issue #97)
+       if (layer.style.msTouchAction === 'none') {
+               return true;
+       }
+
+       return false;
+};
+
+
+/**
+ * Factory method for creating a FastClick object
+ *
+ * @param {Element} layer The layer to listen on
+ * @param {Object} options The options to override the defaults
+ */
+FastClick.attach = function(layer, options) {
+       'use strict';
+       return new FastClick(layer, options);
+};
+
+
+if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) {
+
+       // AMD. Register as an anonymous module.
+       define(function() {
+               'use strict';
+               return FastClick;
+       });
+} else if (typeof module !== 'undefined' && module.exports) {
+       module.exports = FastClick.attach;
+       module.exports.FastClick = FastClick;
+} else {
+       window.FastClick = FastClick;
+}
diff --git a/favicon_128.png b/favicon_128.png
new file mode 100644 (file)
index 0000000..e3e1762
Binary files /dev/null and b/favicon_128.png differ
diff --git a/favicon_192.png b/favicon_192.png
new file mode 100644 (file)
index 0000000..ab3a4d2
Binary files /dev/null and b/favicon_192.png differ
diff --git a/favicon_256.png b/favicon_256.png
new file mode 100644 (file)
index 0000000..4f2cd94
Binary files /dev/null and b/favicon_256.png differ
diff --git a/favicon_32.png b/favicon_32.png
new file mode 100644 (file)
index 0000000..0e6dc9d
Binary files /dev/null and b/favicon_32.png differ
diff --git a/heroik.appcache.in b/heroik.appcache.in
new file mode 100644 (file)
index 0000000..b152b5b
--- /dev/null
@@ -0,0 +1,23 @@
+CACHE MANIFEST
+# <Last-Updated>
+
+heroik.html
+abilities.html
+variants.html
+scenario.html
+
+fastclick.js
+heroik.css
+heroik.js
+names.js
+scenarios.js
+
+Cardo-Bold.woff
+Cardo-Italic.woff
+Cardo-Regular.woff
+Oranienbaum-Regular.woff
+
+favicon_32.png
+favicon_128.png
+favicon_192.png
+favicon_256.png
diff --git a/heroik.css b/heroik.css
new file mode 100644 (file)
index 0000000..6d827ac
--- /dev/null
@@ -0,0 +1,439 @@
+/* The person who associated a work with this deed has dedicated the
+   work to the public domain by waiving all of his or her rights to
+   the work worldwide under copyright law, including all related and
+   neighboring rights, to the extent allowed by law.
+
+   You can copy, modify, distribute and perform the work, even for
+   commercial purposes, all without asking permission.
+
+   See https://creativecommons.org/publicdomain/zero/1.0/ for details.
+*/
+
+@font-face {
+    font-family: Oranienbaum;
+    font-style: normal;
+    font-weight: 400;
+    src: url('Oranienbaum-Regular.woff') format('woff');
+}
+
+@font-face {
+    font-family: Cardo;
+    font-style: normal;
+    font-weight: 400;
+    src: url('Cardo-Regular.woff') format('woff');
+}
+
+@font-face {
+    font-family: Cardo;
+    font-style: normal;
+    font-weight: 700;
+    src: url('Cardo-Bold.woff') format('woff');
+}
+
+@font-face {
+    font-family: Cardo;
+    font-style: italic;
+    font-weight: 400;
+    src: url('Cardo-Italic.woff') format('woff');
+}
+
+* {
+    margin: 0;
+    padding: 0;
+}
+
+#change {
+    position: relative;
+    top: 1em;
+}
+
+ul {
+    margin-left: 1em;
+    margin-bottom: 1em;
+}
+
+ul.cards {
+    list-style-type: none;
+    display: inline-block;
+    margin: auto;
+}
+
+html {
+    font-family: Cardo, serif;
+    font-size: 20px;
+    background-color: black;
+    min-height: 100%;
+    height: 100%;
+}
+
+@media (max-width: 639px) {
+    html { font-size: 14px; }
+}
+
+body {
+    background-color: black;
+    min-height: 100%;
+    height: 100%;
+    box-sizing: border-box;
+}
+
+body.standalone {
+    padding-top: 20px;
+}
+
+main {
+    background-color: white;
+    max-width: 25.2em;
+    margin: 0 auto;
+    padding: 1em;
+    display: block;
+    min-height: 100%;
+    box-sizing: border-box;
+}
+
+body.standalone main {
+    padding-top: 0.5em;
+}
+
+table {
+    border-collapse: collapse;
+    margin: auto;
+    max-width: 25em;
+}
+
+thead {
+    font-size: 0.8em;
+    font-variant: small-caps;
+    vertical-align: bottom;
+    text-align: left;
+}
+
+th {
+    font-weight: bold;
+}
+
+tbody, tfoot {
+    vertical-align: top;
+    text-align: left;
+}
+
+
+tbody tr:nth-last-child(odd) {
+    background-color: hsl(270, 30%, 85%);
+}
+
+#fate th:first-child, #fate td:first-child {
+    text-align: center;
+    width: 2em;
+}
+
+#abilities td {
+    text-align: justify;
+    -webkit-hyphens: auto;
+    -moz-hyphens: auto;
+    -ms-hyphens: auto;
+    hyphens: auto;
+}
+
+#abilities td:first-child {
+    white-space: nowrap;
+}
+
+
+th, td {
+    padding: 0 0.5rem;
+}
+
+h1:before {
+    content: '\25cf';
+    color: #dae9bc;
+    text-shadow: -0.08333em -0.08333em 0.25em #aec38b,
+                 0.08333em -0.08333em 0.25em #aec38b,
+                 0.08333em 0.08333em 0 #534f53,
+                 -0.08333em 0.08333em 0 #534f53,
+                 0 0.1667em 0 #414045;
+    -webkit-text-stroke: 0;
+    display: inline-block;
+    width: 1em;
+    margin-top: -0.1em;
+    vertical-align: top;
+    margin-left: -1em;
+}
+
+h1 {
+    padding-left: 1em;
+    margin-top: 1em;
+    margin-bottom: 0.5em;
+    font-family: Oranienbaum, Cardo, serif;
+    color: hsl(270, 25%, 75%);
+    font-size: 2em;
+    text-shadow: none;
+    border-bottom: solid hsl(270, 25%, 50%) 0.0625em;
+    line-height: 0.8em;
+    font-weight: normal;
+    text-shadow: -1px 0 black, 1px 0 black,
+                 0 1px black, 0 -1px black,
+                 -0.707px -0.707px black, 0.707px 0.707px black,
+                 -0.707px 0.707px black, 0.707px -0.707px black;
+}
+
+h1:first-child {
+    margin-top: 0;
+}
+
+h2:before {
+    content: '\25fc';
+    color: #00a1eb;
+    font-size: 1.5em;
+    width: 0.55em;
+    display: inline-block;
+}
+
+h2 {
+    height: 1.15em;
+    font-size: 1.25em;
+    font-family: Oranienbaum, Cardo, serif;
+    text-align: left;
+    letter-spacing: -1px;
+    z-index: 1;
+    border-bottom: solid hsl(270, 30%, 85%) 0.25em;
+    padding-right: 1em;
+    white-space: nowrap;
+    margin-bottom: 0.5em;
+    font-weight: bold;
+}
+
+a:link, a:visited, a:active {
+    color: hsl(270, 25%, 50%);
+    font-weight: bold;
+    text-decoration: none;
+}
+
+.fate-name { font-weight: bold; }
+.fate-effect { font-style: italic; font-size: 0.95em; }
+.fate-action { font-size: 0.95em; }
+
+input[type=checkbox] {
+    margin-right: 0.5em;
+}
+
+.button {
+    font-size: 1.1em;
+    font-weight: bold;
+    color: black !important;
+    background-color: hsl(270, 30%, 85%);
+    margin: 0.0625em 0;
+    padding: 0.375em 0.5em;
+    display: inline-block;
+    border-radius: 0 0.5em 0 0.5em;
+    transition: box-shadow 0.167s, border-color 0.167s, opacity 0.333s;
+    -webkit-transition: box-shadow 0.167s, border-color 0.167s, opacity 0.333s;
+    box-shadow: 0.125em 0.125em 0.25em 0.0625em #aaa;
+    border: solid hsl(270, 30%, 85%) 1px;
+    min-width: 6em;
+}
+
+.button.small {
+    min-width: 1.25em;
+}
+
+.button:hover, .button:focus {
+    box-shadow: 0.125em 0.125em 0.25em 0.0625em #888;
+    border-color: hsl(270, 25%, 50%);
+}
+
+.button:active {
+    box-shadow: 0.0625em 0.0625em 0.0625em 0.0625em #888;
+}
+
+main > div {
+    text-align: center;
+    margin-bottom: 1rem;
+}
+
+ul.cards li {
+    padding: 0.25em 0;
+    display: inline-block;
+    width: 8em;
+}
+
+select {
+    -webkit-appearance: none;
+    -moz-appearance: none;
+    appearance: none;
+    font-family: Cardo, serif;
+    font-size: 1.1em;
+    margin-top: 0.5em;
+    width: 60%;
+    margin-left: 20%;
+    padding: 0 0.5em;
+    border: solid hsl(270, 30%, 85%) 1px;
+    border-radius: 0 0.5em 0 0.5em;
+    background-color: hsl(270, 30%, 85%);
+    box-shadow: 0.125em 0.125em 0.25em 0.0625em #aaa;
+    transition: box-shadow 0.167s, border-color 0.167s;
+    -webkit-transition: box-shadow 0.167s, border-color 0.167s;
+    text-align:-webkit-center !important;
+    font-weight: bold;
+}
+
+select:hover, select:focus {
+    box-shadow: 0.125em 0.125em 0.25em 0.0625em #888;
+    border-color: hsl(270, 25%, 50%);
+    outline: none;
+}
+
+select:active {
+    box-shadow: 0.0625em 0.0625em 0.0625em 0.0625em #888;
+    outline: none;
+}
+
+option {
+    -webkit-appearance: none;
+    -moz-appearance: none;
+    appearance: none;
+    font-family: Cardo, serif;
+    background-color: white;
+    text-align: center;
+    font-weight: normal;
+}
+
+p, li {
+    text-align: justify;
+    -webkit-hyphens: auto;
+    -moz-hyphens: auto;
+    -ms-hyphens: auto;
+    hyphens: auto;
+}
+
+p {
+    margin-bottom: 0.5em;
+}
+
+blockquote {
+    text-align: justify;
+    font-style: italic;
+    margin-left: 1em;
+    margin-right: 1em;
+    margin-bottom: 0.5em;
+    font-size: 0.95em;
+}
+
+@keyframes row-content-in {
+    0% { max-height: 0; opacity: 0; }
+    50% { max-height: 10em; }
+    100% { opacity: 1; }
+}
+@-webkit-keyframes row-content-in {
+    0% { max-height: 0; opacity: 0; }
+    50% { max-height: 10em; }
+    100% { opacity: 1; }
+}
+
+table {
+    width: 100%;
+}
+
+#fate tbody tr div {
+    max-height: 10em;
+    animation: row-content-in 0.6667s;
+    -webkit-animation: row-content-in 0.6667s;
+}
+
+#fate tbody ~ tfoot {
+    transition: opacity 0.3333s, visibility 0s 0.3333s;
+    -webkit-transition: opacity 0.3333s, visibility 0s 0.3333s;
+    opacity: 0;
+    visibility: hidden;
+}
+
+#fate tbody:empty ~ tfoot {
+    opacity: 1;
+    visibility: visible;
+}
+
+.button.big {
+    display: block;
+    width: 80%;
+    margin: 1em auto;
+}    
+
+.card1 {
+    width: 2.5em;
+    background-color: white;
+    display: inline-block;
+    margin: 0.2em 0.375em;
+    border: solid 1px black;
+}
+
+.card2 {
+    width: 2.5em;
+    background-color: black;
+    display: inline-block;
+    margin: 0.2em 0.375em;
+    border: solid 1px black;
+    color: white;
+}
+
+#statusbar {
+    display: none;
+    position: fixed;
+    left: 0;
+    top: 0;
+    right: 0;
+    height: 20px;
+    background-color: black;
+}
+
+body.standalone #statusbar {
+    display: block;
+}
+
+h1, h2, [onclick], label {
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    user-select: none;
+    -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+    cursor: default;
+}
+
+[onclick], select, label {
+    cursor: pointer;
+}
+
+td:target {
+    font-weight: bold;
+}
+
+@media print {
+
+    @page {
+        size: 3.5in 7in;
+        margin: 0.5em;
+    }
+    .no-print {
+        display: none !important;
+    }
+
+    body, html {
+        background-color: white;
+        font-size: 10pt;
+    }
+
+    main {
+        max-width: 3.5in;
+    }
+}
+
+@media screen and (-webkit-min-device-pixel-ratio:0) {
+    h1 {
+        text-shadow: none;
+        -webkit-text-stroke: 1px black;
+        latter-spacing: -1px;
+    }
+
+    .broken-on-webkit {
+        display: none !important;
+    }
+
+}
diff --git a/heroik.html b/heroik.html
new file mode 100644 (file)
index 0000000..c04ac79
--- /dev/null
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<html manifest=heroik.appcache>
+  <!--
+  The person who associated a work with this deed has dedicated the work
+  to the public domain by waiving all of his or her rights to the work
+  worldwide under copyright law, including all related and neighboring
+  rights, to the extent allowed by law.
+
+  You can copy, modify, distribute and perform the work, even for
+  commercial purposes, all without asking permission.
+
+  See https://creativecommons.org/publicdomain/zero/1.0/ for details.
+  -->
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
+    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
+    <meta name="apple-mobile-web-app-capable" content="yes">
+    <meta name="apple-mobile-web-app-title" content="Hero: IK">
+    <meta name="mobile-web-app-capable" content="yes">
+    <link rel="apple-touch-icon" href="favicon_192.png">
+    <link rel=icon type="image/png" sizes="32x32" href="favicon_32.png">
+    <link rel=icon type="image/png" sizes="192x192" href="favicon_192.png">
+    <link rel=icon type="image/png" sizes="256x256" href="favicon_256.png">
+    <link rel=stylesheet href="heroik.css" type="text/css">
+    <script src="fastclick.js" type="text/javascript"></script>
+    <script src="heroik.js" type="text/javascript"></script>
+    <title>Hero: Immortal King</title>
+  </head>
+  <body>
+    <div id=statusbar></div>
+    <main>
+    <h1>Find a Dungeon</h1>
+    <h2>Difficulty</h2>
+    <select>
+      <option data-events=5 data-nop=4>Easy</option>
+      <option data-events=5 data-nop=3 selected>Medium</option>
+      <option data-events=5 data-nop=2>Difficult</option>
+      <option data-events=6 data-nop=1>Perilous</option>
+      <option data-events=7 data-nop=0>Vicious</option>
+      <option data-events=6 data-nop=0>Heroic</option>
+      <option data-events=79 data-nop=20>Endless</option>
+    </select>
+    <h2>Denizens</h2>
+    <div>
+      <ul class=cards>
+        <li><label><input type=checkbox data-flags=demons checked>Demons</label></li>
+        <li><label><input type=checkbox data-flags=undead checked>Undead</label></li>
+        <li><label><input type=checkbox data-flags=dragons checked>Dragons</label></li>
+        <li><label><input type=checkbox data-flags=bubbleyes>Bubbleyes</label></li>
+        <li><label><input type=checkbox data-flags=traps>Traps</label></li>
+        <li><label><input type=checkbox data-flags=abunakkashii>Abunakkashii</label></li>
+      </ul>
+    </div>
+    <div style="font-size: 1.1em;">
+      <label><input type=checkbox data-flags=noncanonical checked>Non-canonical fates</label>
+    </div>
+    <div>
+      <a class=button onclick="ventureForth()">Venture Forth</a>
+    </div>
+
+    <h1>Information</h1>
+
+    <h2>Rules</h2>
+    <div>
+      <a class="big button" href="abilities.html">Special Abilities</a>
+      <a class="big button" href="variants.html">Suggestions &amp; Variants</a>
+    </div>
+    <h2>Acknowledgments</h2>
+    <div style="font-size: 0.9em">
+      <p>
+        <em>Hero: Immortal King</em> is designed by
+        <a target=_blank href="http://www.moonstergames.com/">Emmanuel Beltrando</a>
+        and published by
+        <a target=_blank href="http://www.asmodee.com/">Asmodee</a>.
+        This tool is not affiliated with or authorized by either. Some
+        events used are from <a target=_blank href="http://boardgamegeek.com/filepage/57107/random-scenario-generator">Sean Allen's random scenario
+          generator</a> and <a target=_blank href="http://docfox.free.fr/spip.php?article129">Stephane Renard's solo scenario</a>. Fonts used are <a target=_blank href="http://scholarsfonts.net/">David Perry's Cardo</a> and <a target=_blank href="http://pospelov.com/2012/08/01/oranienbaum-font/">Oleg
+        Pospelov</a> and <a target=_blank href="http://www.jovanny.ru/">Jovanny Lemonad's</a> Oranienbaum.
+      </p>
+    </div>
+    <div>
+      <a target=_blank href="https://yukkurigames.com/">Yukkuri Games</a>
+    </div>
+    </main>
+  </body>
+</html>
diff --git a/heroik.js b/heroik.js
new file mode 100644 (file)
index 0000000..401268b
--- /dev/null
+++ b/heroik.js
@@ -0,0 +1,91 @@
+/* The person who associated a work with this deed has dedicated the
+   work to the public domain by waiving all of his or her rights to
+   the work worldwide under copyright law, including all related and
+   neighboring rights, to the extent allowed by law.
+
+   You can copy, modify, distribute and perform the work, even for
+   commercial purposes, all without asking permission.
+
+   See https://creativecommons.org/publicdomain/zero/1.0/ for details.
+*/
+
+"use strict";
+
+function offsetTop (el) {
+    var offset = 0;
+    for (offset = 0; el && el.offsetTop !== undefined; el = el.offsetParent) {
+        offset += el.offsetTop;
+    }
+    return offset;
+}
+
+function scrollToId (id) {
+    var el = id ? document.getElementById(id) : null;
+    if (el) {
+        window.scrollTo(0, offsetTop(el) - 20);
+        history.replaceState(null, "", "#" + id);
+    }
+}
+
+function open (event) {
+    var href = this.href;
+    var target = this.target === "_parent" ? window.parent : window;
+
+    if (this.getAttribute('href')[0] === "#")
+        scrollToId(this.getAttribute('href').slice(1));
+    else
+        target.location.href = this.href;
+    event.preventDefault();
+}
+
+window.addEventListener('load', function () {
+    if (typeof FastClick !== "undefined")
+        FastClick.attach(document.body, { tapDelay: 50 });
+    scrollToId(location.hash.slice(1));
+});
+
+window.addEventListener('DOMContentLoaded', function () {
+    if (navigator.standalone)
+        document.body.className += ' standalone';
+
+    var links = document.querySelectorAll(
+        "a[href]:not([target=_blank])");
+    for (var i = 0; i < links.length; ++i)
+        links[i].addEventListener('click', open);
+});
+
+function choice (seq) {
+    return seq[(Math.random() * seq.length) | 0];
+}
+
+function contains (element) {
+    return this.indexOf(element) >= 0;
+}
+
+function issubset (sub, sup) {
+    return sub.every(contains, sup);
+}
+
+function intersects (a, b) {
+    return Array.prototype.some.call(a, contains, b);
+}
+
+function ventureForth () {
+    var difficulty = document.querySelector("[data-events]:checked");
+    var denizens = document.querySelectorAll("[data-flags]:checked");
+    var hash = "#" + [difficulty.getAttribute("data-events"),
+                      difficulty.getAttribute("data-nop")]
+        .concat(Array.prototype.map.call(denizens, function (element) {
+            return element.getAttribute("data-flags");
+        })).join(",");
+    location.href = "scenario.html" + hash;
+}
+
+if (applicationCache && applicationCache.status) {
+    applicationCache.update();
+    applicationCache.addEventListener('updateready', function () {
+        if (applicationCache.status === applicationCache.UPDATEREADY) {
+            applicationCache.swapCache();
+        }
+    });
+}
diff --git a/names.js b/names.js
new file mode 100644 (file)
index 0000000..edb5dd2
--- /dev/null
+++ b/names.js
@@ -0,0 +1,518 @@
+/* The person who associated a work with this deed has dedicated the
+   work to the public domain by waiving all of his or her rights to
+   the work worldwide under copyright law, including all related and
+   neighboring rights, to the extent allowed by law.
+
+   You can copy, modify, distribute and perform the work, even for
+   commercial purposes, all without asking permission.
+
+   See https://creativecommons.org/publicdomain/zero/1.0/ for details.
+*/
+
+"use strict";
+
+var ADJECTIVE = [
+    "Abandoned",
+    "Abysmal",
+    "Abyssal",
+    "Adamantine",
+    "Ancient",
+    "Angry",
+    "Arcane",
+    "ArCHing",
+    "Arctic",
+    "Arid",
+    "Bare",
+    "Bellowing",
+    "Betrayed",
+    "Bleak",
+    "Blooded",
+    "Boiling",
+    "Bottomless",
+    "Brilliant",
+    "BronZe",
+    "Brutal",
+    "Buried",
+    "Burning",
+    "Burnt",
+    "Chaotic",
+    "CHarnel",
+    "Cobalt",
+    "Cold",
+    "Collapsing",
+    "Conquered",
+    "Coral",
+    "Crescent",
+    "Cruel",
+    "Crying",
+    "Crystal",
+    "Cunning",
+    "Cursed",
+    "Damned",
+    "Dancing",
+    "Dark",
+    "Dead",
+    "Deadly",
+    "Decayed",
+    "Decaying",
+    "Deep",
+    "Deepest",
+    "Demonic",
+    "Depraved",
+    "Desert",
+    "Deserted",
+    "Desolate",
+    "Desolated",
+    "Destroyed",
+    "Diamond",
+    "Dire",
+    "Dishonored",
+    "Distant",
+    "Doomed",
+    "Dread",
+    "Dreaded",
+    "Dreadful",
+    "Dreamy",
+    "Dreary",
+    "Dry",
+    "Dying",
+    "Eastern",
+    "Ebon",
+    "Eclipsed",
+    "Elemental",
+    "Emerald",
+    "Empty",
+    "EnCHanted",
+    "Enigmatic",
+    "Erased",
+    "Eternal",
+    "Ethereal",
+    "Fabled",
+    "Fallen",
+    "False",
+    "Farthest",
+    "Feared",
+    "Fearsome",
+    "Fire",
+    "Flowing",
+    "Foaming",
+    "Forbidden",
+    "Forgotten",
+    "Forsaken",
+    "Forsaken",
+    "Fractured",
+    "FroZen",
+    "Full Moon",
+    "Furious",
+    "Furthest",
+    "Gentle",
+    "Ghostly",
+    "Glistening",
+    "Gloomy",
+    "Glowing",
+    "Goblin",
+    "Golden",
+    "Granite",
+    "Grey",
+    "Grim",
+    "GriZzly",
+    "Haunted",
+    "Hidden",
+    "Hollow",
+    "Hopeless",
+    "Howling",
+    "Hungry",
+    "InFernal",
+    "Infinite",
+    "Invisible",
+    "Iron",
+    "Jade",
+    "Jagged",
+    "Laughing",
+    "Lifeless",
+    "Liminal",
+    "Living",
+    "Lonely",
+    "Lost",
+    "Lower",
+    "Lucent",
+    "Lunar",
+    "Mad",
+    "Mighty",
+    "Mirrored",
+    "Misty",
+    "Moaning",
+    "Molten",
+    "Mourning",
+    "Murky",
+    "Mysterious",
+    "Mystic",
+    "Mythic",
+    "Nameless",
+    "Narrow",
+    "Neglected",
+    "Nether",
+    "Neverending",
+    "Nightmare",
+    "Northern",
+    "Obliterated",
+    "Oblivion",
+    "Obsidian",
+    "Orc",
+    "Pale",
+    "PHantom",
+    "Poisoned",
+    "Prismic",
+    "Quick",
+    "Quiet",
+    "Raging",
+    "RainBow",
+    "Red",
+    "Restless",
+    "Roaring",
+    "Rocky",
+    "Rugged",
+    "Ruthless",
+    "Sad",
+    "Sanguine",
+    "Savage",
+    "Scarlet",
+    "Scorched",
+    "Screaming",
+    "Secret",
+    "Serene",
+    "Shadow",
+    "Shadowed",
+    "Shadowy",
+    "Shimmering",
+    "Shrieking",
+    "Shrouded",
+    "Shunned",
+    "Silent",
+    "Silver",
+    "Sleeping",
+    "Smoky",
+    "Smoldering",
+    "Southern",
+    "Specter",
+    "Spirit",
+    "Steel",
+    "Storm",
+    "Sunken",
+    "Swamp",
+    "Terraced",
+    "Thief",
+    "Thundering",
+    "Tormented",
+    "Tranquil",
+    "Turbulent",
+    "Twilight",
+    "Twisted",
+    "Twisting",
+    "Uncanny",
+    "Unholy",
+    "UnLucky",
+    "Unknown",
+    "Unmourned",
+    "Unseen",
+    "Unspoken",
+    "Unstable",
+    "Vanished",
+    "Vanishing",
+    "Vanquished",
+    "Veiled",
+    "Vicious",
+    "Violent",
+    "Voiceless",
+    "Wailing",
+    "Wasted",
+    "WatCHing",
+    "Western",
+    "Whispering",
+    "Wicked",
+    "Wild",
+    "Windy",
+    "Winter",
+    "Withered",
+    "Yawning",
+    "Zealous",
+];
+
+var LOCATION = [
+    "Abbey",
+    "Abyss",
+    "Alley",
+    "Barracks",
+    "Burrows",
+    "Castle",
+    "Catacomb",
+    "Cave",
+    "Caverns",
+    "CHambers",
+    "Chasm",
+    "Crypt",
+    "Delve",
+    "Demesne",
+    "Den",
+    "Desert",
+    "Domain",
+    "Drop",
+    "Dungeon",
+    "Egress",
+    "Fissure",
+    "Forest",
+    "Forge",
+    "Gate",
+    "Grave",
+    "Grotto",
+    "Hall",
+    "Halls",
+    "Haunt",
+    "Hole",
+    "Hollow",
+    "Ingress",
+    "Jail",
+    "Jungle",
+    "Keep",
+    "Labyrinth",
+    "Lair",
+    "Lake",
+    "Lowlands",
+    "Manse",
+    "Mansion",
+    "Marsh",
+    "Mausoleum",
+    "MaZe",
+    "Mine",
+    "Mound",
+    "Mountain",
+    "Necropolis",
+    "Nightmare",
+    "Ossuary",
+    "Oubliette",
+    "Pale",
+    "Pass",
+    "Passage",
+    "Pit",
+    "Pool",
+    "Prison",
+    "Pyramid",
+    "Quarters",
+    "Refuge",
+    "Rest",
+    "Rift",
+    "Sepulchre",
+    "Shrine",
+    "Stockade",
+    "Swamp",
+    "Tomb",
+    "Tor",
+    "Tower",
+    "Tunnels",
+    "Vale",
+    "Valley",
+    "Vault",
+    "Waste",
+];
+
+var STATE = [
+    "Abomination",
+    "Bloodlust",
+    "Bloodshed",
+    "CHange",
+    "Chaos",
+    "Courage",
+    "Cunning",
+    "DeJection",
+    "Death",
+    "Delusion",
+    "Destruction",
+    "Dread",
+    "Earth",
+    "Ennui",
+    "Fortune",
+    "Grief",
+    "Hate",
+    "Illness",
+    "InJury",
+    "Loneliness",
+    "Loss",
+    "Murder",
+    "Night",
+    "Oblivion",
+    "Pain",
+    "Quiet",
+    "Regret",
+    "Remorse",
+    "Ruin",
+    "Slumber",
+    "Sorrow",
+    "Torment",
+    "Valor",
+    "Vexation",
+    "Woe",
+    "Zealotry",
+];
+
+var DENIZEN = [
+    "Alicorn",
+    "Army",
+    "Baku",
+    "Banshee",
+    "Bat",
+    "Bear",
+    "BogGart",
+    "Bunyip",
+    "CHangling",
+    "Cult",
+    "Demon",
+    "DraGon",
+    "Drake",
+    "Draugr",
+    "Dwarf",
+    "Eagle",
+    "Elf",
+    "Emperor",
+    "Erinyes",
+    "Ettin",
+    "Fae",
+    "Fairy",
+    "Full Moon",
+    "Furies",
+    "Ghoul",
+    "Giant",
+    "Goblin",
+    "Guardian",
+    "Harpy",
+    "Heart",
+    "Hellhound",
+    "Hob",
+    "Horsemen",
+    "Hound",
+    "Huldra",
+    "Huma",
+    "Hunter",
+    "Hydra",
+    "Ifrit",
+    "Imp",
+    "Incubus",
+    "Irrlicht",
+    "Jiangshi",
+    "Jinn",
+    "KaPpa",
+    "Kelpie",
+    "King",
+    "Kirin",
+    "Kobold",
+    "Lamia",
+    "Legion",
+    "Leopard",
+    "Lich",
+    "Lion",
+    "Lynx",
+    "Mage",
+    "Mandrake",
+    "Mara",
+    "Mermaid",
+    "Monk",
+    "Moon",
+    "Naga",
+    "Naiad",
+    "Nekomata",
+    "Nue",
+    "Nymph",
+    "Ogre",
+    "Oni",
+    "Oracle",
+    "Orc",
+    "Panther",
+    "Pegasus",
+    "PHantasm",
+    "PHantom",
+    "PHoenix",
+    "Pixie",
+    "Priest",
+    "Python",
+    "Queen",
+    "QuinoTaur",
+    "RainBow",
+    "Raven",
+    "Roc",
+    "Salamander",
+    "Satyr",
+    "Scorpion",
+    "Selkie",
+    "SerPent",
+    "Slyph",
+    "Soldier",
+    "Sphinx",
+    "Spider",
+    "Strigoi",
+    "Tanuki",
+    "Taotie",
+    "Tarasque",
+    "Tengu",
+    "Titan",
+    "Troll",
+    "Undead",
+    "Undine",
+    "Unicorn",
+    "Valkyrie",
+    "Vampire",
+    "WarLord",
+    "Warrior",
+    "Wendigo",
+    "Werewolf",
+    "WiZard",
+    "Witch",
+    "Wolf",
+    "Wraith",
+    "Wyrm",
+    "Wyvern",
+    "Yama-Uba",
+    "Yeti",
+    "Youkai",
+    "Zombie",
+    "dJinn",
+    "kNight",
+];
+
+function capitalize (s) {
+    return s.split(" ").map(function (s) {
+        return s[0].toUpperCase() + s.slice(1).toLowerCase();
+    }).join(" ");
+}
+
+function consonant (b) {
+    var a = this.replace(/C/g, 'K')
+        .replace(/PH/g, 'F')
+        .replace(/[^A-Z]+/g, ' ')
+        .trimRight().split(/ +/g);
+    var b = b.replace(/C/g, 'K')
+        .replace(/PH/g, 'F')
+        .replace(/[^A-Z]+/g, ' ')
+        .trimRight().split(/ +/g);
+    return intersects(a, b);
+}
+
+function generateName () {
+    var location = choice(LOCATION);
+    var denizen = capitalize(choice(
+        DENIZEN.filter(consonant, location)));
+    var adjective = capitalize(choice(
+        ADJECTIVE.filter(consonant, location)));
+    var state = capitalize(choice(
+        STATE.filter(consonant, location)));
+    location = capitalize(location);
+    return choice([
+        ["The", denizen + "'s", location],
+        ["The", adjective, location],
+        ["The", location, "of", state],
+        [state + "'s", location],
+        ["The", location, "of", "the", denizen],
+        choice([
+            [capitalize(choice(ADJECTIVE)), denizen, location],
+            [adjective, denizen, capitalize(choice(LOCATION))],
+            ]),
+    ]).join(" ");
+}
diff --git a/scenario.html b/scenario.html
new file mode 100644 (file)
index 0000000..259aaa4
--- /dev/null
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html manifest=heroik.appcache>
+  <!--
+  The person who associated a work with this deed has dedicated the work
+  to the public domain by waiving all of his or her rights to the work
+  worldwide under copyright law, including all related and neighboring
+  rights, to the extent allowed by law.
+
+  You can copy, modify, distribute and perform the work, even for
+  commercial purposes, all without asking permission.
+
+  See https://creativecommons.org/publicdomain/zero/1.0/ for details.
+  -->
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
+    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
+    <meta name="apple-mobile-web-app-capable" content="yes">
+    <meta name="apple-mobile-web-app-title" content="Hero: IK">
+    <meta name="mobile-web-app-capable" content="yes">
+    <link rel="apple-touch-icon" href="favicon_192.png">
+    <link rel=icon type="image/png" sizes="32x32" href="favicon_32.png">
+    <link rel=icon type="image/png" sizes="192x192" href="favicon_192.png">
+    <link rel=icon type="image/png" sizes="256x256" href="favicon_256.png">
+    <link rel=stylesheet href="heroik.css" type="text/css">
+    <script src="fastclick.js" type="text/javascript"></script>
+    <script src="heroik.js" type="text/javascript"></script>
+    <script src="names.js" type="text/javascript"></script>
+    <script src="scenarios.js" type="text/javascript"></script>
+    <title>- Hero: Immortal King</title>
+  </head>
+  <body>
+    <div id=statusbar></div>
+    <main>
+    <div style="height: 3em" class="no-print">
+      <a class=button style="float: left; text-align: left"
+         href="heroik.html">
+        ◄ Finished!
+      </a>
+      <a class=button style="float: right; text-align: right"
+         onclick="nextEvent(this)">
+        Press On ▼
+      </a>
+    </div>
+    <h1 id=name onclick="randomizeName()">The Boiling Forest</h1>
+    <table id=fate>
+      <thead><tr><th>Fate Chart</th><th>Event</th></tr></thead>
+      <tbody></tbody>
+      <tfoot>
+        <tr>
+          <td></td>
+          <td>You stand outside the entrance.</td></tr>
+      </tfoot>
+    </table>
+    </main>
+  </body>
+</html>
diff --git a/scenarios.js b/scenarios.js
new file mode 100644 (file)
index 0000000..6b05d7c
--- /dev/null
@@ -0,0 +1,219 @@
+/* The person who associated a work with this deed has dedicated the
+   work to the public domain by waiving all of his or her rights to
+   the work worldwide under copyright law, including all related and
+   neighboring rights, to the extent allowed by law.
+
+   You can copy, modify, distribute and perform the work, even for
+   commercial purposes, all without asking permission.
+
+   See https://creativecommons.org/publicdomain/zero/1.0/ for details.
+*/
+
+"use strict";
+
+var EVENTS = [
+    { name: "The Greenskins are mobilized.",
+      effect: "Greenskins gain a bonus of +1 Strength." },
+    { name: "Big eye is watching you.",
+      effect: "Bubbleyes gain a bonus of +1 Strength.",
+      requires: ["bubbleyes"] },
+    { name: "General invocation.",
+      effect: "Demons gain a bonus of +1 Strength.",
+      requires: ["demons"] },
+    { name: "Stronger than ever.",
+      effect: "Undead gain a bonus of +1 Strength.",
+      requires: ["undead"] },
+    { name: "Meeting with Abunakkashi.",
+      effect: "Abunakkashii and his Offspring gain a bonus of +2 Strength.",
+      requires: ["abunakkashii"],
+      unique: true },
+    { name: "Technological prowess.",
+      effect: "Traps gain a bonus of +1 Strength.",
+      requires: ["traps"] },
+
+    { name: "Mental combat.",
+      effect: "Psi monsters gain a bonus of +1 Strength.", },
+    { name: "A cry in the night.",
+      action: "An adventurer of your choice loses 1 Mana." },
+    { name: "It's an ambush!",
+      action: "Place a Tenacity token on each monster that does not have one." },
+    { name: "The alarm is sounded.",
+      action: "Place a Tenacity token on the weakest monster or monsters that do not already have one." },
+    { name: "Flank attack.",
+      effect: "Cards on either side corridor gain a bonus of +1 Strength.",
+      unique: true },
+    { name: "Battle formation.",
+      effect: "Cards in the central corridor(s) gain a bonus of +1 Strength.",
+      unique: true },
+    { name: "Last bastion.",
+      effect: "All cards gain a bonus of +1 Strength.",
+      unique: true },
+
+    { name: "Destruction.",
+      action: "All equipment and items are destroyed.",
+      unique: true },
+    { name: "Epic combat.",
+      effect: "The Final Monster gains a bonus of +3 Strength.",
+      unique: true },
+    
+    // Events from Sean Allen's random scenario generator.
+    // http://boardgamegeek.com/filepage/57107/random-scenario-generator
+    { name: "Fire from above.",
+      effect: "Dragons gains a bonus of +1 Strength.",
+      requires: ["dragons", "noncanonical"],
+      unique: true },
+    { name: "Bad dreams.",
+      action: "Lose 1 extra Courage token. (The dungeon does not gain another Fear token.)",
+      requires: ["noncanonical"],
+      unique: true },
+    { name: "Backs against the wall.",
+      effect: "Any monster with a Tenacity token is also Fierce.",
+      requires: ["noncanonical"],
+      unique: true },
+    { name: "Dead end.",
+      action: "Shuffle the remaining corridors together and redistribute the cards as if you were setting up the game.",
+      requires: ["noncanonical"] },
+    { name: "Surrounded.",
+      effect: "All monsters have Supremacy.",
+      requires: ["noncanonical"],
+      unique: true },
+
+    // Events from Stephane Renard's scenario.
+    // http://docfox.free.fr/spip.php?article129
+    { name: "What was that?",
+      action: "The next dungeon card is placed face-down.",
+      requires: ["noncanonical"] },
+    { name: "Malediction.",
+      action: "Discard all quests.",
+      requires: ["noncanonical"],
+      unique: true },
+    { name: "Reanimation.",
+      action: "Return a random defeated card to the smallest corridor and shuffle it.",
+      requires: ["noncanonical"] },
+    { name: "Zone of silence.",
+      effect: "Temporary and Ultimate Powers cannot be used until you reveal a new card.",
+      requires: ["noncanonical"] },
+
+    // Events of my own devising.
+    { name: "Unwanted attention.",
+      effect: "Unique monsters gain a bonus of +2 Strength.",
+      requires: ["noncanonical"],
+      unique: true },
+    { name: "Spiked the punch.",
+      effect: "Greenskins have 1d8 Strength.",
+      requires: ["noncanonical"],
+      unique: true },
+    { name: "Dropped the torch.",
+      effect: "After defeating a card, roll a die. On an odd number its replacement is placed face-down.",
+      requires: ["noncanonical"],
+      unique: true },
+    { name: "Closer than you think.",
+      action: "The dungeon gains another Fear token, and a new event occurs.",
+      requires: ["noncanonical"],
+      another: true, unique: true },
+    { name: "I thought you had it.",
+      action: "Randomly discard four of your defeated dungeon cards.",
+      requires: ["noncanonical"] },
+    { name: "Adamantine armor.",
+      effect: "All monsters gain Immunity&nbsp;1.",
+      requires: ["noncanonical"],
+      unique: true },
+    { name: "Normative assumptions.",
+      effect: "Effects concerning ♂ instead concern ♀, and vice versa.",
+      requires: ["noncanonical"],
+      unique: true },
+];
+
+var NOTHING = { name: "Nothing happens." };
+var LOSE = { name: "Your adventuring party is defeated!" };
+
+function randrange (a, b) {
+    return a + (Math.random() * (b - a)) | 0;
+}
+
+function generate (flags, events, nop) {
+    var chosen = [];
+    var i;
+
+    function canStillHappen (event) {
+        return issubset(event.requires || [], flags)
+            && !(event.unique && contains.call(chosen, event));
+    }
+
+    for (i = 0; i < events; ++i)
+        chosen.push(choice(EVENTS.filter(canStillHappen)));
+
+    for (i = 0; i < nop; ++i)
+        chosen.splice(randrange(0, chosen.length), 0, NOTHING);
+
+    chosen.push(LOSE);
+    return chosen;
+}
+
+function toHTML (event) {
+    return ["<span class=fate-name>" + event.name + "</span>",
+            event.action
+            ? "<span class=fate-action>" + event.action + "</span>"
+            : "",
+            event.effect
+            ? "<span class=fate-effect>" + event.effect + "</span>"
+            : "",
+        ].join(" ");
+}
+
+var THEME = ("h1 { color: hsl(XXX, 25%, 75%); }\n\
+h2, .button, select { border-color: hsl(XXX, 30%, 85%); }\n\
+\n\
+a:link, a:visited, a:active {\n\
+    color: hsl(XXX, 25%, 50%);\n\
+}\n\
+\n\
+select, .button, tbody tr:nth-last-child(odd) {\n\
+    background-color: hsl(XXX, 30%, 85%);\n\
+}\n\
+\n\
+h1,\n\
+select:hover, select:focus,\n\
+.button:hover, .button:focus {\n\
+    border-color: hsl(XXX, 25%, 50%);\n\
+}");
+
+function randomizeName () {
+    var name = generateName();
+    document.getElementById("name").textContent = name;
+    document.title = document.title.replace(/[^-]*-/, name + " -");
+    document.head.lastChild.textContent = THEME.replace(
+        /XXX/g, (Math.random() * 256) | 0);
+}
+
+var events = [];
+var style;
+window.addEventListener('DOMContentLoaded', function () {
+    var parts = location.hash.slice(1).split(',');
+    events = generate(parts, parts.shift() | 0, parts.shift() | 0);
+    style = document.createElement("style");
+    document.head.appendChild(style);
+    randomizeName();
+});
+
+function nextEvent (sender) {
+    if (!events.length) {
+        location.reload();
+        return;
+    }
+
+    var event = events.shift();
+    var body = document.querySelector("#fate tbody");
+    var fate = body.children.length + 1;
+    var tr = document.createElement('tr');
+    tr.innerHTML = "<td><div>" + fate + "</div></td>"
+        + "<td><div>" + toHTML(event) + "</div></td>";
+    body.insertBefore(tr, body.firstChild);
+
+    if (events.length === 0) {
+        sender.textContent = "Try Again ▲";
+    }
+
+    if (event.another)
+        setTimeout(function () { nextEvent(sender); }, 333);
+}
diff --git a/variants.html b/variants.html
new file mode 100644 (file)
index 0000000..31fc855
--- /dev/null
@@ -0,0 +1,220 @@
+<!DOCTYPE html>
+<html manifest=heroik.appcache>
+  <!--
+  The person who associated a work with this deed has dedicated the work
+  to the public domain by waiving all of his or her rights to the work
+  worldwide under copyright law, including all related and neighboring
+  rights, to the extent allowed by law.
+
+  You can copy, modify, distribute and perform the work, even for
+  commercial purposes, all without asking permission.
+
+  See https://creativecommons.org/publicdomain/zero/1.0/ for details.
+  -->
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
+    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
+    <meta name="apple-mobile-web-app-capable" content="yes">
+    <meta name="apple-mobile-web-app-title" content="Hero: IK">
+    <meta name="mobile-web-app-capable" content="yes">
+    <meta name="format-detection" content="telephone=no">
+    <link rel="apple-touch-icon" href="favicon_192.png">
+    <link rel=icon type="image/png" sizes="32x32" href="favicon_32.png">
+    <link rel=icon type="image/png" sizes="192x192" href="favicon_192.png">
+    <link rel=icon type="image/png" sizes="256x256" href="favicon_256.png">
+    <link rel=stylesheet href="heroik.css" type="text/css">
+    <script src="fastclick.js" type="text/javascript"></script>
+    <script src="heroik.js" type="text/javascript"></script>
+    <title>Variants - Hero: Immortal King</title>
+  </head>
+  <body>
+    <div id=statusbar></div>
+    <main>
+    <a class="button no-print" href="heroik.html">
+      ◀ Return
+    </a>
+    <h1>Suggested Rules</h1>
+    <h2>Tactical Retreat</h2>
+    <p>
+      During the equipment phase, the adventurer player may, as many
+      times as she likes, discard one Courage token to
+    </p>
+    <ul>
+      <li>remove a Tenacity token from a face-up monster; or</li>
+      <li>return a face-up monster without a Tenacity token to its
+        deck, reshuffle it, and reveal a new card.</li>
+    </ul>
+    <p>
+      The dungeon master gains one Fear token for each Courage token
+      discarded.
+    </p>
+    <p>
+      This helps prevent "lock-out" situations while still imposing
+      a penalty if the player finds themselves in one.
+    </p>
+    <h2>Facing Fears</h2>
+    <p>
+      In the solitaire game, when a <a href="abilities.html#Fierce">Fierce</a>
+      monster is defeated, the dungeon loses one Fear token. However,
+      the Fate Chart continues advancing normally.
+    </p>
+    <p>
+      This keeps abilities that depend on the ratio of the two, like
+      <a href="abilities.html#Supremacy">Supremacy</a>, more relevant
+      in solitaire play.
+    </p>
+    <h2>Nem Akh</h2>
+    <p>
+      Change <strong>Nem Akh</strong>'s skill to:
+    </p>
+    <blockquote>
+      Start with 2 extra Courage tokens. You may gain 1 Courage token
+      when defeating the first card of each deck.
+    </blockquote>
+    <p>
+      In the usual case this provides the same number of Courage
+      tokens for only a little more work, which is offset by reducing
+      the risk of a <a href="abilities.html#C">C</a> card at the start
+      and letting the adventurer player bank the token for later. In
+      the case of the unusual dungeons described below, it makes her
+      ability remain useful.
+    </p>
+      
+    <h1>Larger Dungeons</h1>
+    <h2>Longer Corridors</h2>
+    <p>
+      In the constructed game more than 45 cards can be used, in
+      multiples of six. The deck as a whole must meet the normal
+      deckbuilding criteria — equal amounts of each color, half with a
+      Mana icon. If the deck size is a multiple of six but not four,
+      it must have one card more of two colors, one of which has a
+      Mana icon.
+    </p>
+    <p>
+      For every six extra cards, the adventurer player may start with
+      one more Courage token; or for every 18, one more card. This can
+      be mixed and matched, e.g. 24 extra cards can be used to start
+      with four extra Courage tokens, or one extra Courage token and
+      one card.
+    </p>
+    <h2>Multiple Dungeons</h2>
+    <p>
+      Use the above variant skill for <strong>Nem Akh</strong>. After
+      defeating the final monster, the adventurer player:
+    </p>
+    <ul>
+      <li>
+        takes two Courage tokens.
+      </li>
+      <li>
+        must discard all revealed quests.
+      </li>
+      <li>
+        may discard unwanted adventurers. These are removed from the
+        game and <strong>Aka</strong>'s ultimate power cannot be used
+        to retrieve them.
+      </li>
+      <li>
+        returns all other non-adventurer cards to her collection. She
+        may spend any number of Courage tokens to keep that many
+        previously revealed objects; if she does they remain
+        revealed. (The dungeon master gains no Fear tokens for this.)
+      </li>
+      <li>
+        picks cards from her collection, back up to five total,
+        including kept revealed cards.
+      </li>
+      <li>
+        keeps the defeated final monster. It counts as one of each
+        color, but must be spent all at once.
+      </li>
+    </ul>
+    <p>
+      The dungeon master:
+    </p>
+    <ul>
+      <li>
+        can take up to one undefeated dungeon card of each color from
+        the previous deck and shuffle them into the next dungeon deck.
+      </li>
+      <li>
+        takes one Fear token per adventurer, plus one more.
+      </li>
+      <li>
+        deals out the corridors for the next dungeon.
+      </li>
+      <li>
+        starts the first Construction phase in the new dungeon.
+      </li>
+    </ul>
+    <p>
+      In the solitaire game a new scenario begins with Fear tokens and
+      the Fate Chart already at <strong>1</strong>.
+    </p>
+    <h2>The Deepest Dungeon</h2>
+    <p>
+      Prepare one dungeon deck of 48 cards, and another of 80. The decks
+      must follow the normal construction restrictions, and the same
+      <a href="abilities.html#Unique">Unique</a> card cannot be in
+      both decks.
+    </p>
+    <p>
+      Deal the 48 card deck out into three decks as usual. Below that,
+      deal the 80 card deck out into four decks, with 24 cards in the
+      center rows. Place final monsters face-down (if playing
+      solitaire, randomly and secretly), one at the top of the three
+      decks and the other two above the two center decks. The
+      resulting layout should look like:
+    </p>
+    <div><span class=card2>FM</span><br><span class=card1>16</span><span class=card1>16</span><span class=card1>16</span><br><span class=card2>FM</span><span class=card2>FM</span><br><span class=card1>16</span><span class=card1>24</span><span class=card1>24</span><span class=card1>16</span></div>
+    <p>
+      The adventurer player starts at the four bottom decks. The
+      leftmost three lead to the final monster on the left, and
+      the rightmost three to the final monster on the right.
+      Defeating the left one gives access to the top left and central
+      decks, and the right one, to the top central and right
+      decks. 
+    </p>
+    <p>
+      (Or, if <strong>Aksuetu</strong> or another effect causes an
+      extra deck to be dealt,
+    </p>
+    <div><span class=card2>FM</span><br><span class=card1>12</span><span class=card1>12</span><span class=card1>12</span><span class=card1>12</span><br><span class=card2>FM</span><span style="display: inline-block; width: 3.375em"></span><span class=card2>FM</span><br><span class=card1>16</span><span class=card1>16</span><span class=card1>16</span><span class=card1>16</span><span class=card1>16</span></div>
+    <p>
+      The leftmost three lead to the final monster on the left, and
+      the rightmost three to the final monster on the right. Defeating
+      the left one gives access to the top leftmost two, and the right
+      one, to the top rightmost two.)
+    </p>
+    <p>
+      The turn sequence proceeds as usual. The adventurer player wins
+      by defeating the final monster at the top.
+    </p>
+    <ul>
+      <li>
+        for three Fear tokens the dungeon master may reveal a final
+        monster, enabling its effect. Otherwise, a final monster is
+        revealed when the adventurers empty a deck that reaches
+        it.
+      </li>
+      <li>
+        effects that affect dungeon cards, such as
+        <a href="abilities.html#Bubbleyes">Bubbleyes&nbsp;+X</a>,
+        affect all face-up dungeon cards on both levels.
+      </li>
+      <li>
+        effects that reshuffle or move dungeon cards apply to both
+        levels, but separately. They are never shuffled together, and
+        the dungeon master cannot swap top and bottom decks.
+      </li>
+      <li>
+        upon defeating a final monster, the adventurer player may take
+        two Courage tokens, one new card from her collection, and
+        the final monster. It counts as one of each color, but must be
+        spent all at once.
+      </li>
+    </ul>
+    </main>
+  </body>
+</html>