// ---------------- Load image files (necessary for Webpack) ----------------- // import HEALTH_MASK_IMAGE from "../img/health-mask.png"; import HEALTH_MASK_STEEL_IMAGE from "../img/health-mask-steel.png"; import SOUL_ORB_IMAGE from "../img/soul-orb.png"; import NOTCH_IMAGE from "../img/notch.png"; import NOTCH_FILLED_IMAGE from "../img/notch-filled.png"; import NOTCH_OVERCHARMED_IMAGE from "../img/notch-overcharmed.png"; import GEO_IMAGE from "../img/geo.png"; import GEO_SHADE_IMAGE from "../img/geo-shade.png"; // ---------------- Constants ----------------- // // const DATA_UNKNOWN = "Data unknown"; const SYMBOL_FALSE = ""; // "❌ " const SYMBOL_TRUE = ""; // "✅ " // const SYMBOL_INFO = ""; // "ℹ " const SYMBOL_CLOCK = ""; // "🕑 " const SYMBOL_EMPTY = ""; const FLEUR_DIVIDE = "
"; const WIKI_LINK = "https://hollowknight.fandom.com/wiki/"; const ROOT = document.documentElement; const SCROLL_BUTTON = document.querySelector(".scroll-up-button"); /* ######################################################################################### */ function ScrollToElement(element) { /* Scroll to the element top */ element.scrollTo({ top: 0, behavior: "smooth" }); } function ShowElement(element) { element.classList.add("visible-block"); element.classList.add("opacity-full"); element.classList.remove("hidden"); } function HideElement(element) { element.classList.remove("visible-block"); element.classList.remove("opacity-full"); element.classList.add("hidden"); } function TogglePageScrollElement(root, element, ratio) { /* Maximum number of pixels that can be scrolled by the user */ let scrollTotal = root.scrollHeight - root.clientHeight; if ((root.scrollTop / scrollTotal) > ratio) { /* Show button */ ShowElement(element); } else { /* Hide button */ HideElement(element); } } /** * Replaces the h2 titles with a current percent/max percent values as read from the save file * @param {object} jsObj Object with HTML data to fill * @param {number} hkGameCompletion Total completion percentage in a save file */ function CompletionHTML(jsObj, hkGameCompletion) { let h2 = ""; let h2id = ""; let cl = ""; let clGreen = "box-green"; let clRed = "box-red"; let cp = 0; // current Percent let mp = 0; // max Percent let fillText = ""; for (let i in jsObj) { h2 = jsObj[i].h2; h2id = "h2-" + jsObj[i].id; (jsObj[i].hasOwnProperty("percent")) ? cp = jsObj[i].percent: cp = 0; if (i === "intro") cp = hkGameCompletion; // Don't use percent-box for Essentials, Achievements, Statistics if (!jsObj[i].hasOwnProperty("maxPercent")) { fillText = ""; } // otherwise use percent-box with values cp/mp% else { mp = jsObj[i].maxPercent; if (i === "maskShards") { let perc = jsObj[i].percent; (perc % 4) ? cp = Math.floor(perc / 4): cp = perc / 4; } else if (i === "vesselFragments") { let perc = jsObj[i].percent; (perc % 3) ? cp = Math.floor(perc / 3): cp = perc / 3; } // switches the box to red when a section (h2) is 0 if (cp === 0) { cl = ` ${clRed}`; } // switches the box to green when a section (h2) is completed else if (cp === mp) { cl = ` ${clGreen}`; } // default is blue (partially completed and starting value) else cl = ""; // needed for Game Status to show percentage properly (adds a slash for all boxes except the Game Status one) if (jsObj[i].id != "hk-intro") cp += "/"; fillText = `
${(i === "intro") ? cp: cp + jsObj[i].maxPercent}%
`; } } } /* ################################### Optimized Functions ###################################### */ function GenerateInnerHTML(db) { let sections = db.sections; console.log(sections); let finalHTMLFill = ""; let textFill = ""; let entries = {}; for (let section in sections) { entries = sections[section].entries; /* starts a new
with the current section id */ textFill = SectionStart(sections[section]); /* creates a

tag for the current section and fills with current%/max% If the save file was not analyzed, then fill only max% on blue background */ if (db.saveAnalyzed === true) { textFill += CompletionFill(sections[section]); } else { textFill += CompletionFillNoSave(sections[section]); } /* create all main entries */ switch (section) { case "hints": /* display only one (current) hint */ textFill += SingleEntryFill(section, entries[sections[section].current]); break; default: for (let entry in entries) { textFill += SingleEntryFill(section, entries[entry]); if (entry === "saveVersion") textFill += FLEUR_DIVIDE; } } /* Cumulate all section texts into one variable for final HTML filling. End section div tag */ finalHTMLFill += `${textFill}\n

\n\n`; } /* Final HTML Fill here */ console.groupCollapsed("finalHTMLFill"); console.log(finalHTMLFill); console.groupEnd(); document.getElementById("generated").innerHTML = finalHTMLFill; } function SectionStart(section) { return `
\n`; } /** * Replaces the h2 titles with max percent values as read from the database */ function CompletionFillNoSave(section) { let id = ""; let h2 = ""; let h2id = ""; let mp = ""; // max Percent let cl = ""; // class id = section.id; h2 = section.h2; h2id = "h2-" + section.id; mp = `
${(id === "hk-intro") ? 0: section.maxPercent}%
`; if (!section.hasOwnProperty("maxPercent")) mp = ""; return [ `
`, `

${h2}${mp}

`, "
\n" ].join(""); } /** * Replaces the h2 titles with a current percent/max percent values as read from the database */ function CompletionFill(section) { let h2 = section.h2; let h2id = `

`; let cl = ""; let clGreen = "box-green"; let clRed = "box-red"; let cp = 0; // current Percent let mp = 0; // max Percent let fillText = ""; (section.hasOwnProperty("percent")) ? cp = section.percent: cp = 0; // Don't use percent-box for Essentials, Achievements, Statistics if (!section.hasOwnProperty("maxPercent")) { fillText = ""; } // otherwise use percent-box with values cp/mp% else { mp = section.maxPercent; if (section.id === "hk-maskshards") { let perc = section.percent; (perc % 4) ? cp = Math.floor(perc / 4): cp = perc / 4; } else if (section.id === "hk-vesselfragments") { let perc = section.percent; (perc % 3) ? cp = Math.floor(perc / 3): cp = perc / 3; } // switches the box to red when a section (h2) is 0 if (cp === 0) { cl = ` ${clRed}`; } // switches the box to green when a section (h2) is completed else if (cp === mp) { cl = ` ${clGreen}`; } // default is blue (partially completed and starting value) else cl = ""; // needed for Game Status to show percentage properly (adds a slash for all boxes except the Game Status one) if (section.id != "hk-intro") cp += "/"; fillText = `
${(section.id === "hk-intro") ? cp: cp + section.maxPercent}%
`; } return `\t${h2id}${h2}${fillText}

