1034 lines
45 KiB
JavaScript
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);
|
|
});
|
|
}; |