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*", 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: "What do you want to do?", remark: "*click on the right page or press the spacebar
to fast forward the text-animation
", 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*", 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: "*Klicke auf die rechte Buchseite oder drücke die Leertaste
um die Textanimation zu überspringen
", 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 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) == ' 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) == ' 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 = `${SmartyPants.smartypantsu(choice.text, 1)}` 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 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 1) first_word += ' ' + words[1]; text = '' + first_word + ' ' + 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 { 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 { 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); }); };