98222227f15b08edcbe180b8aceff4c780e5e98b
[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
59 // Events from Sean Allen's random scenario generator.
60 // http://boardgamegeek.com/filepage/57107/random-scenario-generator
61 { name: "Fire from above.",
62 effect: "Dragons gains a bonus of +1 Strength.",
63 requires: ["dragons", "noncanonical"],
64 unique: true },
65 { name: "Bad dreams.",
66 action: "Lose 1 extra Courage token. (The dungeon does not gain another Fear token.)",
67 requires: ["noncanonical"],
68 unique: true },
69 { name: "Backs against the wall.",
70 effect: "Any monster with a Tenacity token is also Fierce.",
71 requires: ["noncanonical"],
72 unique: true },
73 { name: "Dead end.",
74 action: "Shuffle the remaining corridors together and redistribute the cards as if you were setting up the game.",
75 requires: ["noncanonical"] },
76 { name: "Surrounded.",
77 effect: "All monsters have Supremacy.",
78 requires: ["noncanonical"],
79 unique: true },
80
81 // Events from Stephane Renard's scenario.
82 // http://docfox.free.fr/spip.php?article129
83 { name: "What was that?",
84 action: "The next dungeon card is placed face-down.",
85 requires: ["noncanonical"] },
86 { name: "Malediction.",
87 action: "Discard all quests.",
88 requires: ["noncanonical"],
89 unique: true },
90 { name: "Reanimation.",
91 action: "Return a random spent dungeon card to the smallest corridor and shuffle it. (If you haven't spent any cards, nothing happens.)",
92 requires: ["noncanonical"] },
93 { name: "Zone of silence.",
94 effect: "Temporary and Ultimate Powers cannot be used until you reveal a new card.",
95 requires: ["noncanonical"] },
96
97 // Events of my own devising.
98 { name: "Unwanted attention.",
99 effect: "Unique monsters gain a bonus of +2 Strength.",
100 requires: ["noncanonical"],
101 unique: true },
102 { name: "Spiked the punch.",
103 effect: "Greenskins have 1d8 Strength.",
104 requires: ["noncanonical"],
105 unique: true },
106 { name: "Dropped the torch.",
107 effect: "After defeating a card, roll a die. On an odd number its replacement is placed face-down.",
108 requires: ["noncanonical"],
109 unique: true },
110 { name: "Closer than you think.",
111 action: "The dungeon gains another Fear token, and a new event occurs.",
112 requires: ["noncanonical"],
113 another: true, unique: true },
114 { name: "I thought you had it.",
115 action: "Randomly discard four of your defeated dungeon cards.",
116 requires: ["noncanonical"] },
117 { name: "Adamantine armor.",
118 effect: "All monsters gain Immunity 1.",
119 requires: ["noncanonical"],
120 unique: true },
121 { name: "Normative assumptions.",
122 effect: "Effects concerning ♂ instead concern ♀, and vice versa.",
123 requires: ["noncanonical"],
124 unique: true },
125 ];
126
127 var NOTHING = { name: "Nothing happens." };
128 var LOSE = { name: "Your adventuring party is defeated!" };
129
130 function randrange (a, b) {
131 return a + (Math.random() * (b - a)) | 0;
132 }
133
134 function generate (flags, events, nop) {
135 var chosen = [];
136 var i;
137
138 function canStillHappen (event) {
139 return issubset(event.requires || [], flags)
140 && !(event.unique && contains.call(chosen, event));
141 }
142
143 for (i = 0; i < events; ++i)
144 chosen.push(choice(EVENTS.filter(canStillHappen)));
145
146 for (i = 0; i < nop; ++i)
147 chosen.splice(randrange(0, chosen.length), 0, NOTHING);
148
149 chosen.push(LOSE);
150 return chosen;
151 }
152
153 function toHTML (event) {
154 return ["<span class=fate-name>" + event.name + "</span>",
155 event.action
156 ? "<span class=fate-action>" + event.action + "</span>"
157 : "",
158 event.effect
159 ? "<span class=fate-effect>" + event.effect + "</span>"
160 : "",
161 ].join(" ");
162 }
163
164 var THEME = ("h1 { color: hsl(XXX, 25%, 75%); }\n\
165 h2, .button, select { border-color: hsl(XXX, 30%, 85%); }\n\
166 \n\
167 a:link, a:visited, a:active {\n\
168 color: hsl(XXX, 25%, 50%);\n\
169 }\n\
170 \n\
171 select, .button, tbody tr:nth-last-child(odd) {\n\
172 background-color: hsl(XXX, 30%, 85%);\n\
173 }\n\
174 \n\
175 h1,\n\
176 select:hover, select:focus,\n\
177 .button:hover, .button:focus {\n\
178 border-color: hsl(XXX, 25%, 50%);\n\
179 }");
180
181 function randomizeName () {
182 var name = generateName();
183 document.getElementById("name").textContent = name;
184 document.title = document.title.replace(/[^-]*-/, name + " -");
185 document.head.lastChild.textContent = THEME.replace(
186 /XXX/g, (Math.random() * 256) | 0);
187 }
188
189 var events = [];
190 var style;
191 window.addEventListener('DOMContentLoaded', function () {
192 var parts = location.hash.slice(1).split(',');
193 events = generate(parts, parts.shift() | 0, parts.shift() | 0);
194 style = document.createElement("style");
195 document.head.appendChild(style);
196 randomizeName();
197 });
198
199 function nextEvent (sender) {
200 if (!events.length) {
201 location.reload();
202 return;
203 }
204
205 var event = events.shift();
206 var body = document.querySelector("#fate tbody");
207 var fate = body.children.length + 1;
208 var tr = document.createElement('tr');
209 tr.innerHTML = "<td><div>" + fate + "</div></td>"
210 + "<td><div>" + toHTML(event) + "</div></td>";
211 body.insertBefore(tr, body.firstChild);
212
213 if (events.length === 0) {
214 sender.textContent = "Try Again ▲";
215 }
216
217 if (event.another)
218 setTimeout(function () { nextEvent(sender); }, 333);
219 }