blob: c9d82435df58f7a3a7f0d05dccb774317d8a3c66 [file] [log] [blame]
Jon West197faff2019-05-02 18:27:18 -04001console.clear();
2
3/**
4 * Choose Your Own Adventure-style data structure.
5 *
6 * Works on the principle of 'entries'/'chapters', like the oldschool 'turn to 100'
7 * style Fighting Fantasy gamebooks - this is *not* a proper text adventure engine
8 * with full location, inventory and state management. It could probably be adapted
9 * to that quite easily, though.
10 *
11 * Definition format:
12 *
13 * Content:
14 * - id: unique string ID for use in 'goto'/'next'.
15 * - text: body text for this entry.
16 * - extra: array of additional paragraphs:
17 * - text: text for this paragraph
18 * - requires: string/array of item(s) required for this paragraph
19 * to be included. Use '!itemname' to invert logic.
20 *
21 * Inventory / state:
22 * - gives: string/array of item(s) gained when entry is visited
23 * - takes: string/array of item(s) lost when entry is visited
24 * - gameover: if 'win' or 'lose', changes game state when entry is visited
25 *
26 * Navigation (ONE of the following):
27 * - next: id of next entry (will convert to a single 'Continue...' option)
28 * - options: array of options for this entry:
29 * - text: text used for option
30 * - goto: id of entry this option leads to
31 * - requires: string/array of item(s) required for this option
32 * to be available. Use '!itemname' to invert logic.
33 *
34 **/
35
36var ENTRIES = [{
37 // Recreation room
38 id: 'e_recroom',
39 text: 'You are in a small-ish room with a computer in front of you ' +
40 'The computer screen shows a terminal, and on the terminal ' +
41 'error showing that you need to repo init before running repo ' +
42 'sync. What do you do',
43 extra: [{
44 text: 'You have the BlissROMs thread for a different device ' +
45 'open on a second screen.',
46 }],
47 options: [{
48 text: 'Examine the webpage for the GitHub link',
49 goto: 'e_github'
50 }, {
51 text: 'Search Google for your answer',
52 goto: 'e_google'
53 }],
54 start: true
55 }, {
56 // Look at the plushie
57 id: 'e_github',
58 text: 'Upon closer inspection, the webpage has a section for Sources, Links, and ' +
59 'Social Media. You scan through them quickly to see if anything could be useful ' +
60 'What section do you decide on',
61 options: [{
62 text: 'Copy the link for the BlissROMs github',
63 goto: 'e_got_github_link'
64 }, {
65 text: 'Open a new tab and go to Google Search instead',
66 goto: 'e_google'
67 }]
68 }, {
69 // Get the mask off the plushie
70 id: 'e_got_github_link',
71 text: 'You grab the link to the BlissROMs GitHub page, and paste it into a new tab. It loads and shows ' +
72 'a list of repo links is shown, and you are a little overwhelmed by the amount of choices available. ' +
73 'You copy the link to the manifest repo, but are unsure. So you decide to head to Google instead ',
74 next: 'e_google'
75 }, {
76 // Main atrium
77 id: 'e_google',
78 text: 'You sit there, staring at the Google Search page. ' +
79 'As you type in BlissROMs repo init, the page results spit out you notice a few things in the ' +
80 'list that look familiar. The top result is for the manifest link, while the second result is for ' +
81 'a how-to unlock and root any device. The third link shown is for a YouTube video on building ' +
82 'Android ROMs, and the fourth link down leads to the BlissROMs webpage ',
83 options: [{
84 text: 'Follow the think to the BlissROMs Manifest.',
85 goto: 'e_link_manifest'
86 }, {
87 text: 'Follow the link to unlock and root your device',
88 goto: 'e_link_droid'
89 }, {
90 text: 'Head to YouTube for the video on building Android ROMs',
91 goto: 'e_link_youtube'
92 }, {
93 text: 'Head over to the BlissROMs webpage',
94 goto: 'e_link_blissroms'
95 }],
96 }, {
97 // Manifest link
98 id: 'e_link_manifest',
99 text: 'You head to the manifest page and it shows a few files on the top, followed by a descriptive ' +
100 'readme detailing all the steps needed to install dependencies, repo init, repo sync and even ' +
101 'steps to build the ROM, GSI, EFI & x86. You remember that you were looking for the init link ' +
102 'so you copy that text snippet. ',
103 next: 'e_back_to_terminal'
104 }, {
105 // Killed by malware
106 id: 'e_link_droid',
107 text: 'You follow the link to unlock and root your device. Upon loading the page, a pop-up dialog shows ' +
108 'and mentioned that you need to click OK in order to root your device. You click it, and the screen ' +
109 'displays a ton of terminal windows opening and closing. You smell smoke from the PC, and before you ' +
110 'can reach the power button, the machine shuts off. smoke is still pouring from the case. ' +
111 ' You have failed, go back to your iPhone looser',
112 gameover: 'lose'
113 }, {
114 // Killed by youtube
115 id: 'e_link_youtube',
116 text: 'You follow the link to watch the YouTube video on building Android, and start to soak up all the ' +
117 'info provided. At the one hour mark, you remember that you left the meatloaf in the oven, and you ' +
118 'are starting to smell a combination of burnt BBQ sauce and beef. You rush to the kitchen to find ' +
119 'half the kitchen on fire already. You reach for a fire extinguisher and try to use it. There was no ' +
120 'pressure in the extinguisher. You then remember seeing a YouTube video on extinguishing a fire with ' +
121 'water, so you quickly throw a bowl of water on the flames. ' +
122 'The flames quickly grow because you threw water on a grease fire and you die. Dont believe everything ' +
123 'you see on youtube ',
124 gameover: 'lose'
125 }, {
126 // BlissROMs link
127 id: 'e_link_blissroms',
128 text: 'You load the BlissROMs page, and scroll through it. At the bottom, you notice the BlissROMs github ' +
129 'link from earlier. You follow it and are shown a list of repos. ',
130 extra: [{
131 text: 'You study your options and notice there are a few repos pinned to the top for manifest, ' +
132 'frameworks/base, vendor/bliss, etc. ',
133 }],
134 options: [{
135 text: 'Check out the manifest repo',
136 goto: 'e_link_manifest'
137 }, {
138 text: 'Check out frameworks/base repo.',
139 goto: 'e_link_fwb'
140 }, {
141 text: 'Check out the vendor/bliss repo',
142 goto: 'e_vendor_bliss'
143 }]
144 }, {
145 // Link fwb
146 id: 'e_link_fwb',
147 text: 'You click the link to frameworks/base, and are presented with a page of source files. and code. None of ' +
148 'this looks like something you are looking for. So you decide to go back to the main repo page' ,
149 next: 'e_link_github_again'
150 }, {
151 // Link vendor/bliss
152 id: 'e_vendor_bliss',
153 text: 'You click the link to vendor/bliss, and are presented with a page of source files. and code. None of ' +
154 'this looks like something you are looking for. So you decide to go back to the main repo page' ,
155 next: 'e_link_github_again'
156 }, {
157 // GitHub again
158 id: 'e_link_github_again',
159 text: 'You head back to the BlissROMs github link from earlier. You see the same list of repos. ',
160 extra: [{
161 text: 'You study your options and notice there are a few repos pinned to the top for manifest, ' +
162 'frameworks/base, vendor/bliss, etc. ',
163 }],
164 options: [{
165 text: 'Check out the manifest repo',
166 goto: 'e_link_manifest'
167 }, {
168 text: 'Check out frameworks/base repo.',
169 goto: 'e_link_fwb'
170 }, {
171 text: 'Check out the vendor/bliss repo',
172 goto: 'e_vendor_bliss'
173 }]
174 }, {
175 // Back to terminal
176 id: 'e_back_to_terminal',
177 text: 'In the terminal window, you take the text snippet you grabbed and paste it into the terminal ' +
178 'window, then hit Return. It spits out a bunch of text and presents you with a message that reads ' +
179 'Repo has been successfully initiated. ',
180 options: [{
181 text: 'View the Github manifest page for the next step',
182 goto: 'e_manifest_next'
183 }, {
184 text: 'Enter repo sync into the terminal',
185 goto: 'e_repo_sync'
186 }, {
187 text: 'Search Google for an answer',
188 goto: 'e_google_2'
189 }, {
190 text: 'Search Youtube for an answer',
191 goto: 'e_link_youtube'
192 }]
193 }, {
194 // Google again
195 id: 'e_google_2',
196 text: 'You head back to google to find out what you do next, so you search for blissroms repo init and ' +
197 'are presented a few options. The top two links lead to the BlissROMs github page, so you follow that ',
198 next: 'e_link_github_again'
199 },{
200 // manifest
201 id: 'e_manifest_next',
202 text: 'You return to the manifest repo, and scroll down to the list of instructions. Once there, you notice ' +
203 ' that the next item after the repo init part are about repo sync. ' +
204 'What do you decide to do',
205 options: [{
206 text: 'Copy the command for repo sync',
207 goto: 'e_repo_sync'
208 }, {
209 text: 'Give up and go watch some TV',
210 goto: 'e_go_watch_tv'
211 }, {
212 text: 'Leave and go outside.',
213 goto: 'e_outside'
214 }]
215 }, {
216 // repo sync
217 id: 'e_repo_sync',
218 text: 'You head back to the terminal and enter the repo sync command into it, and press return ' +
219 'The screen lights up with hundreds of lines of text scrolling past the screen endlessly. ' +
220 'After about 30 min, the repo sync finishes and you are presented with a blinking cursor. ' +
221 'What do you do now',
222 options: [{
223 text: 'Head back to the Bliss manifest page to see whats next',
224 goto: 'e_go_build'
225 }, {
226 text: 'Give up and go watch some TV',
227 goto: 'e_go_watch_tv'
228 }, {
229 text: 'Leave and go outside',
230 goto: 'e_outside'
231 }]
232 },{
233 // build
234 id: 'e_go_build',
235 text: 'You head back to the Bliss manifest repo and follow the instructions a little further until ' +
236 'you find the section for building. ' +
237 'After finding the section, you see a code snippet to initiate the lunch command and build.' +
238 'What do you do now',
239 options: [{
240 text: 'Leave and go outside',
241 goto: 'e_outside'
242 }, {
243 text: 'Copy the code to the terminal window and press enter',
244 goto: 'e_go_compile'
245 }, {
246 text: 'Give up and go watch some TV',
247 goto: 'e_go_watch_tv'
248 }]
249 }, {
250 // Die outside
251 id: 'e_outside',
252 text: 'You decide to give up for now and go outside. Once there, you are confronted with a huge thunderstorm ' +
253 ' and lightning clashes and catches your house on fire.' ,
254 gameover: 'lose'
255 }, {
256 // Die watching TV
257 id: 'e_go_watch_tv',
258 text: 'You decide to give up for now and go watch some TV. Once the TV is turned on, an alien mothership enters ' +
259 'earths atmosphere and takes over all TV broadcasts. They state that the people of earth have 30 seconds to ' +
260 'say their prayers before they git the reset button on human life. You waste the last 30 seconds of your life ' +
261 'by flipping through Facebook on your phone. ',
262 gameover: 'lose'
263 },
264 {
265 // WINNING
266 id: 'e_go_compile',
267 text: 'You copy the lunch command into the terminal, then press return, and select the number representing your ' +
268 'device. The build launches and you safely go watch TV for a bit while things are compiling ',
269 gameover: 'win',
270 },
271
272];
273
274/**
275 * Parser module for the data format.
276 * Reads the data object format and creates an internal copy with required
277 * transformations and parsing. Exposes methods to start/reset the game,
278 * advance the game via choices/actions, and read the currently active entry.
279 *
280 * The module is just data-driven, and returns objects from its methods; it
281 * does no handling of game display or user input directly. It needs a frontend
282 * written for it in order for a player to interact with it.
283 **/
284var CYOA = (function() {
285
286 var ENTRY_DATA,
287 currentEntryId, currentEntryData,
288 inventory;
289
290 function _init(entryData) {
291 // clear state
292 ENTRY_DATA = {};
293 currentEntryId = null;
294 currentEntryData = {};
295 inventory = [];
296
297 var startEntryId = null;
298
299 // Parse entry data into internal object
300 entryData.forEach(function(entry) {
301 ENTRY_DATA[entry.id] = Object.create(entry);
302
303 // Track the starting entry and warn of duplicates
304 if (entry.start === true) {
305 if (startEntryId !== null) {
306 console.error('More than one starting state defined:', startEntryId, entry.id);
307 } else {
308 startEntryId = entry.id;
309 }
310 }
311
312 // Process extra paragraphs if present
313 if (entry.extra) {
314 entry.extra.forEach(function(ext) {
315 // convert string options to single-item arrays for easier parsing
316 if (ext.requires && (typeof ext.requires === 'string')) {
317 ext.requires = [ext.requires];
318 }
319 });
320 }
321
322 // 'Next' overrides all other options
323 if (entry.next) {
324 entry.options = [{
325 text: 'Continue...',
326 goto: entry.next
327 }];
328 }
329 // Process and validate options
330 if (entry.options) {
331 entry.options.forEach(function(opt) {
332 // options must have a 'goto'
333 if (!opt.goto) console.error('Entry', entry.id, ' has option without a goto: ', opt.text);
334 // convert string options to single-item arrays for easier parsing
335 if (opt.requires && (typeof opt.requires === 'string')) {
336 opt.requires = [opt.requires];
337 }
338 });
339 }
340 });
341
342 // Set initial state from starting entry
343 if (startEntryId === null) console.error('No start entry found');
344 _setEntry(startEntryId);
345 }
346
347 // Inventory methods (accept string or array)
348
349 function _addToInventory(items) {
350 if (typeof items === 'string') items = [items];
351 inventory = inventory.concat(items);
352 }
353
354 function _takeFromInventory(items) {
355 if (typeof items === 'string') items = [items];
356 var newInv = [];
357 inventory.forEach(function(item) {
358 if (items.indexOf(item) === -1) newInv.push(item);
359 });
360 inventory = newInv;
361 }
362
363 function _checkInventory(item) {
364 return (inventory.indexOf(item) > -1);
365 }
366
367 // Utility method to check a 'requires'-format array against the current inventory
368 function _hasRequirements(opt) {
369 var isAvailable = true;
370 if (opt.requires) {
371 opt.requires.forEach(function(req) {
372 if (req.charAt(0) === '!' && _checkInventory(req.substr(1))) isAvailable = false;
373 if (req.charAt(0) !== '!' && !_checkInventory(req)) isAvailable = false;
374 });
375 }
376 return isAvailable;
377 }
378
379 // Updates the current entry data to the given entry ID.
380 // Composes the current entry data based on conditionals set in the entry data,
381 // including required inventory to display options, etc.
382 // Also makes changes to inventory and state based on the definition data.
383 function _setEntry(id) {
384 if (!id in ENTRY_DATA) console.error('Unable to change entry: invalid entry id', id);
385 currentEntryId = id;
386
387 var data = ENTRY_DATA[id];
388 currentEntryData = {
389 id: data.id,
390 text: data.text,
391 extra: []
392 };
393
394 // Add/remove inventory items in this entry
395 if (data.gives) _addToInventory(data.gives);
396 if (data.takes) _takeFromInventory(data.takes);
397
398 // Update text with extras
399 if (data.extra) {
400 data.extra.forEach(function(ext) {
401 if (_hasRequirements(ext)) currentEntryData.extra.push(ext.text);
402 });
403 }
404
405 // State modifiers
406 // TODO: make this more definitive and mutate options accordingly
407 if (data.gameover) currentEntryData.gameover = data.gameover;
408
409 // Define available options based on inventory requirements
410 if (data.options) {
411 currentEntryData.options = [];
412 data.options.forEach(function(opt, idx) {
413 if (_hasRequirements(opt)) {
414 currentEntryData.options.push({
415 text: opt.text,
416 goto: opt.goto
417 });
418 }
419 });
420 }
421 return currentEntryData;
422 }
423
424 function startGame(data) {
425 _init(data);
426 }
427
428 function getCurrentEntry() {
429 if (currentEntryData === {}) console.error('No current entry; has the game started?');
430 return currentEntryData;
431 }
432
433 function getInventory() {
434 return inventory;
435 }
436
437 // Changes the active entry according to the numeric ID of the option passed in,
438 // if it is present in the current entry.
439 function doOption(idx) {
440 if (!currentEntryData.options) console.error('Can not complete option', idx);
441 var opt = currentEntryData.options[idx];
442 var newEntryId = opt.goto;
443 if (!newEntryId in ENTRY_DATA) console.error('Cannot do option: invalid goto id', newEntryId);
444 return _setEntry(newEntryId);
445 }
446
447 return {
448 startGame: startGame,
449 getCurrentEntry: getCurrentEntry,
450 getInventory: getInventory,
451 doOption: doOption
452 };
453})();
454
455/**
456 * Some simple jQuery DOM logic for demo purposes.
457 * This could easily be expanded for better presentation,
458 * per-location graphics, all kinds of stuff.
459 **/
460var Game = (function() {
461
462 var DATA;
463
464 // Container element to render into
465 var $el = $('#output_a');
466
467 // Text for game over scenarios
468 var endMsgs = {
469 win: 'You won! Play again...',
470 lose: 'You failed. Restart...'
471 };
472
473 // Reads the current entry data and puts DOM nodes
474 // in the container to display the text and options
475 function render(isStart) {
476 var d = CYOA.getCurrentEntry();
477
478 // Clear the container and write the body text
479 $el.html('');
480 if (isStart) $el.append('<p class="title">*** Build BlissROM ***</p>');
481 $el.append('<p>' + d.text + '</p>');
482
483 d.extra.forEach(function(ext) {
484 $el.append('<p>' + ext + '</p>');
485 });
486
487 // Write out a list of option or restart links in a list
488 // (click handlers bound in init() will handle these)
489 var $opts = $('<ul/>');
490 if (d.gameover) {
491 var $action = $('<li><a class="opt gameover ' + d.gameover + '" href="#">' +
492 endMsgs[d.gameover] + '</a></li>');
493 $opts.append($action);
494 }
495 if (d.options) {
496 d.options.forEach(function(opt, idx) {
497 var $opt = $('<li><a class="opt action" href="#" data-opt="' + idx + '">' +
498 opt.text + '</a></li>');
499 $opts.append($opt);
500 });
501 }
502 $el.append($opts);
503
504 // Show current inventory
505 if (!d.gameover) {
506 var inv = CYOA.getInventory();
507 if (inv.length) {
508 $el.append('<p class="inv">You are carrying: ' + inv.join(', ') + '</p>');
509 }
510 }
511 }
512
513 function init(entryData) {
514
515 DATA = entryData;
516
517 // Click handlers for option links. Bound to the document
518 // as we destroy and rebuild the links per-entry.
519 $(document).on('click', '.action', function(e) {
520 e.preventDefault();
521 var opt = $(this).attr('data-opt');
522 console.log('do option', opt);
523 if (CYOA.doOption(opt)) render();
524 });
525
526 // As above but for win/lose links. Restart the game when used
527 $(document).on('click', '.gameover', function(e) {
528 e.preventDefault();
529 _start();
530 });
531
532 _start();
533 }
534
535 function _start() {
536 // Init the game and render the first entry
537 CYOA.startGame(DATA);
538 render(true);
539 }
540
541 return {
542 init: init
543 }
544
545})();
546
547// Kick off
548Game.init(ENTRIES);