Refactored app to include all the ink.js typography.
This commit is contained in:
@@ -0,0 +1,719 @@
|
||||
/**
|
||||
* InkStoryPlayer Module
|
||||
* Orchestrates the narrative flow specific to the Ink story.
|
||||
*/
|
||||
export class InkStoryPlayer {
|
||||
/**
|
||||
* Create a new InkStoryPlayer
|
||||
* @param {Object} config - Configuration options
|
||||
* @param {Function} config.InkStory - The inkjs.Story constructor
|
||||
* @param {Object} config.textProcessor - The TextProcessor instance
|
||||
* @param {Object} config.paragraphLayout - The ParagraphLayout instance
|
||||
* @param {Object} config.layoutRenderer - The LayoutRenderer instance
|
||||
* @param {Object} config.audioManager - The AudioManager instance
|
||||
* @param {Object} config.ttsPlayer - The TtsPlayer instance
|
||||
* @param {Object} config.persistenceManager - The PersistenceManager instance
|
||||
*/
|
||||
constructor(config = {}) {
|
||||
this.InkStory = config.InkStory;
|
||||
this.textProcessor = config.textProcessor;
|
||||
this.paragraphLayout = config.paragraphLayout;
|
||||
this.layoutRenderer = config.layoutRenderer;
|
||||
this.audioManager = config.audioManager;
|
||||
this.ttsPlayer = config.ttsPlayer;
|
||||
this.persistenceManager = config.persistenceManager;
|
||||
|
||||
this.story = null;
|
||||
this.storyContainer = document.getElementById('story');
|
||||
this.choiceContainer = document.getElementById('choices');
|
||||
this.savePoint = "";
|
||||
this.running = false;
|
||||
this.keyEventListener = null;
|
||||
this.indentedParagraphs = 0;
|
||||
this.chapterBegin = false;
|
||||
this.measure = [];
|
||||
this.locale = 'en-us';
|
||||
this.translations = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a story from JSON content
|
||||
* @param {Object} storyContent - The compiled Ink story JSON
|
||||
* @param {string} initialState - Optional initial state to load
|
||||
*/
|
||||
loadStory(storyContent, initialState = null) {
|
||||
this.story = new this.InkStory(storyContent);
|
||||
|
||||
if (initialState) {
|
||||
this.story.state.LoadJson(initialState);
|
||||
}
|
||||
|
||||
this.savePoint = this.story.state.toJson();
|
||||
|
||||
// Process global tags
|
||||
this.processGlobalTags();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process global tags from the story
|
||||
*/
|
||||
processGlobalTags() {
|
||||
if (!this.story || !this.story.globalTags) return;
|
||||
|
||||
for (let i = 0; i < this.story.globalTags.length; i++) {
|
||||
const globalTag = this.story.globalTags[i];
|
||||
const splitTag = this.splitPropertyTag(globalTag);
|
||||
|
||||
if (splitTag && splitTag.property == "title") {
|
||||
const title = document.querySelector('.title');
|
||||
if (title) title.innerHTML = splitTag.val;
|
||||
} else if (splitTag && splitTag.property == "author") {
|
||||
const byline = document.querySelector('.byline');
|
||||
if (byline) byline.textContent += splitTag.val;
|
||||
} else if (splitTag && splitTag.property == "subtitle") {
|
||||
const subtitle = document.querySelector('.subtitle');
|
||||
if (subtitle) subtitle.textContent += splitTag.val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Continue the story
|
||||
* @param {boolean} firstTime - Whether this is the first time continuing
|
||||
*/
|
||||
async continueStory(firstTime = true) {
|
||||
if (!this.story) {
|
||||
console.error('No story loaded');
|
||||
return;
|
||||
}
|
||||
|
||||
this.running = true;
|
||||
this.layoutRenderer.setFastForwardingAll(false);
|
||||
let chapterBegin = false;
|
||||
|
||||
if (this.keyEventListener) {
|
||||
window.removeEventListener('keypress', this.keyEventListener);
|
||||
this.keyEventListener = null;
|
||||
}
|
||||
|
||||
document.querySelectorAll('#story p').forEach((p) => {
|
||||
p.classList.remove("latest-paragraph");
|
||||
});
|
||||
|
||||
// Generate story text - loop through available content
|
||||
while (this.story.canContinue) {
|
||||
if (this.layoutRenderer.getFastForwardingAll()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.layoutRenderer.animationQueue) {
|
||||
this.layoutRenderer.animationQueue.setDelay(0.0);
|
||||
}
|
||||
|
||||
// Get ink to generate the next paragraph
|
||||
const paragraphText = this.story.Continue();
|
||||
const tags = this.story.currentTags;
|
||||
|
||||
// Process tags and get custom classes
|
||||
const customClasses = this.processTags(tags);
|
||||
|
||||
// Skip empty paragraphs
|
||||
if (paragraphText.trim().length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process paragraph text and layout
|
||||
await this.processParagraph(paragraphText, customClasses, chapterBegin);
|
||||
|
||||
chapterBegin = false;
|
||||
}
|
||||
|
||||
if (this.layoutRenderer.animationQueue) {
|
||||
this.layoutRenderer.animationQueue.setDelay(0.0);
|
||||
}
|
||||
|
||||
// Process choices
|
||||
await this.processChoices();
|
||||
|
||||
// Dispatch turn complete event
|
||||
const tce = new CustomEvent("turnCompleteEvent", {
|
||||
detail: { messages: "All text and choices have been set up." },
|
||||
bubbles: true,
|
||||
cancelable: false
|
||||
});
|
||||
document.dispatchEvent(tce);
|
||||
|
||||
// Check for end of story
|
||||
if (this.story.canContinue === false && this.story.currentChoices.length === 0) {
|
||||
const end = document.createElement("p");
|
||||
end.style.textTransform = "uppercase";
|
||||
end.style.textAlign = "center";
|
||||
end.classList.add("fade-in");
|
||||
end.classList.add("choice");
|
||||
|
||||
const endText = this.translations[this.locale] && this.translations[this.locale]['end']
|
||||
? this.translations[this.locale]['end']
|
||||
: "The End";
|
||||
|
||||
end.appendChild(document.createTextNode(endText));
|
||||
this.choiceContainer.appendChild(end);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process tags for a paragraph
|
||||
* @param {Array<string>} tags - The tags for the paragraph
|
||||
* @returns {Array<string>} Custom CSS classes to apply
|
||||
*/
|
||||
processTags(tags) {
|
||||
if (!tags || tags.length === 0) return [];
|
||||
|
||||
const customClasses = [];
|
||||
let tagDebug = "";
|
||||
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
const tag = tags[i];
|
||||
tagDebug += tag + ";";
|
||||
|
||||
// Detect tags of the form "X: Y"
|
||||
const splitTag = this.splitPropertyTag(tag);
|
||||
|
||||
// AUDIO: src
|
||||
if (splitTag && splitTag.property == "AUDIO") {
|
||||
if (this.audioManager) {
|
||||
this.audioManager.playSoundFromUrl(splitTag.val);
|
||||
}
|
||||
}
|
||||
|
||||
// AUDIOLOOP: src
|
||||
else if (splitTag && splitTag.property == "AUDIOLOOP") {
|
||||
if (this.audioManager) {
|
||||
this.audioManager.playSoundFromUrl(splitTag.val, true);
|
||||
}
|
||||
}
|
||||
|
||||
// IMAGE: src
|
||||
else if (splitTag && splitTag.property == "IMAGE") {
|
||||
if (this.layoutRenderer) {
|
||||
const imageElement = this.layoutRenderer.renderVisualTag("IMAGE", splitTag.val, this.storyContainer);
|
||||
if (imageElement && this.layoutRenderer.animationQueue) {
|
||||
this.layoutRenderer.showAfter(this.layoutRenderer.animationQueue.getDelay(), imageElement);
|
||||
this.layoutRenderer.animationQueue.incrementDelay(this.layoutRenderer.animationQueue.getSpeed());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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") {
|
||||
if (this.layoutRenderer) {
|
||||
this.layoutRenderer.renderVisualTag("BACKGROUND", splitTag.val);
|
||||
}
|
||||
}
|
||||
|
||||
// CLASS: className
|
||||
else if (splitTag && splitTag.property == "CLASS") {
|
||||
customClasses.push(splitTag.val);
|
||||
}
|
||||
|
||||
// CLEAR - removes all existing content
|
||||
else if (tag == "CLEAR") {
|
||||
this.removeAll("p");
|
||||
this.removeAll("img");
|
||||
}
|
||||
|
||||
// RESTART - clears everything and restarts the story from the beginning
|
||||
else if (tag == "RESTART") {
|
||||
this.removeAll("p");
|
||||
this.removeAll("img");
|
||||
this.restart();
|
||||
return [];
|
||||
}
|
||||
|
||||
// CHAPTER: Chapter Heading
|
||||
else if (splitTag && splitTag.property == "CHAPTER") {
|
||||
if (this.layoutRenderer) {
|
||||
this.layoutRenderer.renderVisualTag("CHAPTER", splitTag.val, this.storyContainer);
|
||||
}
|
||||
this.chapterBegin = true;
|
||||
this.indentedParagraphs = 2;
|
||||
}
|
||||
|
||||
// SEPARATOR
|
||||
else if (tag == "SEPARATOR") {
|
||||
if (this.layoutRenderer) {
|
||||
this.layoutRenderer.renderVisualTag("SEPARATOR", splitTag.val, this.storyContainer);
|
||||
}
|
||||
this.chapterBegin = true;
|
||||
}
|
||||
}
|
||||
|
||||
return customClasses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a paragraph of text
|
||||
* @param {string} paragraphText - The paragraph text
|
||||
* @param {Array<string>} customClasses - Custom CSS classes to apply
|
||||
* @param {boolean} chapterBegin - Whether this is the beginning of a chapter
|
||||
*/
|
||||
async processParagraph(paragraphText, customClasses, chapterBegin) {
|
||||
const indentWidth = 2 * parseFloat(window.getComputedStyle(document.querySelector("#indent")).lineHeight);
|
||||
let text = paragraphText;
|
||||
let dropCap = null;
|
||||
let dropQuote = null;
|
||||
|
||||
if (this.chapterBegin) {
|
||||
this.measure.push(parseFloat(window.getComputedStyle(document.getElementById("story")).width));
|
||||
this.measure.push(parseFloat(window.getComputedStyle(document.getElementById("story")).width) - indentWidth);
|
||||
this.measure.push(parseFloat(window.getComputedStyle(document.getElementById("story")).width) - indentWidth * 0.9);
|
||||
|
||||
const words = paragraphText.split(" ");
|
||||
let firstWord = words[0].substr(1, words[0].length);
|
||||
let openingQuote = "";
|
||||
let firstLetter = words[0].substr(0, 1);
|
||||
|
||||
if (firstLetter == "\"" || firstLetter == "'") {
|
||||
openingQuote = window.SmartyPants ? window.SmartyPants.smartypantsu(firstLetter, 1) : firstLetter;
|
||||
firstLetter = words[0].substr(1, 1);
|
||||
firstWord = words[0].substr(2, words[0].length);
|
||||
}
|
||||
|
||||
if (firstWord.length < 5 && words.length > 1) {
|
||||
firstWord += ' ' + words[1];
|
||||
}
|
||||
|
||||
text = '<cap>' + firstWord + '</cap> ' + paragraphText.substr(firstWord.length + 2 + openingQuote.length, paragraphText.length);
|
||||
|
||||
dropCap = document.createElement("span");
|
||||
dropCap.classList.add("drop-cap");
|
||||
dropCap.appendChild(document.createTextNode(firstLetter));
|
||||
dropCap.style.left = '0%';
|
||||
dropCap.style.top = '0%';
|
||||
dropCap.style.position = 'absolute';
|
||||
|
||||
if (openingQuote) {
|
||||
dropQuote = document.createElement("span");
|
||||
dropQuote.classList.add("drop-quote");
|
||||
dropQuote.appendChild(document.createTextNode(openingQuote));
|
||||
dropQuote.style.left = '-4.45%';
|
||||
dropQuote.style.top = '-16%';
|
||||
dropQuote.style.position = 'absolute';
|
||||
}
|
||||
} else {
|
||||
if (this.measure.length < 1) {
|
||||
this.measure.push(parseFloat(window.getComputedStyle(document.getElementById("story")).width));
|
||||
this.measure.push(parseFloat(window.getComputedStyle(document.getElementById("story")).width) - indentWidth * 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
// Process text and calculate layout
|
||||
const processedText = this.textProcessor.process(text);
|
||||
const paragraphData = this.paragraphLayout.calculateLayout(processedText, [...this.measure].reverse(), true);
|
||||
|
||||
// Render paragraph
|
||||
const [p, d] = this.layoutRenderer.renderParagraph(paragraphData, this.layoutRenderer.animationQueue.getDelay(), [...this.measure].reverse());
|
||||
|
||||
// Update measures and indented paragraphs
|
||||
for (let k = 0; k < parseInt(p.dataset.numberOfLines); k++) {
|
||||
this.measure.pop();
|
||||
}
|
||||
|
||||
this.indentedParagraphs -= p.dataset.numberOfLines;
|
||||
|
||||
// Add drop cap and quote if needed
|
||||
if (dropQuote) {
|
||||
this.layoutRenderer.insertAfter(0, p, dropQuote, true);
|
||||
}
|
||||
|
||||
if (dropCap) {
|
||||
this.layoutRenderer.insertAfter(0, p, dropCap, true);
|
||||
}
|
||||
|
||||
// Add custom classes
|
||||
for (let i = 0; i < customClasses.length; i++) {
|
||||
p.classList.add(customClasses[i]);
|
||||
}
|
||||
|
||||
p.lang = this.locale;
|
||||
this.storyContainer.appendChild(p);
|
||||
|
||||
// Smooth scroll to the paragraph
|
||||
this.layoutRenderer.smoothScroll(p, this.layoutRenderer.animationQueue.getSpeed() * 10 * p.dataset.numberOfLines);
|
||||
|
||||
// Wait for animations and speech
|
||||
await Promise.all([
|
||||
new Promise(resolve => {
|
||||
document.addEventListener('allWordsSetEvent', resolve, { once: true });
|
||||
}),
|
||||
new Promise(async resolve => {
|
||||
if (!this.ttsPlayer || !this.ttsPlayer.isEnabled()) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.ttsPlayer.speak(paragraphText);
|
||||
resolve();
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process choices from the story
|
||||
*/
|
||||
async processChoices() {
|
||||
const categoryContainers = { default: null };
|
||||
const categoryNumbers = { default: 0, categorized: 0 };
|
||||
|
||||
this.story.currentChoices.forEach(choice => {
|
||||
if (this.layoutRenderer.getFastForwardingAll()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let tagDebug = "";
|
||||
let action = "default";
|
||||
|
||||
choice.tags.forEach(tag => {
|
||||
tagDebug += tag + ";";
|
||||
const splitTag = this.splitPropertyTag(tag);
|
||||
|
||||
if (splitTag && splitTag.property === "ACTION") {
|
||||
action = splitTag.val;
|
||||
}
|
||||
});
|
||||
|
||||
const prompt = this.translations[this.locale] && this.translations[this.locale][`action_${action}`]
|
||||
? this.translations[this.locale][`action_${action}`]
|
||||
: (action === "default" ? "What do you want to do?" : action);
|
||||
|
||||
if (action != "default") {
|
||||
this.createChoiceContainer(categoryContainers, categoryNumbers, action, prompt, choice, tagDebug);
|
||||
} else {
|
||||
const defaultPrompt = this.translations[this.locale] && this.translations[this.locale]['prompt']
|
||||
? this.translations[this.locale]['prompt']
|
||||
: "What do you want to do?";
|
||||
|
||||
this.createChoiceContainer(categoryContainers, categoryNumbers, "default", defaultPrompt, choice, tagDebug, true);
|
||||
}
|
||||
});
|
||||
|
||||
// Set up key event listener
|
||||
this.keyEventListener = this.setupKeyEventListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a choice container
|
||||
* @param {Object} categoryContainers - Map of category containers
|
||||
* @param {Object} categoryNumbers - Map of category numbers
|
||||
* @param {string} action - The action category
|
||||
* @param {string} prompt - The prompt text
|
||||
* @param {Object} choice - The choice object
|
||||
* @param {string} tagDebug - Debug information for tags
|
||||
* @param {boolean} registerKeys - Whether to register keyboard shortcuts
|
||||
* @returns {HTMLElement} The choice container element
|
||||
*/
|
||||
createChoiceContainer(categoryContainers, categoryNumbers, action, prompt, choice, tagDebug, registerKeys = false) {
|
||||
let choiceCategoryContainer = categoryContainers[action];
|
||||
|
||||
if (!choiceCategoryContainer) {
|
||||
choiceCategoryContainer = document.createElement('ol');
|
||||
const 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 (this.story.currentChoices.length && !this.layoutRenderer.getFastForwardingAll()) {
|
||||
this.choiceContainer.appendChild(choiceCategoryContainer);
|
||||
}
|
||||
}
|
||||
|
||||
categoryContainers[action] = choiceCategoryContainer;
|
||||
|
||||
let choiceNumber = categoryNumbers[action];
|
||||
if (choiceNumber === undefined) {
|
||||
choiceNumber = 0;
|
||||
}
|
||||
|
||||
choiceNumber++;
|
||||
|
||||
const choiceParagraphElement = document.createElement('li');
|
||||
choiceParagraphElement.classList.add("choice");
|
||||
choiceParagraphElement.lang = this.locale;
|
||||
choiceParagraphElement.title = tagDebug;
|
||||
|
||||
// Use SmartyPants if available
|
||||
const choiceText = window.SmartyPants && typeof window.SmartyPants.smartypantsu === 'function'
|
||||
? window.SmartyPants.smartypantsu(choice.text, 1)
|
||||
: choice.text;
|
||||
|
||||
choiceParagraphElement.innerHTML = `<a href='#'>${choiceText}</a>`;
|
||||
|
||||
if (!this.layoutRenderer.getFastForwardingAll()) {
|
||||
this.layoutRenderer.insertAfter(this.layoutRenderer.animationQueue.getDelay(), choiceCategoryContainer, choiceParagraphElement, true);
|
||||
}
|
||||
|
||||
this.layoutRenderer.animationQueue.incrementDelay(this.layoutRenderer.animationQueue.getSpeed());
|
||||
|
||||
// Register key shortcuts
|
||||
if (registerKeys) {
|
||||
choiceParagraphElement.value = choiceNumber;
|
||||
this.registerKey('Digit' + choiceNumber, choice.index);
|
||||
} else {
|
||||
let categorizedNumber = categoryNumbers['categorized'];
|
||||
categorizedNumber++;
|
||||
const keyLetter = String.fromCharCode(64 + categorizedNumber);
|
||||
choiceParagraphElement.value = categorizedNumber;
|
||||
this.registerKey('Key' + keyLetter, choice.index);
|
||||
categoryNumbers['categorized'] = categorizedNumber;
|
||||
}
|
||||
|
||||
// Click on choice
|
||||
const choiceAnchorEl = choiceParagraphElement.querySelectorAll("a")[0];
|
||||
choiceAnchorEl.addEventListener("click", (event) => {
|
||||
// Don't follow <a> link
|
||||
event.preventDefault();
|
||||
this.chooseChoice(choice.index);
|
||||
});
|
||||
|
||||
categoryNumbers[action] = choiceNumber;
|
||||
|
||||
return choiceCategoryContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose a choice by index
|
||||
* @param {number} index - The choice index
|
||||
*/
|
||||
chooseChoice(index) {
|
||||
// Remove all existing choices
|
||||
this.removeAll(".choice", true);
|
||||
this.clearKeyRegistry();
|
||||
|
||||
// Tell the story where to go next
|
||||
this.story.ChooseChoiceIndex(index);
|
||||
|
||||
// This is where the save button will save from
|
||||
this.savePoint = this.story.state.toJson();
|
||||
|
||||
// Continue the story
|
||||
this.continueStory(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a key for keyboard shortcuts
|
||||
* @param {string} key - The key code
|
||||
* @param {number} choice - The choice index
|
||||
*/
|
||||
registerKey(key, choice) {
|
||||
this.keyRegistry[key] = choice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the key registry
|
||||
*/
|
||||
clearKeyRegistry() {
|
||||
this.keyRegistry = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the key event listener for choices
|
||||
* @returns {Function} The key event listener
|
||||
*/
|
||||
setupKeyEventListener() {
|
||||
const keyEventListener = (event) => {
|
||||
for (const key in this.keyRegistry) {
|
||||
if (event.code === key) {
|
||||
window.removeEventListener('keypress', keyEventListener);
|
||||
this.chooseChoice(this.keyRegistry[key]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keypress', keyEventListener);
|
||||
|
||||
return keyEventListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart the story
|
||||
*/
|
||||
restart() {
|
||||
if (!this.story) return;
|
||||
|
||||
if (this.layoutRenderer.animationQueue) {
|
||||
this.layoutRenderer.animationQueue.setDelay(0.0);
|
||||
}
|
||||
|
||||
this.story.ResetState();
|
||||
this.layoutRenderer.setFastForwardingAll(true);
|
||||
this.layoutRenderer.animationQueue.fastForward();
|
||||
|
||||
this.removeAll("p");
|
||||
this.removeAll("img");
|
||||
this.removeAll("h2");
|
||||
this.removeAll("double");
|
||||
this.removeAll(".choice", true);
|
||||
|
||||
if (this.keyEventListener) {
|
||||
window.removeEventListener('keypress', this.keyEventListener);
|
||||
this.keyEventListener = null;
|
||||
}
|
||||
|
||||
// Set save point to here
|
||||
this.savePoint = this.story.state.toJson();
|
||||
|
||||
// Continue the story
|
||||
this.continueStory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current state
|
||||
* @returns {boolean} Whether the save was successful
|
||||
*/
|
||||
saveState() {
|
||||
if (!this.persistenceManager || !this.story) return false;
|
||||
|
||||
try {
|
||||
const history = Array.from(document.querySelectorAll("#story p:not(.latest-paragraph)")).map(p => p.outerHTML);
|
||||
|
||||
return this.persistenceManager.saveState({
|
||||
inkJson: this.savePoint,
|
||||
history: history
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn("Couldn't save state:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a saved state
|
||||
* @returns {boolean} Whether the load was successful
|
||||
*/
|
||||
loadState() {
|
||||
if (!this.persistenceManager || !this.story) return false;
|
||||
|
||||
try {
|
||||
const savedState = this.persistenceManager.loadState();
|
||||
|
||||
if (!savedState) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.removeAll("p");
|
||||
this.removeAll("img");
|
||||
this.removeAll("h2");
|
||||
this.removeAll("double");
|
||||
this.removeAll(".choice", true);
|
||||
|
||||
if (savedState.history) {
|
||||
savedState.history.forEach(p => {
|
||||
const d = document.createElement('div');
|
||||
d.innerHTML = p;
|
||||
this.storyContainer.appendChild(d.firstChild);
|
||||
});
|
||||
}
|
||||
|
||||
if (savedState.inkJson) {
|
||||
this.story.state.LoadJson(savedState.inkJson);
|
||||
this.savePoint = savedState.inkJson;
|
||||
}
|
||||
|
||||
// Update paragraph heights
|
||||
this.updateParagraphHeight();
|
||||
|
||||
if (this.keyEventListener) {
|
||||
window.removeEventListener('keypress', this.keyEventListener);
|
||||
this.keyEventListener = null;
|
||||
}
|
||||
|
||||
// Continue the story
|
||||
this.continueStory();
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.warn("Couldn't load state:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update paragraph heights based on viewport
|
||||
*/
|
||||
updateParagraphHeight() {
|
||||
document.querySelectorAll("#story p").forEach((element) => {
|
||||
if (element.dataset.vpc) {
|
||||
const pHeight = parseFloat(window.getComputedStyle(document.getElementById('page_right')).height);
|
||||
const newHeight = pHeight * element.dataset.vpc / 100 + 'px';
|
||||
element.style.height = newHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all elements that match the given selector
|
||||
* @param {string} selector - The CSS selector
|
||||
* @param {boolean} choices - Whether to remove from the choice container
|
||||
*/
|
||||
removeAll(selector, choices = false) {
|
||||
const container = choices ? this.choiceContainer : this.storyContainer;
|
||||
const allElements = container.querySelectorAll(selector);
|
||||
|
||||
for (let i = 0; i < allElements.length; i++) {
|
||||
const el = allElements[i];
|
||||
el.parentNode.removeChild(el);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for parsing out tags of the form: # PROPERTY: value
|
||||
* @param {string} tag - The tag to parse
|
||||
* @returns {Object|null} The parsed property and value
|
||||
*/
|
||||
splitPropertyTag(tag) {
|
||||
const propertySplitIdx = tag.indexOf(":");
|
||||
|
||||
if (propertySplitIdx !== -1) {
|
||||
const property = tag.substr(0, propertySplitIdx).trim();
|
||||
const val = tag.substr(propertySplitIdx + 1).trim();
|
||||
|
||||
return {
|
||||
property: property,
|
||||
val: val
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the locale for translations
|
||||
* @param {string} locale - The locale code
|
||||
*/
|
||||
setLocale(locale) {
|
||||
this.locale = locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set translations
|
||||
* @param {Object} translations - The translations object
|
||||
*/
|
||||
setTranslations(translations) {
|
||||
this.translations = translations;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user