67e4b5cc2dfd9d08e5b24df795c541c790ca0ec3
[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
80 // Events from Stephane Renard's scenario.
81 // http://docfox.free.fr/spip.php?article129
82 { name: "What was that?",
83 action: "The next dungeon card is placed face-down.",
84 requires: ["noncanonical"] },
85 { name: "Malediction.",
86 action: "Discard all quests.",
87 requires: ["noncanonical"],
88 unique: true },
89 { name: "Reanimation.",
90 action: "Return a random spent dungeon card to the smallest corridor and shuffle it. (If you haven't spent any cards, nothing happens.)",
91 requires: ["noncanonical"] },
92 { name: "Zone of silence.",
93 effect: "Temporary and Ultimate Powers cannot be used until you reveal a new card.",
94 requires: ["noncanonical"] },
95
96 // Events of my own devising.
97 { name: "Unwanted attention.",
98 effect: "Unique monsters gain a bonus of +1 Strength.",
99 requires: ["noncanonical"],
100 unique: true },
101 { name: "Spiked the punch.",
102 effect: "Greenskins have 1d8 Strength.",
103 requires: ["noncanonical"],
104 unique: true },
105 { name: "Dropped the torch.",
106 effect: "After defeating a card, roll a die. On an odd number its replacement is placed face-down.",
107 requires: ["noncanonical"],
108 unique: true },
109 { name: "Closer than you think.",
110 action: "The dungeon gains another Fear token, and a new event occurs.",
111 requires: ["noncanonical"],
112 another: true, unique: true },
113 { name: "I thought you had it.",
114 action: "Randomly discard four of your defeated dungeon cards.",
115 requires: ["noncanonical"] },
116 { name: "Adamantine armor.",
117 effect: "All monsters gain <strong>Immunity&nbsp;1</strong>.",
118 requires: ["noncanonical"],
119 unique: true },
120 { name: "Normative assumptions.",
121 effect: "Effects concerning ♂ instead concern ♀, and vice versa.",
122 requires: ["noncanonical"],
123 unique: true },
124 { name: "Infighting.",
125 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>.",
126 requires: ["noncanonical"],
127 unique: true },
128 { name: "New moon.",
129 effect: "Demons have <strong>Veil of Shadow</strong>.",
130 requires: ["noncanonical", "demons"],
131 unique: true },
132 { name: "Camouflage.",
133 effect: "Greenskins have <strong>Veil of Shadow</strong>.",
134 requires: ["noncanonical"],
135 unique: true },
136 { name: "Cryptic shades.",
137 effect: "Undead have <strong>Veil of Shadow</strong>.",
138 requires: ["noncanonical", "undead"],
139 unique: true },
140 { name: "Lingering smoke.",
141 effect: "Dragons have <strong>Veil of Shadow</strong>.",
142 requires: ["noncanonical", "dragons"],
143 unique: true },
144 { name: "Hypnotizing gaze.",
145 effect: "Bubbleyes have <strong>Veil of Shadow</strong>.",
146 requires: ["noncanonical", "bubbleyes"],
147 unique: true },
148
149 ];
150
151 var NOTHING = { name: "Nothing happens." };
152 var LOSE = { name: "Your adventuring party is defeated!" };
153
154 function randrange (a, b) {
155 return a + (Math.random() * (b - a)) | 0;
156 }
157
158 function generate (flags, events, nop) {
159 var chosen = [];
160 var i;
161
162 function canStillHappen (event) {
163 return issubset(event.requires || [], flags)
164 && !(event.unique && contains.call(chosen, event));
165 }
166
167 for (i = 0; i < events; ++i)
168 chosen.push(choice(EVENTS.filter(canStillHappen)));
169
170 for (i = 0; i < nop; ++i)
171 chosen.splice(randrange(0, chosen.length), 0, NOTHING);
172
173 chosen.push(LOSE);
174 return chosen;
175 }
176
177 function toHTML (event) {
178 return ["<span class=fate-name>" + event.name + "</span>",
179 event.action
180 ? "<span class=fate-action>" + event.action + "</span>"
181 : "",
182 event.effect
183 ? "<span class=fate-effect>" + event.effect + "</span>"
184 : "",
185 ].join(" ");
186 }
187
188 var THEME = ("h1 { color: hsl(XXX, 25%, 75%); }\n\
189 h2, .button, select { border-color: hsl(XXX, 30%, 85%); }\n\
190 \n\
191 a:link, a:visited, a:active {\n\
192 color: hsl(XXX, 25%, 50%);\n\
193 }\n\
194 \n\
195 select, .button, tbody tr:nth-last-child(odd) {\n\
196 background-color: hsl(XXX, 30%, 85%);\n\
197 }\n\
198 \n\
199 h1,\n\
200 select:hover, select:focus,\n\
201 .button:hover, .button:focus {\n\
202 border-color: hsl(XXX, 25%, 50%);\n\
203 }");
204
205 function randomizeName () {
206 var name = generateName();
207 document.getElementById("name").textContent = name;
208 document.title = document.title.replace(/[^-]*-/, name + " -");
209 document.head.lastChild.textContent = THEME.replace(
210 /XXX/g, (Math.random() * 256) | 0);
211 }
212
213 var events = [];
214 var style;
215
216 function generateScenario () {
217 var parts = location.hash.slice(1).split(',');
218 events = generate(parts, parts.shift() | 0, parts.shift() | 0);
219 style = document.createElement("style");
220 document.head.appendChild(style);
221 randomizeName();
222 }
223
224 function getEvents (matcher) {
225 return EVENTS.filter(matcher);
226 }
227
228 function wrapRow (row) {
229 return "<tr><td>" + row + "</td></tr>";
230 }
231
232 function iscanonical (event) {
233 return !isnoncanonical(event);
234 }
235 function isnoncanonical (event) {
236 return ~(event.requires || []).indexOf('noncanonical');
237 }
238
239 function canonicalToHTML (sender) {
240 sender.innerHTML = EVENTS.filter(iscanonical)
241 .map(toHTML).sort().map(wrapRow).join('');
242 }
243
244 function noncanonicalToHTML (sender) {
245 sender.innerHTML = EVENTS.filter(isnoncanonical)
246 .map(toHTML).sort().map(wrapRow).join('');
247 }
248
249 function nextEvent (sender) {
250 if (!events.length) {
251 location.reload();
252 return;
253 }
254
255 var event = events.shift();
256 var body = document.querySelector("#fate tbody");
257 var fate = body.children.length + 1;
258 var tr = document.createElement('tr');
259 tr.innerHTML = "<td><div>" + fate + "</div></td>"
260 + "<td><div>" + toHTML(event) + "</div></td>";
261 body.insertBefore(tr, body.firstChild);
262
263 if (events.length === 0) {
264 sender.textContent = "Try Again ▲";
265 }
266
267 if (event.another)
268 setTimeout(function () { nextEvent(sender); }, 333);
269 }