5412e29f40efa44e96e159a2dc4fa6900820dec7
[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 ];
129
130 var NOTHING = { name: "Nothing happens." };
131 var LOSE = { name: "Your adventuring party is defeated!" };
132
133 function randrange (a, b) {
134 return a + (Math.random() * (b - a)) | 0;
135 }
136
137 function generate (flags, events, nop) {
138 var chosen = [];
139 var i;
140
141 function canStillHappen (event) {
142 return issubset(event.requires || [], flags)
143 && !(event.unique && contains.call(chosen, event));
144 }
145
146 for (i = 0; i < events; ++i)
147 chosen.push(choice(EVENTS.filter(canStillHappen)));
148
149 for (i = 0; i < nop; ++i)
150 chosen.splice(randrange(0, chosen.length), 0, NOTHING);
151
152 chosen.push(LOSE);
153 return chosen;
154 }
155
156 function toHTML (event) {
157 return ["<span class=fate-name>" + event.name + "</span>",
158 event.action
159 ? "<span class=fate-action>" + event.action + "</span>"
160 : "",
161 event.effect
162 ? "<span class=fate-effect>" + event.effect + "</span>"
163 : "",
164 ].join(" ");
165 }
166
167 var THEME = ("h1 { color: hsl(XXX, 25%, 75%); }\n\
168 h2, .button, select { border-color: hsl(XXX, 30%, 85%); }\n\
169 \n\
170 a:link, a:visited, a:active {\n\
171 color: hsl(XXX, 25%, 50%);\n\
172 }\n\
173 \n\
174 select, .button, tbody tr:nth-last-child(odd) {\n\
175 background-color: hsl(XXX, 30%, 85%);\n\
176 }\n\
177 \n\
178 h1,\n\
179 select:hover, select:focus,\n\
180 .button:hover, .button:focus {\n\
181 border-color: hsl(XXX, 25%, 50%);\n\
182 }");
183
184 function randomizeName () {
185 var name = generateName();
186 document.getElementById("name").textContent = name;
187 document.title = document.title.replace(/[^-]*-/, name + " -");
188 document.head.lastChild.textContent = THEME.replace(
189 /XXX/g, (Math.random() * 256) | 0);
190 }
191
192 var events = [];
193 var style;
194
195 function generateScenario () {
196 var parts = location.hash.slice(1).split(',');
197 events = generate(parts, parts.shift() | 0, parts.shift() | 0);
198 style = document.createElement("style");
199 document.head.appendChild(style);
200 randomizeName();
201 }
202
203 function getEvents (matcher) {
204 return EVENTS.filter(matcher);
205 }
206
207 function wrapRow (row) {
208 return "<tr><td>" + row + "</td></tr>";
209 }
210
211 function iscanonical (event) {
212 return !isnoncanonical(event);
213 }
214 function isnoncanonical (event) {
215 return ~(event.requires || []).indexOf('noncanonical');
216 }
217
218 function canonicalToHTML (sender) {
219 sender.innerHTML = EVENTS.filter(iscanonical)
220 .map(toHTML).sort().map(wrapRow).join('');
221 }
222
223 function noncanonicalToHTML (sender) {
224 sender.innerHTML = EVENTS.filter(isnoncanonical)
225 .map(toHTML).sort().map(wrapRow).join('');
226 }
227
228 function nextEvent (sender) {
229 if (!events.length) {
230 location.reload();
231 return;
232 }
233
234 var event = events.shift();
235 var body = document.querySelector("#fate tbody");
236 var fate = body.children.length + 1;
237 var tr = document.createElement('tr');
238 tr.innerHTML = "<td><div>" + fate + "</div></td>"
239 + "<td><div>" + toHTML(event) + "</div></td>";
240 body.insertBefore(tr, body.firstChild);
241
242 if (events.length === 0) {
243 sender.textContent = "Try Again ▲";
244 }
245
246 if (event.another)
247 setTimeout(function () { nextEvent(sender); }, 333);
248 }