Files
ai.interactive.fiction/references/game.js

1034 lines
45 KiB
JavaScript

window.onload = () => {
var element = document.getElementById("lighting");
window.running = false;
window.fastForwardingAll = false;
window.speech = false;
function setRandomDuration(event) {
var randomDuration = Math.random() * (5 - 0.1) + 0.1;
var previousDirection = event.animationName;
// element.style.animation = null;
// console.log("Animation restarts from:", element.style.animationName, event);
if (previousDirection == 'gradient-animation-grow')
element.style.animation = `gradient-animation-shrink ${randomDuration}s 1`;
else
element.style.animation = `gradient-animation-grow ${randomDuration}s 1`;
}
async function fetch_include(filename) {
const response = await fetch(filename);
code = await response.text();
// console.log("Loaded include:", JSON.parse(code));
return JSON.parse(code);
}
var storyContent = {};
// const storyContent = fetch_include('Herrenhaus.js');
// const file_contents_1 = fetch_include("code/Main.ink");
// const file_contents_2 = fetch_include("code/Stats.ink");
// const fileHandler = new inkjs.JsonFileHandler({
// "Main.ink": file_contents_1,
// "Stats.ink": file_contents_2
// });
// const errorHandler = (message, errorType) => {
// console.log(message + "\n");
// }
// const story = new inkjs.Compiler(file_contents_1, {fileHandler, errorHandler}).Compile();
// // story is an inkjs.Story that can be played right away
// storyContent = story.ToJson();
// // the generated json can be further re-used
const translations = {
'en-us': {
by: "",
speed: "speed<sup>*<sup>",
title_speed: "Set speed of text animation",
restart: "restart",
title_restart: "Restart story from beginning",
save: "save",
title_save: "Save progress",
load: "load",
title_load: "Reload from save point",
prompt: "<i>What do you want to do?</i>",
remark: "<i><sup>*</sup><b>click</b> on the right page or press the <b>spacebar</b><br />to fast forward the text-animation</i>",
end: "The End",
action_examine: "objects to examine",
action_comment: "topics to comment on",
action_ask: "things to ask about",
action_interact: "things to interact with",
action_reflect: "things to reflect on",
action_inventory: "things you carry with you",
speech: "Speech",
title_speech: "Toggle text to speech"
},
'de': {
by: "",
speed: "Geschwindikeit<sup>*<sup>",
title_speed: "Geschwindigkeit der Textanimation einstellen",
restart: "Neustart",
title_restart: "Die Geschichte von vorne beginnen",
save: "Speichern",
title_save: "Den Fortschritt der Geschichte speichern",
load: "Laden",
title_load: "Zum gespeicherten Spielfortschritt zurückkehren",
prompt: "Was möchtest du tun?",
remark: "<i><sup>*</sup><b>Klicke</b> auf die rechte Buchseite oder drücke die <b>Leertaste</b><br /> um die Textanimation zu überspringen</i>",
end: "Ende",
action_examine: "Untersuchen",
action_comment: "Kommentieren",
action_ask: "Fragen",
action_interact: "Interagieren",
action_reflect: "Reflektieren",
action_inventory: "Inventar",
speech: "Sprachausgabe",
title_speech: "Sprachausgabe ein und ausschalten"
}
};
// Function to change locale
function setLocale(locale) {
if (translations[locale]) {
Object.keys(translations[locale]).forEach(key => {
const prefix = key.substring(0, 5);
const postfix = key.substring(6, key.length);
// console.log("Detected translation:", key, prefix, postfix);
const elements = document.querySelectorAll(`.l10n-${(prefix === 'title' ? postfix : key)}`);
elements.forEach(element => {
// console.log("Translating:", element, locale, key);
if(prefix === "title")
element.title = translations[locale][key];
else
element.innerHTML = translations[locale][key];
});
});
} else {
console.error(`Locale ${locale} is not defined`);
}
}
setLocale(locale);
// console.log("SmartyPants:", SmartyPants.smartypants('"Dies ist ein Test...", sagte Georg.', 1));
Hyphenopoly.config({
require: {
"en-us": "FORCEHYPHENOPOLY",
"de": "FORCEHYPHENOPOLY"
},
paths: {
maindir: "./",
patterndir: "./patterns/"
},
setup: {
selectors: {
".hyphenate": {
hyphen: "\u00AD"
},
".hyphenatePipe": {
hyphen: "|"
}
}
}
});
Hyphenopoly.hyphenators[locale].then((hyphenator_en) => {
(async function(storyContent) {
// const response = await fetch('TheIntercept.ink.json');
const response = await fetch('Herrenhaus.ink.json');
storyContent = await response.json();
// console.log("Loading game:", response, storyContent);
// Create ink story from the content using inkjs
var story = new inkjs.Story(storyContent);
var cev = () => {};
var savePoint = "";
var rstack = [];
var ruler = document.getElementById('ruler');
var measure = [];
rstack.push(ruler);
var hasSave = false;
element.addEventListener("animationend", setRandomDuration); // Set new duration each time animation ends
window.addEventListener("turnCompleteEvent", event => {
// console.log("Turn ended:", event);
window.running = false;
window.fastForwardingAll = false;
window.indented_paragraphs = 0;
if (hasSave) {
document.getElementById("reload").removeAttribute("disabled");
}
document.getElementById("rewind").removeAttribute("disabled");
});
const speedSlider = document.getElementById('speed');
window.speed = Math.pow(100.0 - speedSlider.value, 3) / 10000 * 10 + 0.01;
window.delay = 0.0;
speedSlider.oninput = function() {
window.speed = Math.pow(100.0 - this.value, 3) / 10000 * 10 + 0.01;
// console.log(`Speed: ${speed}ms`); // Replace this with your animation speed setting function
};
let fade_in = true;
// Global tags - those at the top of the ink file
// We support:
// # title: Your Title
// # author: Your Name
// # subtitle: Your Subtitle
var globalTags = story.globalTags;
if( globalTags ) {
for(var i=0; i<story.globalTags.length; i++) {
var globalTag = story.globalTags[i];
var splitTag = splitPropertyTag(globalTag);
// title: Your Title
if( splitTag && splitTag.property == "title" ) {
var title = document.querySelector('.title');
title.innerHTML = splitTag.val;
}
// author: Your Name
else if( splitTag && splitTag.property == "author" ) {
var byline = document.querySelector('.byline');
byline.textContent += splitTag.val;
}
// subtitle: Your Subtitle
else if( splitTag && splitTag.property == "subtitle" ) {
var byline = document.querySelector('.subtitle');
byline.textContent += splitTag.val;
}
}
}
var storyContainer = document.querySelector('#story');
var choiceContainer = document.querySelector('#choices');
choiceContainer.lang = locale;
var outerScrollContainer = document.querySelector('#book');
let timeoutQueue = [];
function scheduleTimeout(func, delay, ...args) {
const timeoutObject = {
execute: () => func(...args),
timeoutId: null
};
timeoutObject.timeoutId = setTimeout(() => {
timeoutObject.execute();
timeoutQueue = timeoutQueue.filter(t => t !== timeoutObject);
if(timeoutQueue.length <= 0){
let event = new CustomEvent("allWordsSetEvent", {
detail: { messages: "All scheduled word fade in animations were played."},
bubbles: true,
cancelable: false
});
document.dispatchEvent(event);
}
}, delay);
timeoutQueue.push(timeoutObject);
return timeoutObject.timeoutId;
}
function fastForward() {
window.delay = 0.0;
// Sort the queue based on timeoutId (assuming that smaller ids are scheduled earlier)
timeoutQueue.sort((a, b) => a.timeoutId - b.timeoutId);
// Clear and execute all timeouts
timeoutQueue.forEach(timeoutObject => {
clearTimeout(timeoutObject.timeoutId);
timeoutObject.execute();
});
timeoutQueue = [];
let event = new CustomEvent("allWordsSetEvent", {
detail: { messages: "All scheduled word fade in animations were played."},
bubbles: true,
cancelable: false
});
document.dispatchEvent(event);
document.getElementById("page_right").scrollTo({top: document.getElementById("page_right").scrollHeight, behavior: 'smooth'});
}
function fastForwardAll() {
window.fastForwardingAll = true;
fastForward();
}
function smoothScroll(target, duration) {
var display = document.getElementById('page_right');
var targetPosition = target.getBoundingClientRect().top;
var startPosition = display.scrollTop;
var distance = targetPosition;
var startTime = null;
// console.log("Scheduled scrolldown to:", target, duration);
if(duration < 5) {
display.scrollTo(0, targetPosition);
return;
}
function animation(currentTime) {
if (startTime === null) startTime = currentTime;
var timeElapsed = currentTime - startTime;
var run = ease(timeElapsed, startPosition, distance, duration);
display.scrollTo(0, run);
if (timeElapsed < duration) requestAnimationFrame(animation);
}
function ease(t, b, c, d) {
// console.log("Easing:", t, b, c, d);
t /= d / 2;
if (t < 1) return c / 2 * t * t + b;
t--;
return -c / 2 * (t * (t - 2) - 1) + b;
}
requestAnimationFrame(animation);
}
function typesetParagraph(paragraph_data, delay = 0, measure = []) {
var stack = [];
var left = 0;
var p = document.createElement("p");
p.style.position = 'relative';
p.classList.add("latest-paragraph");
p.dataset.numberOfLines = paragraph_data.breaks.length - 1;
var line_height = parseFloat(window.getComputedStyle(document.querySelector('#ruler')).lineHeight);
var line_width = parseFloat(window.getComputedStyle(document.getElementById('story')).width);
var page_height = parseFloat(window.getComputedStyle(document.getElementById('page_right')).height);
p.style.height = line_height * (paragraph_data.breaks.length - 1) + 'px';
var paragraph_height = parseFloat(p.style.height);
p.dataset.vpc = paragraph_height * 100 / page_height;
p.style.marginBlockEnd = 0;
stack.push(p);
for(let i = 1; i < paragraph_data.breaks.length; i++) {
left = measure[measure.length - 1] - measure[Math.min(i - 1, measure.length - 1)];
var lastChild = null;
var syllable = "";
for(let j = paragraph_data.breaks[i-1].position; j <= paragraph_data.breaks[i].position; j++) {
if(paragraph_data.nodes[j].type === 'box' && paragraph_data.nodes[j].value !== '' && j < paragraph_data.breaks[i].position) {
if(j > paragraph_data.breaks[i-1].position + 1 && paragraph_data.nodes[j-1].type === 'penalty' && lastChild) {
syllable += '\u200c' + paragraph_data.nodes[j].value;
lastChild.innerHTML = syllable;
left += paragraph_data.nodes[j].width;
} else {
let word = document.createElement("span");
word.style.position = 'absolute';
word.classList.add("fade-in");
word.style.animationDuration = speed * 10 + 'ms';
word.style.top = line_height * (i - 1) * 100 / paragraph_height + '%';
// word.style.left = left + 'px';
word.style.left = left * 100 / line_width + '%';
syllable = paragraph_data.nodes[j].value;
word.innerHTML = syllable;
lastChild = word;
if(!window.fastForwardingAll)
insertAfter(delay, stack[stack.length-1], word);
delay += window.speed;
left += paragraph_data.nodes[j].width;
}
} else if(paragraph_data.nodes[j].type === 'tag') {
if(paragraph_data.nodes[j].value.substr(0,2) == '</') {
stack.pop();
} else {
let tmp = document.createElement('div');
tmp.innerHTML = paragraph_data.nodes[j].value;
word = tmp.firstChild;
// word.style.left = left + 'px';
word.style.left = left * 100 / line_width + '%';
stack[stack.length-1].appendChild(word);
stack.push(word);
}
} else if(j > paragraph_data.breaks[i-1].position && paragraph_data.nodes[j].type === 'glue' && paragraph_data.nodes[j].width !== 0 && j <= paragraph_data.breaks[i].position) {
// Insert space character
if(paragraph_data.breaks[i].ratio > 0) {
left += paragraph_data.nodes[j].width + paragraph_data.breaks[i].ratio * paragraph_data.nodes[j].stretch;
} else {
left += paragraph_data.nodes[j].width + paragraph_data.breaks[i].ratio * paragraph_data.nodes[j].shrink;
}
let word = document.createElement("span");
word.style.position = 'absolute';
word.classList.add("fade-in");
word.style.top = line_height * (i - 1) * 100 / paragraph_height + '%';
// word.style.left = left + 'px';
word.style.left = left * 100 / line_width + '%';
word.innerHTML = " ";
if(!window.fastForwardingAll)
insertAfter(delay, stack[stack.length-1], word);
} else if(paragraph_data.nodes[j].type === 'penalty' && paragraph_data.nodes[j].penalty === 100 && j === paragraph_data.breaks[i].position) {
let word = document.createElement("span");
word.style.position = 'absolute';
word.style.top = line_height * (i - 1) * 100 / paragraph_height + '%';
// word.style.left = left + 'px';
word.style.left = left * 100 / line_width + '%';
word.innerHTML = "-";
if(!window.fastForwardingAll)
insertAfter(delay, stack[stack.length-1], word);
delay += window.speed;
}
}
};
return [p, delay];
}
function measureText(str) {
if(str.substr(0, 2) == '</') {
let child = rstack.pop();
ruler = rstack[rstack.length-1];
ruler.removeChild(child);
return 0;
} else if(str.substr(0, 1) == '<') {
let tmp = document.createElement('div');
tmp.innerHTML = str;
word = tmp.firstChild;
ruler = rstack[rstack.length-1];
rstack.push(word);
ruler.appendChild(word);
return 0;
} else if(str === '|') {
return 0;
}else if (str === ' ') {
str = '\u00A0';
}
ruler = rstack[rstack.length-1];
let textNode = document.createTextNode(str);
ruler.appendChild(textNode);
let width = ruler.getClientRects()[0].width;
// console.log("Measuring:", str, ruler.cloneNode(true), width);
ruler.removeChild(textNode);
return width;
}
function updateBookDimensions() {
const vw = window.innerWidth;
const vh = window.innerHeight;
const viewportAspectRatio = vw / vh;
const imageAspectRatio = 2727 / 1691;
let bookWidth, bookHeight;
if (viewportAspectRatio > imageAspectRatio) {
bookWidth = vh * imageAspectRatio;
bookHeight = vh;
} else {
bookWidth = vw;
bookHeight = vw / imageAspectRatio;
}
document.documentElement.style.setProperty('--book-width', `${bookWidth}px`);
document.documentElement.style.setProperty('--book-height', `${bookHeight}px`);
// Setting a CSS variable that will be either vw or vh depending on the viewport aspect ratio
document.documentElement.style.setProperty(
"--viewport-dimension",
viewportAspectRatio > imageAspectRatio ? 'vw' : 'vh'
);
document.documentElement.style.setProperty('--viewport-aspect-ratio', viewportAspectRatio);
let story = document.getElementById("story");
let paddingTop = window.getComputedStyle(story).paddingTop;
let paddingBottom = window.getComputedStyle(story).paddingBottom;
document.documentElement.style.setProperty('--story-line-height', (story.clientHeight - paddingTop - paddingBottom) / 28);
updateParagraphHeight();
}
function updateParagraphHeight() {
document.querySelectorAll("#story p").forEach((element) => {
let pHeight = parseFloat(window.getComputedStyle(document.getElementById('page_right')).height);
let newHeight = pHeight * element.dataset.vpc / 100 + 'px';
element.style.height = newHeight;
});
}
// Update the aspect ratio when the page loads
updateBookDimensions();
// Update the aspect ratio whenever the window is resized
window.addEventListener('resize', updateBookDimensions);
window.addEventListener('keydown', (event) => {
if (event.code === 'Space') {
fade_in = false;
fastForward();
}
});
document.getElementById('page_right').addEventListener('click', (event) => {
fade_in = false;
fastForward();
});
// page features setup
hasSave = loadSavePoint();
setupButtons(hasSave);
// Set initial save point
savePoint = story.state.toJson();
// Kick off the start of the story!
continueStory();
// Main story processing function. Each time this is called it generates
// all the next content up as far as the next set of choices.
async function continueStory(first_time = true) {
createChoiceContainer = (categoryContainers, categoryNumbers, action, prompt, choice, tagDebug, registerKeys = false) => {
var choiceCategoryContainer = categoryContainers[action];
if(!choiceCategoryContainer) {
console.log("Creating new category choice container for:", categoryContainers, categoryNumbers, action, prompt, choice, registerKeys, choiceContainer);
choiceCategoryContainer = document.createElement('ol');
var p = document.createElement('p');
p.innerHTML = (prompt);
choiceCategoryContainer.appendChild(p);
choiceCategoryContainer.classList.add("choice");
choiceCategoryContainer.classList.add("fade-in");
if(!registerKeys)
choiceCategoryContainer.classList.add("categorized");
if(story.currentChoices.length && !window.fastForwardingAll)
choiceContainer.appendChild(choiceCategoryContainer);
}
categoryContainers[action] = choiceCategoryContainer;
var choiceNumber = categoryNumbers[action];
if(choiceNumber === undefined)
choiceNumber = 0;
choiceNumber++;
var choiceParagraphElement = document.createElement('li');
choiceParagraphElement.classList.add("choice");
choiceParagraphElement.lang = locale;
choiceParagraphElement.title = tagDebug;
choiceParagraphElement.innerHTML = `<a href='#'>${SmartyPants.smartypantsu(choice.text, 1)}</a>`
if(!window.fastForwardingAll)
insertAfter(window.delay, choiceCategoryContainer, choiceParagraphElement, fade_in);
window.delay += window.speed;
// Press choice key
if(registerKeys) {
choiceParagraphElement.value = choiceNumber;
registerKey('Digit' + choiceNumber, choice.index);
} else {
var categorizedNumber = categoryNumbers['categorized'];
categorizedNumber++;
var keyLetter = String.fromCharCode(64 + categorizedNumber);
console.log("Registering key:", keyLetter, categorizedNumber, choice.index);
choiceParagraphElement.value = categorizedNumber;
registerKey('Key' + keyLetter, choice.index);
categoryNumbers['categorized'] = categorizedNumber;
}
// Click on choice
var choiceAnchorEl = choiceParagraphElement.querySelectorAll("a")[0];
choiceAnchorEl.addEventListener("click", (event) => {
// Don't follow <a> link
event.preventDefault();
choose(choice.index);
});
categoryNumbers[action] = choiceNumber;
}
var fade_in = true;
window.running = true;
window.fastForwardingAll = false;
chapter_begin = false;
this.keyRegistry = {};
if(measure.length == 1)
measure.pop(); // Remove lingering measures if all that is left is the full line.
document.querySelectorAll('#story p').forEach((p) => { p.classList.remove("latest-paragraph")});
// Generate story text - loop through available content
while(story.canContinue) {
if(window.fastForwardingAll)
return;
window.delay = 0.0;
// Get ink to generate the next paragraph
var paragraphText = story.Continue();
var tags = story.currentTags;
// Any special tags included with this line
var customClasses = [];
var tagDebug = "";
for(var i=0; i<tags.length; i++) {
var tag = tags[i];
tagDebug += tag + ";";
// Detect tags of the form "X: Y". Currently used for IMAGE and CLASS but could be
// customised to be used for other things too.
var splitTag = splitPropertyTag(tag);
// AUDIO: src
if( splitTag && splitTag.property == "AUDIO" ) {
if('audio' in this) {
this.audio.pause();
this.audio.removeAttribute('src');
this.audio.load();
}
this.audio = new Audio(splitTag.val);
this.audio.play();
}
// AUDIOLOOP: src
else if( splitTag && splitTag.property == "AUDIOLOOP" ) {
if('audioLoop' in this) {
this.audioLoop.pause();
this.audioLoop.removeAttribute('src');
this.audioLoop.load();
}
this.audioLoop = new Audio(splitTag.val);
this.audioLoop.play();
this.audioLoop.loop = true;
}
// IMAGE: src
if( splitTag && splitTag.property == "IMAGE" ) {
var imageElement = document.createElement('img');
imageElement.src = splitTag.val;
storyContainer.appendChild(imageElement);
showAfter(window.delay, imageElement);
window.delay += window.speed;
}
// LINK: url
else if( splitTag && splitTag.property == "LINK" ) {
window.location.href = splitTag.val;
}
// LINKOPEN: url
else if( splitTag && splitTag.property == "LINKOPEN" ) {
window.open(splitTag.val);
}
// BACKGROUND: src
else if( splitTag && splitTag.property == "BACKGROUND" ) {
outerScrollContainer.style.backgroundImage = 'url('+splitTag.val+')';
}
// CLASS: className
else if( splitTag && splitTag.property == "CLASS" ) {
customClasses.push(splitTag.val);
}
// CLEAR - removes all existing content.
// RESTART - clears everything and restarts the story from the beginning
else if( tag == "CLEAR" || tag == "RESTART" ) {
removeAll("p");
removeAll("img");
// Comment out this line if you want to leave the header visible when clearing
// setVisible(".header", false);
if( tag == "RESTART" ) {
restart();
return;
}
}
// CHAPTER: Chapter Heading
else if( splitTag.property == "CHAPTER" ) {
var h = document.createElement('H2');
h.appendChild(document.createTextNode(splitTag.val));
h.classList.add("chapter-heading");
h.classList.add("fade-in");
storyContainer.appendChild(h);
chapter_begin = true;
window.indented_paragraphs = 2;
}
// SEPARATOR
else if( tag == "SEPARATOR") {
var d = document.createElement('double');
d.appendChild(document.createTextNode('\u2766'));
d.classList.add("fade-in");
d.classList.add("separator");
storyContainer.appendChild(d);
chapter_begin = true;
}
}
// Create paragraph element (initially hidden)
if(paragraphText.trim().length === 0)
continue;
var indentWidth = 2 * parseFloat(window.getComputedStyle(document.querySelector("#indent")).lineHeight);
var text = paragraphText;
var drop_cap = null;
var drop_quote = null;
if(chapter_begin) {
measure.push(parseFloat(window.getComputedStyle(document.getElementById("story")).width));
measure.push(parseFloat(window.getComputedStyle(document.getElementById("story")).width) - indentWidth);
measure.push(parseFloat(window.getComputedStyle(document.getElementById("story")).width) - indentWidth * 0.9);
let words = paragraphText.split(" ");
let first_word = words[0].substr(1,words[0].length);
let opening_quote = "";
let first_letter = words[0].substr(0,1);
if(first_letter == "\"" || first_letter == "'") {
opening_quote = SmartyPants.smartypantsu(first_letter, 1);
first_letter = words[0].substr(1,1);
first_word = words[0].substr(2,words[0].length);
}
if(first_word.length < 5 && words.length > 1)
first_word += ' ' + words[1];
text = '<cap>' + first_word + '</cap> ' + paragraphText.substr(first_word.length + 2 + opening_quote.length, paragraphText.length);
console.log("Created chapter begin:", words, first_word, first_letter);
drop_cap = document.createElement("span");
drop_cap.classList.add("drop-cap");
drop_cap.appendChild(document.createTextNode(first_letter));
drop_cap.style.left = '0%';
drop_cap.style.top = '0%';
drop_cap.style.position = 'absolute';
if(opening_quote) {
drop_quote = document.createElement("span");
drop_quote.classList.add("drop-quote");
drop_quote.appendChild(document.createTextNode(opening_quote));
drop_quote.style.left = '-4.45%';
drop_quote.style.top = '-16%';
drop_quote.style.position = 'absolute';
}
} else {
if(measure.length < 1) {
measure.push(parseFloat(window.getComputedStyle(document.getElementById("story")).width));
measure.push(parseFloat(window.getComputedStyle(document.getElementById("story")).width) - indentWidth * 0.5);
}
}
var preview_data = kap(hyphenator_en(SmartyPants.smartypantsu(text, 1), '.hyphenatePipe'), measureText, measure.toReversed(), true);
var p, d;
[p, d] = typesetParagraph(preview_data, window.delay, measure.toReversed());
for(let k = 0; k < parseInt(p.dataset.numberOfLines); k++) {
measure.pop();
}
window.indented_paragraphs -= p.dataset.numberOfLines;
console.log("Reducing indented_paragraphes to:", window.indented_paragraphs, p, preview_data, measure);
if(drop_quote)
insertAfter(0, p, drop_quote, true);
if(drop_cap)
insertAfter(0, p, drop_cap, true);
// window.delay = d;
// Add any custom classes derived from ink tags
for(var i=0; i<customClasses.length; i++)
p.classList.add(customClasses[i]);
p.lang = locale;
p.title = tagDebug;
storyContainer.appendChild(p);
smoothScroll(p, window.speed * 10 * p.dataset.numberOfLines);
chapter_begin = false;
await Promise.all([new Promise(resolve => {
document.addEventListener('allWordsSetEvent', resolve, { once: true });
}), new Promise(async resolve => {
if(!window.speech) {
resolve();
return;
}
let filepath = await window.elevenlabs.getSpeech(text);
const audio = new Audio(`${filepath}`);
audio.onended = resolve; // Resolve the promise when the audio ends
audio.play();
// Listen for a click event to fade out the audio
storyContainer.addEventListener('click', fadeOutAudio);
// Listen for a keypress event to fade out the audio
window.addEventListener('keydown', fadeOutAudio);
audio.play();
function fadeOutAudio(event) {
if((event instanceof KeyboardEvent && event.key === ' ') || (event instanceof MouseEvent && event.type === 'click')) {
// Stop listening for the click and keypress events
storyContainer.removeEventListener('click', fadeOutAudio);
window.removeEventListener('keydown', fadeOutAudio);
// Fade out the audio by decrementing the volume
let volume = 1.0;
const fadeInterval = setInterval(() => {
if (volume > 0.1) {
volume -= 0.1; // Change this to make the fade out faster or slower
audio.volume = volume;
} else {
// Stop the fade out
clearInterval(fadeInterval);
// Stop the audio
audio.pause();
resolve();
}
}, window.speed); // Change this to make the fade out faster or slower
}
}
})])
}
window.delay = 0.0;
// Create HTML choices from ink choices
var categoryContainers = { default: null }
var categoryNumbers = { default: 0, categorized: 0 }
story.currentChoices.forEach(function(choice) {
if(window.fastForwardingAll)
return;
// Create paragraph with anchor element
var tagDebug = "";
var action = "default";
choice.tags.forEach(tag => {
tagDebug += tag + ";"
var splitTag = splitPropertyTag(tag);
// console.log("Split choice tag:", splitTag);
if(splitTag.property === "ACTION")
action = splitTag.val;
});
if(action != "default") {
createChoiceContainer(categoryContainers, categoryNumbers, action, translations[locale]['action_' + action], choice, tagDebug);
} else {
createChoiceContainer(categoryContainers, categoryNumbers, "default", translations[locale]['prompt'], choice, tagDebug, true);
}
});
cev = (event) => {
console.log("Key pressed:", event, this.keyRegistry);
for(const key in this.keyRegistry) {
if(event.code === key) {
window.removeEventListener('keypress', cev);
choose(this.keyRegistry[key]);
break;
}
}
};
window.addEventListener('keypress', cev);
function choose(index) {
// Remove all existing choices
removeAll(".choice", true);
clearKeyRegistry();
// Tell the story where to go next
story.ChooseChoiceIndex(index);
// This is where the save button will save from
savePoint = story.state.toJson();
// Aaand loop
continueStory(false);
}
function registerKey(key, choice) {
this.keyRegistry[key] = choice;
}
function clearKeyRegistry() {
this.keyRegistry = {};
}
var tce = new CustomEvent("turnCompleteEvent", {
detail: { messages: "All text and choices have been set up."},
bubbles: true,
cancelable: false
});
document.dispatchEvent(tce);
if(story.canContinue === false && story.currentChoices.length === 0) {
var end = document.createElement("p");
end.style.textTransform = "uppercase";
end.style.textAlign = "center";
end.classList.add("fade-in");
end.classList.add("choice");
end.appendChild(document.createTextNode(translations[locale]['end']));
choiceContainer.appendChild(end);
}
}
function restartStory() {
window.delay = 0.0;
story.ResetState();
fastForwardAll();
setVisible(".header", true);
removeAll("p");
removeAll("img");
removeAll("h2");
removeAll("double");
removeAll(".choice", true);
window.removeEventListener('keypress', cev);
// set save point to here
savePoint = story.state.toJson();
}
// -----------------------------------
// Various Helper functions
// -----------------------------------
// Fades in an element after a specified delay
function showAfter(delay, el) {
el.classList.add("hide");
setTimeout(function() {
setTimeout(function() { el.classList.remove("hide") }, delay);
});
}
function insertAfter(delay, target, el, fade_in = true) {
if(fade_in) {
el.classList.add("fade-in");
scheduleTimeout(function() {
target.appendChild(el);
// el.scrollIntoView({ behavior: 'smooth'});
}, delay);
} else {
scheduleTimeout(function() {
target.appendChild(el);
}, delay);
}
}
// Remove all elements that match the given selector. Used for removing choices after
// you've picked one, as well as for the CLEAR and RESTART tags.
function removeAll(selector, choices = false)
{
if(choices)
var allElements = choiceContainer.querySelectorAll(selector);
else
var allElements = storyContainer.querySelectorAll(selector);
for(var i=0; i<allElements.length; i++) {
var el = allElements[i];
el.parentNode.removeChild(el);
}
}
// Used for hiding and showing the header when you CLEAR or RESTART the story respectively.
function setVisible(selector, visible)
{
var allElements = storyContainer.querySelectorAll(selector);
for(var i=0; i<allElements.length; i++) {
var el = allElements[i];
if( !visible )
el.classList.add("invisible");
else
el.classList.remove("invisible");
}
}
// Helper for parsing out tags of the form:
// # PROPERTY: value
// e.g. IMAGE: source path
function splitPropertyTag(tag) {
var propertySplitIdx = tag.indexOf(":");
if( propertySplitIdx != null ) {
var property = tag.substr(0, propertySplitIdx).trim();
var val = tag.substr(propertySplitIdx+1).trim();
return {
property: property,
val: val
};
}
return null;
}
// Loads save state if exists in the browser memory
function loadSavePoint() {
try {
let savedState = window.localStorage.getItem('save-state');
let savedHistory = window.localStorage.getItem('save-history');
if (savedState && savedHistory) {
let history = JSON.parse(savedHistory);
// console.log('Loaded history:', history);
history.forEach(p => {
let d = document.createElement('div');
d.innerHTML = p;
document.getElementById('story').appendChild(d.firstChild);
});
story.state.LoadJson(savedState);
updateParagraphHeight();
window.removeEventListener('keypress', cev);
return true;
}
} catch (e) {
console.debug("Couldn't load save state");
}
return false;
}
// Used to hook up the functionality for global functionality buttons
function setupButtons(hasSave) {
let rewindEl = document.getElementById("rewind");
let saveEl = document.getElementById("save");
let reloadEl = document.getElementById("reload");
let speedEl = document.getElementById("speed_reset");
let speechEl = document.getElementById("speech");
if (rewindEl) rewindEl.addEventListener("click", function(event) {
if (rewindEl.getAttribute("disabled") == "disabled")
return;
rewindEl.setAttribute("disabled", "disabled");
reloadEl.setAttribute("disabled", "disabled");
restartStory();
if(window.running)
window.addEventListener("turnCompleteEvent", continueStory());
else {
// if (hasSave) {
// document.getElementById("reload").removeAttribute("disabled");
// }
// document.getElementById("rewind").removeAttribute("disabled");
continueStory();
}
});
if (saveEl) saveEl.addEventListener("click", function(event) {
if (save.getAttribute("disabled") == "disabled")
return;
try {
let history = Array.from(document.querySelectorAll("#story p:not(.latest-paragraph)")).map(p => p.outerHTML);
// console.log("Saving history:", history);
window.localStorage.setItem('save-history', JSON.stringify(history));
window.localStorage.setItem('save-state', savePoint);
hasSave = true;
reloadEl.removeAttribute("disabled");
} catch (e) {
console.warn("Couldn't save state");
}
});
reloadEl.addEventListener("click", function(event) {
if (reloadEl.getAttribute("disabled") == "disabled")
return;
reloadEl.setAttribute("disabled", "disabled");
rewindEl.setAttribute("disabled", "disabled");
fastForwardAll();
removeAll("p");
removeAll("img");
removeAll("h2");
removeAll("double");
removeAll(".choice", true);
loadSavePoint();
if(window.running)
window.addEventListener("turnCompleteEvent", continueStory());
else {
// if (hasSave) {
// document.getElementById("reload").removeAttribute("disabled");
// }
// document.getElementById("rewind").removeAttribute("disabled");
continueStory();
}
});
speedEl.addEventListener('click', () => {
let range = document.getElementById('speed');
range.value = 50;
range.dispatchEvent(new Event('input'));
});
speechEl.addEventListener('click', () => {
window.speech = !window.speech;
if(speechEl.getAttribute('disabled') === 'disabled')
speechEl.removeAttribute('disabled');
else
speechEl.setAttribute('disabled', 'disabled');
})
}
})(storyContent);
});
};