Terrible font hacks to get an outline.
[heroik.git] / scenarios.js
1 /* The person who associated a work with this deed has dedicated the
2 work to the public domain by waiving all of his or her rights to
3 the work worldwide under copyright law, including all related and
4 neighboring rights, to the extent allowed by law.
5
6 You can copy, modify, distribute and perform the work, even for
7 commercial purposes, all without asking permission.
8
9 See https://creativecommons.org/publicdomain/zero/1.0/ for details.
10 */
11
12 "use strict";
13
14 var EVENTS = [
15 { name: "The Greenskins are mobilized.",
16 effect: "Greenskins gain a bonus of +1 Strength." },
17 { name: "Big eye is watching you.",
18 effect: "Bubbleyes gain a bonus of +1 Strength.",
19 requires: ["bubbleyes"] },
20 { name: "General invocation.",
21 effect: "Demons gain a bonus of +1 Strength.",
22 requires: ["demons"] },
23 { name: "Stronger than ever.",
24 effect: "Undead gain a bonus of +1 Strength.",
25 requires: ["undead"] },
26 { name: "Meeting with Abunakkashi.",
27 effect: "Abunakkashii and his Offspring gain a bonus of +2 Strength.",
28 requires: ["abunakkashii"],
29 unique: true },
30 { name: "Technological prowess.",
31 effect: "Traps gain a bonus of +1 Strength.",
32 requires: ["traps"] },
33
34 { name: "Mental combat.",
35 effect: "Psi monsters gain a bonus of +1 Strength.", },
36 { name: "A cry in the night.",
37 action: "An adventurer of your choice loses 1 Mana." },
38 { name: "It's an ambush!",
39 action: "Place a Tenacity token on each monster that does not have one." },
40 { name: "The alarm is sounded.",
41 action: "Place a Tenacity token on the weakest monster or monsters that do not already have one." },
42 { name: "Flank attack.",
43 effect: "Cards on either side corridor gain a bonus of +1 Strength.",
44 unique: true },
45 { name: "Battle formation.",
46 effect: "Cards in the central corridor(s) gain a bonus of +1 Strength.",
47 unique: true },
48 { name: "Last bastion.",
49 effect: "All cards gain a bonus of +1 Strength.",
50 unique: true },
51
52 { name: "Destruction.",
53 action: "All equipment and items are destroyed.",
54 unique: true },
55 { name: "Epic combat.",
56 effect: "The Final Monster gains a bonus of +3 Strength.",
57 unique: true },
58 { name: "Red herring.",
59 action: "Shuffle the remaining corridors together and redistribute the cards as if you were setting up the game." },
60
61 // Events from Sean Allen's random scenario generator.
62 // http://boardgamegeek.com/filepage/57107/random-scenario-generator
63 { name: "Fire from above.",
64 effect: "Dragons gain a bonus of +1 Strength.",
65 requires: ["dragons", "noncanonical"],
66 unique: true },
67 { name: "Bad dreams.",
68 action: "Lose 1 extra Courage token. (The dungeon does not gain another Fear token.)",
69 requires: ["noncanonical"],
70 unique: true },
71 { name: "Backs against the wall.",
72 effect: "Any monster with a Tenacity token is also <strong>Fierce</strong>.",
73 requires: ["noncanonical"],
74 unique: true },
75 { name: "Surrounded.",
76 effect: "All monsters have <strong>Supremacy</strong>.",
77 requires: ["noncanonical"],
78 unique: true },
79 { name: "The torch has gone out.",
80 action: "The next dungeon card is placed face-down.", },
81
82 // Events from Stephane Renard's scenario.
83 // http://docfox.free.fr/spip.php?article129
84 { name: "Malediction.",
85 action: "Discard all quests.",
86 requires: ["noncanonical"],
87 unique: true },
88 { name: "Reanimation.",
89 action: "Return a random spent dungeon card to the smallest corridor and shuffle it. (If you haven't spent any cards, nothing happens.)",
90 requires: ["noncanonical"] },
91 { name: "Zone of silence.",
92 effect: "Temporary and Ultimate Powers cannot be used until you reveal a new card.",
93 requires: ["noncanonical"] },
94
95 // Events of my own devising.
96 { name: "Unwanted attention.",
97 effect: "Unique monsters gain a bonus of +1 Strength.",
98 requires: ["noncanonical"],
99 unique: true },
100 { name: "Spiked the punch.",
101 effect: "Greenskins have 1d8 Strength.",
102 requires: ["noncanonical"],
103 unique: true },
104 { name: "Lost in the fog.",
105 effect: "After defeating a card, roll a die. On an odd number its replacement is placed face-down.",
106 requires: ["noncanonical"],
107 unique: true },
108 { name: "Closer than you think.",
109 action: "The dungeon gains another Fear token, and a new event occurs.",
110 requires: ["noncanonical"],
111 another: true, unique: true },
112 { name: "I thought you had it.",
113 action: "Randomly discard four of your defeated dungeon cards.",
114 requires: ["noncanonical"] },
115 { name: "Adamantine armor.",
116 effect: "All monsters gain <strong>Immunity&nbsp;1</strong>.",
117 requires: ["noncanonical"],
118 unique: true },
119 { name: "Normative assumptions.",
120 effect: "Effects concerning ♂ instead concern ♀, and vice versa.",
121 requires: ["noncanonical"],
122 unique: true },
123 { name: "Infighting.",
124 effect: "Greenskins have <strong>Undead&nbsp;+1</strong>, <strong>Demons&nbsp;-1</strong>. Demons have <strong>Greenskins&nbsp;+1</strong>, <strong>Undead&nbsp;-1</strong>. Undead have <strong>Demons&nbsp;+1</strong>, <strong>Greenskins&nbsp;-1</strong>.",
125 requires: ["noncanonical"],
126 unique: true },
127 { name: "New moon.",
128 effect: "Demons have <strong>Veil of Shadow</strong>.",
129 requires: ["noncanonical", "demons"],
130 unique: true },
131 { name: "Camouflage.",
132 effect: "Greenskins have <strong>Veil of Shadow</strong>.",
133 requires: ["noncanonical"],
134 unique: true },
135 { name: "Cryptic shades.",
136 effect: "Undead have <strong>Veil of Shadow</strong>.",
137 requires: ["noncanonical", "undead"],
138 unique: true },
139 { name: "Lingering smoke.",
140 effect: "Dragons have <strong>Veil of Shadow</strong>.",
141 requires: ["noncanonical", "dragons"],
142 unique: true },
143 { name: "Hypnotizing gaze.",
144 effect: "Bubbleyes have <strong>Veil of Shadow</strong>.",
145 requires: ["noncanonical", "bubbleyes"],
146 unique: true },
147
148 ];
149
150 var NOTHING = { name: "Nothing happens." };
151 var LOSE = { name: "Your adventuring party is defeated!" };
152
153 function randrange (a, b) {
154 return a + (Math.random() * (b - a)) | 0;
155 }
156
157 function generate (flags, events, nop) {
158 var chosen = [];
159 var i;
160
161 function canStillHappen (event) {
162 return issubset(event.requires || [], flags)
163 && !(event.unique && contains.call(chosen, event));
164 }
165
166 for (i = 0; i < events; ++i)
167 chosen.push(choice(EVENTS.filter(canStillHappen)));
168
169 for (i = 0; i < nop; ++i)
170 chosen.splice(randrange(0, chosen.length), 0, NOTHING);
171
172 chosen.push(LOSE);
173 return chosen;
174 }
175
176 function toHTML (event) {
177 return ["<span class=fate-name>" + event.name + "</span>",
178 event.action
179 ? "<span class=fate-action>" + event.action + "</span>"
180 : "",
181 event.effect
182 ? "<span class=fate-effect>" + event.effect + "</span>"
183 : "",
184 ].join(" ");
185 }
186
187 var THEME = ("h1 { color: hsl(XXX, 25%, 75%); }\n\
188 h2, .button, select { border-color: hsl(XXX, 30%, 85%); }\n\
189 \n\
190 a:link, a:visited, a:active {\n\
191 color: hsl(XXX, 25%, 50%);\n\
192 }\n\
193 \n\
194 select, .button, tbody tr:nth-last-child(odd) {\n\
195 background-color: hsl(XXX, 30%, 85%);\n\
196 }\n\
197 \n\
198 h1,\n\
199 select:hover, select:focus,\n\
200 .button:hover, .button:focus {\n\
201 border-color: hsl(XXX, 25%, 50%);\n\
202 }");
203
204 function randomizeName () {
205 var name = generateName();
206 document.getElementById("name").textContent = name;
207 document.title = document.title.replace(/[^-]*-/, name + " -");
208 document.head.lastChild.textContent = THEME.replace(
209 /XXX/g, (Math.random() * 256) | 0);
210 }
211
212 var events = [];
213 var style;
214
215 function generateScenario () {
216 var parts = location.hash.slice(1).split(',');
217 events = generate(parts, parts.shift() | 0, parts.shift() | 0);
218 style = document.createElement("style");
219 document.head.appendChild(style);
220 randomizeName();
221 }
222
223 function getEvents (matcher) {
224 return EVENTS.filter(matcher);
225 }
226
227 function wrapRow (row) {
228 return "<tr><td>" + row + "</td></tr>";
229 }
230
231 function iscanonical (event) {
232 return !isnoncanonical(event);
233 }
234 function isnoncanonical (event) {
235 return ~(event.requires || []).indexOf('noncanonical');
236 }
237
238 function canonicalToHTML (sender) {
239 sender.innerHTML = EVENTS.filter(iscanonical)
240 .map(toHTML).sort().map(wrapRow).join('');
241 }
242
243 function noncanonicalToHTML (sender) {
244 sender.innerHTML = EVENTS.filter(isnoncanonical)
245 .map(toHTML).sort().map(wrapRow).join('');
246 }
247
248 function nextEvent (sender) {
249 if (!events.length) {
250 location.reload();
251 return;
252 }
253
254 var event = events.shift();
255 var body = document.querySelector("#fate tbody");
256 var fate = body.children.length + 1;
257 var tr = document.createElement('tr');
258 tr.innerHTML = "<td><div>" + fate + "</div></td>"
259 + "<td><div>" + toHTML(event) + "</div></td>";
260 body.insertBefore(tr, body.firstChild);
261
262 if (events.length === 0) {
263 sender.textContent = "Try Again ▲";
264 }
265
266 if (event.another)
267 setTimeout(function () { nextEvent(sender); }, 333);
268 }