\n`; } function SingleEntryFill(section, entry) { let icon = ""; let iconGreen = SYMBOL_TRUE; let iconRed = SYMBOL_FALSE; let iconClock = SYMBOL_CLOCK; let iconNull = SYMBOL_EMPTY; let textPrefix = ""; let textSuffix = ""; let textFill = ""; let Img = ""; let maskNormal = `health mask image`; let maskSteel = `steel health mask image`; let soulNormal = `soul orb image`; let notchNormalImage = `notch image`; let notchFilledImage = `notch image`; let notchOvercharmedImage = `notch image`; let geoNormalImage = `geo symbol image`; let geoShadeImage = `shade geo symbol image`; let wiki = ""; let div = `
`; let divFlex = `
`; let b = ["", ""]; let p = ""; let span = ["", ""]; let spoiler = ["", ""]; let spoilerAfter = ""; if (entry.hasOwnProperty("icon")) { switch (entry.icon) { case "clock": icon = iconClock; break; case "green": icon = iconGreen; break; case "red": icon = iconRed; break; default: icon = iconNull; } } else { icon = iconRed; } if (entry.hasOwnProperty("name")) { textPrefix = entry.name; } if (entry.hasOwnProperty("spoiler")) { textSuffix = entry.spoiler; } if (entry.hasOwnProperty("wiki")) { wiki = entry.wiki; } if (!textPrefix.length) b = ["", ""]; if (wiki.length) b = [``, ""]; /* #################### Different behaviour depending on the section ####################### */ switch (section) { case "intro": b = ["", ""]; span = ["", ""]; /* Different text and images for each entry in the "Game Status" section */ switch (entry.id) { case "gameCompletion": textSuffix = `${entry.spoiler} %`; break; case "health": div = divFlex; span = ["", ""]; (entry.permadeathMode) ? Img = maskSteel: Img = maskNormal; for (let i = 0, total = entry.amountTotal; i < total; i++) { textSuffix += Img; } textSuffix += `${p}(${entry.amountTotal})`; break; case "soul": div = divFlex; span = ["", ""]; Img = soulNormal; for (let i = 0, total = Math.round(entry.amountTotal / 33); i < total; i++) { textSuffix += Img; } textSuffix += `${p}(${Math.round(entry.amountTotal / 33)})`; break; case "notches": div = divFlex; span = ["", ""]; /* First, check filled (used) notches and fill them (skips if no filled notches) */ if (entry.amountFilled > 0) { for (let i = 0, total = entry.amountFilled; i < total; i++) { textSuffix += notchFilledImage; } } /* Second, check overcharmed notches and fill them (skips if player is not overcharmed) */ if (entry.amountOvercharmed > 0) { for (let i = 0, total = entry.amountOvercharmed; i < total; i++) { textSuffix += notchOvercharmedImage; } } /* Last, fill all unused notches */ if (entry.amountUnused > 0) { for (let i = 0, total = entry.amountUnused; i < total; i++) { textSuffix += notchNormalImage; } } textSuffix += `${p}(${entry.amountTotal})`; break; case "geo": div = divFlex; span = ["", ""]; textSuffix += `${geoNormalImage}${entry.amount}`; // Show Shade Geo value and image only if Shade has at least 1 Geo on it if (entry.amountShade > 0) textSuffix += `${p}+${geoShadeImage}${entry.amountShade}`; // Show also total Geo (Geo + Shade Geo) if player has at least 1 geo alongside the shade geo if (entry.amount > 0 && entry.amountShade > 0) textSuffix += `${p}=${p}${entry.amountTotal}`; p = ""; break; default: } /* Optimized only for the "intro" section! */ if (entry.hasOwnProperty("spoilerAfter")) { spoilerAfter = ` ${entry.spoilerAfter}`; span[1] = ""; } break; case "hints": b = ["", ""]; span = ["", ""]; icon = iconNull; break; default: span = ["", ""]; spoiler = ["", ""]; if (textSuffix.length && textPrefix.length) textSuffix = `— ${textSuffix}`; if (textPrefix.includes("")) textSuffix = `${textSuffix}`; } return [ div, icon, `${b[0]}${textPrefix}${b[1]}`, span[0], p, spoiler[0], `${textSuffix}${spoilerAfter}`, spoiler[1], span[1], "
\n" ].join(""); } /** * Adds HTML string to an element with a given ID. * @param {object} divId object containing div ID of the HTML element to append to * @param {string} content HTML contents to append */ function AppendHTML(divId, content) { document.getElementById(divId.id).innerHTML += "\n" + content; } /** * Toggles display of "hk-hints". On click with no parameters or on demand when called with a parameter * @param {string} param "hide", "show" or none (optional) */ function CheckboxHintsToggle(param = "none") { let checkboxId = document.getElementById("checkbox-hints"); switch (param) { case "hide": document.getElementById("hk-hints").classList.add("hidden"); checkboxId.value = "hints-off"; checkboxId.checked = false; // remember this choice for subsequent page visits and browser restarts if (StorageAvailable('localStorage')) { localStorage.setItem("hkCheckboxHints", "unchecked"); } break; case "show": document.getElementById("hk-hints").classList.remove("hidden"); checkboxId.value = "hints-on"; checkboxId.checked = true; // remember this choice for subsequent page visits and browser restarts if (StorageAvailable('localStorage')) { localStorage.setItem("hkCheckboxHints", "checked"); } break; default: // This runs when the checkbox is not checked if (checkboxId.checked === false) { document.getElementById("hk-hints").classList.add("hidden"); checkboxId.value = "hints-off"; // remember this choice for subsequent page visits and browser restarts if (StorageAvailable('localStorage')) { localStorage.setItem("hkCheckboxHints", "unchecked"); } } // This runs when the checkbox is checked else { document.getElementById("hk-hints").classList.remove("hidden"); checkboxId.value = "hints-on"; // remember this choice for subsequent page visits and browser restarts if (StorageAvailable('localStorage')) { localStorage.setItem("hkCheckboxHints", "checked"); } } } } /** * Toggles display of ".spoiler-span" class. On click with no parameters or on demand when called with a parameter * @param {string} param "hide", "show" or none (optional) */ function CheckboxSpoilersToggle(param = "none") { let checkboxId = document.getElementById("checkbox-spoilers"); let allClassElements = document.querySelectorAll(".spoiler-span"); let length = allClassElements.length; switch (param) { case "hide": for (let i = 0; i < length; i++) { allClassElements[i].classList.add("hidden"); } checkboxId.value = "spoilers-off"; checkboxId.checked = false; // remember this choice for subsequent page visits and browser restarts if (StorageAvailable('localStorage')) { localStorage.setItem("hkCheckboxSpoilers", "unchecked"); } break; case "show": for (let i = 0; i < length; i++) { allClassElements[i].classList.remove("hidden"); } checkboxId.value = "spoilers-on"; checkboxId.checked = true; // remember this choice for subsequent page visits and browser restarts if (StorageAvailable('localStorage')) { localStorage.setItem("hkCheckboxSpoilers", "checked"); } break; default: // This runs when the checkbox is not checked if (checkboxId.checked === false) { for (let i = 0; i < length; i++) { allClassElements[i].classList.add("hidden"); } checkboxId.value = "spoilers-off"; // remember this choice for subsequent page visits and browser restarts if (StorageAvailable('localStorage')) { localStorage.setItem("hkCheckboxSpoilers", "unchecked"); } break; } // This runs when the checkbox is checked else { for (let i = 0; i < length; i++) { allClassElements[i].classList.remove("hidden"); } checkboxId.value = "spoilers-on"; // remember this choice for subsequent page visits and browser restarts if (StorageAvailable('localStorage')) { localStorage.setItem("hkCheckboxSpoilers", "checked"); } } } } /** * Detects whether Storage is both supported and available. * MDN WebDocs https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API#feature-detecting_localstorage * @param {Storage} type type of storage. Ex. "localStorage" or "sessionStorage" * @returns {Boolean} */ function StorageAvailable(type) { var storage; try { storage = window[type]; var x = '__storage_test__'; storage.setItem(x, x); storage.removeItem(x); return true; } catch (e) { return e instanceof DOMException && ( // everything except Firefox e.code === 22 || // Firefox e.code === 1014 || // test name field too, because code might not be present // everything except Firefox e.name === 'QuotaExceededError' || // Firefox e.name === 'NS_ERROR_DOM_QUOTA_REACHED') && // acknowledge QuotaExceededError only if there's something already stored (storage && storage.length !== 0); } } /* ----------------------- Event Listeners -------------------------- */ document.addEventListener("scroll", () => { TogglePageScrollElement( /* the document element root () */ ROOT, /* Which element to toggle visibility */ SCROLL_BUTTON, /* How far the user has to scroll to show the element */ 0.1); }); SCROLL_BUTTON.addEventListener("click", () => { ScrollToElement(ROOT); }); // Checkboxes functions document.getElementById("checkbox-hints").addEventListener("click", CheckboxHintsToggle, false); document.getElementById("checkbox-spoilers").addEventListener("click", CheckboxSpoilersToggle, false); /* ------------------------- Exports ------------------------------- */ export { CompletionHTML, GenerateInnerHTML, AppendHTML, CheckboxHintsToggle, CheckboxSpoilersToggle, StorageAvailable };