window._ = {}; window.Game = {}; Game.sections = {}; Game.queue = []; Game.dom = $("#game_container"); Game.wordsDOM = $("#game_words"); Game.choicesDOM = $("#game_choices"); Game.canvas = $("#game_canvas"); window.SceneSetup = {}; // A big ol' singleton class that just makes it easy to create scenes. // HELPER FUNCS window.bb = function(){ publish("bb", arguments); }; window.hong = function(){ publish("hong", arguments); }; window.attack = function(damage, type){ publish("attack", ["hong", damage, type]); _["attack_"+type+"_ch"+_.CHAPTER]++; // HACK }; window.attackBB = function(damage, type){ publish("attack", ["bb", damage]); }; // Init Game.init = function(){ // Create the section debug menu Object.keys(Game.sections).forEach(function(key){ const link = document.createElement('div'); link.className = "section_link"; link.innerText = key; link.addEventListener('click', function() { Game.goto(key); }); document.getElementById("section_debug_list").appendChild(link); }) // HP! window.HP = new HitPoints(); // Animation! console.log("init"); var animloop = function(){ Game.update(); requestAnimationFrame(animloop); }; requestAnimationFrame(animloop); }; // Call to toggle debug rendering Game.debug = function(){ document.body.classList.toggle('show_debug'); } // Parse scene markdown! Game.parseSceneMarkdown = function(md){ // Split into sections... md = md.trim().replace(/\r/g, ""); md = "\n" + md; var sections = md.split(/\n\#\s*/); sections.shift(); sections.forEach(function(section){ var split_index = section.indexOf("\n\n"); var id = section.slice(0, split_index).toLocaleLowerCase(); var text = section.slice(split_index+2); // Split into lines text = text.trim(); var lines = text.split("\n\n"); for(var i=0; i { tc.timeLeft -= 1000 * delta; if(tc.timeLeft <= 0) tc.callback(); }); Game.timeoutCallbacks = Game.timeoutCallbacks.filter(tc => tc.timeLeft > 0); // The interface Game.updateText(); Game.updateCanvas(delta); // Ayyy publish("update"); } // Options update Options.update(); }; // PAUSING THE GAME Game.paused = false; Game.pausedDOM = $("#paused"); Game.pause = function(){ Game.paused = true; Game.pausedDOM.style.display = "block"; Howler.mute(true); $("#paused").setAttribute("modal", (Options.showing||About.showing||ContentNotes.showing) ? "yes" : "no" ); publish("GAME_PAUSED"); }; window.addEventListener("blur", Game.pause); Game.onUnpause = function(){ if(Game.paused && !(Options.showing||About.showing||ContentNotes.showing)){ Game.paused = false; Game.pausedDOM.style.display = "none"; Howler.mute(false); } publish("GAME_UNPAUSED"); }; Game.pausedDOM.onclick = function(e){ if(Options.showing){ publish("hide_options"); }else if(About.showing){ $("#close_about").onclick(); }else if(About.showing){ publish("hide_cn"); }else{ Game.onUnpause(); } e.stopPropagation(); }; // UNPAUSE OR SKIP DIALOGUE? var _unpauseOrSkip = function(){ if(Game.paused){ Game.onUnpause(); }else{ Game.clearAllTimeouts(); publish("super_hack_skip_intro"); } }; window.addEventListener("click", _unpauseOrSkip); window.addEventListener("touchstart", _unpauseOrSkip); // "SET TIMEOUT" for text and stuff Game.timeoutCallbacks = []; Game.setTimeout = function(callback, interval){ Game.timeoutCallbacks.push({ callback: callback, timeLeft: interval }); }; // SKIP TEXT WHEN CLICK ANYWHERE (but NOT capture in choice) Game.clearAllTimeouts = function(){ // NOPE if(Game.FORCE_CANT_SKIP) return; // Is this DURING while someone is talking? var isInterrupting = (Game.WHO_IS_SPEAKING!=null); // If not, clear all // Otherwise, clear all BUT last one... UNLESS there's only one left if(Game.timeoutCallbacks.length==1) isInterrupting=false; for(var i=0; i0){ promiseNext.then(function(){ Game.executeNextLine(); }); } }; Game.clearQueue = function(){ Game.queue = []; }; Game.addToQueue = function(line){ Game.queue.push(line); } //////////////////////////////////////////////////////////////////////////////////////////////// // TEXT AND STUFF ////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////// // Immediate Promise Game.immediatePromise = function(){ return new RSVP.Promise(function(resolve){ resolve(); }); }; // Move the text DOM to latest Game.FORCE_TEXT_Y = -1; Game.WORDS_HEIGHT_BOTTOM = -1; (function(){ const wordsObserver = new TickableObserver(() => { const offset = 80 if(Game.WORDS_HEIGHT_BOTTOM < 0) Game.WORDS_HEIGHT_BOTTOM = 250; let advanceTextPosition = 0 // Either force the text somewhere... if(Game.FORCE_TEXT_Y != -1){ Game.wordsDOM.style.transform = `translateY(${Game.FORCE_TEXT_Y}px)`; advanceTextPosition = Game.wordsDOM.clientHeight + Game.FORCE_TEXT_Y + 5 } // Or calculate its position based on a window... else { const wordsHeight = Game.wordsDOM.clientHeight; let diff = wordsHeight - (Game.WORDS_HEIGHT_BOTTOM - offset) if(diff < 0) diff = 0 Game.wordsDOM.style.transform = `translateY(${offset - diff}px)`; advanceTextPosition = offset - diff + wordsHeight + 5 } // "Instant mode" was only used for clearing... so lets just do it when it's clear? if(Game.wordsDOM.children.length == 0) flushElementTransitions(Game.wordsDOM); // Also, move the click_to_advance DOM $('#click_to_advance').style.transform = `translateY(${Math.round(advanceTextPosition)}px)`; }); // The words UI depends on these things: wordsObserver.watch(() => Game.FORCE_TEXT_Y); wordsObserver.watch(() => Game.WORDS_HEIGHT_BOTTOM); wordsObserver.watch(() => Game.wordsDOM.children.length); Game.updateText = () => wordsObserver.tick(); })() // CLEAR TEXT Game.clearText = function(){ Game.wordsDOM.innerHTML = ""; Game.updateText(); }; Game.clearAll = function(){ Game.clearText(); Game.resetScene(); music(null); stopAllSounds(); }; window.clearText = Game.clearText; // CUSSING?! window.NO_CUSS_MODE = false; var GRAWLIXES = ["@","#","✩","$","%","&"]; var GRAWLIX_INDEX = 0; // Execute text! Just add it to text DOM. Game.TEXT_SPEED = 50; Game.CLICK_TO_ADVANCE = true; Game.FORCE_CANT_SKIP = false; Game.OVERRIDE_TEXT_SPEED = 1; Game.FORCE_TEXT_DURATION = -1; Game.WHO_IS_SPEAKING = null; // "h", "b", "n" etc... Game.CURRENT_SPEAKING_SPEED = 1; Game.FORCE_NO_VOICE = false; Game.NO_NARRATOR_SOUNDS = false; Game.executeText = function(line){ return new RSVP.Promise(function(resolve){ // Who's speaking? // b: Beebee, h: Hong, n: Narrator, n2: Narrator 2, n3: Narrator 3 var regex = /^([^\:]+)\:(.*)/ var speaker = line.match(regex)[1].trim(); var dialogue = line.match(regex)[2].trim(); // IF IT'S A SPECIAL ATTACK, SKIP ALL THIS if(speaker=="fear_harm" || speaker=="fear_alone" || speaker=="fear_bad"){ Game.setTimeout(function(){ publish("hide_click_to_advance"); resolve(); // DONE WITH IT. }, Game.TEXT_SPEED*7); return; } // Add the bubble, with animation var div = document.createElement("div"); Game.wordsDOM.appendChild(div); Game.WHO_IS_SPEAKING = speaker; // WHO'S SPEAKING?! Game.CURRENT_SPEAKING_SPEED = Game.OVERRIDE_TEXT_SPEED; if(Game.OVERRIDE_FONT_SIZE){ div.style.fontSize = Game.OVERRIDE_FONT_SIZE+"px"; } switch(speaker){ case "b": div.className = "beebee-bubble"; break; case "h": div.className = "hong-bubble"; break; case "h2": case "h3": div.className = "hong2-bubble"; break; case "a": div.className = "al-bubble"; break; case "s": div.className = "shire-bubble"; break; case "r": div.className = "hunter-bubble"; break; case "n": div.className = "narrator-bubble"; break; case "n2": // narrator 2 div.className = "narrator-bubble-2"; break; case "n3": // narrator 3 div.className = "narrator-bubble-3"; break; case "n4": // narrator 4 div.className = "narrator-bubble-4"; break; case "n5": // narrator 5 div.className = "narrator-bubble-5"; break; case "i": // Intermission div.className = "narrator-bubble-i"; break; } requestAnimationFrame(function(){ requestAnimationFrame(function(){ div.style.opacity = 1; div.style.left = 0; }); }); // Clear both var clearBoth = document.createElement("div"); clearBoth.className = "clear-both"; Game.wordsDOM.appendChild(clearBoth); // CUSSING OR NO CUSSING? if(window.NO_CUSS_MODE){ // Slice it out with Grawlixes, 1 by 1. var censorMode = false; for(var i=0; i0){ SPEED = Math.round(Game.FORCE_TEXT_DURATION/dialogue.length); } // IF IT'S BEEBEE, HONG, or NARRATOR 3, or HUNTER, or AL or SHIRE if(speaker=="b" || speaker=="h" || speaker=="h2" || speaker=="h3" || speaker=="n3" || speaker=="r" || speaker=="a" || speaker=="s"){ // Put in the text, each character a DIFFERENT SPAN... var span, chr; var isItalicized = false; for(var i=0; i"+chr+"" : chr; } span.style.opacity = 0; div.appendChild(span); } // Then REVEAL letters one-by-one for(var i=0; i *Emphasize* *multiple* *words* var regex = /\*([^\*]*)\*/g; var emphasized = dialogue.match(regex) || []; for(var i=emphasized.length-1; i>=0; i--){ // backwards coz replacing // Convert var originalEm = emphasized[i] var em = originalEm; em = em.substr(1,em.length-2); // remove * var ems = em.split(" "); ems = ems.map(function(word){ return "*"+word+"*"; }); em = ems.join(" "); // Replace in main string var startIndex = dialogue.indexOf(originalEm); dialogue = dialogue.slice(0, startIndex) + em + dialogue.slice(startIndex+originalEm.length); } // Put in the text, each word a DIFFERENT SPAN var span; var dialogueWords = dialogue.split(" "); for(var i=0; i"; } // Actual emphasis reggie = /\_(.*)\_/; if(reggie.test(word)){ word = "" + word.match(reggie)[1].trim() + ""; } // Add the span span = document.createElement("span"); span.innerHTML = word+" "; span.style.opacity = 0; div.appendChild(span); } // Then REVEAL words one-by-one for(var i=0; iVISIBLE_LIMIT){ w[ w.length - VISIBLE_LIMIT - 1 ].style.visibility = "hidden"; w[ w.length - VISIBLE_LIMIT - 2 ].style.visibility = "hidden"; } }); } // CHOICE UI SOUNDS Loader.addSounds([ { id:"ui_show_choice", src:"sounds/ui/show_choice.mp3" }, { id:"ui_click", src:"sounds/ui/click.mp3" }, { id:"ui_hover", src:"sounds/ui/hover.mp3" } ]); // Execute choice! Add it to choice DOM. Game.OVERRIDE_CHOICE_LINE = false; Game.OVERRIDE_CHOICE_SPEAKER = null; Game.OVERRIDE_FONT_SIZE = false; Game.executeChoice = function(line){ var choiceText = line.match(/\[([^\]]*)\]/)[1].trim(); var choiceID = line.match(/\(\#([^\)]*)\)/); var THERE_IS_NO_CHOICE; if(!choiceID){ THERE_IS_NO_CHOICE = true; }else{ choiceID = choiceID[1].trim().toLocaleLowerCase(); } var preChoiceCodeIfAny = null; if(/\`(.*)\`/.test(line)){ preChoiceCodeIfAny = line.match(/\`(.*)\`/)[0]; // 0, with backticks } // SUPER HACK: #play1# and #play2# choiceText = choiceText.replace("#play1#", "
"); choiceText = choiceText.replace("#play2#", "
"); // Choice text, add italics where *word word words* var originalChoiceText = choiceText; var italicsRegex = /\*([^\*]*)\*/g; var results; while(results=italicsRegex.exec(choiceText)){ // Modify choiceText in place, it's fine. var startOfMatch = results.index; var endOfMatch = results.index + results[0].length; choiceText = choiceText.slice(0,startOfMatch) + "" + results[1] + "" + choiceText.slice(endOfMatch); } // Add bold where _word word word_ var boldRegex = /\_([^\*]*)\_/g; var results; while(results=boldRegex.exec(choiceText)){ // Modify choiceText in place, it's fine. var startOfMatch = results.index; var endOfMatch = results.index + results[0].length; choiceText = choiceText.slice(0,startOfMatch) + "" + results[1] + "" + choiceText.slice(endOfMatch); } var div = document.createElement("div"); div.innerHTML = choiceText; div.setAttribute("speaker", Game.OVERRIDE_CHOICE_SPEAKER ? Game.OVERRIDE_CHOICE_SPEAKER : "b"); if(!THERE_IS_NO_CHOICE){ div.onclick = function(event){ // Any pre-choice code? if(preChoiceCodeIfAny) Game.executeCode(preChoiceCodeIfAny); // Override line... ONCE if(!Game.OVERRIDE_CHOICE_LINE){ if(Game.OVERRIDE_CHOICE_SPEAKER){ Game.addToQueue(Game.OVERRIDE_CHOICE_SPEAKER+": "+originalChoiceText); }else{ Game.addToQueue("b: "+originalChoiceText); } } Game.OVERRIDE_CHOICE_SPEAKER = null; Game.OVERRIDE_CHOICE_LINE = false; // Play sound sfx("ui_click"); // Goto that choice, now! Game.goto(choiceID); // STOP THE PROP event.stopPropagation(); }; div.onmouseover = function(){ sfx("ui_hover"); }; }else{ div.setAttribute("speaker", Game.OVERRIDE_CHOICE_SPEAKER ? Game.OVERRIDE_CHOICE_SPEAKER : "none"); } // Add choice, animated! div.classList.add("hidden"); Game.choicesDOM.appendChild(div); requestAnimationFrame(function(){ div.classList.remove("hidden"); sfx("ui_show_choice", {volume:0.4}); }); // Or... FORCE if(Game.OVERRIDE_FONT_SIZE){ div.style.fontSize = Game.OVERRIDE_FONT_SIZE+"px"; }else{ // If it's too big, shrink font size setTimeout(function(){ var choiceHeight = div.getBoundingClientRect().height; if(choiceHeight>40) div.style.fontSize = "18px"; // And if still too much??? setTimeout(function(){ var choiceHeight = div.getBoundingClientRect().height; if(choiceHeight>40) div.style.fontSize = "17px"; // And if still too much??? setTimeout(function(){ var choiceHeight = div.getBoundingClientRect().height; if(choiceHeight>40) div.style.fontSize = "16px"; // And if still too much??? setTimeout(function(){ var choiceHeight = div.getBoundingClientRect().height; if(choiceHeight>40) div.style.fontSize = "15px"; // And if still too much??? setTimeout(function(){ var choiceHeight = div.getBoundingClientRect().height; if(choiceHeight>40) div.style.fontSize = "14px"; // And if still too much??? setTimeout(function(){ var choiceHeight = div.getBoundingClientRect().height; if(choiceHeight>40) div.style.fontSize = "13px"; // And if still too much??? setTimeout(function(){ var choiceHeight = div.getBoundingClientRect().height; if(choiceHeight>40) div.style.fontSize = "12px"; },1); },1); },1); },1); },1); },1); },1); } Game.OVERRIDE_FONT_SIZE = false; // Wait a bit before adding new line return new RSVP.Promise(function(resolve){ Game.setTimeout(resolve, 100); }); } // Execute code! Game.executeCode = function(line){ var code = line.match(/\`+([^\`]*)\`+/)[1].trim(); try{ eval(code); }catch(e){ console.log(e); } // Return immediate promise return Game.immediatePromise(); } // Execute wait! Just wait. Game.executeWait = function(line){ // Get integer from (...NN) var waitTime = parseInt(line.match(/^\(\.\.\.(\d+)\)/)[1].trim()); // Unless it's click to advance, then IGNORE ALL WAITS if(!Game.FORCE_CANT_SKIP){ // Specific wait-time, don't skip? var waitTimeString = waitTime+""; var lastDigit = waitTimeString[waitTimeString.length-1]; var cantSkip = (lastDigit!="0"); // CAN'T SKIP. if(!cantSkip && Game.CLICK_TO_ADVANCE && waitTime<=999){ // hack: unless the wait is long. waitTime = 0; } } // Delayed promise return new RSVP.Promise(function(resolve){ Game.setTimeout(resolve, waitTime); }); }; // Execute goto! Just goto. Game.executeGoto = function(line){ var gotoID = line.match(/^\(\#(.*)\)/)[1].trim().toLocaleLowerCase(); Game.goto(gotoID); } // Determine line type... text, choice, or code? Game.getLineType = function(line){ // Is it a choice? var isChoice = /\[.*\]\(.*\)/.test(line); if(isChoice) return "choice"; // Is it a goto? var isGoto = /^\(\#(.*)\)/.test(line); if(isGoto) return "goto"; // Is it code? var isCode = /^\`/.test(line); if(isCode) return "code"; // Is it a wait? var isWait = /^\(\.\.\.\d+\)/.test(line); if(isWait) return "wait"; // Otherwise, it's text. return "text"; }; // Parse all the handlebars... Game.parseLine = function(line){ // Get rid of newlines line = line.replace(/\n/gi,""); // Get the IFs, if any var lookForIfs = true; while(lookForIfs){ lookForIfs = false; // Look for an IF! var regex = /\{\{if[^\/]*\/if\}\}/ig; // the reason it's inside here is to reset .exec var regexResult = regex.exec(line); if(regexResult){ // The result... var fullConditional = regexResult[0]; var startsAtIndex = regexResult.index; var endsAtIndex = startsAtIndex + fullConditional.length; // Extract the condition var condition = fullConditional.match(/\{\{if\s+([^\{\}]*)\}\}/)[1]; // Extract the inside text var insideText = fullConditional.match(/\}\}([^\{\}]*)\{\{/)[1].trim()+" "; // Eval condition! var conditionIsTrue = false; try{ conditionIsTrue = eval(condition); }catch(e){ console.log(e); } // Edit the line var insert = conditionIsTrue ? insideText : ""; line = line.slice(0,startsAtIndex) + insert + line.slice(endsAtIndex); // Keep searching... lookForIfs = true; } } // Evaluate {{expressions}}, if any var lookForExpressions = true; while(lookForExpressions){ lookForExpressions = false; // Look for an IF! //debugger; var regex = /\{\{[^\}]*\}\}/ig; // the reason it's inside here is to reset .exec var regexResult = regex.exec(line); if(regexResult){ // The result... var fullExpression = regexResult[0]; var startsAtIndex = regexResult.index; var endsAtIndex = startsAtIndex + fullExpression.length; // Extract the expression var expression = fullExpression.match(/\{\{([^\}]*)\}\}/)[1]; // Eval condition! var evaluated = ""; try{ evaluated = eval(expression); }catch(e){ console.log(e); } // Edit the line line = line.slice(0,startsAtIndex) + evaluated + line.slice(endsAtIndex); // Keep searching... lookForExpressions = true; } } // Return line! return line; }; //////////////////////////////////////////////////////////////////////////////////////////////// // WHERE STUFF WILL BE DRAWN /////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////// Game.canvas.width = 360 * 2; Game.canvas.height = 600 * 2; //450 * 2; Game.canvas.style.width = Game.canvas.width/2 + "px"; Game.canvas.style.height = Game.canvas.height/2 + "px"; Game.context = Game.canvas.getContext("2d"); // A blank scene Game.scene = null; Game.resetScene = function(){ // Kill all of previous scene if(Game.scene){ Game.scene.children.forEach(function(child){ if(child.kill) child.kill(); }); if(Game.scene.kill) Game.scene.kill(); } // New scene! Game.scene = {}; Game.scene.children = []; // Misc Game.WORDS_HEIGHT_BOTTOM = -1; }; Game.resetScene(); // Update & draw all the kids! Game.updateCanvas = function(delta){ // UPDATING: // ------------------------------------------------------------- for(const child of Game.scene.children) { if(child.update) child.update(delta); } // RENDERING: // ------------------------------------------------------------- // For retina var ctx = Game.context; ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height); ctx.scale(2, 2); // Update/Draw all kids for(const child of Game.scene.children) child.draw(ctx, delta); // Restore ctx.scale(0.5, 0.5); // Draw HP HP.draw(); }; // HACK: PREVENT ACCIDENTALLY TABBING & BREAKING UI window.addEventListener("keydown", function(e){ if(e.keyCode==9){ e.preventDefault(); e.stopPropagation(); } if(e.keyCode==32){ // SPACE TO ADVANCE _unpauseOrSkip(); } }); // CUSSING var queryParams = {}; if(window.location.search){ window.location.search.substr(1).split("&").forEach(function(item){ var split = item.split("="); queryParams[split[0]] = split[1]; }); } if(queryParams.c){ window.NO_CUSS_MODE = true; }else{ var doCuss = document.createElement("style"); doCuss.innerHTML = ".hide-if-cuss-free{ display:inline; }"; document.body.appendChild(doCuss); }