anxiety/scripts/game/Game.js

1123 lines
28 KiB
JavaScript
Raw Normal View History

2019-02-11 17:04:23 +00:00
window._ = {};
window.Game = {};
Game.sections = {};
2019-02-12 19:59:45 +00:00
Game.queue = [];
2019-04-18 11:40:22 +00:00
Game.dom = $("#game_container");
Game.wordsDOM = $("#game_words");
Game.choicesDOM = $("#game_choices");
Game.canvas = $("#game_canvas");
2019-02-18 20:59:10 +00:00
2019-04-18 11:40:22 +00:00
window.SceneSetup = {}; // A big ol' singleton class that just makes it easy to create scenes.
2019-02-22 23:48:24 +00:00
2019-03-17 18:41:19 +00:00
// HELPER FUNCS
window.bb = function(){
publish("bb", arguments);
};
2019-04-18 11:40:22 +00:00
window.hong = function(){
publish("hong", arguments);
};
window.attack = function(damage, type){
publish("attack", ["hong", damage, type]);
2019-06-26 15:03:27 +00:00
_["attack_"+type+"_ch"+_.CHAPTER]++; // HACK
2019-04-18 11:40:22 +00:00
};
window.attackBB = function(damage, type){
2019-06-12 16:23:26 +00:00
publish("attack", ["bb", damage]);
2019-04-18 11:40:22 +00:00
};
2019-03-17 18:41:19 +00:00
2019-02-18 20:59:10 +00:00
// 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);
})
2019-04-09 18:59:19 +00:00
// HP!
window.HP = new HitPoints();
2019-02-18 20:59:10 +00:00
// Animation!
2019-04-11 18:44:15 +00:00
console.log("init");
2019-02-18 20:59:10 +00:00
var animloop = function(){
Game.update();
requestAnimationFrame(animloop);
};
requestAnimationFrame(animloop);
};
2019-02-11 17:04:23 +00:00
// Call to toggle debug rendering
Game.debug = function(){
document.body.classList.toggle('show_debug');
}
2019-02-18 20:59:10 +00:00
// Parse scene markdown!
Game.parseSceneMarkdown = function(md){
2019-02-11 17:04:23 +00:00
// Split into sections...
md = md.trim().replace(/\r/g, "");
2019-02-18 20:59:10 +00:00
md = "\n" + md;
var sections = md.split(/\n\#\s*/);
2019-02-11 17:04:23 +00:00
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<lines.length; i++) lines[i]=lines[i].trim(); // trim it all!
// New section
Game.sections[id] = {
id: id,
lines: lines
};
});
2019-02-18 20:59:10 +00:00
};
2019-02-11 17:04:23 +00:00
2019-02-17 21:54:29 +00:00
////////////////////////////////////////////////////////////////////////////////////////////////
// SCENE MANAGEMENT ////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////
2019-02-11 17:04:23 +00:00
Game.start = function(){
2019-09-09 16:37:58 +00:00
Game.FORCE_CANT_SKIP = false; // for the replay
2019-02-11 17:04:23 +00:00
window._ = {}; // global var, reset
};
var last_frame = Date.now();
2019-02-12 19:59:45 +00:00
Game.update = function(){
2019-02-20 18:45:23 +00:00
2019-09-09 16:37:58 +00:00
// TIME
const now = Date.now();
const delta = (now - last_frame) / 1000;
last_frame = now;
// Removing this till I can see why it's needed...
// Reshaping the passing of time will come back to haunt you! [Spacie]
// DELTA = Math.min(DELTA, 1/20); // no slower than 20fps
2019-09-09 16:37:58 +00:00
2019-02-20 18:45:23 +00:00
if(!Game.paused){
// Timeout callbacks...
Game.timeoutCallbacks.forEach(tc => {
tc.timeLeft -= 1000 * delta;
if(tc.timeLeft <= 0) tc.callback();
});
Game.timeoutCallbacks = Game.timeoutCallbacks.filter(tc => tc.timeLeft > 0);
2019-02-20 18:45:23 +00:00
// The interface
Game.updateText();
Game.updateCanvas(delta);
2019-02-20 18:45:23 +00:00
// Ayyy
publish("update");
}
2019-04-28 16:02:55 +00:00
// Options update
Options.update();
2019-02-20 18:45:23 +00:00
};
2019-02-20 20:22:23 +00:00
// PAUSING THE GAME
2019-02-20 18:45:23 +00:00
Game.paused = false;
2019-04-18 11:40:22 +00:00
Game.pausedDOM = $("#paused");
2019-02-20 18:45:23 +00:00
Game.pause = function(){
2019-04-29 19:19:44 +00:00
2019-02-20 18:45:23 +00:00
Game.paused = true;
2019-02-20 20:22:23 +00:00
Game.pausedDOM.style.display = "block";
2019-04-23 19:12:41 +00:00
Howler.mute(true);
2019-04-29 19:19:44 +00:00
2019-09-06 20:25:14 +00:00
$("#paused").setAttribute("modal", (Options.showing||About.showing||ContentNotes.showing) ? "yes" : "no" );
2019-09-09 16:37:58 +00:00
publish("GAME_PAUSED");
2019-04-29 19:19:44 +00:00
2019-02-20 18:45:23 +00:00
};
window.addEventListener("blur", Game.pause);
Game.onUnpause = function(){
2019-09-06 20:25:14 +00:00
if(Game.paused && !(Options.showing||About.showing||ContentNotes.showing)){
2019-02-20 18:45:23 +00:00
Game.paused = false;
2019-02-20 20:22:23 +00:00
Game.pausedDOM.style.display = "none";
2019-04-23 19:12:41 +00:00
Howler.mute(false);
2019-02-20 18:45:23 +00:00
}
2019-09-09 16:37:58 +00:00
publish("GAME_UNPAUSED");
2019-02-20 18:45:23 +00:00
};
2019-06-26 15:03:27 +00:00
Game.pausedDOM.onclick = function(e){
2019-04-30 15:20:45 +00:00
if(Options.showing){
publish("hide_options");
2019-06-26 15:03:27 +00:00
}else if(About.showing){
2019-04-30 15:20:45 +00:00
$("#close_about").onclick();
2019-09-06 20:25:14 +00:00
}else if(About.showing){
publish("hide_cn");
2019-06-26 15:03:27 +00:00
}else{
Game.onUnpause();
}
e.stopPropagation();
};
// UNPAUSE OR SKIP DIALOGUE?
var _unpauseOrSkip = function(){
if(Game.paused){
Game.onUnpause();
}else{
Game.clearAllTimeouts();
2019-09-06 17:34:09 +00:00
publish("super_hack_skip_intro");
2019-04-30 15:20:45 +00:00
}
};
2019-06-26 15:03:27 +00:00
window.addEventListener("click", _unpauseOrSkip);
window.addEventListener("touchstart", _unpauseOrSkip);
2019-02-20 18:45:23 +00:00
2019-02-20 20:22:23 +00:00
// "SET TIMEOUT" for text and stuff
2019-02-20 18:45:23 +00:00
Game.timeoutCallbacks = [];
Game.setTimeout = function(callback, interval){
Game.timeoutCallbacks.push({
callback: callback,
timeLeft: interval
});
2019-02-12 19:59:45 +00:00
};
2019-05-03 16:13:22 +00:00
// SKIP TEXT WHEN CLICK ANYWHERE (but NOT capture in choice)
2019-02-20 20:22:23 +00:00
Game.clearAllTimeouts = function(){
2019-05-02 15:55:41 +00:00
2019-05-05 19:56:00 +00:00
// NOPE
if(Game.FORCE_CANT_SKIP) return;
2019-05-02 15:55:41 +00:00
// 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; i<Game.timeoutCallbacks.length; i++){
if(isInterrupting && i==Game.timeoutCallbacks.length-1) break;
Game.timeoutCallbacks[i].callback();
}
if(isInterrupting){
Game.timeoutCallbacks = [ Game.timeoutCallbacks[Game.timeoutCallbacks.length-1] ]; // last one
}else{
Game.timeoutCallbacks = [];
}
2019-02-20 20:22:23 +00:00
};
2019-06-26 15:03:27 +00:00
// CLICK TO SKIP DIALOGUE
//window.addEventListener("click", Game.clearAllTimeouts);
//window.addEventListener("touchstart", Game.clearAllTimeouts);
/*Game.canvas.addEventListener("click", Game.clearAllTimeouts);
2019-02-20 20:22:23 +00:00
Game.canvas.addEventListener("touchstart", Game.clearAllTimeouts);
2019-04-29 19:19:44 +00:00
Game.choicesDOM.addEventListener("click", Game.clearAllTimeouts);
2019-06-26 15:03:27 +00:00
Game.choicesDOM.addEventListener("touchstart", Game.clearAllTimeouts);*/
2019-02-12 19:59:45 +00:00
2019-02-11 17:04:23 +00:00
Game.goto = function(sectionID){
// Clear choices
Game.choicesDOM.innerHTML = "";
// Show each line...
var section = Game.sections[sectionID];
2019-02-12 19:59:45 +00:00
if(!section){
throw "NO SECTION NAMED "+sectionID;
}
2019-02-11 17:04:23 +00:00
var lines = section.lines;
2019-02-12 19:59:45 +00:00
Game.queue = Game.queue.concat(lines);
Game.executeNextLine();
2019-02-11 17:04:23 +00:00
2019-02-12 19:59:45 +00:00
};
2019-02-17 21:54:29 +00:00
Game.executeNextLine = function(){
2019-02-12 19:59:45 +00:00
// Parse handlebars
var originalLine = Game.queue.shift();
if(!originalLine) return; // END OF QUEUE.
line = Game.parseLine(originalLine);
// Execute line
var promiseNext;
2019-02-17 21:54:29 +00:00
if(line==""){
// If no line, get immediate promise...
2019-02-18 20:59:10 +00:00
promiseNext = Game.immediatePromise();
2019-02-17 21:54:29 +00:00
}else{
2019-02-12 19:59:45 +00:00
// Execute based on what type it is!
var lineType = Game.getLineType(line);
switch(lineType){
case "text":
promiseNext = Game.executeText(line);
2019-02-11 17:04:23 +00:00
break;
2019-02-12 19:59:45 +00:00
case "choice":
promiseNext = Game.executeChoice(line);
break;
case "code":
promiseNext = Game.executeCode(line);
break;
2019-02-18 17:24:52 +00:00
case "wait":
promiseNext = Game.executeWait(line);
break;
2019-02-12 19:59:45 +00:00
}
2019-02-11 17:04:23 +00:00
2019-02-12 19:59:45 +00:00
// If it's a goto, end THIS section immediately.
if(lineType=="goto"){
Game.clearQueue(); // CLEAR ALL ELSE IN QUEUE
Game.executeGoto(line);
return;
2019-02-11 17:04:23 +00:00
}
}
2019-02-12 19:59:45 +00:00
// Do next line?
if(Game.queue.length>0){
promiseNext.then(function(){
Game.executeNextLine();
});
}
};
Game.clearQueue = function(){
Game.queue = [];
2019-02-11 17:04:23 +00:00
};
2019-02-12 19:59:45 +00:00
Game.addToQueue = function(line){
Game.queue.push(line);
}
2019-02-11 17:04:23 +00:00
2019-02-17 21:54:29 +00:00
////////////////////////////////////////////////////////////////////////////////////////////////
// TEXT AND STUFF //////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////
2019-02-18 20:59:10 +00:00
// Immediate Promise
Game.immediatePromise = function(){
return new RSVP.Promise(function(resolve){
resolve();
});
};
2019-02-17 21:54:29 +00:00
// Move the text DOM to latest
2019-04-26 17:08:31 +00:00
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();
})()
2019-02-17 21:54:29 +00:00
2019-02-22 23:48:24 +00:00
// CLEAR TEXT
Game.clearText = function(){
2019-04-19 15:57:10 +00:00
Game.wordsDOM.innerHTML = "";
Game.updateText();
2019-02-22 23:48:24 +00:00
};
2019-06-21 15:33:04 +00:00
Game.clearAll = function(){
Game.clearText();
Game.resetScene();
2019-06-26 15:03:27 +00:00
music(null);
stopAllSounds();
2019-06-21 15:33:04 +00:00
};
2019-04-18 11:40:22 +00:00
window.clearText = Game.clearText;
2019-02-22 23:48:24 +00:00
2019-09-06 20:25:14 +00:00
// CUSSING?!
window.NO_CUSS_MODE = false;
var GRAWLIXES = ["@","#","✩","$","%","&"];
var GRAWLIX_INDEX = 0;
2019-02-11 17:04:23 +00:00
// Execute text! Just add it to text DOM.
2019-05-02 15:55:41 +00:00
Game.TEXT_SPEED = 50;
Game.CLICK_TO_ADVANCE = true;
2019-05-05 19:56:00 +00:00
Game.FORCE_CANT_SKIP = false;
2019-02-18 20:59:10 +00:00
Game.OVERRIDE_TEXT_SPEED = 1;
2019-04-25 18:07:41 +00:00
Game.FORCE_TEXT_DURATION = -1;
2019-03-17 18:41:19 +00:00
Game.WHO_IS_SPEAKING = null; // "h", "b", "n" etc...
Game.CURRENT_SPEAKING_SPEED = 1;
2019-04-25 18:07:41 +00:00
Game.FORCE_NO_VOICE = false;
2019-06-21 15:33:04 +00:00
Game.NO_NARRATOR_SOUNDS = false;
2019-02-11 17:04:23 +00:00
Game.executeText = function(line){
2019-02-12 19:59:45 +00:00
2019-02-18 20:59:10 +00:00
return new RSVP.Promise(function(resolve){
2019-02-12 19:59:45 +00:00
2019-02-20 18:45:23 +00:00
// Who's speaking?
2019-04-29 15:35:25 +00:00
// b: Beebee, h: Hong, n: Narrator, n2: Narrator 2, n3: Narrator 3
var regex = /^([^\:]+)\:(.*)/
2019-02-20 18:45:23 +00:00
var speaker = line.match(regex)[1].trim();
var dialogue = line.match(regex)[2].trim();
2019-02-18 20:59:10 +00:00
2019-09-06 20:25:14 +00:00
// IF IT'S A SPECIAL ATTACK, SKIP ALL THIS
2019-07-18 14:45:01 +00:00
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;
}
2019-02-18 20:59:10 +00:00
// Add the bubble, with animation
var div = document.createElement("div");
Game.wordsDOM.appendChild(div);
2019-03-17 18:41:19 +00:00
Game.WHO_IS_SPEAKING = speaker; // WHO'S SPEAKING?!
Game.CURRENT_SPEAKING_SPEED = Game.OVERRIDE_TEXT_SPEED;
2019-07-30 20:17:36 +00:00
if(Game.OVERRIDE_FONT_SIZE){
div.style.fontSize = Game.OVERRIDE_FONT_SIZE+"px";
}
2019-02-20 18:45:23 +00:00
switch(speaker){
case "b":
div.className = "beebee-bubble";
break;
case "h":
div.className = "hong-bubble";
break;
2019-06-19 14:49:05 +00:00
case "h2": case "h3":
2019-06-12 16:23:26 +00:00
div.className = "hong2-bubble";
break;
2019-06-19 14:49:05 +00:00
case "a":
div.className = "al-bubble";
break;
case "s":
div.className = "shire-bubble";
break;
2019-06-12 16:23:26 +00:00
case "r":
div.className = "hunter-bubble";
break;
2019-02-20 18:45:23 +00:00
case "n":
div.className = "narrator-bubble";
break;
2019-04-29 15:35:25 +00:00
case "n2": // narrator 2
2019-04-24 19:42:13 +00:00
div.className = "narrator-bubble-2";
break;
2019-04-29 15:35:25 +00:00
case "n3": // narrator 3
div.className = "narrator-bubble-3";
break;
2019-06-21 15:33:04 +00:00
case "n4": // narrator 4
2019-04-29 15:35:25 +00:00
div.className = "narrator-bubble-4";
break;
2019-06-21 15:33:04 +00:00
case "n5": // narrator 5
div.className = "narrator-bubble-5";
break;
case "i": // Intermission
div.className = "narrator-bubble-i";
break;
2019-02-20 18:45:23 +00:00
}
2019-02-12 19:59:45 +00:00
requestAnimationFrame(function(){
2019-02-18 20:59:10 +00:00
requestAnimationFrame(function(){
div.style.opacity = 1;
div.style.left = 0;
});
2019-02-12 19:59:45 +00:00
});
2019-03-17 18:41:19 +00:00
// Clear both
var clearBoth = document.createElement("div");
clearBoth.className = "clear-both";
Game.wordsDOM.appendChild(clearBoth);
2019-09-06 20:25:14 +00:00
// CUSSING OR NO CUSSING?
if(window.NO_CUSS_MODE){
// Slice it out with Grawlixes, 1 by 1.
var censorMode = false;
for(var i=0; i<dialogue.length; i++){
var chr = dialogue[i];
if(chr=="^"){
censorMode = !censorMode; // toggle
dialogue = dialogue.slice(0,i) + dialogue.slice(i+1); // slice out the one character
i--; // step back
}else if(censorMode){
var grawlix = GRAWLIXES[GRAWLIX_INDEX];
GRAWLIX_INDEX = (GRAWLIX_INDEX+1) % GRAWLIXES.length;
dialogue = dialogue.slice(0,i) + grawlix + dialogue.slice(i+1); // replace the character
}
}
}else{
// remove all "^" signs
dialogue = dialogue.replace(/\^/g, "");
}
2019-02-20 18:45:23 +00:00
// Add the text
2019-02-18 20:59:10 +00:00
var interval = 0;
2019-02-20 20:22:23 +00:00
var SPEED = Math.round(Game.TEXT_SPEED / Game.OVERRIDE_TEXT_SPEED);
2019-04-25 18:07:41 +00:00
if(Game.FORCE_TEXT_DURATION>0){
SPEED = Math.round(Game.FORCE_TEXT_DURATION/dialogue.length);
}
2019-04-29 15:35:25 +00:00
2019-06-19 14:49:05 +00:00
// 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"){
2019-02-20 18:45:23 +00:00
2019-03-17 18:41:19 +00:00
// Put in the text, each character a DIFFERENT SPAN...
var span, chr;
var isItalicized = false;
for(var i=0; i<dialogue.length; i++){
// Is it italicized?
chr = dialogue[i];
if(chr=="*") isItalicized = !isItalicized; // toggle!
// Add letter span
span = document.createElement("span");
if(chr=="*"){
// else, empty. can't NOT add span, coz screws up indexing.
}else{
span.innerHTML = isItalicized ? "<i>"+chr+"</i>" : chr;
}
span.style.opacity = 0;
div.appendChild(span);
}
// Then REVEAL letters one-by-one
2019-02-20 18:45:23 +00:00
for(var i=0; i<dialogue.length; i++){
var chr = dialogue[i];
// If it's the last char & it's "-", skip
if(i==dialogue.length-1 && chr=="-") break;
// for scopin'
2019-04-25 18:07:41 +00:00
(function(index, interval, speaker, forceNoSound){
2019-02-20 18:45:23 +00:00
Game.setTimeout(function(){
2019-04-23 19:12:41 +00:00
// Show it
2019-03-17 18:41:19 +00:00
div.children[index].style.opacity = 1;
2019-04-23 19:12:41 +00:00
// And SOUND?
2019-04-25 18:07:41 +00:00
if(!forceNoSound){
var chr = div.children[index].innerHTML;
if(chr!=" "){
2019-06-12 16:23:26 +00:00
if(speaker=="h" || speaker=="h2"){
2019-04-25 18:07:41 +00:00
voice("hong", {volume:0.3});
}
2019-06-19 14:49:05 +00:00
if(speaker=="b" || speaker=="h3"){
2019-04-25 18:07:41 +00:00
voice("beebee", {volume:0.5});
}
2019-05-03 16:13:22 +00:00
if(speaker=="n3"){
voice("typewriter", {volume:0.5});
}
if(speaker=="r"){
voice("hunter", {volume:0.17});
}
2019-06-19 14:49:05 +00:00
if(speaker=="a"){
voice("al", {volume:0.3});
}
if(speaker=="s"){
voice("shire", {volume:0.4});
}
2019-04-23 19:12:41 +00:00
}
}
2019-02-20 18:45:23 +00:00
}, interval);
2019-04-25 18:07:41 +00:00
})(i, interval, speaker, Game.FORCE_NO_VOICE);
2019-02-20 18:45:23 +00:00
// Bigger interval
if(i!=dialogue.length-1){ // NOT last
2019-04-19 15:57:10 +00:00
if(chr=="."){
if(dialogue[i+1]=="\""){ // UNLESS next one's a punctuation!
interval += 0;
}else{
interval += SPEED*10;
}
}else if(chr=="?" || chr=="!"){ // gap unless next one's ALSO punctuation.
if(dialogue[i+1]==" "){ // next one's a space? gap!
interval += SPEED*10;
}else{ // if not, no!
interval += SPEED;
}
2019-04-29 15:35:25 +00:00
}else if(chr=="," || chr==":"){
2019-02-20 18:45:23 +00:00
interval += SPEED*5;
}else{
interval += SPEED;
}
}
}
}else{
2019-06-21 15:33:04 +00:00
// IF NARRATOR
2019-02-20 18:45:23 +00:00
// *Emphasize multiple words* => *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);
2019-02-18 20:59:10 +00:00
}
2019-03-17 18:41:19 +00:00
// Put in the text, each word a DIFFERENT SPAN
var span;
2019-02-20 18:45:23 +00:00
var dialogueWords = dialogue.split(" ");
for(var i=0; i<dialogueWords.length; i++){
2019-03-17 18:41:19 +00:00
// Is it an emphasized word?
2019-04-29 15:35:25 +00:00
var reggie = /\*(.*)\*/;
2019-02-20 18:45:23 +00:00
var word = dialogueWords[i];
2019-04-29 15:35:25 +00:00
if(reggie.test(word)){
word = "<i>" + word.match(reggie)[1].trim() + "</i>";
}
// Actual emphasis
reggie = /\_(.*)\_/;
if(reggie.test(word)){
word = "<i class='italics'>" + word.match(reggie)[1].trim() + "</i>";
2019-02-20 18:45:23 +00:00
}
2019-03-17 18:41:19 +00:00
// Add the span
span = document.createElement("span");
span.innerHTML = word+" ";
span.style.opacity = 0;
div.appendChild(span);
2019-02-20 18:45:23 +00:00
2019-03-17 18:41:19 +00:00
}
// Then REVEAL words one-by-one
for(var i=0; i<dialogueWords.length; i++){
2019-02-20 18:45:23 +00:00
2019-03-17 18:41:19 +00:00
var word = dialogueWords[i];
// for scopin'
2019-04-23 19:12:41 +00:00
(function(index, interval, word){
2019-03-17 18:41:19 +00:00
Game.setTimeout(function(){
2019-04-23 19:12:41 +00:00
// Show
2019-03-17 18:41:19 +00:00
div.children[index].style.opacity = 1;
2019-04-23 19:12:41 +00:00
// SOUND
var chr = word.slice(-1);
var isEmphasis = (chr=="*");
2019-06-21 15:33:04 +00:00
if(!Game.NO_NARRATOR_SOUNDS){
voice(isEmphasis ? "narrator_emphasis" : "narrator");
}
2019-04-23 19:12:41 +00:00
2019-02-20 18:45:23 +00:00
}, interval);
2019-04-23 19:12:41 +00:00
})(i, interval, word);
2019-02-20 18:45:23 +00:00
// Interval
2019-02-22 23:48:24 +00:00
interval += SPEED*6;
2019-02-20 18:45:23 +00:00
// Larger interval if punctuation...
2019-03-17 18:41:19 +00:00
var chr = word.slice(-1);
2019-04-19 17:44:45 +00:00
var isIcon = (word[0]=="#" && chr=="#");
2019-03-17 18:41:19 +00:00
if(chr=="*") chr = word[word.length-2]; // coz emphasis
2019-02-22 23:48:24 +00:00
if(chr=="," || chr==":") interval += SPEED*5;
if(chr=="." || chr=="?" || chr=="!") interval += SPEED*10;
2019-03-17 18:41:19 +00:00
if(word.slice(-3)=="...") interval += SPEED*15;
2019-02-20 18:45:23 +00:00
2019-04-19 17:44:45 +00:00
// Oh, was it an ICON?
if(word[0]=="#" && chr=="#"){
interval += SPEED*10;
var span = div.children[i];
span.innerHTML = "";
span.style.display = "block";
var iconName = word.slice(1,-1)
var icon = new Image();
icon.src = Library.images["fear_"+iconName].hackSrc;
2019-04-19 17:44:45 +00:00
div.children[i].appendChild(icon);
icon.style.display = "block";
2019-06-21 15:33:04 +00:00
if(speaker!="i"){
icon.style.margin = "0 auto";
}
2019-04-19 17:44:45 +00:00
icon.style.width = "80px";
icon.style.height = "80px";
}
2019-02-20 18:45:23 +00:00
2019-04-19 17:44:45 +00:00
}
2019-02-20 18:45:23 +00:00
2019-02-12 19:59:45 +00:00
}
2019-02-20 20:22:23 +00:00
// Return overrides to default
Game.OVERRIDE_TEXT_SPEED = 1;
2019-07-30 20:17:36 +00:00
Game.OVERRIDE_FONT_SIZE = false;
2019-04-25 18:07:41 +00:00
Game.FORCE_TEXT_DURATION = -1;
Game.FORCE_NO_VOICE = false;
2019-02-20 20:22:23 +00:00
2019-02-18 20:59:10 +00:00
// Return promise
2019-04-19 15:57:10 +00:00
var nextLineDelay = Game.TEXT_SPEED*7; // don't override this
2019-02-18 20:59:10 +00:00
if(dialogue.slice(-1)=="-") nextLineDelay=0; // sudden interrupt!
2019-05-05 19:56:00 +00:00
if(!Game.FORCE_CANT_SKIP){
if(Game.CLICK_TO_ADVANCE){ // IF IT'S CLICK-TO-ADVANCE, "INFINITE" TIMEOUT.
nextLineDelay = 1000*10000; // ten thousand seconds
}
2019-04-28 16:02:55 +00:00
}
2019-05-02 15:55:41 +00:00
// No one's speaking anymore.
Game.setTimeout(function(){
Game.WHO_IS_SPEAKING = null;
2019-07-30 20:17:36 +00:00
publish("DONE_SPEAKING");
2019-05-02 15:55:41 +00:00
}, interval);
// Show the clicky UI
2019-05-05 19:56:00 +00:00
if(!Game.FORCE_CANT_SKIP){
if(Game.CLICK_TO_ADVANCE){
Game.setTimeout(function(){
publish("show_click_to_advance");
}, interval+Game.TEXT_SPEED*2);
}
2019-05-02 15:55:41 +00:00
}
// DONE WITH IT
2019-03-17 18:41:19 +00:00
Game.setTimeout(function(){
2019-05-02 15:55:41 +00:00
publish("hide_click_to_advance");
resolve(); // DONE WITH IT.
2019-03-17 18:41:19 +00:00
}, interval+nextLineDelay);
2019-02-12 19:59:45 +00:00
2019-09-09 16:37:58 +00:00
// VISIBLE: FALSE FOR ANY ELEMENTS PAST 10
var VISIBLE_LIMIT = 10;
var w = Game.wordsDOM.children;
if(w.length>VISIBLE_LIMIT){
w[ w.length - VISIBLE_LIMIT - 1 ].style.visibility = "hidden";
w[ w.length - VISIBLE_LIMIT - 2 ].style.visibility = "hidden";
}
2019-02-18 20:59:10 +00:00
});
2019-02-12 19:59:45 +00:00
2019-02-11 17:04:23 +00:00
}
2019-04-25 18:07:41 +00:00
// 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" }
]);
2019-02-11 17:04:23 +00:00
// Execute choice! Add it to choice DOM.
2019-02-18 20:59:10 +00:00
Game.OVERRIDE_CHOICE_LINE = false;
2019-06-26 15:03:27 +00:00
Game.OVERRIDE_CHOICE_SPEAKER = null;
2019-07-30 20:17:36 +00:00
Game.OVERRIDE_FONT_SIZE = false;
2019-02-11 17:04:23 +00:00
Game.executeChoice = function(line){
2019-02-18 20:59:10 +00:00
var choiceText = line.match(/\[([^\]]*)\]/)[1].trim();
2019-09-06 17:34:09 +00:00
var choiceID = line.match(/\(\#([^\)]*)\)/);
var THERE_IS_NO_CHOICE;
if(!choiceID){
THERE_IS_NO_CHOICE = true;
}else{
choiceID = choiceID[1].trim().toLocaleLowerCase();
}
2019-02-18 20:59:10 +00:00
var preChoiceCodeIfAny = null;
if(/\`(.*)\`/.test(line)){
preChoiceCodeIfAny = line.match(/\`(.*)\`/)[0]; // 0, with backticks
}
2019-02-11 17:04:23 +00:00
2019-09-17 14:24:40 +00:00
// SUPER HACK: #play1# and #play2#
choiceText = choiceText.replace("#play1#", "<div class='mini-icon' pic='play1'></div>");
choiceText = choiceText.replace("#play2#", "<div class='mini-icon' pic='play2'></div>");
2019-04-19 15:57:10 +00:00
// 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) + "<i>" + results[1] + "</i>" + choiceText.slice(endOfMatch);
}
2019-09-06 20:25:14 +00:00
// 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) + "<b>" + results[1] + "</b>" + choiceText.slice(endOfMatch);
}
2019-04-19 15:57:10 +00:00
2019-02-11 17:04:23 +00:00
var div = document.createElement("div");
div.innerHTML = choiceText;
2019-06-26 15:03:27 +00:00
div.setAttribute("speaker", Game.OVERRIDE_CHOICE_SPEAKER ? Game.OVERRIDE_CHOICE_SPEAKER : "b");
2019-09-06 17:34:09 +00:00
if(!THERE_IS_NO_CHOICE){
div.onclick = function(event){
2019-02-18 20:59:10 +00:00
2019-09-06 17:34:09 +00:00
// Any pre-choice code?
if(preChoiceCodeIfAny) Game.executeCode(preChoiceCodeIfAny);
2019-02-18 20:59:10 +00:00
2019-09-06 17:34:09 +00:00
// 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);
}
2019-06-26 15:03:27 +00:00
}
2019-09-06 17:34:09 +00:00
Game.OVERRIDE_CHOICE_SPEAKER = null;
Game.OVERRIDE_CHOICE_LINE = false;
2019-02-18 20:59:10 +00:00
2019-09-06 17:34:09 +00:00
// Play sound
sfx("ui_click");
2019-04-25 18:07:41 +00:00
2019-09-06 17:34:09 +00:00
// Goto that choice, now!
Game.goto(choiceID);
2019-02-18 20:59:10 +00:00
2019-09-06 17:34:09 +00:00
// STOP THE PROP
event.stopPropagation();
2019-04-29 19:19:44 +00:00
2019-09-06 17:34:09 +00:00
};
div.onmouseover = function(){
sfx("ui_hover");
};
}else{
div.setAttribute("speaker", Game.OVERRIDE_CHOICE_SPEAKER ? Game.OVERRIDE_CHOICE_SPEAKER : "none");
}
2019-02-11 17:04:23 +00:00
2019-04-18 11:40:22 +00:00
// Add choice, animated!
2019-09-12 07:51:08 +00:00
div.classList.add("hidden");
2019-02-11 17:04:23 +00:00
Game.choicesDOM.appendChild(div);
2019-09-12 07:51:08 +00:00
requestAnimationFrame(function(){
div.classList.remove("hidden");
2019-04-25 18:07:41 +00:00
sfx("ui_show_choice", {volume:0.4});
2019-09-12 07:51:08 +00:00
});
2019-02-11 17:04:23 +00:00
2019-05-05 19:56:00 +00:00
// Or... FORCE
2019-07-30 20:17:36 +00:00
if(Game.OVERRIDE_FONT_SIZE){
div.style.fontSize = Game.OVERRIDE_FONT_SIZE+"px";
2019-05-05 19:56:00 +00:00
}else{
// If it's too big, shrink font size
2019-04-19 15:57:10 +00:00
setTimeout(function(){
var choiceHeight = div.getBoundingClientRect().height;
2019-09-17 14:24:40 +00:00
if(choiceHeight>40) div.style.fontSize = "18px";
2019-04-19 15:57:10 +00:00
// And if still too much???
setTimeout(function(){
var choiceHeight = div.getBoundingClientRect().height;
2019-09-17 14:24:40 +00:00
if(choiceHeight>40) div.style.fontSize = "17px";
2019-05-05 19:56:00 +00:00
// And if still too much???
setTimeout(function(){
var choiceHeight = div.getBoundingClientRect().height;
2019-09-17 14:24:40 +00:00
if(choiceHeight>40) div.style.fontSize = "16px";
2019-06-26 15:03:27 +00:00
// And if still too much???
setTimeout(function(){
var choiceHeight = div.getBoundingClientRect().height;
2019-09-17 14:24:40 +00:00
if(choiceHeight>40) div.style.fontSize = "15px";
2019-08-24 16:57:09 +00:00
// And if still too much???
setTimeout(function(){
var choiceHeight = div.getBoundingClientRect().height;
2019-09-17 14:24:40 +00:00
if(choiceHeight>40) div.style.fontSize = "14px";
2019-08-24 16:57:09 +00:00
// And if still too much???
setTimeout(function(){
var choiceHeight = div.getBoundingClientRect().height;
2019-09-17 14:24:40 +00:00
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);
2019-08-24 16:57:09 +00:00
},1);
},1);
2019-06-26 15:03:27 +00:00
},1);
2019-05-05 19:56:00 +00:00
},1);
},1);
},1);
}
2019-07-30 20:17:36 +00:00
Game.OVERRIDE_FONT_SIZE = false;
2019-04-19 15:57:10 +00:00
2019-04-18 11:40:22 +00:00
// Wait a bit before adding new line
return new RSVP.Promise(function(resolve){
Game.setTimeout(resolve, 100);
});
2019-02-12 19:59:45 +00:00
2019-02-11 17:04:23 +00:00
}
// Execute code!
Game.executeCode = function(line){
2019-02-12 19:59:45 +00:00
2019-02-11 17:04:23 +00:00
var code = line.match(/\`+([^\`]*)\`+/)[1].trim();
try{
eval(code);
}catch(e){
console.log(e);
}
2019-02-12 19:59:45 +00:00
2019-02-18 20:59:10 +00:00
// Return immediate promise
return Game.immediatePromise();
2019-02-12 19:59:45 +00:00
2019-02-11 17:04:23 +00:00
}
2019-02-18 17:24:52 +00:00
// Execute wait! Just wait.
Game.executeWait = function(line){
// Get integer from (...NN)
var waitTime = parseInt(line.match(/^\(\.\.\.(\d+)\)/)[1].trim());
2019-04-28 16:02:55 +00:00
// Unless it's click to advance, then IGNORE ALL WAITS
2019-05-05 19:56:00 +00:00
if(!Game.FORCE_CANT_SKIP){
// Specific wait-time, don't skip?
var waitTimeString = waitTime+"";
var lastDigit = waitTimeString[waitTimeString.length-1];
2019-06-14 15:03:30 +00:00
var cantSkip = (lastDigit!="0"); // CAN'T SKIP.
2019-05-05 19:56:00 +00:00
2019-06-17 19:33:15 +00:00
if(!cantSkip && Game.CLICK_TO_ADVANCE && waitTime<=999){ // hack: unless the wait is long.
2019-05-05 19:56:00 +00:00
waitTime = 0;
}
2019-04-28 16:02:55 +00:00
}
2019-02-18 17:24:52 +00:00
// Delayed promise
2019-02-22 23:48:24 +00:00
return new RSVP.Promise(function(resolve){
2019-02-20 18:45:23 +00:00
Game.setTimeout(resolve, waitTime);
2019-02-18 20:59:10 +00:00
});
2019-02-18 17:24:52 +00:00
};
2019-02-11 17:04:23 +00:00
// 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?
2019-09-06 17:34:09 +00:00
var isChoice = /\[.*\]\(.*\)/.test(line);
2019-02-11 17:04:23 +00:00
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";
2019-02-18 17:24:52 +00:00
// Is it a wait?
var isWait = /^\(\.\.\.\d+\)/.test(line);
if(isWait) return "wait";
2019-02-11 17:04:23 +00:00
// Otherwise, it's text.
return "text";
};
// Parse all the handlebars...
Game.parseLine = function(line){
2019-02-22 23:48:24 +00:00
// Get rid of newlines
line = line.replace(/\n/gi,"");
2019-02-11 17:04:23 +00:00
// Get the IFs, if any
var lookForIfs = true;
while(lookForIfs){
lookForIfs = false;
// Look for an IF!
2019-04-29 19:19:44 +00:00
var regex = /\{\{if[^\/]*\/if\}\}/ig; // the reason it's inside here is to reset .exec
2019-02-11 17:04:23 +00:00
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
2019-05-05 19:56:00 +00:00
var insideText = fullConditional.match(/\}\}([^\{\}]*)\{\{/)[1].trim()+" ";
2019-02-11 17:04:23 +00:00
// 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;
}
}
2019-04-28 16:02:55 +00:00
// Evaluate {{expressions}}, if any
var lookForExpressions = true;
while(lookForExpressions){
lookForExpressions = false;
// Look for an IF!
2019-06-26 15:03:27 +00:00
//debugger;
2019-04-29 19:19:44 +00:00
var regex = /\{\{[^\}]*\}\}/ig; // the reason it's inside here is to reset .exec
2019-04-28 16:02:55 +00:00
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;
}
}
2019-02-11 17:04:23 +00:00
// Return line!
return line;
};
2019-02-17 21:54:29 +00:00
////////////////////////////////////////////////////////////////////////////////////////////////
// WHERE STUFF WILL BE DRAWN ///////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////
2019-02-12 19:59:45 +00:00
Game.canvas.width = 360 * 2;
2019-06-21 15:33:04 +00:00
Game.canvas.height = 600 * 2; //450 * 2;
2019-02-12 19:59:45 +00:00
Game.canvas.style.width = Game.canvas.width/2 + "px";
Game.canvas.style.height = Game.canvas.height/2 + "px";
2019-02-17 21:54:29 +00:00
Game.context = Game.canvas.getContext("2d");
// A blank scene
2019-04-18 11:40:22 +00:00
Game.scene = null;
2019-02-17 21:54:29 +00:00
Game.resetScene = function(){
2019-04-18 11:40:22 +00:00
// 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!
2019-02-17 21:54:29 +00:00
Game.scene = {};
Game.scene.children = [];
2019-04-18 11:40:22 +00:00
2019-08-16 16:37:01 +00:00
// Misc
Game.WORDS_HEIGHT_BOTTOM = -1;
2019-02-17 21:54:29 +00:00
};
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:
// -------------------------------------------------------------
2019-02-17 21:54:29 +00:00
// For retina
var ctx = Game.context;
ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height);
2019-09-12 04:39:45 +00:00
ctx.scale(2, 2);
2019-09-09 16:37:58 +00:00
2019-02-17 21:54:29 +00:00
// Update/Draw all kids
for(const child of Game.scene.children) child.draw(ctx, delta);
2019-02-17 21:54:29 +00:00
// Restore
2019-09-12 04:39:45 +00:00
ctx.scale(0.5, 0.5);
2019-02-17 21:54:29 +00:00
// Draw HP
2019-04-11 18:44:15 +00:00
HP.draw();
2019-02-17 21:54:29 +00:00
};
2019-06-26 15:03:27 +00:00
// HACK: PREVENT ACCIDENTALLY TABBING & BREAKING UI
window.addEventListener("keydown", function(e){
if(e.keyCode==9){
e.preventDefault();
e.stopPropagation();
}
2019-09-17 14:24:40 +00:00
if(e.keyCode==32){ // SPACE TO ADVANCE
_unpauseOrSkip();
}
2019-06-26 15:03:27 +00:00
});
2019-09-17 14:24:40 +00:00
// 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);
}