12bfa3e857c16bd2559200fbb1d6357711478ad8
[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: "The start of a legend.",
31 effect: "Abunakkashii and his Offspring gain a bonus of +1 Strength.",
32 requires: ["abunakkashii"],
33 unique: true },
34 { name: "Technological prowess.",
35 effect: "Traps gain a bonus of +1 Strength.",
36 requires: ["traps"] },
37
38 { name: "Mental combat.",
39 effect: "Psi monsters gain a bonus of +1 Strength.", },
40 { name: "A cry in the night.",
41 action: "An adventurer of your choice loses 1 Mana." },
42 { name: "Vague psi.",
43 action: "Lose all your Mana tokens." },
44 { name: "It's an ambush!",
45 action: "Place a Tenacity token on each monster that does not have one." },
46 { name: "The alarm is sounded.",
47 action: "Place a Tenacity token on the weakest monster or monsters that do not already have one." },
48 { name: "Flank attack.",
49 effect: "Cards on either side corridor gain a bonus of +1 Strength.",
50 unique: true },
51 { name: "Battle formation.",
52 effect: "Cards in the central corridor(s) gain a bonus of +1 Strength.",
53 unique: true },
54 { name: "Last bastion.",
55 effect: "All cards gain a bonus of +1 Strength.",
56 unique: true },
57
58 { name: "Destruction.",
59 action: "All equipment and items are destroyed.",
60 unique: true },
61 { name: "Epic combat.",
62 effect: "The Final Monster gains a bonus of +3 Strength.",
63 unique: true },
64 { name: "Red herring.",
65 action: "Shuffle the remaining corridors together and redistribute the cards as if you were setting up the game." },
66 { name: "The torch has gone out.",
67 action: "The next dungeon card is placed face-down.", },
68
69 { name: "Anti-psi zone.",
70 effect: "You do not gain Mana tokens for the next three creatures you defeat, even if they have a Mana icon.",
71 unique: true },
72
73 { name: "Necromancy.",
74 action: "Return a random defeated dungeon card to the lowest corridor and shuffle it." },
75 { name: "Reanimation.",
76 action: "Return a random defeated dungeon card to the highest corridor and shuffle it." },
77
78 { name: "Malediction.",
79 action: "Discard a quest or a magic item." },
80
81 { name: "False brethren.",
82 effect: "All Demons gain Immunity 5 and Immunity 7.",
83 requires: ["demons"],
84 unique: true },
85
86 { name: "Psi assault.",
87 effect: "You cannot use ultimate powers.",
88 lock: ["ultimates"],
89 unique: true },
90 { name: "Entering an anti-magic zone.",
91 effect: "You cannot use ultimate powers.",
92 lock: ["ultimates"],
93 duration: 3,
94 later: { name: "Leaving the anti-magic zone.",
95 effect: "You may use ultimate powers again.",
96 clear: ["ultimates"] }
97 },
98
99 // Events from Sean Allen's random scenario generator.
100 // http://boardgamegeek.com/filepage/57107/random-scenario-generator
101 { name: "Fire from above.",
102 effect: "Dragons gain a bonus of +1 Strength.",
103 requires: ["dragons", "noncanonical"],
104 unique: true },
105 { name: "Overwhelming fear.",
106 action: "Lose 1 extra Courage token. (The dungeon does not gain another Fear token.)",
107 requires: ["noncanonical"],
108 unique: true },
109 { name: "Backs against the wall.",
110 effect: "Any monster with a Tenacity token is also <strong>Fierce</strong>.",
111 requires: ["noncanonical"],
112 unique: true },
113 { name: "The tide has turned.",
114 effect: "All monsters have <strong>Supremacy</strong>.",
115 requires: ["noncanonical"],
116 unique: true },
117 { name: "Lost.",
118 action: "Shuffle the remaining corridors together and redistribute the cards as if you were setting up the game.",
119 requires: ["noncanonical"] },
120 { name: "Into the darkness.",
121 effect: "Choose one corridor. Its cards are now always placed face-down.",
122 requires: ["noncanonical"] },
123
124 // Events of my own devising.
125 { name: "Unwanted attention.",
126 effect: "Unique monsters gain a bonus of +1 Strength.",
127 requires: ["noncanonical"],
128 unique: true },
129 { name: "Spiked the punch.",
130 effect: "Greenskins have 1d8 Strength.",
131 requires: ["noncanonical"],
132 unique: true },
133 { name: "Twisty passages.",
134 effect: "After defeating a card, roll a die. On an odd number its replacement is placed face-down.",
135 requires: ["noncanonical"],
136 unique: true },
137 { name: "Closer than you think.",
138 action: "The dungeon gains another Fear token, and a new event occurs.",
139 requires: ["noncanonical"],
140 another: true, unique: true },
141 { name: "I thought you had it.",
142 action: "Randomly discard four of your defeated dungeon cards.",
143 requires: ["noncanonical"] },
144 { name: "Adamantine armor.",
145 effect: "All monsters gain <strong>Immunity&nbsp;1</strong>.",
146 requires: ["noncanonical"],
147 unique: true },
148 { name: "Normative assumptions.",
149 effect: "Effects concerning ♂ instead concern ♀, and vice versa.",
150 requires: ["noncanonical"],
151 unique: true },
152 { name: "Infighting.",
153 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>.",
154 requires: ["noncanonical", "demons", "undead"],
155 unique: true },
156 { name: "New moon.",
157 effect: "Demons have <strong>Veil of Shadow</strong>.",
158 requires: ["noncanonical", "demons"],
159 unique: true },
160 { name: "Camouflage.",
161 effect: "Greenskins have <strong>Veil of Shadow</strong>.",
162 requires: ["noncanonical"],
163 unique: true },
164 { name: "Cryptic shades.",
165 effect: "Undead have <strong>Veil of Shadow</strong>.",
166 requires: ["noncanonical", "undead"],
167 unique: true },
168 { name: "Lingering smoke.",
169 effect: "Dragons have <strong>Veil of Shadow</strong>.",
170 requires: ["noncanonical", "dragons"],
171 unique: true },
172 { name: "Hypnotizing gaze.",
173 effect: "Bubbleyes have <strong>Veil of Shadow</strong>.",
174 requires: ["noncanonical", "bubbleyes"],
175 unique: true },
176 { name: "Leeched power.",
177 action: "A random adventurer loses a Mana token.",
178 requires: ["noncanonical"] },
179
180 ];
181
182 var NOP = [
183 { name: "Nothing happens." },
184 { name: "A draft blows down the hallway.",
185 unique: true, requires: ["noncanonical"] },
186 { name: "You sneeze.",
187 unique: true, requires: ["noncanonical"] },
188 { name: "There's a skittering in the distance.",
189 unique: true, requires: ["noncanonical"] },
190 { name: "The torch flickers.",
191 unique: true, requires: ["noncanonical"] },
192 { name: "Shadows dance across the walls.",
193 unique: true, requires: ["noncanonical"] },
194 ];
195
196 var HELPFUL = [
197 { name: "It cuts both ways.",
198 action: "Next time you roll a 1, put a Tenacity token on all face-up monsters and roll again.",
199 requires: ["helpful"] },
200 { name: "Breached their defense.",
201 action: "Gain 2 Courage tokens and the dungeon gains 1 Fear token and the Fate Chart advances; <em>or</em> gain 1 Mana token.",
202 requires: ["helpful"], unique: true },
203 { name: "Mana ritual.",
204 action: "Discard up to 2 Mana tokens. For each one discarded, gain 1 Courage token.",
205 requires: ["helpful"] }
206 ];
207
208 var LOSE = { name: "Your adventuring party is defeated!" };
209
210 function randrange (a, b) {
211 return a + (Math.random() * (b - a)) | 0;
212 }
213
214 function generate (flags, events, nop) {
215 var chosen = [];
216 var i;
217
218 var pending = [];
219 var locks = [];
220 var event;
221
222 function pend (event, i) {
223 while (pending[i]) ++i
224 pending[i] = event;
225 }
226
227 function canStillHappen (event) {
228 return issubset(event.requires || [], flags)
229 && !(event.unique && contains.call(chosen, event))
230 && !(event.lock && intersects(event.lock || [], locks));
231 }
232
233 for (i = 0; i < events; ++i) {
234 event = pending.shift()
235 || choice(EVENTS.filter(canStillHappen));
236 chosen.push(event);
237 if (event.later)
238 pend(event.later, (Math.random() * chosen[i].duration) | 0);
239 locks = locks.concat(event.lock || [])
240 .filter(not(contains), event.clear || []);
241 }
242
243 for (i = 0; i < pending.length; ++i)
244 if (pending[i])
245 chosen.push(pending[i]);
246
247
248 for (i = 0; i < nop / 2; ++i) {
249 var helpful = HELPFUL.filter(canStillHappen);
250 var neutral = NOP.filter(canStillHappen);
251 chosen.splice(
252 randrange(0, chosen.length), 0,
253 choice(helpful.length ? helpful : neutral));
254
255 }
256 for (;i < nop; ++i) {
257 var neutral = NOP.filter(canStillHappen);
258 chosen.splice(randrange(0, chosen.length), 0, choice(neutral));
259
260 }
261
262 chosen.push(LOSE);
263 return chosen;
264 }
265
266 function toHTML (event) {
267 return ["<span class=fate-name>" + event.name + "</span>",
268 event.action
269 ? "<span class=fate-action>" + event.action + "</span>"
270 : "",
271 event.effect
272 ? "<span class=fate-effect>" + event.effect + "</span>"
273 : "",
274 ].join(" ");
275 }
276
277 var THEME = ("h1 { color: hsl(XXX, 25%, 75%); }\n\
278 h2, .button, select { border-color: hsl(XXX, 30%, 85%); }\n\
279 \n\
280 a:link, a:visited, a:active {\n\
281 color: hsl(XXX, 25%, 50%);\n\
282 }\n\
283 \n\
284 select, .button, tbody tr:nth-last-child(odd) {\n\
285 background-color: hsl(XXX, 30%, 85%);\n\
286 }\n\
287 \n\
288 h1,\n\
289 select:hover, select:focus,\n\
290 .button:hover, .button:focus {\n\
291 border-color: hsl(XXX, 25%, 50%);\n\
292 }");
293
294 function randomizeName () {
295 var name = generateName();
296 document.getElementById("name").textContent = name;
297 document.title = document.title.replace(/[^-]*-/, name + " -");
298 document.head.lastChild.textContent = THEME.replace(
299 /XXX/g, (Math.random() * 256) | 0);
300 }
301
302 var events = [];
303 var style;
304
305 function generateScenario () {
306 var parts = location.hash.slice(1).split(',');
307 events = generate(parts, parts.shift() | 0, parts.shift() | 0);
308 style = document.createElement("style");
309 document.head.appendChild(style);
310 randomizeName();
311 }
312
313 function getEvents (matcher) {
314 return EVENTS.filter(matcher);
315 }
316
317 function wrapRow (row) {
318 return "<tr><td>" + row + "</td></tr>";
319 }
320
321 function iscanonical (event) {
322 return !isnoncanonical(event);
323 }
324 function isnoncanonical (event) {
325 return ~(event.requires || []).indexOf('noncanonical');
326 }
327
328 function canonicalToHTML (sender) {
329 sender.innerHTML = EVENTS.filter(iscanonical)
330 .map(toHTML).sort().map(wrapRow).join('');
331 }
332
333 function noncanonicalToHTML (sender) {
334 sender.innerHTML = EVENTS.filter(isnoncanonical)
335 .map(toHTML).sort().map(wrapRow).join('');
336 }
337
338 function nextEvent (sender) {
339 if (!events.length) {
340 location.reload();
341 return;
342 }
343
344 var event = events.shift();
345 var body = document.querySelector("#fate tbody");
346 var fate = body.children.length + 1;
347 var tr = document.createElement('tr');
348 tr.innerHTML = "<td><div>" + fate + "</div></td>"
349 + "<td><div>" + toHTML(event) + "</div></td>";
350 body.insertBefore(tr, body.firstChild);
351
352 if (events.length === 0) {
353 sender.textContent = "Try Again ▲";
354 }
355
356 if (event.another)
357 setTimeout(function () { nextEvent(sender); }, 333);
358 }