Skip to content

Instantly share code, notes, and snippets.

@Hotrian
Last active June 22, 2025 18:42
Show Gist options
  • Select an option

  • Save Hotrian/8b6c38e1fe899f99826f3b857ea8b9b9 to your computer and use it in GitHub Desktop.

Select an option

Save Hotrian/8b6c38e1fe899f99826f3b857ea8b9b9 to your computer and use it in GitHub Desktop.
Web Based Starcraft 2 Save Editor (https://hotrian.com/sc2/editor)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>(Starcraft 2) Save Editor</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<style>
:root {
--background: #ffffff;
--foreground: #000000;
--primary: #4b0082;
--secondary: #add8e6;
--button-bg: #4b0082;
--button-text: #ffffff;
--modal-bg: #ffffff;
--input-bg: #f9f9f9;
--box-shadow: rgba(0, 0, 0, 0.2);
}
/* Light mode */
.light-mode {
--background: #ffffff;
--foreground: #000000;
--primary: #4b0082;
--secondary: #add8e6;
--button-bg: #4b0082;
--button-text: #ffffff;
--modal-bg: #ffffff;
--input-bg: #f9f9f9;
--box-shadow: rgba(0, 0, 0, 0.2);
--git-image: url('https://hotrian.com/sc2/github-mark.png');
--editor-bg: #dadada;
--editor-text: #000000;
--editor-bg2: #c6c6c6;
--custom-header-text: #4b0082;
--section-header-color: #000000;
--section-title-color: #000000;
--section-text-color: #666666;
--section-locked-color: #eeeeee;
--section-unlocked-color: #f0e984;
--text-area-bg: #ffffff;
}
/* Dark mode */
.dark-mode {
--background: #2c2a5c;
--foreground: #ffffff;
--primary: #933aff;
--secondary: #3700b3;
--button-bg: #bb86fc;
--button-text: #000000;
--modal-bg: #1f1f1f;
--input-bg: #1f1f1f;
--box-shadow: rgba(255, 255, 255, 0.1);
--git-image: url('https://hotrian.com/sc2/github-mark-white.png');
--editor-bg: #5d5d5d;
--editor-text: #e7e7e7;
--editor-bg2: #393939;
--custom-header-text: #b348ff;
--section-header-color: #e7e7e7;
--section-title-color: #000000;
--section-text-color: #666666;
--section-locked-color: #eeeeee;
--section-unlocked-color: #f0e984;
--text-area-bg: #c9c9c9;
}
body {
margin: 0;
height: 100vh;
background: linear-gradient(to bottom, var(--secondary), var(--primary));
color: var(--foreground);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-family: Arial, sans-serif;
overflow: hidden;
}
@keyframes fadeInWindow {
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInTitle {
to {
opacity: 1;
transform: translateY(0);
}
}
.title {
font-size: 32px;
font-weight: bold;
color: white;
margin-bottom: 20px;
opacity: 0;
transform: translateY(-20px);
animation: fadeInTitle 1s ease-out forwards;
}
.window {
background: var(--background);
box-shadow: 0 8px 24px var(--box-shadow);
border-radius: 12px;
min-width: 800px;
width: 60%;
max-height: 70vh;
padding: 20px;
overflow-y: auto;
position: relative;
opacity: 0;
transform: translateY(20px);
animation: fadeInWindow 1s ease-out forwards;
text-align: left;
}
@media (max-width: 768px) {
.window {
width: 80%;
}
}
.top-bar {
display: flex;
align-items: center;
justify-content: center;
position: relative;
margin-bottom: 20px;
}
.left-version {
position: absolute;
left: 0;
display: flex;
gap: 10px;
padding-left: 20px;
}
.home-center {
text-align: center;
flex: 1;
}
.right-buttons {
position: absolute;
right: 0;
display: flex;
gap: 10px;
padding-right: 20px;
}
.home-button {
background-color: var(--button-bg);
color: var(--button-text);
display: block;
margin: 0px auto 0px auto;
padding: 10px 20px;
border: none;
border-radius: 8px;
text-decoration: none;
font-size: 16px;
transition: background-color 0.3s ease;
text-align: center;
width: fit-content;
}
.topbar-btn {
padding: 8px 12px;
border-radius: 6px;
background-color: var(--button-bg);
color: var(--button-text);
border: none;
cursor: pointer;
}
.github-link {
display: block;
width: 32px;
height: 32px;
background-image: var(--git-image);
background-size: contain;
background-repeat: no-repeat;
background-position: center;
transition: background-image 0.3s ease;
}
.button-icon {
height: 24px;
width: 24px;
vertical-align: -6px;
margin-left: 4px;
}
.floating-logo {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
flex-direction: column;
align-items: center;
z-index: 1000;
}
.floating-logo-img {
width: 64px;
height: 64px;
margin-bottom: 6px;
}
.floating-bubble {
background-color: white;
color: #4b0082;
font-weight: bold;
padding: 6px 10px;
border-radius: 12px;
font-size: 14px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
white-space: nowrap;
}
.customHeader {
display:none;
font-weight:bold;
font-size:16px;
color: var(--custom-header-text);
}
#editorBasic {
overflow-y: auto;
border: 1px solid #ccc;
padding: 10px;
border-radius: 8px;
background: var(--editor-bg);
color: var(--editor-text);
}
#editorGraphical {
max-height: 400px;
overflow-y: auto;
border: 1px solid #ccc;
padding: 10px;
border-radius: 8px;
background: var(--editor-bg);
color: var(--editor-text);
width: 100%;
box-sizing: border-box;
}
#graphicalTabs {
margin-bottom: 10px;
}
#editorFooter {
position: sticky;
width:max-content;
margin-left:auto;
bottom: 0px;
}
.section {
margin-bottom: 20px;
}
.section-title {
font-weight: bold;
font-size: 18px;
margin-top: 10px;
}
.entry {
margin-left: 20px;
margin-bottom: 5px;
}
.entry input {
width: 200px;
margin-left: 10px;
}
.icon {
cursor: pointer;
margin-left: 5px;
}
.tab-button {
padding: 8px 16px;
margin-right: 10px;
font-weight: bold;
border: none;
border-radius: 8px;
background-color: #eee;
cursor: pointer;
}
.tab-button.active {
background-color: var(--button-bg);
color: var(--button-text);
}
.tab-button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
@keyframes fadeInModal {
from { background-color: rgba(0, 0, 0, 0); }
to { background-color: rgba(0, 0, 0, 0.5); }
}
@keyframes slideUpFade {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal {
display: none;
position: fixed;
z-index: 999;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
animation: fadeInModal 0.3s ease forwards;
}
.modal-content {
background-color: var(--modal-bg);
padding: 20px;
border-radius: 12px;
width: 80%;
max-width: 1200px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
transform: translateY(20px);
opacity: 0;
animation: slideUpFade 0.3s ease forwards;
position: relative;
}
.modal-body {
max-height: 400px;
overflow-y: auto;
padding-right: 10px;
}
.close-btn {
position: absolute;
top: 8px;
right: 12px;
color: #aaa;
font-size: 24px;
font-weight: bold;
cursor: pointer;
}
.theme-toggle {
position: fixed;
top: 20px;
right: 20px;
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--button-bg);
color: var(--button-text);
border: none;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
cursor: pointer;
box-shadow: 0 4px 8px var(--box-shadow);
transition: background 0.3s, color 0.3s, transform 0.2s, font-size 0.2s;
z-index: 1000;
}
.theme-toggle:hover {
transform: scale(1.1);
}
</style>
</head>
<body>
<button id="themeToggleBtn" class="theme-toggle">&#x1F319;</button>
<div class="title">(Starcraft 2) Save Editor</div>
<div id="window" class="window">
<div class="top-bar">
<div class="left-version">Version 1.0.2</div>
<div class="home-center">
<a href="https://hotrian.com/sc2/" class="home-button">
<b>Return to Base</b>
<img src="https://hotrian.com/sc2/logo.png" alt="Home" class="button-icon">
</a>
</div>
<div class="right-buttons">
<button class="topbar-btn" onclick="openModal('modal1')">What is this?</button>
<button class="topbar-btn" onclick="openModal('modal2')">Version History</button>
<a href="https://gist.github.com/Hotrian/8b6c38e1fe899f99826f3b857ea8b9b9" class="github-link"></a>
</a>
</div>
</div>
<label for="bankFile">Select SC2Bank File:</label>
<input type="file" id="bankFile" accept=".SC2Bank,.xml" /><br/><br/>
<div id="editorWrapper" style="display:none;">
<label for="mapSelect">Select Map:</label>
<select id="mapSelect">
<option value="generic">Generic</option>
<option value="minzEvoNA">-Mineralz Evolution- (NA)</option>
<option value="minzEvoEU">-Mineralz Evolution- (EU)</option>
<option value="minzEvoKR">-Mineralz Evolution- (KR)</option>
</select><br/>
<label for="authorId">Author ID (optional, for signature generation):</label>
<input type="text" id="authorId" style="margin-bottom: 2px"/><br/>
<label for="userId">User ID (optional, for signature generation):</label>
<input type="text" id="userId" style="margin-bottom: 2px"/><br/><br/>
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;">
<div id="editorTabs">
<button id="tabGraphical" class="tab-button">Graphical</button>
<button id="tabBasic" class="tab-button">Basic</button>
<button id="tabXML" class="tab-button">XML</button>
</div>
<div id="customFloatingHeader" class="customHeader"></div>
</div>
<div id="editorGraphical" style="display:none;">
<div id="graphicalTabs">
</div>
</div>
<div id="editorBasic">
</div>
<div id="editorXML" style="display:none;">
<textarea id="xmlEditor" rows="30" cols="50" style="width: 100%; background-color: var(--text-area-bg);"></textarea>
</div>
<br/>
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 0px;">
<div id="editorFooter">
<button>Save *.SC2Bank</button>
</div>
</div>
</div>
</div>
<div id="output"></div>
<div class="floating-logo">
<img src="https://hotrian.com/sc2/axolittles.png" alt="Axolittles Logo" class="floating-logo-img">
<div class="floating-bubble">Powered by <a href="https://axolittles.com/">Axolittles</a></div>
</div>
<div id="modal1" class="modal">
<div class="modal-content">
<span class="close-btn" onclick="closeModal('modal1')">&times;</span>
<h2>What is this?</h2>
<p>This is a Save Editor for the game Starcraft 2 by Blizzard Entertainment! This save editor supports Signature Generation to pass basic verification checks, but does not currently support any custom encryption required for some maps. Blizzard allows bank editing, however, some custom Arcade maps such as Mafia do monitor their community and issue blacklisting for "bank hacking". Use this tool wisely!</p>
<p>This tool was made without the express permission or knowledge of Blizzard Entertainment and is not affiliated with Blizzard Entertainment or Starcraft in any way.</p>
<h2>Who made this?</h2>
<p>This tool was created by <a href="https://github.com/Hotrian">Hotrian</a> and is open source. You can find the source code on <a href="https://gist.github.com/Hotrian/8b6c38e1fe899f99826f3b857ea8b9b9">GitHub</a>. If you have any questions or suggestions, feel free to reach out!</p>
<p>This tool was made for the <a href="https://discord.com/invite/Jb9zWP7">Mineralz Evolution discord</a>, if you find it useful, please join us! If you're feeling generous, feel free to <a href="https://www.paypal.com/donate/?hosted_button_id=L82ESZUGHWSEJ">donate on PayPal</a> ♥</p>
</div>
</div>
<div id="modal2" class="modal">
<div class="modal-content">
<span class="close-btn" onclick="closeModal('modal2')">&times;</span>
<div class="modal-body">
<h2>Version History</h2>
<li>1.0.0 - April 23rd 2025</li>
<ul>&#x1F852; Initial release.</ul>
<ul>&#x1F852; Basic support for editing SC2Banks, no custom encryption support.</ul>
<ul>&#x1F852; Custom support for -Mineralz Evolution- (NA), graphical editor mode with basic "valid save" enforcement.</ul>
<li>1.0.1 - April 26th 2025</li>
<ul>&#x1F852; Added Dark Mode, defaults to current Operating System mode.</ul>
<ul>&#x1F852; Fixed some bugs.</ul>
<ul>&#x1F852; Modularized map support code, supported maps now load as javascript modules and submodules.</ul>
<ul>&#x1F852; Added support for MinzEvoEU and MinzEvoKR.</ul>
<li>1.0.2 - April 27th 2025</li>
<ul>&#x1F852; Added MinzEvo Save File Validation - the save editor should generally no longer be able to produce an invalid MinzEvo save when in Graphical Mode.</ul>
<ul>&#x1F852; Save files are now validated when entering Graphical Mode. Switching back to the Graphical tab revalidates any changes.</ul>
<ul>&#x1F852; Modularized the cheevo code. Added Dependency chains and reworked locking/unlocking logic. Possible I missed a dependency somewhere.</ul>
<ul>&#x1F852; The page now waits while the modules are loading, which should prevent race conditions. The first time loading each module might take a few seconds.</ul>
</div>
</div>
</div>
<script>
function updateThemeIcon() {
const button = document.getElementById('themeToggleBtn');
if (document.body.classList.contains('dark-mode')) {
button.textContent = '\u{1F31E}'; // 🌞
} else {
button.textContent = '\u{1F319}'; // 🌙
}
}
function toggleDarkMode() {
if (document.body.classList.contains('dark-mode')) {
document.body.classList.remove('dark-mode');
document.body.classList.add('light-mode');
} else {
document.body.classList.remove('light-mode');
document.body.classList.add('dark-mode');
}
updateThemeIcon();
}
document.getElementById('themeToggleBtn').addEventListener('click', toggleDarkMode);
// Auto-detect system setting on load
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.body.classList.add('dark-mode');
} else {
document.body.classList.add('light-mode');
}
updateThemeIcon();
</script>
<script>
function openModal(id) {
const modal = document.getElementById(id);
modal.style.display = "flex";
const content = modal.querySelector('.modal-content');
content.style.animation = "none";
void content.offsetWidth; // reflow to reset animation
content.style.animation = null;
}
function closeModal(id) {
document.getElementById(id).style.display = "none";
}
// Optional: Close modal if clicked outside content
window.addEventListener("click", function (event) {
const modals = document.querySelectorAll(".modal");
modals.forEach(modal => {
if (event.target === modal) {
modal.style.display = "none";
}
});
});
</script>
<script type="module">
const mapCache = {};
let parsedXML = null;
globalThis.parsedXML = parsedXML;
let xmlDoc = null;
let uploadedBankFilename = "edited.SC2Bank"; // default fallback
let activeGraphicalTab = "";
let lastActiveTab = "";
let MinzEvoNA = null;
let MinzEvoEU = null;
let MinzEvoKR = null;
let currentGraphicalTabs = [];
let currentGraphicalTabModes = [];
globalThis.currentGraphicalTabs = currentGraphicalTabs;
globalThis.currentGraphicalTabModes = currentGraphicalTabModes;
function insertStyle(selector, newRule) {
let styleTag = document.getElementById("dynamic-style");
if (!styleTag) {
styleTag = document.createElement("style");
styleTag.id = "dynamic-style";
styleTag.appendChild(document.createTextNode("")); // Required for Firefox
document.head.appendChild(styleTag);
}
const sheet = styleTag.sheet;
const rules = sheet.cssRules || sheet.rules;
// Check if the rule for the selector already exists
for (let i = 0; i < rules.length; i++) {
if (rules[i].selectorText === selector) {
return; // Don't insert it again
}
}
// If rule doesn't exist, add it
try {
sheet.insertRule(`${selector} { ${newRule} }`, sheet.cssRules.length);
} catch (e) {
console.warn("Failed to insert rule:", e);
}
}
globalThis.insertStyle = insertStyle;
export async function getMapModule(mapName) {
if (!mapCache[mapName]) {
console.log(`Loading map module: ${mapName}`);
mapCache[mapName] = await import(`./maps/${mapName}.js`);
console.log(mapCache[mapName]);
await mapCache[mapName].init();
}
return mapCache[mapName];
}
document.getElementById("mapSelect").value = "";
document.getElementById("mapSelect").addEventListener("change", async () => { await updateMapSelection(); });
async function updateMapSelection() {
console.log("Updating map selection...");
const authorIdInput = document.getElementById("authorId");
const selected = document.getElementById("mapSelect").value;
const tabGraphical = document.getElementById("tabGraphical");
tabGraphical.disabled = true;
if (selected === "minzEvoNA") {
document.getElementById("window").style.minWidth = "960px";
authorIdInput.value = "1-S2-1-334168";
authorIdInput.disabled = true;
const mod = await getMapModule(selected);
MinzEvoNA = mod?.MinzEvoNAModule ?? null;
await MinzEvoNA?.validateAndRebuildSave();
await setActiveTab(MinzEvoNA ? "graphical" : "basic");
tabGraphical.disabled = MinzEvoNA == null;
} else if (selected === "minzEvoEU") {
document.getElementById("window").style.minWidth = "960px";
authorIdInput.value = "2-S2-1-2526080";
authorIdInput.disabled = true;
const mod = await getMapModule(selected);
MinzEvoEU = mod?.MinzEvoEUModule ?? null;
await MinzEvoEU?.validateAndRebuildSave();
await setActiveTab(MinzEvoEU ? "graphical" : "basic");
tabGraphical.disabled = MinzEvoEU == null;
} else if (selected === "minzEvoKR") {
document.getElementById("window").style.minWidth = "960px";
authorIdInput.value = "3-S2-2-1186573";
authorIdInput.disabled = true;
const mod = await getMapModule(selected);
MinzEvoKR = mod?.MinzEvoKRModule ?? null;
await MinzEvoKR?.validateAndRebuildSave();
await setActiveTab(MinzEvoKR ? "graphical" : "basic");
tabGraphical.disabled = MinzEvoKR == null;
} else {
document.getElementById("window").style.minWidth = "800px";
authorIdInput.value = "";
authorIdInput.disabled = false;
tabGraphical.disabled = true;
await setActiveTab("basic");
}
}
updateMapSelection();
document.getElementById('bankFile').addEventListener('change', function (e) {
const file = e.target.files[0];
uploadedBankFilename = file.name;
if (file.name == "MineralZBankStats.SC2Bank") {
document.getElementById("mapSelect").value = "minzEvoNA";
}else{
document.getElementById("mapSelect").value = "generic";
}
updateMapSelection();
const reader = new FileReader();
reader.onload = async function (evt) {
const parser = new DOMParser();
xmlDoc = parser.parseFromString(evt.target.result, "text/xml");
const json = convertXmlToJson(xmlDoc);
parsedXML = json;
globalThis.parsedXML = parsedXML;
document.getElementById("editorBasic").dataset.order = "";
document.getElementById("editorWrapper").style = "display: block;";
document.getElementById("editorXML").style.display = "none";
document.getElementById("editorGraphical").style.display = "none";
document.getElementById("editorBasic").style.display = "block";
await rebuildBasicWindow(json);
};
reader.readAsText(file);
});
function ensureStatKeyExists(key) {
if (!parsedXML?.Bank?.Data?.Stats) return;
if (!parsedXML.Bank.Data.Stats[key]) {
parsedXML.Bank.Data.Stats[key] = { type: "int", value: "0", nodeType: "Value" };
}
}
globalThis.ensureStatKeyExists = ensureStatKeyExists;
function updateBasicTabInput(section, key) {
const editor = document.getElementById("editorBasic");
let input = editor.querySelector(
`.section input[data-section="${section}"][data-key="${key}"]`
);
if (input && parsedXML?.Bank?.Data?.[section]?.[key]) {
input.value = parsedXML.Bank.Data[section][key].value;
}
if (!input)
{
rebuildBasicWindow(parsedXML);
}
rebuildXMLWindow(parsedXML);
}
globalThis.updateBasicTabInput = updateBasicTabInput;
let defaultEditorContent = null;
async function rebuildGraphicalWindow()
{
const selected = document.getElementById("mapSelect").value;
const editorGraphicalWindow = document.getElementById("editorGraphical");
const subTabs = document.getElementById("graphicalTabs");
subTabs.innerHTML = "";
if (!defaultEditorContent) {
defaultEditorContent = editorGraphicalWindow.innerHTML;
}
editorGraphicalWindow.innerHTML = defaultEditorContent;
currentGraphicalTabs.length = 0;
currentGraphicalTabModes.length = 0;
if (activeGraphicalTab == "") return;
if (selected == "minzEvoNA")
{
if (!MinzEvoNA) return;
MinzEvoNA.rebuildGraphicalWindowContent();
}else if (selected == "minzEvoEU")
{
if (!MinzEvoEU) return;
MinzEvoEU.rebuildGraphicalWindowContent();
}else if (selected == "minzEvoKR")
{
if (!MinzEvoKR) return;
MinzEvoKR.rebuildGraphicalWindowContent();
}
}
globalThis.rebuildGraphicalWindow = rebuildGraphicalWindow;
// Switch between Graphical and Basic Window Content
async function setActiveTab(tab) {
const editorGraphical = document.getElementById("editorGraphical");
const editorBasic = document.getElementById("editorBasic");
const editorXML = document.getElementById("editorXML");
const tabGraphical = document.getElementById("tabGraphical");
const tabBasic = document.getElementById("tabBasic");
const tabXML = document.getElementById("tabXML");
const subTabs = document.getElementById("graphicalTabs");
if (lastActiveTab === "xml" && tab !== "xml") {
console.log("Leaving XML editor... Applying changes and validating save.");
await applyXmlChanges();
const selected = document.getElementById("mapSelect").value;
if (selected === "minzEvoNA" && MinzEvoNA) await MinzEvoNA.validateAndRebuildSave();
if (selected === "minzEvoEU" && MinzEvoEU) await MinzEvoEU.validateAndRebuildSave();
if (selected === "minzEvoKR" && MinzEvoKR) await MinzEvoKR.validateAndRebuildSave();
}
lastActiveTab = tab;
tabGraphical.classList.remove("active");
tabBasic.classList.remove("active");
tabXML.classList.remove("active");
editorGraphical.style.display = "none";
editorBasic.style.display = "none";
editorXML.style.display = "none";
subTabs.style.display = "none";
const customFloatingHeader = document.getElementById("customFloatingHeader");
if (tab === "graphical") {
tabGraphical.classList.add("active");
editorGraphical.style.display = "block";
activeGraphicalTab = "graphical";
const mapSelected = document.getElementById("mapSelect").value;
if (mapSelected === "minzEvoNA") {
subTabs.style.display = "block";
customFloatingHeader.innerHTML = `Achievement Points: <span id="cheevosPointsValue"></span>`;
customFloatingHeader.style.display = "block";
await MinzEvoNA.tallyAchievements();
} else if (mapSelected === "minzEvoEU") {
subTabs.style.display = "block";
customFloatingHeader.innerHTML = `Achievement Points: <span id="cheevosPointsValue"></span>`;
customFloatingHeader.style.display = "block";
await MinzEvoEU.tallyAchievements();
} else if (mapSelected === "minzEvoKR") {
subTabs.style.display = "block";
customFloatingHeader.innerHTML = `Achievement Points: <span id="cheevosPointsValue"></span>`;
customFloatingHeader.style.display = "block";
await MinzEvoKR.tallyAchievements();
} else {
customFloatingHeader.innerHTML = "";
customFloatingHeader.style.display = "none";
}
await rebuildGraphicalWindow();
} else if (tab === "xml") {
customFloatingHeader.innerHTML = "";
tabXML.classList.add("active");
editorXML.style.display = "block";
activeGraphicalTab = "";
customFloatingHeader.style.display = "none";
} else {
customFloatingHeader.innerHTML = "";
tabBasic.classList.add("active");
editorBasic.style.display = "block";
activeGraphicalTab = "";
customFloatingHeader.style.display = "none";
}
}
// Switch between sub tabs on the Graphical tab
async function setGraphicalTab(tab) {
for (const sTab of currentGraphicalTabs) {
document.getElementById(sTab).classList.remove("active");
document.getElementById(sTab + "_Window").style.display = "none";
}
document.getElementById(currentGraphicalTabs[tab]).classList.add("active");
document.getElementById(currentGraphicalTabs[tab] + "_Window").style.display = currentGraphicalTabModes[tab];
}
globalThis.setGraphicalTab = setGraphicalTab;
document.getElementById("tabGraphical").addEventListener("click", async () => await setActiveTab("graphical"));
document.getElementById("tabBasic").addEventListener("click", async () => await setActiveTab("basic"));
document.getElementById("tabXML").addEventListener("click", async () => await setActiveTab("xml"));
async function rebuildBasicWindow(json) {
const editorBasic = document.getElementById("editorBasic");
editorBasic.innerHTML = "";
const data = json.Bank.Data;
const orderedSectionNames = Object.keys(data);
for (const originalSection of editorBasic.dataset.order ? JSON.parse(editorBasic.dataset.order) : orderedSectionNames) {
const sectionName = originalSection;
if (!(sectionName in data)) continue;
const section = document.createElement("div");
section.className = "section";
const sectionTitle = document.createElement("div");
sectionTitle.className = "section-title";
sectionTitle.style.marginBottom = "5px";
sectionTitle.textContent = sectionName;
const sectionTitlePencil = document.createElement("span");
sectionTitlePencil.className = "icon";
sectionTitlePencil.textContent = "\u270F\uFE0F"; // ✏️
const sectionTitleTrash = document.createElement("span");
sectionTitleTrash.className = "icon";
sectionTitleTrash.textContent = "\u{1F5D1}\uFE0F"; // 🗑️
sectionTitlePencil.addEventListener("click", () => {
const input = document.createElement("input");
input.value = sectionTitle.firstChild.textContent;
input.style.fontSize = "16px";
input.style.fontWeight = "bold";
sectionTitle.innerHTML = "";
sectionTitle.appendChild(input);
const sectionTitleCheck = document.createElement("span");
sectionTitleCheck.className = "icon";
sectionTitleCheck.textContent = "\u2705"; // ✅
sectionTitleCheck.style.color = "green";
sectionTitleCheck.addEventListener("click", async () => {
const newName = input.value.trim();
if (!newName) {
alert("Section name cannot be empty.");
return;
}
const sectionOrder = editorBasic.dataset.order ? JSON.parse(editorBasic.dataset.order) : Object.keys(data);
const index = sectionOrder.indexOf(sectionName);
const nameAlreadyExists = Object.keys(json.Bank.Data).some(name => name === newName && name !== sectionName);
if (nameAlreadyExists) {
alert("Section name already exists.");
return;
}
if (newName !== sectionName) {
json.Bank.Data[newName] = json.Bank.Data[sectionName];
delete json.Bank.Data[sectionName];
if (index !== -1) sectionOrder[index] = newName;
editorBasic.dataset.order = JSON.stringify(sectionOrder);
}
await rebuildBasicWindow(json);
});
const sectionTitleCancel = document.createElement("span");
sectionTitleCancel.className = "icon";
sectionTitleCancel.textContent = "\u274C"; // ❌
sectionTitleCancel.style.color = "red";
sectionTitleCancel.addEventListener("click", async () => {
await rebuildBasicWindow(json);
});
sectionTitle.appendChild(sectionTitleCheck);
sectionTitle.appendChild(sectionTitleCancel);
});
sectionTitleTrash.addEventListener("click", async () => {
if (confirm(`Delete section "${sectionName}"?`)) {
delete json.Bank.Data[sectionName];
const sectionOrder = editorBasic.dataset.order ? JSON.parse(editorBasic.dataset.order) : Object.keys(data);
const index = sectionOrder.indexOf(sectionName);
if (index !== -1) sectionOrder.splice(index, 1);
editorBasic.dataset.order = JSON.stringify(sectionOrder);
await rebuildBasicWindow(json);
}
});
sectionTitle.appendChild(sectionTitlePencil);
sectionTitle.appendChild(sectionTitleTrash);
section.appendChild(sectionTitle);
// Add Keys
for (const key in data[sectionName]) {
const entry = document.createElement("div");
entry.className = "entry";
const keyLabel = document.createElement("strong");
keyLabel.textContent = key;
keyLabel.style.padding = "4px";
keyLabel.style.marginLeft = "10px";
const keyLabelPencil = document.createElement("span");
keyLabelPencil.className = "icon";
keyLabelPencil.textContent = "\u270F\uFE0F"; // ✏️
const keyLabelTrash = document.createElement("span");
keyLabelTrash.className = "icon";
keyLabelTrash.textContent = "\u{1F5D1}\uFE0F"; // 🗑️
keyLabelPencil.addEventListener("click", () => {
const input = document.createElement("input");
input.value = keyLabel.textContent;
entry.innerHTML = "";
entry.appendChild(input);
const keyLabelCheck = document.createElement("span");
keyLabelCheck.className = "icon";
keyLabelCheck.textContent = "\u2705"; // ✅
keyLabelCheck.style.color = "green";
keyLabelCheck.addEventListener("click", async () => {
const newKey = input.value.trim();
if (!newKey) {
alert("Key name cannot be empty.");
return;
}
if (newKey !== key) {
if (data[sectionName][newKey]) {
alert("Key already exists.");
return;
}
data[sectionName][newKey] = data[sectionName][key];
delete data[sectionName][key];
}
await rebuildBasicWindow(json);
});
const keyLabelCancel = document.createElement("span");
keyLabelCancel.className = "icon";
keyLabelCancel.textContent = "\u274C"; // ❌
keyLabelCancel.style.color = "red";
keyLabelCancel.addEventListener("click", async () => {
await rebuildBasicWindow(json);
});
entry.appendChild(keyLabelCheck);
entry.appendChild(keyLabelCancel);
});
keyLabelTrash.addEventListener("click", async () => {
if (confirm(`Delete key "${key}"?`)) {
delete data[sectionName][key];
await rebuildBasicWindow(json);
}
});
const keyStateSelect = document.createElement("select");
keyStateSelect.style.marginLeft = "10px";
keyStateSelect.style.padding = "4px";
keyStateSelect.style.borderRadius = "4px";
keyStateSelect.style.width = "60px";
["Value", "StateValue"].forEach(opt => {
const option = document.createElement("option");
option.value = opt;
option.textContent = opt;
if (data[sectionName][key].nodeType === opt) option.selected = true;
keyStateSelect.appendChild(option);
});
keyStateSelect.addEventListener("change", () => {
parsedXML.Bank.Data[sectionName][key].nodeType = keyStateSelect.value;
});
const keyTypeSelect = document.createElement("select");
keyTypeSelect.style.marginLeft = "10px";
keyTypeSelect.style.padding = "4px";
keyTypeSelect.style.borderRadius = "4px";
keyTypeSelect.style.width = "60px";
["string", "int", "fixed", "bool", "flag", "text"].forEach(opt => {
const option = document.createElement("option");
option.value = opt;
option.textContent = opt;
if (data[sectionName][key].type === opt) option.selected = true;
keyTypeSelect.appendChild(option);
});
keyTypeSelect.addEventListener("change", () => {
parsedXML.Bank.Data[sectionName][key].type = keyTypeSelect.value;
});
entry.appendChild(keyStateSelect);
entry.appendChild(keyTypeSelect);
entry.appendChild(keyLabel);
entry.appendChild(keyLabelPencil);
entry.appendChild(keyLabelTrash);
entry.appendChild(document.createTextNode(" : "));
const input = document.createElement("input");
input.value = data[sectionName][key].value;
input.dataset.section = sectionName;
input.dataset.key = key;
input.addEventListener("input", () => {
parsedXML.Bank.Data[sectionName][key].value = input.value;
});
entry.appendChild(input);
section.appendChild(entry);
}
// Add New Key Row
const addKeyRow = document.createElement("div");
addKeyRow.style.marginTop = "10px";
addKeyRow.style.fontWeight = "bold";
addKeyRow.style.marginLeft = "30px";
addKeyRow.innerHTML = `<span class="add-key-collapsed">Add new key <button class="expand-add-key" style="margin-left: 10px; padding: 4px 8px; border-radius: 6px; background-color: #4b0082; color: white; border: none; cursor: pointer;">\u2795</button></span>
<span class="add-key-expanded" style="display:none;">
<input type="text" placeholder="Key Name" style="margin-left: 10px; padding: 4px; border-radius: 4px; border: 1px solid #ccc;" />
<select style="margin-left: 10px; padding: 4px; border-radius: 4px;">
<option value="string">string</option>
<option value="int">int</option>
<option value="fixed">fixed</option>
<option value="bool">bool</option>
<option value="flag">flag</option>
<option value="text">text</option>
</select>
<select style="margin-left: 10px; padding: 4px; border-radius: 4px;">
<option value="Value">Value</option>
<option value="StateValue">StateValue</option>
</select>
<input type="text" placeholder="Value" style="margin-left: 10px; padding: 4px; border-radius: 4px; border: 1px solid #ccc; width: 120px;" />
<span class="icon" style="color: green; margin-left: 8px; cursor: pointer;">\u2705</span>
<span class="icon" style="color: red; margin-left: 4px; cursor: pointer;">\u274C</span>
</span>`;
section.appendChild(addKeyRow);
const collapsed = addKeyRow.querySelector(".add-key-collapsed");
const expanded = addKeyRow.querySelector(".add-key-expanded");
const expandBtn = addKeyRow.querySelector(".expand-add-key");
const checkBtn = expanded.querySelector(".icon:nth-of-type(1)");
const cancelBtn = expanded.querySelector(".icon:nth-of-type(2)");
expandBtn.addEventListener("click", () => {
collapsed.style.display = "none";
expanded.style.display = "inline";
});
cancelBtn.addEventListener("click", () => {
collapsed.style.display = "inline";
expanded.style.display = "none";
});
checkBtn.addEventListener("click", async () => {
const inputs = expanded.querySelectorAll("input[type='text']");
const selects = expanded.querySelectorAll("select");
const keyName = inputs[0].value.trim();
const type = selects[0].value;
const nodeType = selects[1].value;
const value = inputs[1].value.trim();
if (!keyName) {
alert("Key name cannot be empty.");
return;
}
if (data[sectionName][keyName]) {
alert("Key already exists.");
return;
}
data[sectionName][keyName] = {
type: type,
value: value,
nodeType: nodeType
};
await rebuildBasicWindow(json);
});
editorBasic.appendChild(section);
if (originalSection === orderedSectionNames[orderedSectionNames.length - 1]) {
const addSectionRow = document.createElement("div");
addSectionRow.style.marginTop = "10px";
addSectionRow.style.fontWeight = "bold";
addSectionRow.innerHTML = `Add new section <input type="text" placeholder="Section Name" style="margin-left: 10px; padding: 4px; border-radius: 4px; border: 1px solid #ccc;" /> <button class="add-section-button" style="margin-left: 10px; padding: 4px 8px; border-radius: 6px; background-color: #4b0082; color: white; border: none; cursor: pointer;">\u2795</button>`;
editorBasic.appendChild(addSectionRow);
addSectionRow.querySelector('.add-section-button').addEventListener('click', function () {
const input = addSectionRow.querySelector('input');
const sectionName = input.value.trim();
if (!sectionName) {
alert("Section name cannot be empty.");
return;
}
if (sectionName in parsedXML.Bank.Data) {
alert("Section name already exists.");
return;
}
parsedXML.Bank.Data[sectionName] = {};
const sectionOrder = editorBasic.dataset.order ? JSON.parse(editorBasic.dataset.order) : Object.keys(parsedXML.Bank.Data);
sectionOrder.push(sectionName);
editorBasic.dataset.order = JSON.stringify(sectionOrder);
rebuildBasicWindow(parsedXML);
});
}
if (!editorBasic.dataset.order) {
editorBasic.dataset.order = JSON.stringify(orderedSectionNames);
}
}
await rebuildXMLWindow();
}
function escapeXml(unsafe) {
console.log(unsafe);
return unsafe.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
async function rebuildXMLWindow(json) {
let xml = await getBankAsXML();
const editorXML = document.getElementById("editorXML");
const xmlEditor = document.getElementById("xmlEditor");
xmlEditor.value = xml;
}
async function downloadBank() {
if (!parsedXML || !parsedXML.Bank || !parsedXML.Bank.Data) {
alert("No data to export.");
return;
}
const authorId = document.getElementById("authorId").value.trim();
const userId = document.getElementById("userId").value.trim();
const bankName = "MineralZBankStats"; // Or parse from filename if needed
const isValidId = id => /^[0-9]+-S2-[0-9]+-\d{6,10}$/.test(id);
let effectiveUserId = userId;
// Dev bypass: if map is minzevoNA/minzevoEU and userId is empty, use authorId. Minz does not actually check signatures even though they're turned on
if ((document.getElementById("mapSelect").value === "minzEvoNA" || document.getElementById("mapSelect").value === "minzEvoEU" || document.getElementById("mapSelect").value === "minzEvoKR") && !userId) {
effectiveUserId = authorId;
}
const includeSignature = isValidId(authorId) && isValidId(effectiveUserId);
let xml = await getBankAsXML(includeSignature, authorId, effectiveUserId, bankName);
if (!uploadedBankFilename.toLowerCase().endsWith(".sc2bank")) {
uploadedBankFilename = uploadedBankFilename.replace(/\.[^.]+$/, "") + ".SC2Bank";
}
const blob = new Blob([xml], { type: "application/xml" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = uploadedBankFilename || "edited.SC2Bank";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
document.getElementById("editorFooter").querySelector("button").addEventListener("click", downloadBank);
async function getBankAsXML(includeSignature, authorId, effectiveUserId, bankName)
{
const bankData = parsedXML.Bank.Data;
let xml = '<?xml version="1.0" encoding="utf-8"?>\n<Bank version="1">\n';
// Ensure TimesPlayed = 2 for MinzEvo saves
if (document.getElementById("mapSelect").value === "minzEvoNA" || document.getElementById("mapSelect").value === "minzEvoEU" || document.getElementById("mapSelect").value === "minzEvoKR") {
bankData["Stats"]["TimesPlayed"] = {
type: "int",
value: "2",
nodeType: "Value"
};
}
for (const sectionName of JSON.parse(document.getElementById("editorBasic").dataset.order || "[]")) {
const keys = bankData[sectionName];
xml += ` <Section name="${sectionName}">\n`;
for (const key in keys) {
const entry = keys[key];
xml += ` <Key name="${key}">\n`;
xml += ` <${entry.nodeType} ${entry.type}="${entry.value}"/>\n`;
xml += ` </Key>\n`;
}
xml += ` </Section>\n`;
}
if (includeSignature) {
const signature = await generateBankSignature(authorId, effectiveUserId, bankName, bankData);
xml += ` <Signature value="${signature}"/>\n`;
}
xml += '</Bank>';
return xml;
}
async function generateBankSignature(authorId, userId, bankName, bankData) {
const items = [authorId, userId, bankName];
const sortedSections = Object.keys(bankData).sort();
for (const sectionName of sortedSections) {
items.push(sectionName);
const keys = Object.keys(bankData[sectionName]).sort();
for (const key of keys) {
const entry = bankData[sectionName][key];
items.push(key, "Value", entry.type, entry.value);
}
}
const text = items.join('');
const buffer = new TextEncoder().encode(text);
const digest = await crypto.subtle.digest("SHA-1", buffer);
return Array.from(new Uint8Array(digest)).map(b => b.toString(16).padStart(2, '0')).join('').toUpperCase();
}
function convertXmlToJson(xml) {
const result = { Bank: { Data: {} } };
const sections = xml.querySelectorAll("Section");
sections.forEach(section => {
const sectionName = section.getAttribute("name");
if (!sectionName) return;
result.Bank.Data[sectionName] = {};
const keys = section.querySelectorAll("Key");
keys.forEach(key => {
const keyName = key.getAttribute("name");
const valNode = key.querySelector("Value, StateValue");
if (!valNode || !keyName) return;
const valueEntry = {};
for (const attr of valNode.attributes) {
valueEntry.type = attr.name;
valueEntry.value = attr.value;
valueEntry.nodeType = valNode.nodeName // Value or StateValue
}
result.Bank.Data[sectionName][keyName] = valueEntry;
});
});
return result;
}
async function applyXmlChanges() {
const xmlEditor = document.getElementById("xmlEditor").value;
try {
const parser = new DOMParser();
const newDoc = parser.parseFromString(xmlEditor, "text/xml");
parsedXML = convertXmlToJson(newDoc);
globalThis.parsedXML = parsedXML;
await rebuildBasicWindow(parsedXML); // if you want to update GUI
} catch (e) {
alert("Failed to parse XML. Please check your formatting.");
}
}
document.getElementById("xmlEditor").addEventListener("blur", async () => {
await applyXmlChanges();
});
</script>
</body>
</html>
let modName = '';
let mapName = '';
export function setModuleConfig(config) {
modName = config.modName;
mapName = config.mapName;
}
const setMap = {
"set0": "Hard",
"set1": "General2",
"set2": "Insane",
"set3": "Easy",
"set4": "Collections",
"set5": "Normal",
"set6": "General1",
"set7": "Bonus",
"set8": "General3",
"set9": "General4",
"set10": "General5",
"set11": "Nightmare"
};
const experienceLevels = [
0, 0, 300, 600, 1000, 1500, 2100, 2800, 3600, 4500, 5500,
6600, 7800, 9100, 10500, 12000, 13600, 15300, 17100, 19000,
21000, 23100, 25300, 27600, 30000, 32500, 35100, 37800, 40600, 43500,
46500, 49600, 52800, 56100, 59500, 63000, 66600, 70300, 74100, 78000,
82000, 86100, 90300, 94600, 99000, 103500, 108100, 112800, 117600, 122500,
127500, 132600, 137800, 143100, 148500, 154000, 159600, 165300, 171100, 177000,
183000, 189100, 195300, 201600, 208000, 214500, 221100, 227800, 234600, 241500,
248500, 255600, 262800, 270100, 277500, 285000, 292600, 300300, 308100, 316000,
324000, 332100, 340300, 348600, 357000, 365500, 374100, 382800, 391600, 400500,
409500, 418600, 427800, 437100, 446500, 456000, 465600, 475300, 485100, 495000
];
function getLevelFromExperience(exp) {
for (let i = 1; i < 99; i++) {
if (experienceLevels[i + 1] > exp) return i;
}
return 99;
}
function getExperienceFromLevel(level) {
if (isNaN(level) || level <= 1) return 0;
if (level >= 99) return 495000;
return experienceLevels[level];
}
let cheevosModule = null;
export function init() {
console.log("Initializing Module:", mapName);
globalThis.document.addEventListener("input", async (e) => {
// Validate Level and Experience
if (!globalThis.parsedXML?.Bank?.Data?.Stats) return;
let val = 0;
if (e.target && e.target.id === "levelInput") {
val = parseInt(e.target.value || "0");
if (isNaN(val) || val < 1) val = 1;
if (val > 99) val = 99;
ensureStatKeyExists("Lvl");
ensureStatKeyExists("Exp");
globalThis.parsedXML.Bank.Data.Stats["Lvl"].value = val.toString();
let currentExp = parseInt(globalThis.parsedXML.Bank.Data.Stats["Exp"]?.value || "0");
let minExp = getExperienceFromLevel(val);
let maxExp = getExperienceFromLevel(val + 1);
if (currentExp < minExp) {
currentExp = minExp;
} else if (currentExp >= maxExp) {
currentExp = (currentExp >= 495000) ? 495000 : maxExp - 1;
}
globalThis.parsedXML.Bank.Data.Stats["Exp"].value = currentExp.toString();
updateBasicTabInput("Stats", "Lvl");
updateBasicTabInput("Stats", "Exp");
updateBasicTabInput("Stats", "Lvl");
updateBasicTabInput("Stats", "Exp");
await handleLevelCheevos();
globalThis.document.getElementById("levelInput").value = globalThis.parsedXML.Bank.Data.Stats["Lvl"].value;
globalThis.document.getElementById("xpInput").value = globalThis.parsedXML.Bank.Data.Stats["Exp"].value;
}
if (e.target && e.target.id === "xpInput") {
val = parseInt(e.target.value || "0");
if (isNaN(val) || val < 0) val = 0;
if (val > 495000) val = 495000;
ensureStatKeyExists("Lvl");
ensureStatKeyExists("Exp");
globalThis.parsedXML.Bank.Data.Stats["Exp"].value = val.toString();
globalThis.parsedXML.Bank.Data.Stats["Lvl"].value = getLevelFromExperience(val).toString();
updateBasicTabInput("Stats", "Lvl");
updateBasicTabInput("Stats", "Exp");
await handleLevelCheevos();
globalThis.document.getElementById("levelInput").value = globalThis.parsedXML.Bank.Data.Stats["Lvl"].value;
globalThis.document.getElementById("xpInput").value = globalThis.parsedXML.Bank.Data.Stats["Exp"].value;
}
// Validate Absorbed/Genned/Healed/Mined cheevos
if (e.target && e.target.id === "dmgInput") {
val = parseInt(e.target.value || "0");
if (isNaN(val) || val < 0) val = 0;
if (val >= 2000000000 ) val = 2000000000;
ensureStatKeyExists("Absorbed");
globalThis.parsedXML.Bank.Data.Stats["Absorbed"].value = val.toString();
updateBasicTabInput("Stats", "Absorbed");
globalThis.document.getElementById("dmgInput").value = val.toString();
await handleAbsorbCheevos(true);
}
if (e.target && e.target.id === "genInput") {
val = parseInt(e.target.value || "0");
if (isNaN(val) || val < 0) val = 0;
if (val >= 2000000000 ) val = 2000000000;
ensureStatKeyExists("Gens");
globalThis.parsedXML.Bank.Data.Stats["Gens"].value = val.toString();
updateBasicTabInput("Stats", "Gens");
globalThis.document.getElementById("genInput").value = val.toString();
await handleGeneratedCheevos(true);
}
if (e.target && e.target.id === "healsInput") {
val = parseInt(e.target.value || "0");
if (isNaN(val) || val < 0) val = 0;
if (val >= 2000000000 ) val = 2000000000;
ensureStatKeyExists("Heals");
globalThis.parsedXML.Bank.Data.Stats["Heals"].value = val.toString();
updateBasicTabInput("Stats", "Heals");
globalThis.document.getElementById("healsInput").value = val.toString();
await handleHealedCheevos(true);
}
if (e.target && e.target.id === "minedInput") {
val = parseInt(e.target.value || "0");
if (isNaN(val) || val < 0) val = 0;
if (val >= 2000000000 ) val = 2000000000;
ensureStatKeyExists("Mined");
globalThis.parsedXML.Bank.Data.Stats["Mined"].value = val.toString();
updateBasicTabInput("Stats", "Mined");
globalThis.document.getElementById("minedInput").value = val.toString();
await handleMinedCheevos(true);
}
if (e.target && e.target.id === "timePlayedInput") {
val = parseInt(e.target.value || "0");
if (isNaN(val) || val < 0) val = 0;
if (val >= 2000000000 ) val = 2000000000;
ensureStatKeyExists("TimePlayed");
globalThis.parsedXML.Bank.Data.Stats["TimePlayed"].value = val.toString();
updateBasicTabInput("Stats", "TimePlayed");
globalThis.document.getElementById("timePlayedInput").value = val.toString();
await handleTimeCheevos(true);
}
if (e.target && e.target.id === "killsInput") {
val = parseInt(e.target.value || "0");
if (isNaN(val) || val < 0) val = 0;
if (val >= 2000000000 ) val = 2000000000;
ensureStatKeyExists("TotalKills");
globalThis.parsedXML.Bank.Data.Stats["TotalKills"].value = val.toString();
updateBasicTabInput("Stats", "TotalKills");
globalThis.document.getElementById("killsInput").value = val.toString();
await handleKillsCheevos(true);
}
if (e.target && e.target.id === "playsTotalInput")
{
val = parseInt(e.target.value || "0");
if (isNaN(val) || val < 0) val = 0;
if (val >= 2000000000 ) val = 2000000000;
ensureStatKeyExists("Games");
globalThis.parsedXML.Bank.Data.Stats["Games"].value = val.toString();
updateBasicTabInput("Stats", "Games");
globalThis.document.getElementById("playsTotalInput").value = val.toString();
handleGamesPlayed(true);
}
if (e.target && e.target.id === "winsEasyInput")
{
val = parseInt(e.target.value || "0");
if (isNaN(val) || val < 0) val = 0;
if (val >= 2000000000 ) val = 2000000000;
ensureStatKeyExists("EasyWin");
globalThis.parsedXML.Bank.Data.Stats["EasyWin"].value = val.toString();
updateBasicTabInput("Stats", "EasyWin");
globalThis.document.getElementById("winsEasyInput").value = val.toString();
await handleEasyWins(true);
}
if (e.target && e.target.id === "winsNormalInput")
{
val = parseInt(e.target.value || "0");
if (isNaN(val) || val < 0) val = 0;
if (val >= 2000000000 ) val = 2000000000;
ensureStatKeyExists("NormalWin");
globalThis.parsedXML.Bank.Data.Stats["NormalWin"].value = val.toString();
updateBasicTabInput("Stats", "NormalWin");
globalThis.document.getElementById("winsNormalInput").value = val.toString();
await handleNormalWins(true);
}
if (e.target && e.target.id === "winsHardInput")
{
val = parseInt(e.target.value || "0");
if (isNaN(val) || val < 0) val = 0;
if (val >= 2000000000 ) val = 2000000000;
ensureStatKeyExists("HardWin");
globalThis.parsedXML.Bank.Data.Stats["HardWin"].value = val.toString();
updateBasicTabInput("Stats", "HardWin");
globalThis.document.getElementById("winsHardInput").value = val.toString();
await handleHardWins(true);
}
if (e.target && e.target.id === "winsHard2Input")
{
val = parseInt(e.target.value || "0");
if (isNaN(val) || val < 0) val = 0;
if (val >= 2000000000 ) val = 2000000000;
ensureStatKeyExists("HardWinFinalBoss");
globalThis.parsedXML.Bank.Data.Stats["HardWinFinalBoss"].value = val.toString();
updateBasicTabInput("Stats", "HardWinFinalBoss");
globalThis.document.getElementById("winsHard2Input").value = val.toString();
await handleHard2Wins(true);
}
if (e.target && e.target.id === "winsInsaneInput")
{
val = parseInt(e.target.value || "0");
if (isNaN(val) || val < 0) val = 0;
if (val >= 2000000000 ) val = 2000000000;
ensureStatKeyExists("InsaneWin");
globalThis.parsedXML.Bank.Data.Stats["InsaneWin"].value = val.toString();
updateBasicTabInput("Stats", "InsaneWin");
globalThis.document.getElementById("winsInsaneInput").value = val.toString();
await handleInsaneWins(true);
}
if (e.target && e.target.id === "winsInsane2Input")
{
val = parseInt(e.target.value || "0");
if (isNaN(val) || val < 0) val = 0;
if (val >= 2000000000 ) val = 2000000000;
ensureStatKeyExists("InsaneWinFinalBoss");
globalThis.parsedXML.Bank.Data.Stats["InsaneWinFinalBoss"].value = val.toString();
updateBasicTabInput("Stats", "InsaneWinFinalBoss");
globalThis.document.getElementById("winsInsane2Input").value = val.toString();
await handleInsane2Wins(true);
}
if (e.target && e.target.id === "winsInsane3Input")
{
val = parseInt(e.target.value || "0");
if (isNaN(val) || val < 0) val = 0;
if (val >= 2000000000 ) val = 2000000000;
ensureStatKeyExists("InsaneHighestNight");
globalThis.parsedXML.Bank.Data.Stats["InsaneHighestNight"].value = val.toString();
updateBasicTabInput("Stats", "InsaneHighestNight");
globalThis.document.getElementById("winsInsane3Input").value = val.toString();
await handleInsane3Wins(true);
}
});
}
export function getModName() {
return modName;
}
export function getMapName() {
return mapName;
}
async function loadCheevos() {
if (!cheevosModule) {
cheevosModule = await import(`./minzEvoCheevos.js`);
}
return cheevosModule;
}
export async function getAchievements() {
const mod = await loadCheevos();
return mod.getAchievements();
}
export async function getAchievementsByCategory(category) {
const mod = await loadCheevos();
return mod.getAchievementsByCategory(category);
}
export async function getAchievement(category, index) {
const mod = await loadCheevos();
return mod.getAchievement(category, index);
}
export async function getAchievementCount(category) {
const mod = await loadCheevos();
return mod.getAchievementCount(category);
}
export async function tallyAchievements() {
if (!globalThis.parsedXML) return 0;
let score = 0;
const cheevos = await getAchievements();
for (let i = 0; i < 12; i++) {
const key = Object.keys(cheevos)[i];
const dbGroup = cheevos[key];
const statsGroup = globalThis.parsedXML.Bank.Data.Stats;
// Match setMap to get the correct stat key (e.g., "set3" -> "Easy")
const statKey = Object.entries(setMap).find(([, v]) => v === key)?.[0];
const statEntry = statsGroup?.[statKey];
if (!statEntry || statEntry.type !== "int") continue;
const value = parseInt(statEntry.value);
if (isNaN(value)) continue;
for (let j = 0; j < 16; j++) {
if (!dbGroup[j]) break;
const unlocked = (value & (1 << j)) !== 0;
if (unlocked) {
score += dbGroup[j].Value;
}
}
}
if (globalThis.parsedXML?.Bank?.Data?.Stats?.AchievementPoints) {
globalThis.parsedXML.Bank.Data.Stats.AchievementPoints.value = score.toString();
}
updateBasicTabInput("Stats", "AchievementPoints");
const floatingPoints = globalThis.document.getElementById("cheevosPointsValue");
if (floatingPoints) floatingPoints.textContent = score;
return score;
}
function insertStyles() {
insertStyle("label.MinzInputs", `
padding-right: 4px;
margin-bottom: 1px;`
);
insertStyle("input.MinzInputs", `
padding-right: 4px;
margin-bottom: 1px;
width: 80px`
);
insertStyle("input.MinzInputsSlim", `
padding-right: 4px;
margin-bottom: 1px;
width: 80px`
);
insertStyle(".trophy", `
color: black;
position: relative;
display: inline-flex;
justify-content: center;
align-items: center;
font-weight: bold;
font-size: 18px;
background: gold;
border-radius: 6px;
margin: 4px;
cursor: pointer;
overflow: hidden;
border: 2px solid black;`
);
insertStyle(".trophy::after", `
content: "\u274C";
position: absolute;
color: rgba(255, 0, 0, 0.6);
font-size: 32px;
font-weight: bold;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;`
);
insertStyle(".trophy:hover::after",
`opacity: 1;`
);
insertStyle(".trophy.disabled", `
background-color: #ccc;
cursor: not-allowed;`
);
insertStyle(".addTrophyBtn", `
padding: 4px 8px;
border-radius: 6px;
background-color: #4b0082;
color: white;
border: none;
cursor: pointer;`);
insertStyle(".addTrophyBtn.disabled", `
background-color: #ccc;
cursor: not-allowed;`
);
insertStyle(".cheevo", `
padding: 8px 16px;
margin: 5px;
font-weight: bold;
border: none;
border-radius: 8px;
background-color: var(--section-locked-color);
cursor: pointer;`
);
insertStyle(".cheevo.unlocked",
`background-color: var(--section-unlocked-color);`
);
insertStyle(".minzEditorColumn", `
background-color: var(--editor-bg2);
padding: 10px;
width: 33%;
border-radius: 8px;`
);
}
export async function rebuildGraphicalWindowContent() {
insertStyles();
const editorGraphicalWindow = globalThis.document.getElementById("editorGraphical");
const subTabs = globalThis.document.getElementById("graphicalTabs");
const statsTab = globalThis.document.createElement("button");
statsTab.className = "tab-button";
statsTab.id = "graphicalTabsStats";
statsTab.innerText = "Stats";
const cheevosTab = globalThis.document.createElement("button");
cheevosTab.className = "tab-button";
cheevosTab.id = "graphicalTabsCheevos";
cheevosTab.innerText = "Cheevos";
subTabs.appendChild(statsTab);
subTabs.appendChild(cheevosTab);
const statsWindow = globalThis.document.createElement("div");
statsWindow.id = statsTab.id + "_Window";
statsWindow.style.gap = "10px";
statsWindow.style.columnCount = "3";
const c1 = globalThis.document.createElement("div");
c1.className = "minzEditorColumn";
c1.style.minWidth = "235px";
c1.innerHTML = "<strong>General Stats</strong><br/>";
function createInputField(c1, innerText, value, id, inputStyle = "MinzInputs") {
const tLabel = globalThis.document.createElement("label");
const tInput = globalThis.document.createElement("input");
tLabel.className = "MinzInputs";
tInput.className = inputStyle;
tInput.id = id;
tLabel.innerText = innerText;
tLabel.htmlFor = tInput.id;
tInput.value = value;
c1.appendChild(tLabel);
c1.appendChild(tInput);
c1.appendChild(globalThis.document.createElement("br"));
}
// Stats Column
createInputField(c1, "Level:", "0", "levelInput");
createInputField(c1, "Experience:", "0", "xpInput");
createInputField(c1, "Damage Absorbed:", "0", "dmgInput");
createInputField(c1, "Energy Generated:", "0", "genInput");
createInputField(c1, "Damaged Healed:", "0", "healsInput");
createInputField(c1, "Mineralz Mined:", "0", "minedInput");
createInputField(c1, "Time Played:", "0", "timePlayedInput");
createInputField(c1, "Total Kills:", "0", "killsInput");
if (globalThis.parsedXML?.Bank?.Data?.Stats) {
if (globalThis.parsedXML.Bank.Data.Stats["AchievementPoints"]) {
globalThis.parsedXML.Bank.Data.Stats["AchievementPoints"].value = await tallyAchievements();
updateBasicTabInput("Stats", "AchievementPoints");
}
}
statsWindow.appendChild(c1);
// Plays and Wins Column
const c2 = globalThis.document.createElement("div");
c2.className = "minzEditorColumn";
c2.style.minWidth = "260px";
c2.innerHTML = "<strong>Plays and Wins</strong><br/>";
createInputField(c2, "Games Played:", "0", "playsTotalInput", "MinzInputsSlim");
createInputField(c2, "Wins (Easy):", "0", "winsEasyInput", "MinzInputsSlim");
createInputField(c2, "Wins (Normal):", "0", "winsNormalInput", "MinzInputsSlim");
createInputField(c2, "Wins (Hard):", "0", "winsHardInput", "MinzInputsSlim");
createInputField(c2, "Final Boss (Hard):", "0", "winsHard2Input", "MinzInputsSlim");
createInputField(c2, "Wins (Insane):", "0", "winsInsaneInput", "MinzInputsSlim");
createInputField(c2, "Final Boss (Insane):", "0", "winsInsane2Input", "MinzInputsSlim");
createInputField(c2, "Highest Night (Insane):", "0", "winsInsane3Input", "MinzInputsSlim");
statsWindow.appendChild(c2);
// Trophies
const c3 = globalThis.document.createElement("div");
c3.className = "minzEditorColumn";
c3.style.minWidth = "345px";
c3.innerHTML = "<strong>Trophies</strong>";
const trophyWindow = globalThis.document.createElement("div");
trophyWindow.style.display = "flex";
trophyWindow.style.flexDirection = "row";
trophyWindow.style.flexWrap = "wrap";
trophyWindow.style.width = "345px";
trophyWindow.style.minHeight = "116px";
trophyWindow.id = "trophyWindow";
c3.appendChild(trophyWindow);
// Add Trophy Button
const tAddWrapper = globalThis.document.createElement("div");
tAddWrapper.style.marginTop = "10px";
tAddWrapper.innerHTML = `
<label style="font-weight: bold;">Add Trophy:</label><br/>
<select id="trophySelector" style="margin-right: 10px; padding: 4px; border-radius: 4px;">
${Array.from({ length: 9 }, (_, i) => `<option value="${i + 1}">${i + 1}</option>`).join("")}
</select>
<button id="addTrophyBtn" class="addTrophyBtn">\u2795</button>
`;
c3.appendChild(tAddWrapper);
// Enable/disable [+] button
const addBtn = tAddWrapper.querySelector("#addTrophyBtn");
const isMaxed = trophyCount >= 10;
addBtn.disabled = isMaxed;
addBtn.classList.toggle("disabled", isMaxed);
// Trophy add logic
addBtn.addEventListener("click", async () => {
const selected = globalThis.document.getElementById("trophySelector").value;
let currentVal = 0;
if (globalThis.parsedXML.Bank.Data.Trophies[selected]) {
currentVal = parseInt(globalThis.parsedXML.Bank.Data.Trophies[selected].value) || 0;
}
globalThis.parsedXML.Bank.Data.Trophies[selected] = {
type: "int",
value: (currentVal + 1).toString(),
nodeType: "Value"
};
// Sync Basic tab
updateBasicTabInput("Trophies", selected);
handleTrophies();
// Force re-render of trophies
await rebuildGraphicalWindow();
});
statsWindow.appendChild(c3);
// Mineralz Evolution Achievements Tab
const cheevosWindow = globalThis.document.createElement("div");
cheevosWindow.id = cheevosTab.id + "_Window";
cheevosWindow.style.display = "grid";
cheevosWindow.style.gridTemplateColumns = "1fr 1fr 1fr";
//insertStyle(".cheevo", "background-color: #0000ff; ")
const cheevoOrder = [
"Bonus", "Easy", "Normal", "Hard", "Insane", "Nightmare",
"General1", "General2", "General3", "General4", "General5", "Collections"
];
const cheevoOrderProper = [
"Bonus", "Easy", "Normal", "Hard", "Insane", "Nightmare",
"General 1", "General 2", "General 3", "General 4", "General 5", "Collections"
];
const cheevos = await getAchievements();
for (let i = 0; i < cheevoOrder.length; i++) {
const group = cheevoOrder[i];
if (!cheevos[group]) continue;
insertCheevoHeader(cheevosWindow, cheevoOrderProper[i]);
for (let j = 0; j < cheevos[group].length; j++) {
insertCheevo(cheevos, cheevosWindow, group, j);
}
if (i < cheevoOrder.length - 1) {
insertCheevoBreak(cheevosWindow);
}
}
editorGraphicalWindow.appendChild(statsWindow);
editorGraphicalWindow.appendChild(cheevosWindow);
updateMinzStats();
updateTrophies();
updateCheevoTabLockStatus();
await tallyAchievements();
globalThis.currentGraphicalTabs.push(statsTab.id);
globalThis.currentGraphicalTabs.push(cheevosTab.id);
globalThis.currentGraphicalTabModes.push("flex");
globalThis.currentGraphicalTabModes.push("grid");
await setGraphicalTab(0);
globalThis.document.getElementById(statsTab.id).addEventListener("click", async () => await setGraphicalTab(0));
globalThis.document.getElementById(cheevosTab.id).addEventListener("click", async () => await setGraphicalTab(1));
}
function insertCheevoHeader(cheevosWindow, text)
{
const cHeader = globalThis.document.createElement("div");
cHeader.innerHTML = text;
cHeader.style.gridColumn = "1 / -1";
cHeader.style.textAlign = "center";
cHeader.style.fontWeight = "bold";
cHeader.style.fontSize = "20px"; // Adjust as needed
cHeader.style.margin = "12px 0"; // Optional spacing
cHeader.style.color = "var(--section-header-color)";
cheevosWindow.appendChild(cHeader);
}
function insertCheevo(cheevos, cheevosWindow, page, index) {
const data = cheevos[page][index];
const cheevo = globalThis.document.createElement("div");
cheevo.className = "cheevo";
cheevo.id = `cheevo.${page}.${index}`;
// --- Top Row ---
const topRow = globalThis.document.createElement("div");
topRow.style.display = "flex";
topRow.style.justifyContent = "space-between";
topRow.style.alignItems = "center";
topRow.style.width = "100%";
const title = globalThis.document.createElement("span");
title.textContent = data.Title;
title.style.fontWeight = "bold";
title.style.color = "var(--section-title-color)";
const rightSide = globalThis.document.createElement("span");
rightSide.style.display = "flex";
rightSide.style.alignItems = "center";
rightSide.style.gap = "10px";
const score = globalThis.document.createElement("span");
score.textContent = `(${data.Value})`;
score.style.color = "var(--section-title-color)";
const checkbox = globalThis.document.createElement("input");
checkbox.id = `cheevo.${page}.${index}.checkbox`;
checkbox.type = "checkbox";
checkbox.checked = false;
checkbox.disabled = false;
checkbox.addEventListener("change", async () => {
const statKey = Object.entries(setMap).find(([, v]) => v === page)?.[0];
if (!statKey) return;
if (!globalThis.parsedXML.Bank.Data.Stats[statKey]) {
globalThis.parsedXML.Bank.Data.Stats[statKey] = {
type: "int",
value: "0",
nodeType: "Value"
};
}
let currentValue = parseInt(globalThis.parsedXML.Bank.Data.Stats[statKey].value) || 0;
if (checkbox.checked) {
currentValue |= (1 << index); // set bit
} else {
currentValue &= ~(1 << index); // clear bit
}
globalThis.parsedXML.Bank.Data.Stats[statKey].value = currentValue.toString();
//console.log(`Setting ${statKey} to ${currentValue}`);
updateBasicTabInput("Stats", statKey);
const container = globalThis.document.getElementById(`cheevo.${page}.${index}`);
if (checkbox.checked) {
container.classList.add("unlocked");
await unlockDependencies(page, index);
} else {
container.classList.remove("unlocked");
await lockDependents(page, index);
}
// Retally points
await tallyAchievements();
handleGamesPlayed(true);
handleTrophies();
});
rightSide.appendChild(score);
rightSide.appendChild(checkbox);
topRow.appendChild(title);
topRow.appendChild(rightSide);
// --- Description ---
const desc = globalThis.document.createElement("div");
desc.textContent = data.Description;
desc.style.marginTop = "6px";
desc.style.fontSize = "80%";
desc.style.color = "var(--section-text-color)";
cheevo.appendChild(topRow);
cheevo.appendChild(desc);
cheevosWindow.appendChild(cheevo);
}
async function unlockDependencies(category, idx) {
//console.log(`Unlocking dependencies for ${category} ${idx}`);
const cheevos = await getAchievements();
const achievement = cheevos[category]?.[idx];
//console.log(`Checking requirements for ${category} ${idx}: ${achievement.Requires}`);
if (achievement.Requires) {
for (const req of achievement.Requires) {
//console.log(`Checking ${req.Type} for ${req.Value}`);
ensureStatKeyExists(req.Type);
const val = parseInt(globalThis.parsedXML.Bank.Data.Stats[req.Type].value) || 0;
if (val < req.Value) {
globalThis.parsedXML.Bank.Data.Stats[req.Type].value = req.Value;
updateBasicTabInput("Stats", req.Type);
//console.log(`Setting ${req.Type} to ${req.Value}`);
}
if (req.Type == "Lvl") {
const xp = getExperienceFromLevel(req.Value);
const xpVal = parseInt(globalThis.parsedXML.Bank.Data.Stats["Exp"].value) || 0;
if (xpVal < xp) {
globalThis.parsedXML.Bank.Data.Stats["Exp"].value = xp.toString();
updateBasicTabInput("Stats", "Exp");
//console.log(`Setting Exp to ${xp}`);
}
}
}
}
//console.log(`Checking dependencies for ${category} ${idx}: ${achievement.Dependencies}`);
if (achievement.Dependencies) {
for (const dep of achievement.Dependencies) {
const depCategory = dep.Category;
const depIndex = dep.Value;
const depStatKey = Object.entries(setMap).find(([, v]) => v === depCategory)?.[0];
if (!depStatKey) continue;
if (!globalThis.parsedXML.Bank.Data.Stats[depStatKey]) {
globalThis.parsedXML.Bank.Data.Stats[depStatKey] = {
type: "int",
value: "0",
nodeType: "Value"
};
}
let depValue = parseInt(globalThis.parsedXML.Bank.Data.Stats[depStatKey].value) || 0;
let depList = [];
if (depIndex === "all") {
// Unlock all achievements in this category
const depAchievements = cheevos[depCategory] || [];
for (let i = 0; i < depAchievements.length; i++) {
depValue |= (1 << i);
const depCheckbox = document.getElementById(`cheevo.${depCategory}.${i}`)?.querySelector("input[type='checkbox']");
if (depCheckbox) {
depCheckbox.checked = true;
const depContainer = document.getElementById(`cheevo.${depCategory}.${i}`);
depContainer?.classList.add("unlocked");
depList.push({ "Category" : depCategory, "Value" : i});
}
}
} else {
depValue |= (1 << depIndex);
const depCheckbox = document.getElementById(`cheevo.${depCategory}.${depIndex}`)?.querySelector("input[type='checkbox']");
if (depCheckbox) {
depCheckbox.checked = true;
const depContainer = document.getElementById(`cheevo.${depCategory}.${depIndex}`);
depContainer?.classList.add("unlocked");
}
depList.push({"Category" : depCategory, "Value" : depIndex});
}
globalThis.parsedXML.Bank.Data.Stats[depStatKey].value = depValue.toString();
updateBasicTabInput("Stats", depStatKey);
for (const dep of depList) {
await unlockDependencies(dep.Category, dep.Value);
}
}
}
// Handle Collections
for (const [catKey, achievements] of Object.entries(cheevos)) {
achievements.forEach(async (achievement, achIdx) => {
if (achievement.Dependencies) {
const hasAllDependency = achievement.Dependencies.some(dep => dep.Category === category && dep.Value === "all");
if (hasAllDependency) {
//console.log(`Checking all case for ${cheevos[catKey][achIdx].Title} ${cheevos[catKey][achIdx].Dependencies?.length}`);
for (const dep of achievement.Dependencies) {
if (dep.Value !== "all") continue;
const depAchievements = cheevos[dep.Category] || [];
//console.log(`Checking ${dep.Category} ${dep.Value} ${depAchievements.length}`);
const depStatKey = Object.entries(setMap).find(([, v]) => v === dep.Category)?.[0];
const depVal = parseInt(globalThis.parsedXML.Bank.Data.Stats[depStatKey]?.value || "0");
const allUnlocked = depAchievements.every((ach, idx) => {
if (!ach) return true; // Skip missing achievements
//console.log(`${depVal} & (1 << ${idx}) = ${depVal & (1 << idx)}`);
const unlocked = (depVal & (1 << idx)) !== 0;
//console.log(`Checking ${ach.Title}: ${unlocked}`);
return unlocked;
});
//console.log(`All unlocked: ${allUnlocked}`);
if (!allUnlocked) return; // If not all are unlocked, exit
const collectionKey = Object.entries(setMap).find(([, v]) => v === catKey)?.[0];
if (!collectionKey) return;
if (!globalThis.parsedXML.Bank.Data.Stats[collectionKey]) {
globalThis.parsedXML.Bank.Data.Stats[collectionKey] = {
type: "int",
value: "0",
nodeType: "Value"
};
}
let collectionValue = parseInt(globalThis.parsedXML.Bank.Data.Stats[collectionKey].value) || 0;
collectionValue |= (1 << achIdx);
globalThis.parsedXML.Bank.Data.Stats[collectionKey].value = collectionValue.toString();
updateBasicTabInput("Stats", collectionKey);
const collectionCheckbox = document.getElementById(`cheevo.${catKey}.${achIdx}`)?.querySelector("input[type='checkbox']");
if (collectionCheckbox) {
collectionCheckbox.checked = true;
const collectionContainer = document.getElementById(`cheevo.${catKey}.${achIdx}`);
collectionContainer?.classList.add("unlocked");
}
}
}
}
});
}
await tallyAchievements();
updateMinzStats();
}
async function lockDependents(category, idx) {
//console.log(`Locking dependents for ${category} ${idx}`);
const cheevos = await getAchievements();
let achievement = cheevos[category]?.[idx];
if (!achievement) return;
if (achievement.Requires) {
for (const req of achievement.Requires) {
ensureStatKeyExists(req.Type);
const val = parseInt(globalThis.parsedXML.Bank.Data.Stats[req.Type].value) || 0;
//console.log(`Checking ${req.Type} for ${req.Value} it is ${val}`);
if (val > req.Value) {
//console.log(`Setting ${req.Type} to ${req.Value - 1}`);
globalThis.parsedXML.Bank.Data.Stats[req.Type].value = req.Value - 1;
updateBasicTabInput("Stats", req.Type);
}
}
}
for (const [catKey, achievements] of Object.entries(cheevos)) {
achievements.forEach(async (achievement, achIdx) => {
if (achievement.Dependencies) {
const dependsOnThis = achievement.Dependencies.some(dep => {
if (dep.Category !== category) return false;
if (dep.Value === "all") return true;
return dep.Value === idx;
});
if (dependsOnThis) {
//console.log(`Locking ${catKey} ${achIdx} because it depends on ${category} ${idx}`);
const statKey = Object.entries(setMap).find(([, v]) => v === catKey)?.[0];
if (!statKey) return;
let currentValue = parseInt(globalThis.parsedXML.Bank.Data.Stats[statKey]?.value || "0");
// This achievement was unlocked, need to lock it now
currentValue &= ~(1 << achIdx);
globalThis.parsedXML.Bank.Data.Stats[statKey].value = currentValue.toString();
updateBasicTabInput("Stats", statKey);
const depCheckbox = document.getElementById(`cheevo.${catKey}.${achIdx}`)?.querySelector("input[type='checkbox']");
if (depCheckbox) {
if (depCheckbox.checked) {
depCheckbox.checked = false;
const depContainer = document.getElementById(`cheevo.${catKey}.${achIdx}`);
depContainer?.classList.remove("unlocked");
}
}
await lockDependents(catKey, achIdx);
}
}
});
}
//await handleLevelCheevos();
await tallyAchievements();
updateMinzStats();
}
function setCheevoUnlocked(page, index, unlocked)
{
var item = globalThis.document.getElementById("cheevo." + page + "." + index);
if (item)
{
var check = globalThis.document.getElementById("cheevo." + page + "." + index + ".checkbox");
if (unlocked)
{
item.classList.add("unlocked");
check.checked = true;
}else
{
item.classList.remove("unlocked");
check.checked = false;
}
}
}
function updateCheevoTabLockStatus() {
if (!globalThis.parsedXML?.Bank?.Data?.Stats) return;
const stats = globalThis.parsedXML.Bank.Data.Stats;
for (const [key, value] of Object.entries(stats)) {
//console.log(key + " = " + value);
if (!setMap[key]) continue;
if (value.type !== "int") continue;
const intValue = parseInt(value.value);
if (isNaN(intValue)) continue;
const group = setMap[key];
for (let i = 0; i < 16; i++) {
const unlocked = (intValue & (1 << i)) !== 0;
setCheevoUnlocked(group, i, unlocked);
}
}
}
function insertCheevoBreak(cheevosWindow)
{
var cBreak = globalThis.document.createElement("div");
cBreak.innerHTML = "<hr>";
cBreak.style.gridColumn = "1 / -1";
cheevosWindow.appendChild(cBreak);
}
export async function validateAndRebuildSave() {
console.log("Validating uploaded save...");
const originalStats = JSON.parse(JSON.stringify(globalThis.parsedXML?.Bank?.Data?.Stats));
if (!originalStats) return;
// Create a fresh validated stats object for the sets
const validatedStats = {};
// Unlock forced achievements immediately
await unlockForcedAchievements(validatedStats);
// Replay unlocks
const cheevos = await getAchievements();
for (const [category, group] of Object.entries(cheevos)) {
const statKey = Object.entries(setMap).find(([, v]) => v === category)?.[0];
if (!statKey) continue;
const currentValue = parseInt(globalThis.parsedXML.Bank.Data.Stats?.[statKey]?.value || "0");
for (let i = 0; i < group.length; i++) {
if ((currentValue & (1 << i)) !== 0) {
await unlockCheevoInto(validatedStats, category, i);
}
}
}
// Apply validated sets back into parsedXML
for (const [key, val] of Object.entries(validatedStats)) {
globalThis.parsedXML.Bank.Data.Stats[key] = val;
}
await validateStatsAfterRebuild();
await updateCheevoTabLockStatus();
await tallyAchievements();
const changes = await compareStats(originalStats, globalThis.parsedXML.Bank.Data.Stats);
console.log("Validation complete. Changes detected:", changes.length);
for (const change of changes) {
console.log(`${change.key} : ${change.before} -> ${change.after}`);
}
}
async function compareStats(original, updated) {
const changes = [];
const cheevos = await getAchievements();
for (const key in original) {
const originalVal = original[key]?.value ?? null;
const updatedVal = updated[key]?.value ?? null;
if (originalVal !== updatedVal) {
if (key.startsWith("set")) {
const setName = setMap[key];
if (setName && cheevos[setName]) {
for (let i = 0; i < 16; i++) {
const before = (originalVal >> i) & 1;
const after = (updatedVal >> i) & 1;
if (before === 0 && after === 1) {
const achievement = cheevos[setName]?.[i];
if (achievement) {
console.log(`Unlocked Achievement: "${achievement.Title}" (${setName})`);
}
}
}
}
}else{
changes.push({
key: key,
before: originalVal,
after: updatedVal
});
}
}
}
// Also find any new keys that didn't exist before
for (const key in updated) {
if (!(key in original)) {
if (updated[key] == null || updated[key].value == "" || updated[key].value == "0") continue;
changes.push({
key: key,
before: null,
after: updated[key]?.value ?? null
});
}
}
return changes;
}
async function validateStatsAfterRebuild() {
if (!globalThis.parsedXML?.Bank?.Data?.Stats) return;
const stats = globalThis.parsedXML.Bank.Data.Stats;
ensureStatKeyExists("Lvl");
ensureStatKeyExists("Exp");
ensureStatKeyExists("Absorbed");
ensureStatKeyExists("Gens");
ensureStatKeyExists("Heals");
ensureStatKeyExists("Mined");
ensureStatKeyExists("TimePlayed");
ensureStatKeyExists("TotalKills");
ensureStatKeyExists("Games");
ensureStatKeyExists("EasyWin");
ensureStatKeyExists("NormalWin");
ensureStatKeyExists("HardWin");
ensureStatKeyExists("HardWinFinalBoss");
ensureStatKeyExists("InsaneWin");
ensureStatKeyExists("InsaneWinFinalBoss");
ensureStatKeyExists("InsaneHighestNight");
// Level and Experience
let lvl = parseInt(stats["Lvl"]?.value || "0");
if (isNaN(lvl) || lvl < 1) lvl = 1;
if (lvl > 99) lvl = 99;
stats["Lvl"].value = lvl.toString();
// Clamp experience to the range of the level
let currentExp = parseInt(stats["Exp"]?.value || "0");
let minExp = getExperienceFromLevel(lvl);
let maxExp = getExperienceFromLevel(lvl + 1);
if (currentExp < minExp) {
currentExp = minExp;
} else if (currentExp >= maxExp) {
currentExp = (currentExp >= 495000) ? 495000 : maxExp - 1;
}
stats["Exp"].value = currentExp.toString();
// Clamping stats to 0–2,000,000,000
const clamp = (v) => Math.max(0, Math.min(2000000000, parseInt(v || "0") || 0));
stats["Absorbed"].value = clamp(stats["Absorbed"]?.value).toString();
stats["Gens"].value = clamp(stats["Gens"]?.value).toString();
stats["Heals"].value = clamp(stats["Heals"]?.value).toString();
stats["Mined"].value = clamp(stats["Mined"]?.value).toString();
stats["TimePlayed"].value = clamp(stats["TimePlayed"]?.value).toString();
stats["TotalKills"].value = clamp(stats["TotalKills"]?.value).toString();
stats["Games"].value = clamp(stats["Games"]?.value).toString();
stats["EasyWin"].value = clamp(stats["EasyWin"]?.value).toString();
stats["NormalWin"].value = clamp(stats["NormalWin"]?.value).toString();
stats["HardWin"].value = clamp(stats["HardWin"]?.value).toString();
stats["HardWinFinalBoss"].value = clamp(stats["HardWinFinalBoss"]?.value).toString();
stats["InsaneWin"].value = clamp(stats["InsaneWin"]?.value).toString();
stats["InsaneWinFinalBoss"].value = clamp(stats["InsaneWinFinalBoss"]?.value).toString();
stats["InsaneHighestNight"].value = clamp(stats["InsaneHighestNight"]?.value).toString();
// Re-run handlers to validate cheevos based on corrected values
await handleAbsorbCheevos(true);
await handleGeneratedCheevos(true);
await handleHealedCheevos(true);
await handleMinedCheevos(true);
await handleKillsCheevos(true);
await handleLevelCheevos();
await handleTimeCheevos(true);
await handleEasyWins(true);
await handleNormalWins(true);
await handleHardWins(true);
await handleHard2Wins(true);
await handleInsaneWins(true);
await handleInsane2Wins(true);
await handleInsane3Wins(true);
handleGamesPlayed(true);
handleTrophies();
}
async function unlockForcedAchievements(validatedStats) {
const alwaysUnlocked = [
{ category: "General2", index: 0 }, // Beta Tester
{ category: "General2", index: 1 }, // Play A Game Of MineralZ
{ category: "General1", index: 8 }, // Casino
{ category: "General1", index: 9 }, // Casino
{ category: "General1", index: 10 }, // Casino
{ category: "General1", index: 11 }, // Casino
{ category: "General1", index: 12 }, // Casino
{ category: "General1", index: 13 }, // Casino
{ category: "General1", index: 14 }, // Casino
{ category: "General1", index: 15 } // Casino
];
for (const { category, index } of alwaysUnlocked) {
await unlockCheevoInto(validatedStats, category, index);
}
}
async function unlockCheevoInto(validatedStats, category, index) {
const statKey = Object.entries(setMap).find(([, v]) => v === category)?.[0];
if (!statKey) return;
if (!validatedStats[statKey]) {
validatedStats[statKey] = {
type: "int",
value: "0",
nodeType: "Value"
};
}
let currentValue = parseInt(validatedStats[statKey].value || "0");
currentValue |= (1 << index);
validatedStats[statKey].value = currentValue.toString();
}
function updateMinzStats() {
if (!globalThis.parsedXML?.Bank?.Data?.Stats) return;
const stats = globalThis.parsedXML.Bank.Data.Stats;
const levelInput = globalThis.document.getElementById("levelInput");
const xpInput = globalThis.document.getElementById("xpInput");
const dmgInput = globalThis.document.getElementById("dmgInput");
const genInput = globalThis.document.getElementById("genInput");
const healsInput = globalThis.document.getElementById("healsInput");
const minedInput = globalThis.document.getElementById("minedInput");
const timePlayedInput = globalThis.document.getElementById("timePlayedInput");
const killsInput = globalThis.document.getElementById("killsInput");
const playsTotalInput = globalThis.document.getElementById("playsTotalInput");
const winsEasyInput = globalThis.document.getElementById("winsEasyInput");
const winsNormalInput = globalThis.document.getElementById("winsNormalInput");
const winsHardInput = globalThis.document.getElementById("winsHardInput");
const winsHard2Input = globalThis.document.getElementById("winsHard2Input");
const winsInsaneInput = globalThis.document.getElementById("winsInsaneInput");
const winsInsane2Input = globalThis.document.getElementById("winsInsane2Input");
const winsInsane3Input = globalThis.document.getElementById("winsInsane3Input");
if (levelInput && stats["Lvl"]) levelInput.value = stats["Lvl"].value;
if (xpInput && stats["Exp"]) xpInput.value = stats["Exp"].value;
if (dmgInput && stats["Absorbed"]) dmgInput.value = stats["Absorbed"].value;
if (genInput && stats["Gens"]) genInput.value = stats["Gens"].value;
if (healsInput && stats["Heals"]) healsInput.value = stats["Heals"].value;
if (minedInput && stats["Mined"]) minedInput.value = stats["Mined"].value;
if (timePlayedInput && stats["TimePlayed"]) timePlayedInput.value = stats["TimePlayed"].value;
if (killsInput && stats["TotalKills"]) killsInput.value = stats["TotalKills"].value;
if (playsTotalInput && stats["Games"]) playsTotalInput.value = stats["Games"].value;
if (winsEasyInput && stats["EasyWin"]) winsEasyInput.value = stats["EasyWin"].value;
if (winsNormalInput && stats["NormalWin"]) winsNormalInput.value = stats["NormalWin"].value;
if (winsHardInput && stats["HardWin"]) winsHardInput.value = stats["HardWin"].value;
if (winsHard2Input && stats["HardWinFinalBoss"]) winsHard2Input.value = stats["HardWinFinalBoss"].value;
if (winsInsaneInput && stats["InsaneWin"]) winsInsaneInput.value = stats["InsaneWin"].value;
if (winsInsane2Input && stats["InsaneWinFinalBoss"]) winsInsane2Input.value = stats["InsaneWinFinalBoss"].value;
if (winsInsane3Input && stats["InsaneHighestNight"]) winsInsane3Input.value = stats["InsaneHighestNight"].value;
}
let trophyCount = 0;
function updateTrophies() {
const trophyWindow = globalThis.document.getElementById("trophyWindow");
trophyCount = 0;
const trophyElements = [];
if (!trophyWindow) return;
if (globalThis.parsedXML?.Bank?.Data?.Trophies) {
const trophies = globalThis.parsedXML.Bank.Data.Trophies;
for (const [trophyId, data] of Object.entries(trophies)) {
const count = parseInt(data.value);
if (isNaN(count) || count <= 0) continue;
for (let i = 0; i < count; i++) {
if (trophyCount === 11) break;
const trophy = globalThis.document.createElement("div");
trophy.className = "trophy";
trophy.innerText = trophyId;
trophy.style.width = "46px";
trophy.style.height = "46px";
trophy.dataset.trophyId = trophyId;
// Add removal logic
trophy.addEventListener("click", async () => {
const id = trophy.dataset.trophyId;
let val = parseInt(globalThis.parsedXML.Bank.Data.Trophies[id]?.value || "0");
if (!isNaN(val) && val > 0) {
val--;
globalThis.parsedXML.Bank.Data.Trophies[id].value = val.toString();
updateBasicTabInput("Trophies", id);
await rebuildGraphicalWindow();
}
});
trophyElements.push(trophy);
trophyCount++;
}
}
}
// Add all trophies to the DOM, inserting a <br> after the 6th one
trophyElements.forEach((el, i) => {
trophyWindow.appendChild(el);
if (i === 5) {
trophyWindow.appendChild(globalThis.document.createElement("br"));
}
});
}
export async function handleLevelCheevos() {
if (!globalThis.parsedXML?.Bank?.Data?.Stats) return;
const stats = globalThis.parsedXML.Bank.Data.Stats;
const lvl = parseInt(stats["Lvl"]?.value || "0");
let val2 = parseInt(stats["set1"]?.value || "0"); // General2
let val4 = parseInt(stats["set4"]?.value || "0"); // Collections
const achievements = [
{ bit: 2, level: 99 },
{ bit: 3, level: 53 },
{ bit: 8, level: 20 },
{ bit: 9, level: 30 },
{ bit: 10, level: 40 },
{ bit: 11, level: 50 },
{ bit: 12, level: 60 },
{ bit: 13, level: 70 },
{ bit: 14, level: 80 },
{ bit: 15, level: 90 }
];
achievements.forEach(({ bit, level }) => {
if (lvl >= level) val2 |= (1 << bit);
else val2 &= ~(1 << bit);
});
// Collections[5] = Level 90
if (lvl >= 90) val4 |= (1 << 5);
else val4 &= ~(1 << 5);
stats["set1"] = { type: "int", value: val2.toString(), nodeType: "Value" };
stats["set4"] = { type: "int", value: val4.toString(), nodeType: "Value" };
updateBasicTabInput("Stats", "set1");
updateBasicTabInput("Stats", "set4");
updateCheevoTabLockStatus();
await tallyAchievements();
}
export async function handleAbsorbCheevos(allowDrop = false) {
if (!globalThis.parsedXML?.Bank?.Data?.Stats) return;
const stats = globalThis.parsedXML.Bank.Data.Stats;
const absorbed = parseInt(stats["Absorbed"]?.value || "0");
const thresholds = [
5000000, 50000000, 100000000, 500000000, // General4 [12-15]
750000000, 1000000000, 1500000000, 2000000000 // General5 [0-3]
];
let val4 = parseInt(stats["set9"]?.value || "0"); // General4
let val5 = parseInt(stats["set10"]?.value || "0"); // General5
// General4 bits 12-15
for (let i = 0; i < 4; i++) {
const bit = 1 << (i + 12);
if (absorbed >= thresholds[i]) {
val4 |= bit;
} else if (allowDrop) {
val4 &= ~bit;
}
}
// General5 bits 0-3
for (let i = 0; i < 4; i++) {
const bit = 1 << i;
if (absorbed >= thresholds[i + 4]) {
val5 |= bit;
} else if (allowDrop) {
val5 &= ~bit;
}
}
stats["set9"] = { type: "int", value: val4.toString(), nodeType: "Value" };
stats["set10"] = { type: "int", value: val5.toString(), nodeType: "Value" };
updateBasicTabInput("Stats", "set9");
updateBasicTabInput("Stats", "set10");
updateCheevoTabLockStatus();
await tallyAchievements();
}
export async function handleGeneratedCheevos(allowDrop = false) {
if (!globalThis.parsedXML?.Bank?.Data?.Stats) return;
const stats = globalThis.parsedXML.Bank.Data.Stats;
const generated = parseInt(stats["Gens"]?.value || "0");
const thresholds = [
5000000,
50000000,
500000000,
750000000,
1000000000,
2000000000
];
let val4 = parseInt(stats["set9"]?.value || "0"); // set9 = General4
for (let i = 0; i < thresholds.length; i++) {
const bit = 1 << i; // General4[0-5] = bits 0-5
if (generated >= thresholds[i]) {
val4 |= bit;
} else if (allowDrop) {
val4 &= ~bit;
}
}
stats["set9"] = {
type: "int",
value: val4.toString(),
nodeType: "Value"
};
updateBasicTabInput("Stats", "set9");
updateCheevoTabLockStatus();
await tallyAchievements();
}
export async function handleHealedCheevos(allowDrop = false) {
if (!globalThis.parsedXML?.Bank?.Data?.Stats) return;
const stats = globalThis.parsedXML.Bank.Data.Stats;
const healed = parseInt(stats["Heals"]?.value || "0");
const thresholds = [
250000, 1000000, 5000000, 9999999, // General1 [0-3]
100000000, 250000000, 500000000, // General4 [6-8]
1000000000, 1500000000, 2000000000 // General4 [9-11]
];
let val1 = parseInt(stats["set6"]?.value || "0"); // General1
let val4 = parseInt(stats["set9"]?.value || "0"); // General4
// General1
for (let i = 0; i <= 3; i++) {
const bit = 1 << i;
if (healed >= thresholds[i]) {
val1 |= bit;
} else if (allowDrop) {
val1 &= ~bit;
}
}
// General4 (bits 6 to 11)
for (let i = 0; i <= 5; i++) {
const bit = 1 << (i + 6);
if (healed >= thresholds[i + 4]) {
val4 |= bit;
} else if (allowDrop) {
val4 &= ~bit;
}
}
stats["set6"] = { type: "int", value: val1.toString(), nodeType: "Value" };
stats["set9"] = { type: "int", value: val4.toString(), nodeType: "Value" };
updateBasicTabInput("Stats", "set6");
updateBasicTabInput("Stats", "set9");
updateCheevoTabLockStatus();
await tallyAchievements();
}
export async function handleMinedCheevos(allowDrop = false) {
if (!globalThis.parsedXML?.Bank?.Data?.Stats) return;
const stats = globalThis.parsedXML.Bank.Data.Stats;
const mined = parseInt(stats["Mined"]?.value || "0");
const thresholds = [
250000, 500000, 750000, 1000000, 5000000, 10000000,
50000000, 100000000, 250000000, 500000000,
1000000000, 2000000000
];
let val = parseInt(stats["set10"]?.value || "0"); // General5
for (let i = 0; i < thresholds.length; i++) {
const bit = 1 << (i + 4);
if (mined >= thresholds[i]) {
val |= bit;
} else if (allowDrop) {
val &= ~bit;
}
}
stats["set10"] = {
type: "int",
value: val.toString(),
nodeType: "Value"
};
updateBasicTabInput("Stats", "set10");
// Force update UI
updateCheevoTabLockStatus();
await tallyAchievements();
}
export async function handleTimeCheevos(allowDrop = false) {
if (!globalThis.parsedXML?.Bank?.Data?.Stats) return;
const stats = globalThis.parsedXML.Bank.Data.Stats;
const timePlayed = parseInt(stats["TimePlayed"]?.value || "0");
let val2 = parseInt(stats["set1"]?.value || "0"); // General2
if (timePlayed >= 600) {
val2 |= (1 << 7);
} else if (allowDrop) {
val2 &= ~(1 << 7);
}
if (timePlayed >= 1200) {
val2 |= (1 << 5);
} else if (allowDrop) {
val2 &= ~(1 << 5);
}
stats["set1"] = {
type: "int",
value: val2.toString(),
nodeType: "Value"
};
updateBasicTabInput("Stats", "set1");
updateCheevoTabLockStatus();
await tallyAchievements();
}
export async function handleKillsCheevos(allowDrop = false) {
if (!globalThis.parsedXML?.Bank?.Data?.Stats) return;
const stats = globalThis.parsedXML.Bank.Data.Stats;
const totalKills = parseInt(stats["TotalKills"]?.value || "0");
let val1 = parseInt(stats["set6"]?.value || "0"); // General1
const thresholds = [
{ bit: 4, value: 5000 },
{ bit: 5, value: 15000 },
{ bit: 6, value: 75000 },
{ bit: 7, value: 150000 }
];
// Special check for Insane[5]: must have at least 3000 kills
const insaneVal = parseInt(stats["set2"]?.value || "0"); // Insane
if ((insaneVal >> 5) & 1) {
if (totalKills < 3000) {
// Clear the bit if kills are below 3000
stats["set2"] = {
type: "int",
value: (insaneVal & ~(1 << 5)).toString(),
nodeType: "Value"
};
updateBasicTabInput("Stats", "set2");
}
}
for (const { bit, value } of thresholds) {
if (totalKills >= value) {
val1 |= (1 << bit);
} else if (allowDrop) {
val1 &= ~(1 << bit);
}
}
stats["set6"] = {
type: "int",
value: val1.toString(),
nodeType: "Value"
};
updateBasicTabInput("Stats", "set6");
updateCheevoTabLockStatus();
await tallyAchievements();
}
export function handleGamesPlayed(allowDrop = false) {
if (!globalThis.parsedXML?.Bank?.Data?.Stats) return;
const stats = globalThis.parsedXML.Bank.Data.Stats;
const easy = parseInt(stats["EasyWin"]?.value || "0");
const normal = parseInt(stats["NormalWin"]?.value || "0");
const hard = parseInt(stats["HardWin"]?.value || "0");
const insane = parseInt(stats["InsaneWin"]?.value || "0");
const total = easy + normal + hard + insane;
const currentGames = parseInt(stats["Games"]?.value || "0");
// Total Games cannot be less than Total Wins
if (currentGames >= total) return;
ensureStatKeyExists("Games");
stats["Games"].value = total.toString();
updateBasicTabInput("Stats", "Games");
const input = globalThis.document.getElementById("playsTotalInput");
if (input) input.value = total.toString();
}
export async function handleEasyWins(allowDrop = false) {
if (!globalThis.parsedXML?.Bank?.Data?.Stats) return;
ensureStatKeyExists("EasyWin");
const stats = globalThis.parsedXML.Bank.Data.Stats;
let wins = parseInt(stats["EasyWin"]?.value || "0");
if (isNaN(wins) || wins < 0) wins = 0;
let easyVal = parseInt(stats["set3"]?.value || "0"); // set3 = Easy
// Set Easy[0] if any wins
if (wins > 0) {
easyVal |= (1 << 0);
} else if (allowDrop) {
easyVal &= ~(1 << 0);
}
stats["set3"] = {
type: "int",
value: easyVal.toString(),
nodeType: "Value"
};
updateBasicTabInput("Stats", "set3");
stats["EasyWin"].value = wins.toString();
updateBasicTabInput("Stats", "EasyWin");
updateCheevoTabLockStatus();
await tallyAchievements();
handleGamesPlayed(allowDrop);
}
export async function handleNormalWins(allowDrop = false) {
if (!globalThis.parsedXML?.Bank?.Data?.Stats) return;
ensureStatKeyExists("NormalWin");
const stats = globalThis.parsedXML.Bank.Data.Stats;
let wins = parseInt(stats["NormalWin"]?.value || "0");
if (isNaN(wins) || wins < 0) wins = 0;
let normalVal = parseInt(stats["set5"]?.value || "0"); // set5 = Normal
if (wins > 0) {
normalVal |= (1 << 0);
} else if (allowDrop) {
normalVal &= ~(1 << 0);
}
if (wins >= 5) {
normalVal |= (1 << 15);
} else if (allowDrop) {
normalVal &= ~(1 << 15);
}
stats["set5"] = {
type: "int",
value: normalVal.toString(),
nodeType: "Value"
};
updateBasicTabInput("Stats", "set5");
stats["NormalWin"].value = wins.toString();
updateBasicTabInput("Stats", "NormalWin");
updateCheevoTabLockStatus();
await tallyAchievements();
handleGamesPlayed(allowDrop);
}
export async function handleHardWins(allowDrop = false) {
if (!globalThis.parsedXML?.Bank?.Data?.Stats) return;
ensureStatKeyExists("HardWin");
const stats = globalThis.parsedXML.Bank.Data.Stats;
let wins = parseInt(stats["HardWin"]?.value || "0");
if (isNaN(wins) || wins < 0) wins = 0;
let hardVal = parseInt(stats["set0"]?.value || "0"); // set0 = Hard
if (wins > 0) {
hardVal |= (1 << 9);
} else
{
hardVal &= ~(1 << 1);
hardVal &= ~(1 << 8);
hardVal &= ~(1 << 9);
}
stats["set0"] = {
type: "int",
value: hardVal.toString(),
nodeType: "Value"
};
updateBasicTabInput("Stats", "set0");
stats["HardWin"].value = wins.toString();
updateBasicTabInput("Stats", "HardWin");
updateCheevoTabLockStatus();
await tallyAchievements();
handleGamesPlayed(allowDrop);
}
export async function handleHard2Wins(allowDrop = false) {
if (!globalThis.parsedXML?.Bank?.Data?.Stats) return;
ensureStatKeyExists("HardWin");
ensureStatKeyExists("HardWinFinalBoss");
const stats = globalThis.parsedXML.Bank.Data.Stats;
let finalBossWins = parseInt(stats["HardWinFinalBoss"]?.value || "0");
let hardVal = parseInt(stats["set0"]?.value || "0"); // set0 = Hard
if (finalBossWins > 0) {
hardVal |= (1 << 8);
} else {
hardVal &= ~(1 << 8);
}
// Ensure total HardWins is at least equal to finalBossWins
let wins = parseInt(stats["HardWin"]?.value || "0");
if (finalBossWins > wins) {
stats["HardWin"].value = finalBossWins.toString();
const input = globalThis.document.getElementById("winsHardInput");
if (input) input.value = finalBossWins.toString();
await handleHardWins(true);
}
stats["set0"] = {
type: "int",
value: hardVal.toString(),
nodeType: "Value"
};
updateBasicTabInput("Stats", "set0");
stats["HardWinFinalBoss"].value = finalBossWins.toString();
updateBasicTabInput("Stats", "HardWinFinalBoss");
updateCheevoTabLockStatus();
await tallyAchievements();
}
export async function handleInsaneWins(allowDrop = false) {
if (!globalThis.parsedXML?.Bank?.Data?.Stats) return;
ensureStatKeyExists("InsaneWin");
ensureStatKeyExists("InsaneWinFinalBoss");
ensureStatKeyExists("InsaneHighestNight");
const stats = globalThis.parsedXML.Bank.Data.Stats;
let wins = parseInt(stats["InsaneWin"]?.value || "0");
let winsFinal = parseInt(stats["InsaneWinFinalBoss"]?.value || "0");
if (isNaN(wins) || wins < 0) wins = 0;
let insaneVal = parseInt(stats["set2"]?.value || "0"); // set2 = Insane
let night = parseInt(stats["InsaneHighestNight"]?.value || "0");
if (wins > 0) {
// Insane[0,1,6-14]
const bitsToSet = [0, 1, 6, 7, 8, 9, 10, 11, 12, 13, 14];
for (const bit of bitsToSet) {
insaneVal |= (1 << bit);
}
if (winsFinal > 0) {
// Ensure night >= 51
if (night < 51) {
night = 51;
stats["InsaneHighestNight"].value = night.toString();
const input = globalThis.document.getElementById("winsInsane3Input");
if (input) input.value = night.toString();
updateBasicTabInput("Stats", "InsaneHighestNight");
}
}
else{
// Ensure night >= 50
if (night < 50) {
night = 50;
stats["InsaneHighestNight"].value = night.toString();
const input = globalThis.document.getElementById("winsInsane3Input");
if (input) input.value = night.toString();
updateBasicTabInput("Stats", "InsaneHighestNight");
}
}
} else {
// Clear Insane[1], [3]
insaneVal &= ~(1 << 1);
insaneVal &= ~(1 << 3);
// Clear Final Boss kills
stats["InsaneWinFinalBoss"].value = "0";
const input = globalThis.document.getElementById("winsInsane2Input");
if (input) input.value = "0";
updateBasicTabInput("Stats", "InsaneWinFinalBoss");
}
// Set/clear Insane[15] based on win count
if (wins >= 5) {
insaneVal |= (1 << 15);
} else {
insaneVal &= ~(1 << 15);
}
// Write back updated values
stats["set2"] = {
type: "int",
value: insaneVal.toString(),
nodeType: "Value"
};
updateBasicTabInput("Stats", "set2");
stats["InsaneWin"] = { type: "int", value: wins.toString(), nodeType: "Value" };
stats["InsaneHighestNight"] = { type: "int", value: night.toString(), nodeType: "Value" };
updateBasicTabInput("Stats", "InsaneWin");
updateBasicTabInput("Stats", "InsaneHighestNight");
updateCheevoTabLockStatus();
await tallyAchievements();
handleGamesPlayed(allowDrop);
}
export async function handleInsane2Wins(allowDrop = false) {
if (!globalThis.parsedXML?.Bank?.Data?.Stats) return;
ensureStatKeyExists("InsaneWin");
ensureStatKeyExists("InsaneWinFinalBoss");
ensureStatKeyExists("InsaneHighestNight");
const stats = globalThis.parsedXML.Bank.Data.Stats;
const kills = parseInt(stats["InsaneWinFinalBoss"]?.value || "0");
let insaneVal = parseInt(stats["set2"]?.value || "0"); // Insane
const wins = parseInt(stats["InsaneWin"]?.value || "0");
if (kills > 0) {
// Unlock bit 3 = Final Boss win
insaneVal |= (1 << 3);
} else if (allowDrop) {
// Remove bit 3
insaneVal &= ~(1 << 3);
}
let night = parseInt(stats["InsaneHighestNight"]?.value || "0");
if (kills > 0 && night < 51) {
night = 51;
stats["InsaneHighestNight"].value = night.toString();
const input = globalThis.document.getElementById("winsInsane3Input");
if (input) input.value = night.toString();
updateBasicTabInput("Stats", "InsaneHighestNight");
}
// Ensure kills do not exceed win count
if (kills > wins) {
stats["InsaneWin"].value = kills.toString();
const input = globalThis.document.getElementById("winsInsaneInput");
if (input) input.value = kills.toString();
await handleInsaneWins(true);
}
stats["set2"] = {
type: "int",
value: insaneVal.toString(),
nodeType: "Value"
};
updateBasicTabInput("Stats", "set2");
updateBasicTabInput("Stats", "InsaneWin");
updateBasicTabInput("Stats", "InsaneWinFinalBoss");
updateCheevoTabLockStatus();
await tallyAchievements();
}
export async function handleInsane3Wins(allowDrop = false) {
if (!globalThis.parsedXML?.Bank?.Data?.Stats) return;
ensureStatKeyExists("InsaneHighestNight");
const stats = globalThis.parsedXML.Bank.Data.Stats;
const night = parseInt(stats["InsaneHighestNight"]?.value || "0");
let val = parseInt(stats["set2"]?.value || "0");
const requirements = [
{ bit: 0, threshold: 1 },
{ bit: 1, threshold: 50 },
{ bit: 6, threshold: 5 },
{ bit: 7, threshold: 10 },
{ bit: 8, threshold: 15 },
{ bit: 9, threshold: 20 },
{ bit: 10, threshold: 25 },
{ bit: 11, threshold: 30 },
{ bit: 12, threshold: 35 },
{ bit: 13, threshold: 40 },
{ bit: 14, threshold: 45 },
];
for (const { bit, threshold } of requirements) {
if (night >= threshold) {
val |= (1 << bit);
} else {
val &= ~(1 << bit);
}
}
stats["set2"] = {
type: "int",
value: val.toString(),
nodeType: "Value"
};
updateBasicTabInput("Stats", "set2");
stats["InsaneHighestNight"].value = night.toString();
const input = globalThis.document.getElementById("winsInsane3Input");
if (input) input.value = night.toString();
updateBasicTabInput("Stats", "InsaneHighestNight");
updateCheevoTabLockStatus();
await tallyAchievements();
}
function handleTrophies()
{
// If any trophies, ensure "Loot To Boot" is unlocked
if (globalThis.parsedXML?.Bank?.Data?.Trophies) {
const trophies = globalThis.parsedXML.Bank.Data.Trophies;
const hasTrophies = Object.values(trophies).some(t => parseInt(t.value) > 0);
if (hasTrophies) {
ensureStatKeyExists("set1"); // General2
let val = parseInt(globalThis.parsedXML.Bank.Data.Stats["set1"].value || "0");
val |= (1 << 6); // Unlock "Loot To Boot" (bit 6)
globalThis.parsedXML.Bank.Data.Stats["set1"].value = val.toString();
updateBasicTabInput("Stats", "set1");
}
}
}
const cheevos = {
"Hard": [
{ "Title": "Sucker Born Every Minute", "Description": "Salvage A Tier 7 Building On Hard Mode", "Value": 10 },
{ "Title": "Humanitarian", "Description": "Beat MineralZ On Hard Mode Without Any Kills During The Game", "Value": 10, "Requires": [{ "Type": "HardWin", "Value": 1 }] },
{ "Title": "Forget About The Price Tag", "Description": "Unlock Tier 8 In Hard Mode", "Value": 10 },
{ "Title": "Money Can't Buy Happiness", "Description": "Kill The Night 25 Bosses Without The Team Unlocking Tier 7", "Value": 10 },
{ "Title": "Stonehenge", "Description": "Build A T7 Wall On Hard Mode", "Value": 10 },
{ "Title": "Is There A Dr. In The House?", "Description": "Build A T7 Healer On Hard Mode", "Value": 10 },
{ "Title": "Were Gunna Need Guns, Lots Of Guns", "Description": "Build A T7 Cannon/Robo On Hard Mode", "Value": 10 },
{ "Title": "If You Slow Down, You Die", "Description": "Die on night 50 On Hard Mode", "Value": 10 },
{ "Title": "Die Hard", "Description": "Defeat The Final Boss On Hard Mode", "Value": 10, "Requires": [{ "Type": "HardWin", "Value": 1 }, { "Type": "HardWinFinalBoss", "Value": 1 }], "Dependencies": [{ "Category": "Hard", "Value": 9 }] },
{ "Title": "Why So Serious?", "Description": "Beat MineralZ On Hard Mode (Night 50 Not Final Boss)", "Value": 10, "Requires": [{ "Type": "HardWin", "Value": 1 }] },
{ "Title": "Peanut Butter Jelly Time", "Description": "Build A T7 Rift Universe On Hard Mode", "Value": 10 },
{ "Title": "Infamous", "Description": "Build A T7 Generator On Hard Mode", "Value": 10 },
{ "Title": "Crystal Clear", "Description": "Mine Over 100,000 Crsytal On Hard Mode", "Value": 10 },
{ "Title": "Plasma Penetrator", "Description": "Mine Over 100,000 Plasma On Hard Mode", "Value": 10 },
{ "Title": "Ruby Ravager", "Description": "Mine Over 100,000 Ruby On Hard Mode", "Value": 10 },
{ "Title": "Vespene Vaporizer", "Description": "Mine Over 100,000 Vespene On Hard Mode", "Value": 10 }
],
"General2": [
{ "Title": "Beta Tester", "Description": "Beta Test MineralZ", "Value": 5 },
{ "Title": "Loading", "Description": "Play A Game Of MineralZ (Any Difficulty)", "Value": 5 },
{ "Title": "I Can Has Nukes?", "Description": "Unlock the ability Nuclear Strike", "Value": 5, "Requires": [{ "Type": "Lvl", "Value": 99 }] },
{ "Title": "The Seer", "Description": "Unlock the Vortex ability", "Value": 5, "Requires": [{ "Type": "Lvl", "Value": 53 }] },
{ "Title": "Curious Jorje", "Description": "View The Tips using the keyboard hotkey (Hint: Press F12)", "Value": 5 },
{ "Title": "Insomniac", "Description": "Play MineralZ For Over 20 Hours Total", "Value": 5, "Requires": [{ "Type": "TimePlayed", "Value": 72000 }], "Dependencies": [{ "Category": "General2", "Value": 7 }] },
{ "Title": "Loot To Boot", "Description": "Find A Trophy", "Value": 5 },
{ "Title": "Addicted", "Description": "Play MineralZ For Over 10 Hours Total", "Value": 5, "Requires": [{ "Type": "TimePlayed", "Value": 36000 }] },
{ "Title": "One Day At A Time", "Description": "Reach Level 20", "Value": 5, "Requires": [{ "Type": "Lvl", "Value": 20 }] },
{ "Title": "Harder, Better, Faster, Stronger", "Description": "Reach Level 30", "Value": 5, "Requires": [{ "Type": "Lvl", "Value": 30 }], "Dependencies": [{ "Category": "General2", "Value": 8 }] },
{ "Title": "Fourteh Horde", "Description": "Reach Level 40", "Value": 5, "Requires": [{ "Type": "Lvl", "Value": 40 }], "Dependencies": [{ "Category": "General2", "Value": 9 }] },
{ "Title": "Run, Its The Five-O", "Description": "Reach Level 50", "Value": 5, "Requires": [{ "Type": "Lvl", "Value": 50 }], "Dependencies": [{ "Category": "General2", "Value": 10 }] },
{ "Title": "Are We There Yet?", "Description": "Reach Level 60", "Value": 5, "Requires": [{ "Type": "Lvl", "Value": 60 }], "Dependencies": [{ "Category": "General2", "Value": 11 }] },
{ "Title": "I Think I Can, I Think I Can", "Description": "Reach Level 70", "Value": 5, "Requires": [{ "Type": "Lvl", "Value": 70 }], "Dependencies": [{ "Category": "General2", "Value": 12 }] },
{ "Title": "Show Stopper", "Description": "Reach Level 80", "Value": 5, "Requires": [{ "Type": "Lvl", "Value": 80 }], "Dependencies": [{ "Category": "General2", "Value": 13 }] },
{ "Title": "Been There, Done That", "Description": "Reach Level 90", "Value": 5, "Requires": [{ "Type": "Lvl", "Value": 90 }], "Dependencies": [{ "Category": "General2", "Value": 14 }] }
],
"Insane": [
{ "Title": "Are you prepared?", "Description": "Play A Game Of MineralZ On Insane Mode", "Value": 15 },
{ "Title": "Leader Of ThaWolf Pack", "Description": "Survive Night 50 on Insane (Testing: If bugged report on forums)", "Value": 15, "Requires": [{ "Type": "InsaneHighestNight", "Value": 50 }, { "Type": "InsaneWin", "Value": 1 }], "Dependencies": [{ "Category": "Insane", "Value": 14 }] },
{ "Title": "So Close, Yet So Far Away", "Description": "Die on night 50 On Insane", "Value": 15, "Requires": [{ "Type": "InsaneHighestNight", "Value": 50 }], "Dependencies": [{ "Category": "Insane", "Value": 14 }] },
{ "Title": "LeVincible", "Description": "Defeat The Final Boss On Insane (Testing: If bugged report on forums)", "Value": 15, "Requires": [{ "Type": "InsaneHighestNight", "Value": 51 }, { "Type": "InsaneWin", "Value": 1 }, { "Type": "InsaneWinFinalBoss", "Value": 1 }], "Dependencies": [{ "Category": "Insane", "Value": 1 }] },
{ "Title": "All Sales Final", "Description": "Salvage Any Tier 7 or 8 Building On Insane (Excludes base)", "Value": 15 },
{ "Title": "King Of The Hill", "Description": "Reach 3,000 Kills In One Game On Insane", "Value": 15, "Requires": [{ "Type": "TotalKills", "Value": 3000, "RequireLocking": false }] },
{ "Title": "And??", "Description": "Survive Past Night 5", "Value": 15, "Requires": [{ "Type": "InsaneHighestNight", "Value": 5 }] },
{ "Title": "Await Further Instructions", "Description": "Survive Past Night 10", "Value": 15, "Requires": [{ "Type": "InsaneHighestNight", "Value": 10 }], "Dependencies": [{ "Category": "Insane", "Value": 6 }] },
{ "Title": "What Lurks In The Night....", "Description": "Survive Past Night 15", "Value": 15, "Requires": [{ "Type": "InsaneHighestNight", "Value": 15 }], "Dependencies": [{ "Category": "Insane", "Value": 7 }] },
{ "Title": "Warning: Air Wave Dectected.....", "Description": "Survive Past Night 20", "Value": 15, "Requires": [{ "Type": "InsaneHighestNight", "Value": 20 }], "Dependencies": [{ "Category": "Insane", "Value": 8 }] },
{ "Title": "Not a Vampire", "Description": "Survive Past Night 25", "Value": 15, "Requires": [{ "Type": "InsaneHighestNight", "Value": 25 }], "Dependencies": [{ "Category": "Insane", "Value": 9 }] },
{ "Title": "Half Zerg, Half Protoss, Fully Aweso...", "Description": "Survive Past Night 30", "Value": 15, "Requires": [{ "Type": "InsaneHighestNight", "Value": 30 }], "Dependencies": [{ "Category": "Insane", "Value": 10 }] },
{ "Title": "Killer BeeZ", "Description": "Survive Past Night 35", "Value": 15, "Requires": [{ "Type": "InsaneHighestNight", "Value": 35 }], "Dependencies": [{ "Category": "Insane", "Value": 11 }] },
{ "Title": "Dietary Plan", "Description": "Survive Past Night 40", "Value": 15, "Requires": [{ "Type": "InsaneHighestNight", "Value": 40 }], "Dependencies": [{ "Category": "Insane", "Value": 12 }] },
{ "Title": "Kerri-Gone", "Description": "Survive Past Night 45", "Value": 15, "Requires": [{ "Type": "InsaneHighestNight", "Value": 45 }], "Dependencies": [{ "Category": "Insane", "Value": 13 }] },
{ "Title": "Super Hero", "Description": "Beat the Game On Insane 5 Times! (Testing: If bugged report on forums)", "Value": 15, "Requires": [{ "Type": "InsaneHighestNight", "Value": 50 }, { "Type": "InsaneWin", "Value": 5 }], "Dependencies": [{ "Category": "Insane", "Value": 1 }] }
],
"Easy": [
{ "Title": "Look Ma, I Did It", "Description": "Beat MineralZ on Easy", "Value": 5, "Requires": [{ "Type": "EasyWin", "Value": 1 }] },
{ "Title": "Built To Last", "Description": "Beat Easy Mode Without The Team Ever Salvaging A Structure", "Value": 5, "Requires": [{ "Type": "EasyWin", "Value": 1 }], "Dependencies": [{ "Category": "Easy", "Value": 0 }] },
{ "Title": "I've Got A Receipt", "Description": "Salvage A Tier 7+ Building On Easy Mod", "Value": 5 },
{ "Title": "It's Not Rocket Science", "Description": "Launch A Nuclear Strike On Easy Mode On Night 50", "Value": 5 },
{ "Title": "Speed", "Description": "Buy The First Robotic Boots Upgrade On Easy", "Value": 5 },
{ "Title": "I Feel The Need, The Need For Speed", "Description": "Finish The Robotic Boots Upgrades On Easy", "Value": 5, "Dependencies": [{ "Category": "Easy", "Value": 4 }] },
{ "Title": "Show Off", "Description": "Beat Easy mode without the team ever building a single Refinery", "Value": 5, "Requires": [{ "Type": "EasyWin", "Value": 1 }], "Dependencies": [{ "Category": "Easy", "Value": 0 }] },
{ "Title": "Econ Master", "Description": "Unlock Tier 8 Before Day 30", "Value": 5 },
{ "Title": "Playing It Safe", "Description": "Mine Over 500,000 Resources", "Value": 5 },
{ "Title": "Walking Disco Ball", "Description": "Win with skipping to a Boss night.", "Value": 5 },
{ "Title": "Obelisk The Tormenter", "Description": "Win Without The Team Ever Building A Single Robo Or Cannon.", "Value": 5 },
{ "Title": "Time Trial", "Description": "Reach Day 21 In Less Than An Hour.", "Value": 5 },
{ "Title": "Jack Of All Trades", "Description": "Build One Tier 6 Building Of Every Type", "Value": 5 },
{ "Title": "Simple Is As Simple Does", "Description": "View the second page of the easy achievements.", "Value": 5 },
{ "Title": "That Was Fast", "Description": "Defeat The First Boss Before 10 Mins", "Value": 10 },
{ "Title": "If Things Are Under Control, Go Faster!", "Description": "Defeat The Second Boss Before 10 Mins", "Value": 15, "Dependencies": [{ "Category": "Easy", "Value": 14 }] }
],
"Collections": [
{ "Title": "Hail To The King Baby", "Description": "Collect All Insane Mode Achievements", "Value": 20, "Dependencies": [{ "Category": "Insane", "Value": "all" }] },
{ "Title": "Hardcore", "Description": "Collect All Hard Mode Achievements", "Value": 20, "Dependencies": [{ "Category": "Hard", "Value": "all" }] },
{ "Title": "Joe Dirt", "Description": "Collect All Normal Mode Achievements", "Value": 20, "Dependencies": [{ "Category": "Normal", "Value": "all" }] },
{ "Title": "Cake Walk", "Description": "Collect All Easy Mode Achievements", "Value": 20, "Dependencies": [{ "Category": "Easy", "Value": "all" }] },
{ "Title": "Jumanji 1", "Description": "Collect All General 1 Achievements", "Value": 20, "Dependencies": [{ "Category": "General1", "Value": "all" }] },
{ "Title": "Final Transmission", "Description": "Collect all the Level Achievements", "Value": 20, "Dependencies": [
{ "Category": "General2", "Value": 8 },
{ "Category": "General2", "Value": 9 },
{ "Category": "General2", "Value": 10 },
{ "Category": "General2", "Value": 11 },
{ "Category": "General2", "Value": 12 },
{ "Category": "General2", "Value": 13 },
{ "Category": "General2", "Value": 14 },
{ "Category": "General2", "Value": 15 }
]},
{ "Title": "Extra Credit", "Description": "Collect all the Bonus Mode Achievements", "Value": 20, "Dependencies": [{ "Category": "Bonus", "Value": "all" }] },
{ "Title": "Achievement Whore", "Description": "Collect all the Collections", "Value": 20, "Dependencies": [
{ "Category": "Collections", "Value": 0 },
{ "Category": "Collections", "Value": 1 },
{ "Category": "Collections", "Value": 2 },
{ "Category": "Collections", "Value": 3 },
{ "Category": "Collections", "Value": 4 },
{ "Category": "Collections", "Value": 5 },
{ "Category": "Collections", "Value": 6 },
{ "Category": "Collections", "Value": 8 },
{ "Category": "Collections", "Value": 9 },
{ "Category": "Collections", "Value": 10 },
{ "Category": "Collections", "Value": 11 }
]},
{ "Title": "Jumanji 2", "Description": "Collect All The General 2 Achievements", "Value": 20, "Dependencies": [{ "Category": "General2", "Value": "all" }] },
{ "Title": "Jumanji 3", "Description": "Collect All The General 3 Achievements", "Value": 20, "Dependencies": [{ "Category": "General3", "Value": "all" }] },
{ "Title": "Jumanji 4", "Description": "Collect All The General 4 Achievements", "Value": 20, "Dependencies": [{ "Category": "General4", "Value": "all" }] },
{ "Title": "Jumanji 5", "Description": "Collect All The General 5 Achievements", "Value": 20, "Dependencies": [{ "Category": "General5", "Value": "all" }] }
],
"Normal": [
{ "Title": "Vindicator", "Description": "Beat MineralZ On Normal Mode (Night 50)", "Value": 5, "Requires": [{ "Type": "NormalWin", "Value": 1 }] },
{ "Title": "What Just Happened?", "Description": "Lose MineralZ On Normal Mode Night 50 Boss", "Value": 5 },
{ "Title": "It Was Thiiiis Close", "Description": "Survive A Boss Night With No Crystal Walls When The Boss Dies On Normal Mode", "Value": 5 },
{ "Title": "Blind Trial", "Description": "Unlock Tier 7 On Normal Mode", "Value": 5 },
{ "Title": "Super Duper", "Description": "Unlock tier 8 On Normal Mode", "Value": 5, "Dependencies": [{ "Category": "Normal", "Value": 3 }] },
{ "Title": "Window Shopper", "Description": "Kill The Night 25 Bosses Without The Team Ever Unlocking Tier 7", "Value": 5 },
{ "Title": "Never Gunna Give You Up", "Description": "Beat Normal Mode With 6 Or Less Players", "Value": 5, "Requires": [{ "Type": "NormalWin", "Value": 1 }], "Dependencies": [{ "Category": "Normal", "Value": 0 }] },
{ "Title": "Running On Empty", "Description": "Beat Normal Mode Without Ever Building More Then 30 Cores", "Value": 5, "Requires": [{ "Type": "NormalWin", "Value": 1 }], "Dependencies": [{ "Category": "Normal", "Value": 0 }] },
{ "Title": "I'll Do It Myself!", "Description": "Build both a T6 Robo and a T6 Generator", "Value": 5 },
{ "Title": "Not Needed At All", "Description": "Build at least 5 T6 Walls", "Value": 5 }
],
"General1": [
{ "Title": "Preventive Medicine", "Description": "Heal 250,000 points of damage", "Value": 5, "Requires": [{ "Type": "Heals", "Value": 250000 }] },
{ "Title": "Triage", "Description": "Heal 1,000,000 points of damage", "Value": 5, "Requires": [{ "Type": "Heals", "Value": 1000000 }], "Dependencies": [{ "Category": "General1", "Value": 0 }] },
{ "Title": "Medical Breakthrough", "Description": "Heal 5,000,000 points of damage", "Requires": [{ "Type": "Heals", "Value": 5000000 }], "Value": 5, "Dependencies": [{ "Category": "General1", "Value": 1 }] },
{ "Title": "Dr. No", "Description": "Heal 9,999,999 points of damage", "Value": 5, "Requires": [{ "Type": "Heals", "Value": 9999999 }], "Dependencies": [{ "Category": "General1", "Value": 2 }] },
{ "Title": "Easy Come, Easy Go", "Description": "Kill 5,000 Zerg", "Value": 5, "Requires": [{ "Type": "TotalKills", "Value": 5000 }] },
{ "Title": "Black Ops", "Description": "Kill 15,000 Zerg", "Value": 5, "Requires": [{ "Type": "TotalKills", "Value": 15000 }], "Dependencies": [{ "Category": "General1", "Value": 4 }] },
{ "Title": "Gotta Kill Them All", "Description": "Kill 75,000 Zerg", "Value": 5, "Requires": [{ "Type": "TotalKills", "Value": 75000 }], "Dependencies": [{ "Category": "General1", "Value": 5 }] },
{ "Title": "SyCopath", "Description": "Kill 150,000 Zerg", "Value": 5, "Requires": [{ "Type": "TotalKills", "Value": 150000 }], "Dependencies": [{ "Category": "General1", "Value": 6 }] },
{ "Title": "Winner Winner Chicken Dinner", "Description": "Win More Than 250,000 Crystal in the Casino.", "Value": 5 },
{ "Title": "Next Stop Vegas!", "Description": "Win More Than 250,000 Ruby in the Casino.", "Value": 5 },
{ "Title": "Someone Buy Me a Lottery Ticket Now.", "Description": "Win More Than 250,000 Plasma in the Casino.", "Value": 5 },
{ "Title": "Break the House.", "Description": "Win More Than 250,000 Vespene in the Casino.", "Value": 5 },
{ "Title": "If I Didn't Have Bad Luck I'd Have No Luck...", "Description": "Lose 20,000 Crystal in the Casino", "Value": 5 },
{ "Title": "I Can Quit At Any Time.", "Description": "Lose 20,000 Ruby in the Casino", "Value": 5 },
{ "Title": "20,000? Chump Change", "Description": "Lose 20,000 Plasma in the Casino", "Value": 5 },
{ "Title": "No Whammys!", "Description": "Lose 20,000 Vespene in the Casino", "Value": 5 }
],
"Bonus": [
{ "Title": "Organ Donor", "Description": "Play Bonus Mode", "Value": 10 },
{ "Title": "1 hour photo", "Description": "Beat Bonus Mode", "Value": 10, "Dependencies": [{ "Category": "Bonus", "Value": 0 }] },
{ "Title": "One Man Band", "Description": "Build One Tier 6 Building of Every Type.", "Value": 10 },
{ "Title": "Enron", "Description": "Build 50 Refineries", "Value": 10 },
{ "Title": "Is that all you got?", "Description": "Kill the Final Boss in Bonus Mode", "Value": 10, "Dependencies": [{ "Category": "Bonus", "Value": 1 }] },
{ "Title": "Mine-a-manic", "Description": "Unlock Tier 6 before night 15", "Value": 10 },
{ "Title": "EverLast", "Description": "Beat Bonus Mode Without The Team Ever Salvaging A Structure", "Value": 10, "Dependencies": [{ "Category": "Bonus", "Value": 1 }] },
{ "Title": "CannonWhore", "Description": "Kill the final boss without robotic cannons.", "Value": 10, "Dependencies": [{ "Category": "Bonus", "Value": 1 }] },
{ "Title": "8 Year Anniversary", "Description": "Play Bonus mode during our 8th year anniversary", "Value": 10, "Dependencies": [{ "Category": "Bonus", "Value": 0 }] },
{ "Title": "Blobbyblobber the Bonus Baneling", "Description": "Kill Blobbyblobber the Bonus Baneling", "Value": 10 }
],
"General3": [
{ "Title": "Vindicated", "Description": "Play A Game With Vindicator", "Value": 10 },
{ "Title": "Let's Get SyCo", "Description": "Play A Game With SyCo", "Value": 10 },
{ "Title": "Discord", "Description": "Visit The Discord Server", "Value": 10 },
{ "Title": "It's Alive, It's Alive", "Description": "Use Reintegrate 10 Times In A Single Game", "Value": 10 },
{ "Title": "Get To Work Mule", "Description": "Use A Mule More Than 30 Times In A Single Game", "Value": 10 },
{ "Title": "Right To Business", "Description": "Be Safe From The Zerg Within The First 25 Seconds", "Value": 10 },
{ "Title": "That's Just Plain Crazy", "Description": "Survive Past Night 4 Without Ever Being Safe", "Value": 10 },
{ "Title": "Non-Violent", "Description": "Don't Break Any Rocks", "Value": 10 },
{ "Title": "Over Prepared", "Description": "Have 50 Refineries Up By Day 5 And Be Alive On Day 6", "Value": 10 },
{ "Title": "Perfect Storm", "Description": "Use Psi Storm over 15 times in one game.", "Value": 5 },
{ "Title": "Make Great Again", "Description": "Use restore over 30 times in one game", "Value": 5 },
{ "Title": "PayDay", "Description": "Use mineral bonus over 30 times in one game", "Value": 5 },
{ "Title": "Try Hard", "Description": "Beat the game without the team ever building a wall", "Value": 30 },
{ "Title": "VanillaBean", "Description": "Beat Vanilla Mode", "Value": 30 },
{ "Title": "Hardcore Miner", "Description": "Upgrade to +50", "Value": 10 },
{ "Title": "Elite Miner", "Description": "Upgrade to +100", "Value": 20 }
],
"General4": [
{ "Title": "Mr. Ben Franklin", "Description": "Generate Over 5,000,000 Energy", "Value": 5, "Requires": [{ "Type": "Gens", "Value": 5000000 }] },
{ "Title": "5 Redbulls", "Description": "Generate Over 50,000,000 Energy", "Value": 5, "Requires": [{ "Type": "Gens", "Value": 50000000 }], "Dependencies": [{ "Category": "General4", "Value": 0 }] },
{ "Title": "Radio-Active", "Description": "Generate Over 500,000,000 Energy", "Value": 5, "Requires": [{ "Type": "Gens", "Value": 500000000 }], "Dependencies": [{ "Category": "General4", "Value": 1 }] },
{ "Title": "That Makes My Hair Stand Up", "Description": "Generate Over 750,000,000 Energy", "Value": 5, "Requires": [{ "Type": "Gens", "Value": 750000000 }], "Dependencies": [{ "Category": "General4", "Value": 2 }] },
{ "Title": "We Have Gone Nuclear", "Description": "Generate Over 1,000,000,000 Energy", "Value": 5, "Requires": [{ "Type": "Gens", "Value": 1000000000 }], "Dependencies": [{ "Category": "General4", "Value": 3 }] },
{ "Title": "I'm Giving It All She's Got Captain", "Description": "Generate Over 2,000,000,000 Energy", "Value": 5, "Requires": [{ "Type": "Gens", "Value": 2000000000 }], "Dependencies": [{ "Category": "General4", "Value": 4 }] },
{ "Title": "Here's A Bandaid", "Description": "Heal 100,000,000 points of damage", "Value": 5, "Requires": [{ "Type": "Heals", "Value": 100000000 }] },
{ "Title": "It's Just A Scratch", "Description": "Heal 250,000,000 points of damage", "Value": 5, "Requires": [{ "Type": "Heals", "Value": 250000000 }], "Dependencies": [{ "Category": "General4", "Value": 6 }] },
{ "Title": "I'm Not A Doctor, But I Play One On MineralZ", "Description": "Heal 500,000,000 points of damage", "Value": 5, "Requires": [{ "Type": "Heals", "Value": 500000000 }], "Dependencies": [{ "Category": "General4", "Value": 7 }] },
{ "Title": "I Am The Hospital", "Description": "Heal 1,000,000,000 points of damage", "Value": 5, "Requires": [{ "Type": "Heals", "Value": 1000000000 }], "Dependencies": [{ "Category": "General4", "Value": 8 }] },
{ "Title": "The Healing Touch", "Description": "Heal 1,500,000,000 points of damage", "Value": 5, "Requires": [{ "Type": "Heals", "Value": 1500000000 }], "Dependencies": [{ "Category": "General4", "Value": 9 }] },
{ "Title": "Mother Teresa", "Description": "Heal 2,000,000,000 points of damage", "Value": 5, "Requires": [{ "Type": "Heals", "Value": 2000000000 }], "Dependencies": [{ "Category": "General4", "Value": 10 }] },
{ "Title": "That Tickled", "Description": "Absorb Over 5,000,000 Damage", "Value": 5, "Requires": [{ "Type": "Absorbed", "Value": 5000000 }] },
{ "Title": "Just A Ding", "Description": "Absorb Over 50,000,000 Damage", "Value": 5, "Requires": [{ "Type": "Absorbed", "Value": 50000000 }], "Dependencies": [{ "Category": "General4", "Value": 12 }] },
{ "Title": "Like A Rock", "Description": "Absorb Over 100,000,000 Damage", "Value": 5, "Requires": [{ "Type": "Absorbed", "Value": 100000000 }], "Dependencies": [{ "Category": "General4", "Value": 13 }] },
{ "Title": "You Hit Like My Grandma", "Description": "Absorb Over 500,000,000 Damage", "Value": 5, "Requires": [{ "Type": "Absorbed", "Value": 500000000 }], "Dependencies": [{ "Category": "General4", "Value": 14 }] }
],
"General5": [
{ "Title": "I'm A Brick Wall", "Description": "Absorb Over 750,000,000 Damage", "Value": 5, "Requires": [{ "Type": "Absorbed", "Value": 750000000 }], "Dependencies": [{ "Category": "General4", "Value": 15 }] },
{ "Title": "Owwie, Just Kidding", "Description": "Absorb Over 1,000,000,000 Damage", "Value": 5, "Requires": [{ "Type": "Absorbed", "Value": 1000000000 }], "Dependencies": [{ "Category": "General5", "Value": 0 }] },
{ "Title": "You Can Have The First Hit When You Hi...", "Description": "Absorb Over 1,500,000,000 Damage", "Value": 5, "Requires": [{ "Type": "Absorbed", "Value": 1500000000 }], "Dependencies": [{ "Category": "General5", "Value": 1 }] },
{ "Title": "I'm Still Standing!", "Description": "Absorb Over 2,000,000,000 Damage", "Value": 5, "Requires": [{ "Type": "Absorbed", "Value": 2000000000 }], "Dependencies": [{ "Category": "General5", "Value": 2 }] },
{ "Title": "Mining Achievement1", "Description": "Mine over 250,000", "Value": 5, "Requires": [{ "Type": "Mined", "Value": 250000 }] },
{ "Title": "Mining Achievement2", "Description": "Mine over 500,000", "Value": 5, "Requires": [{ "Type": "Mined", "Value": 500000 }], "Dependencies": [{ "Category": "General5", "Value": 4 }] },
{ "Title": "Mining Achievement3", "Description": "Mine over 750,000", "Value": 5, "Requires": [{ "Type": "Mined", "Value": 750000 }], "Dependencies": [{ "Category": "General5", "Value": 5 }] },
{ "Title": "Mining Achievement4", "Description": "Mine over 1,000,000", "Value": 5, "Requires": [{ "Type": "Mined", "Value": 1000000 }], "Dependencies": [{ "Category": "General5", "Value": 6 }] },
{ "Title": "Mining Achievement5", "Description": "Mine over 5,000,000", "Value": 5, "Requires": [{ "Type": "Mined", "Value": 5000000 }], "Dependencies": [{ "Category": "General5", "Value": 7 }] },
{ "Title": "Mining Achievement6", "Description": "Mine over 10,000,000", "Value": 5, "Requires": [{ "Type": "Mined", "Value": 10000000 }], "Dependencies": [{ "Category": "General5", "Value": 8 }] },
{ "Title": "Mining Achievement7", "Description": "Mine over 50,000,000", "Value": 5, "Requires": [{ "Type": "Mined", "Value": 50000000 }], "Dependencies": [{ "Category": "General5", "Value": 9 }] },
{ "Title": "Mining Achievement8", "Description": "Mine over 100,000,000", "Value": 10, "Requires": [{ "Type": "Mined", "Value": 100000000 }], "Dependencies": [{ "Category": "General5", "Value": 10 }] },
{ "Title": "Mining Achievement9", "Description": "Mine over 250,000,000", "Value": 10, "Requires": [{ "Type": "Mined", "Value": 250000000 }], "Dependencies": [{ "Category": "General5", "Value": 11 }] },
{ "Title": "Mining Achievement10", "Description": "Mine over 500,000,000", "Value": 15, "Requires": [{ "Type": "Mined", "Value": 500000000 }], "Dependencies": [{ "Category": "General5", "Value": 12 }] },
{ "Title": "Mining Achievement11", "Description": "Mine over 1,000,000,000", "Value": 15, "Requires": [{ "Type": "Mined", "Value": 1000000000 }], "Dependencies": [{ "Category": "General5", "Value": 13 }] },
{ "Title": "Mining Achievement12", "Description": "Mine over 2,000,000,000", "Value": 15, "Requires": [{ "Type": "Mined", "Value": 2000000000 }], "Dependencies": [{ "Category": "General5", "Value": 14 }] }
],
"Nightmare": [
{ "Title": "Survive Night 14", "Description": "Survive Night 14", "Value": 5 },
{ "Title": "Survive Night 19", "Description": "Survive Night 19", "Value": 5, "Dependencies": [{ "Category": "Nightmare", "Value": 0 }] },
{ "Title": "Survive Night 24", "Description": "Survive Night 24", "Value": 5, "Dependencies": [{ "Category": "Nightmare", "Value": 1 }] },
{ "Title": "Survive Night 29", "Description": "Survive Night 29", "Value": 5, "Dependencies": [{ "Category": "Nightmare", "Value": 2 }] },
{ "Title": "Survive Night 34", "Description": "Survive Night 34", "Value": 5, "Dependencies": [{ "Category": "Nightmare", "Value": 3 }] },
{ "Title": "Survive Night 39", "Description": "Survive Night 39", "Value": 5, "Dependencies": [{ "Category": "Nightmare", "Value": 4 }] }
]
};
export function getAchievements() {
return cheevos;
}
export function getAchievementsByCategory(category) {
return cheevos[category] || null;
}
export function getAchievement(category, index) {
return cheevos[category][index] || null;
}
export function getAchievementCount(category) {
return cheevos[category] ? cheevos[category].length : 0;
}
const Base = await import(`./minzEvoBase.js`);
Base.setModuleConfig({
modName: 'MinzEvoEU',
mapName: 'minzEvoEU'
});
export function init()
{
return Base.init();
}
export const MinzEvoEUModule = {
getModName: Base.getModName,
getMapName: Base.getMapName,
getAchievements: Base.getAchievements,
getAchievementsByCategory: Base.getAchievementsByCategory,
getAchievement: Base.getAchievement,
getAchievementCount: Base.getAchievementCount,
tallyAchievements: Base.tallyAchievements,
rebuildGraphicalWindowContent: Base.rebuildGraphicalWindowContent,
checkLevelCheevos: Base.checkLevelCheevos,
handleLevelCheevos: Base.handleLevelCheevos,
checkTotalAbsorbed: Base.checkTotalAbsorbed,
handleAbsorbCheevos: Base.handleAbsorbCheevos,
checkTotalGenerated: Base.checkTotalGenerated,
handleGeneratedCheevos: Base.handleGeneratedCheevos,
checkTotalHealed: Base.checkTotalHealed,
handleHealedCheevos: Base.handleHealedCheevos,
checkTotalMined: Base.checkTotalMined,
handleMinedCheevos: Base.handleMinedCheevos,
checkTimePlayed: Base.checkTimePlayed,
handleTimeCheevos: Base.handleTimeCheevos,
checkTotalKills: Base.checkTotalKills,
handleKillsCheevos: Base.handleKillsCheevos,
handleGamesPlayed: Base.handleGamesPlayed,
checkEasyWins: Base.checkEasyWins,
handleEasyWins: Base.handleEasyWins,
checkNormalWins: Base.checkNormalWins,
handleNormalWins: Base.handleNormalWins,
checkHardWins: Base.checkHardWins,
handleHardWins: Base.handleHardWins,
handleHard2Wins: Base.handleHard2Wins,
checkInsanePlayed: Base.checkInsanePlayed,
checkInsaneCheevos: Base.checkInsaneCheevos,
handleInsaneWins: Base.handleInsaneWins,
handleInsane2Wins: Base.handleInsane2Wins,
handleInsane3Wins: Base.handleInsane3Wins,
checkNightmareCheevos: Base.checkNightmareCheevos,
checkCollectionsCheevos: Base.checkCollectionsCheevos,
validateAndRebuildSave: Base.validateAndRebuildSave
};
const Base = await import(`./minzEvoBase.js`);
Base.setModuleConfig({
modName: 'MinzEvoKR',
mapName: 'minzEvoKR'
});
export function init()
{
return Base.init();
}
export const MinzEvoKRModule = {
getModName: Base.getModName,
getMapName: Base.getMapName,
getAchievements: Base.getAchievements,
getAchievementsByCategory: Base.getAchievementsByCategory,
getAchievement: Base.getAchievement,
getAchievementCount: Base.getAchievementCount,
tallyAchievements: Base.tallyAchievements,
rebuildGraphicalWindowContent: Base.rebuildGraphicalWindowContent,
checkLevelCheevos: Base.checkLevelCheevos,
handleLevelCheevos: Base.handleLevelCheevos,
checkTotalAbsorbed: Base.checkTotalAbsorbed,
handleAbsorbCheevos: Base.handleAbsorbCheevos,
checkTotalGenerated: Base.checkTotalGenerated,
handleGeneratedCheevos: Base.handleGeneratedCheevos,
checkTotalHealed: Base.checkTotalHealed,
handleHealedCheevos: Base.handleHealedCheevos,
checkTotalMined: Base.checkTotalMined,
handleMinedCheevos: Base.handleMinedCheevos,
checkTimePlayed: Base.checkTimePlayed,
handleTimeCheevos: Base.handleTimeCheevos,
checkTotalKills: Base.checkTotalKills,
handleKillsCheevos: Base.handleKillsCheevos,
handleGamesPlayed: Base.handleGamesPlayed,
checkEasyWins: Base.checkEasyWins,
handleEasyWins: Base.handleEasyWins,
checkNormalWins: Base.checkNormalWins,
handleNormalWins: Base.handleNormalWins,
checkHardWins: Base.checkHardWins,
handleHardWins: Base.handleHardWins,
handleHard2Wins: Base.handleHard2Wins,
checkInsanePlayed: Base.checkInsanePlayed,
checkInsaneCheevos: Base.checkInsaneCheevos,
handleInsaneWins: Base.handleInsaneWins,
handleInsane2Wins: Base.handleInsane2Wins,
handleInsane3Wins: Base.handleInsane3Wins,
checkNightmareCheevos: Base.checkNightmareCheevos,
checkCollectionsCheevos: Base.checkCollectionsCheevos,
validateAndRebuildSave: Base.validateAndRebuildSave
};
const Base = await import(`./minzEvoBase.js`);
Base.setModuleConfig({
modName: 'MinzEvoNA',
mapName: 'minzEvoNA'
});
export function init()
{
return Base.init();
}
export const MinzEvoNAModule = {
getModName: Base.getModName,
getMapName: Base.getMapName,
getAchievements: Base.getAchievements,
getAchievementsByCategory: Base.getAchievementsByCategory,
getAchievement: Base.getAchievement,
getAchievementCount: Base.getAchievementCount,
tallyAchievements: Base.tallyAchievements,
rebuildGraphicalWindowContent: Base.rebuildGraphicalWindowContent,
checkLevelCheevos: Base.checkLevelCheevos,
handleLevelCheevos: Base.handleLevelCheevos,
checkTotalAbsorbed: Base.checkTotalAbsorbed,
handleAbsorbCheevos: Base.handleAbsorbCheevos,
checkTotalGenerated: Base.checkTotalGenerated,
handleGeneratedCheevos: Base.handleGeneratedCheevos,
checkTotalHealed: Base.checkTotalHealed,
handleHealedCheevos: Base.handleHealedCheevos,
checkTotalMined: Base.checkTotalMined,
handleMinedCheevos: Base.handleMinedCheevos,
checkTimePlayed: Base.checkTimePlayed,
handleTimeCheevos: Base.handleTimeCheevos,
checkTotalKills: Base.checkTotalKills,
handleKillsCheevos: Base.handleKillsCheevos,
handleGamesPlayed: Base.handleGamesPlayed,
checkEasyWins: Base.checkEasyWins,
handleEasyWins: Base.handleEasyWins,
checkNormalWins: Base.checkNormalWins,
handleNormalWins: Base.handleNormalWins,
checkHardWins: Base.checkHardWins,
handleHardWins: Base.handleHardWins,
handleHard2Wins: Base.handleHard2Wins,
checkInsanePlayed: Base.checkInsanePlayed,
checkInsaneCheevos: Base.checkInsaneCheevos,
handleInsaneWins: Base.handleInsaneWins,
handleInsane2Wins: Base.handleInsane2Wins,
handleInsane3Wins: Base.handleInsane3Wins,
checkNightmareCheevos: Base.checkNightmareCheevos,
checkCollectionsCheevos: Base.checkCollectionsCheevos,
validateAndRebuildSave: Base.validateAndRebuildSave
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment