The Tree Enchanted丨被附魔的树

针对 TMT 的自定义增强功能,为移动端和桌面端分别添加了超级便捷的QOL按钮!

// ==UserScript==
// @name The Tree Enchanted丨被附魔的树
// @name:zh-TW The Tree Enchanted丨被附魔的樹
// @name:en The Tree Enchanted
// @namespace http://tampermonkey.net/
// @version 2.7.0
// @description  针对 TMT 的自定义增强功能,为移动端和桌面端分别添加了超级便捷的QOL按钮!
// @description:en Enhanced QOL features for TMT, adding convenient buttons for both mobile and desktop!
// @description:zh-TW 針對 TMT 的自訂增強功能,為行動端和桌面端分別添加了超級便利的QOL按鈕!
// @author       LinLei_Baruch & Google Gemini 2.5 Pro
// @license MIT
// @original-author Dimava & Assistant
// @original-license 暂无
// @original-script https://greasyfork.org/zh-CN/scripts/425404-the-tree-enchanted
// @match        *://*/*
// @icon         https://img.cdn1.vip/i/68d4f4068bac5_1758786566.png
// @grant        none
// ==/UserScript==


if (globalThis.TREE_LAYERS) void function() {
    const isNewTmtVersion = typeof TMT_VERSION !== 'undefined' && TMT_VERSION.newtmtNum;

    const translations = {
        en: {
            buy: "Buy (X)",
            reset: "Reset (Z)",
            combo: "Buy & Reset (C)",
            setSpeed: "Set Speed",
            importSave: "Import Save File",
            exportSave: "Export Save File",
            setDelay: "Set Delay ({delay}ms)",
            autoBuy: "Auto Buy: {status}",
            autoReset: "Auto Reset: {status}",
            autoCombo: "Auto Buy & Reset: {status}",
            fontToggle: "System Font: {status}",
            shortcuts: "Hotkeys: {status}",
            on: "ON",
            off: "OFF",
            hidePanel: "Hide Panel",
            showPanel: "Show Panel",
            speedPrompt: "Enter game speed (e.g., 10).\nNote 1: High speeds may cause bugs.\nNote 2: Some mods have anti-cheat and this may not work.\nNote 3: It is recommended to back up your save before changing the speed.",
            speedInvalid: "Invalid input. Please enter a number!",
            exportError: "An error occurred while exporting the save!",
            exportNaN: "Save data is abnormal (NaN) and cannot be exported! Please refresh the page and try again.",
            importFromFile: "Import from File",
            importError: "Import failed! The file may be corrupted or in the wrong format.\nError details: {error}",
            importEmpty: "File is empty or could not be read.",
            importLzError: "Detected standard game save, but couldn't find the in-game importSave function!",
            importManualError: "Decoded successfully, but missing core game functions needed for manual loading.",
            importInvalidFormat: "The file content is not a valid save format.",
            importReadError: "An error occurred while reading the file!",
            delayPrompt: "Enter long-press/auto-action delay (0-1000ms).\nNote: When using auto-actions, the delay is capped at a minimum of 16ms to prevent browser freezing if you set it lower.",
            delayInvalid: "Invalid input! Please enter a number between 0 and 1000."
        },
        'zh-CN': {
            buy: "购买 (X)",
            reset: "重置 (Z)",
            combo: "购买&重置 (C)",
            setSpeed: "设定速度",
            importSave: "导入存档文件",
            exportSave: "导出存档文件",
            setDelay: "设定延迟({delay}ms)",
            autoBuy: "自动购买: {status}",
            autoReset: "自动重置: {status}",
            autoCombo: "自动购买&重置: {status}",
            fontToggle: "系统字体: {status}",
            shortcuts: "快捷键: {status}",
            on: "开",
            off: "关",
            hidePanel: "隐藏面板",
            showPanel: "显示面板",
            speedPrompt: "请输入游戏速度 (例如: 10)丨注意1:不建议设定过快的游戏速度,否则可能会出bug丨注意2:部分模组树存在无法生效的情况,那就是有防作弊丨注意3:输入之前,建议你备份存档,否则出了什么问题,你会骂死我的qwq……",
            speedInvalid: "输入无效,请输入一个数字!",
            exportError: "导出存档时发生错误!",
            exportNaN: "存档数据异常 (NaN),无法导出!请刷新页面后再试。",
            importFromFile: "从文件导入",
            importError: "导入失败!文件可能已损坏或格式不正确。\n错误详情: {error}",
            importEmpty: "文件为空或无法读取。",
            importLzError: "检测到标准游戏存档,但未找到游戏内的 importSave 函数!",
            importManualError: "解码成功,但缺少手动加载所需的游戏核心函数。",
            importInvalidFormat: "文件内容不是有效的存档格式。",
            importReadError: "读取文件时发生错误!",
            delayPrompt: "请输入长按/自动执行的延迟(0-1000毫秒)丨注意:使用自动时,会自动设定延迟为16ms(当你设定延迟低于16ms时),以免浏览器卡死!",
            delayInvalid: "输入无效!请输入一个0到1000之间的数字。"
        },
        'zh-TW': {
            buy: "購買 (X)",
            reset: "重置 (Z)",
            combo: "購買並重置 (C)",
            setSpeed: "設定速度",
            importSave: "匯入存檔檔案",
            exportSave: "匯出存檔檔案",
            setDelay: "設定延遲({delay}ms)",
            autoBuy: "自動購買: {status}",
            autoReset: "自動重置: {status}",
            autoCombo: "自動購買並重置: {status}",
            fontToggle: "系統字體: {status}",
            shortcuts: "快捷鍵: {status}",
            on: "開",
            off: "關",
            hidePanel: "隱藏面板",
            showPanel: "顯示面板",
            speedPrompt: "請輸入遊戲速度(例如:10)。\n注意1:不建議設定過快的遊戲速度,否則可能導致錯誤。\n注意2:部分模組樹有防作弊機制,可能無法生效。\n注意3:建議在變更前備份您的存檔。",
            speedInvalid: "輸入無效,請輸入一個數字!",
            exportError: "匯出存檔時發生錯誤!",
            exportNaN: "存檔資料異常(NaN),無法匯出!請重新整理頁面後再試。",
            importFromFile: "從檔案匯入",
            importError: "匯入失敗!檔案可能已損壞或格式不正確。\n錯誤詳情: {error}",
            importEmpty: "檔案為空或無法讀取。",
            importLzError: "偵測到標準遊戲存檔,但找不到遊戲內的 importSave 函數!",
            importManualError: "解碼成功,但缺少手動載入所需的遊戲核心函數。",
            importInvalidFormat: "檔案內容不是有效的存檔格式。",
            importReadError: "讀取檔案時發生錯誤!",
            delayPrompt: "請輸入長按/自動執行的延遲(0-1000毫秒)。\n注意:使用自動功能時,若您設定的延遲低於16毫秒,將自動設為16毫秒,以防瀏覽器卡死。",
            delayInvalid: "輸入無效!請輸入一個0到1000之間的數字。"
        }
    };

    let currentLang = 'en';
    const browserLang = navigator.language;
    if (browserLang.startsWith('zh-TW') || browserLang.startsWith('zh-HK')) {
        currentLang = 'zh-TW';
    } else if (browserLang.startsWith('zh')) {
        currentLang = 'zh-CN';
    }

    function getText(key, replacements = {}) {
        let text = (translations[currentLang] && translations[currentLang][key]) || translations.en[key] || `[${key}]`;
        for (const placeholder in replacements) {
            text = text.replace(`{${placeholder}}`, replacements[placeholder]);
        }
        return text;
    }

	function __init__() {
		q = s => document.querySelector(s);
		qq = s => [...document.querySelectorAll(s)];
		elm = function elm(sel = '', ...children) {
			let el = document.createElement('div');
			sel.replace(/([\w-]+)|#([\w-]+)|\.([\w-]+)|\[([^\]=]+)(?:=([^\]]*))\]/g, (s, tag, id, cls, attr, val) => {
				if (tag) el = document.createElement(tag);
				if (id) el.id = id;
				if (cls) el.classList.add(cls);
				if (attr) el.setAttribute(attr, val ?? true);
			});
			for (let f of children.filter(e => typeof e == 'function')) {
				let name = f.name || (f + '').match(/\w+/);
				if (!name.startsWith('on')) name = 'on' + name;
				el[name] = f;
			}
			el.append(...children.filter(e => typeof e != 'function'));
			return el;
		}
		Object.defineValue = function defineValue(o, p, value) {
			if (typeof p == 'function') {
				[p, value] = [p.name, p];
			}
			return Object.defineProperty(o, p, {
				value,
				configurable: true,
				enumerable: false,
				writable: true,
			});
		};
		Object.defineValue(Element.prototype, function q(sel) {
			return this.querySelector(sel);
		});
		Object.defineValue(Element.prototype, function qq(sel) {
			return [...this.querySelectorAll(sel)];
		});
		Object.map = function(o, mapper) {
			return Object.fromEntries(Object.entries(o).map(([k, v]) => [k, mapper(v, k, o)]));
		}
		Array.map = function map(length, mapper = i => i) {
			return Array(length).fill(0).map((e, i, a) => mapper(i));
		}
	}
	window.__init__ || __init__();

    const SETTINGS_PREFIX = 'theTreeEnchanted_';
    const getSetting = (key, defaultValue) => localStorage.getItem(SETTINGS_PREFIX + key) ?? defaultValue;
    const setSetting = (key, value) => localStorage.setItem(SETTINGS_PREFIX + key, value);

    let areShortcutsEnabled = getSetting('shortcutsEnabled', 'true') === 'true';
    let longPressDelay = parseInt(getSetting('longPressDelay', '100'), 10);
    let autoBuyActive = getSetting('autoBuyActive', 'false') === 'true';
    let autoResetActive = getSetting('autoResetActive', 'false') === 'true';
    let autoComboActive = getSetting('autoComboActive', 'false') === 'true';
    let forceSystemFont = getSetting('forceSystemFont', 'false') === 'true';

	function performReset() {
		q('.reset.can')?.click();
	}

	function performBuyAll() {
		for (let e of qq(`
					.upg.can:not(.reset), .buyable.can:not(.reset),
					.canComplete .longUpg
				`).reverse()) {
			e.click();
		}
	}

    function performBuyAndReset() {
        performBuyAll();
        setTimeout(performReset, 10);
    }

    function setGameSpeed() {
        const currentSpeed = (typeof player.devSpeed === 'number') ? player.devSpeed : 1;
        const input = prompt(getText('speedPrompt'), currentSpeed);

        if (input === null) {
            return;
        }

        const newSpeed = parseFloat(input);

        if (isNaN(newSpeed)) {
            alert(getText('speedInvalid'));
            return;
        }

        player.devSpeed = newSpeed;
        console.log(`游戏速度已设置为: ${player.devSpeed}`);
    }

    function exportSaveAsFile() {
        if (typeof NaNcheck === 'function') {
            NaNcheck(player);
            if (window.NaNalert) {
                alert(getText('exportNaN'));
                return;
            }
        }

        try {
            let saveData;
            if (typeof LZString !== 'undefined' && typeof LZString.compressToBase64 === 'function') {
                console.log("使用 LZString 压缩存档。");
                saveData = LZString.compressToBase64(JSON.stringify(player));
            } else {
                console.warn("LZString 未找到,回退到 btoa 进行编码。");
                saveData = btoa(JSON.stringify(player));
            }

            const blob = new Blob([saveData], { type: "text/plain;charset=utf-8" });
            const url = URL.createObjectURL(blob);
            const a = document.createElement("a");
            a.href = url;

            const date = new Date();
            const formattedDate = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
            const fileName = (typeof modInfo !== 'undefined' && modInfo.id)
                ? `${modInfo.id}_save_${formattedDate}.txt`
                : `TMT_save_${formattedDate}.txt`;
            a.download = fileName;

            document.body.appendChild(a);
            a.click();

            setTimeout(() => {
                document.body.removeChild(a);
                URL.revokeObjectURL(url);
            }, 0);

        } catch (e) {
            alert(getText('exportError'));
            console.error("导出存档失败:", e);
        }
    }

    function importSaveFromFile() {
        const fileInput = document.createElement('input');
        fileInput.type = 'file';
        fileInput.accept = '.txt,text/plain';
        fileInput.style.display = 'none';

        fileInput.onchange = event => {
            const file = event.target.files[0];
            if (!file) return;

            const reader = new FileReader();

            reader.onload = function(e) {
                try {
                    const saveDataString = e.target.result;
                    if (!saveDataString || saveDataString.trim() === '') {
                        throw new Error(getText('importEmpty'));
                    }

                    if (saveDataString.startsWith("N4IgLghg")) {
                        console.log("检测到 LZString 格式存档,使用游戏原生 importSave 函数导入。");
                        if (typeof importSave === 'function') {
                            importSave(saveDataString);
                            return;
                        } else {
                            throw new Error(getText('importLzError'));
                        }
                    }

                    console.log("未检测到 LZString 格式,尝试作为 Base64 (btoa) 格式解码。");
                    const decodedJson = atob(saveDataString);

                    if (decodedJson.trim().startsWith('{')) {
                         console.log("Base64 解码成功,手动执行加载流程。");
                        if (typeof getStartPlayer === 'function' && typeof fixSave === 'function' && typeof versionCheck === 'function' && typeof save === 'function') {
                            const tempPlr = Object.assign(getStartPlayer(), JSON.parse(decodedJson));
                            player = tempPlr;
                            fixSave();
                            versionCheck();
                            save();
                            window.location.reload();
                        } else {
                            throw new Error(getText('importManualError'));
                        }
                    } else {
                        throw new Error(getText('importInvalidFormat'));
                    }

                } catch (error) {
                    alert(getText('importError', { error: error.message }));
                    console.error("存档导入失败:", error);
                }
            };

            reader.onerror = function() {
                alert(getText('importReadError'));
                console.error("FileReader error:", reader.error);
            };

            reader.readAsText(file);
        };

        document.body.appendChild(fileInput);
        fileInput.click();
        document.body.removeChild(fileInput);
    }

	let keysdown = {};
	function onraf() {
		if (keysdown.x) {

		}
	}
	void async function() {
		while(true) {
			await Promise.frame();
			onraf();
		}
	}

	addEventListener('keydown', async event=>{
        if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') return;

        if (areShortcutsEnabled) {
            if (event.key == 'z' || event.key == 'Z') {
                performReset();
            }
            if (event.key == 'x' || event.key == 'X') {
                performBuyAll();
            }
            if (event.key == 'c' || event.key == 'C') {
                performBuyAndReset();
            }
        }

		let treeNode = qq('.treeNode.can:not(.ghost)').find(e=>e.innerText.startsWith(event.key))
		treeNode?.click();

		if (event.key == 'Tab') {
			let tabs = qq('.tabButton');
			let i = tabs.findIndex(e=>e.innerText.includes(player.subtabs[player.tab].mainTabs)) + 1;
            if (event.shiftKey) i += tabs.length - 2;
			tabs[i % tabs.length].click();
			event.preventDefault();
		}

        switch(event.key) {
            case 'ArrowUp': ArrowLayerMove.moveUp(); break;
            case 'ArrowLeft': ArrowLayerMove.moveLeft(); break;
            case 'ArrowDown': ArrowLayerMove.moveDown(); break;
            case 'ArrowRight': ArrowLayerMove.moveRight(); break;
        }

	});


	Layer = class {
		static hasAllUpgrades(layer) {
			return Object.values(tmp[layer].upgrades).every(e => !hasUpgrade(layer, e.id));
		}
		static status(layerId) {
			let layer = tmp[layerId];
			let ups = Object.values(layer.upgrades || {}).filter(e => e.id);
			let ms = Object.values(layer.milestones || {}).filter(e => e.id);
			let cha = Object.values(layer.challenges || {}).filter(e => e.id);
			return {
				upgrades: {
					total: ups.length,
					unlocked: ups.filter(e => e.unlocked || hasUpgrade(layerId, e.id)).length,
					done: ups.filter(e => hasUpgrade(layerId, e.id)).length,
					available: ups.filter(e => e.unlocked && !hasUpgrade(layerId, e.id) && canAffordUpgrade(layerId, e.id)).length,
				},
				milestones: {
					total: ms.length,
					unlocked: ms.filter(e => e.unlocked).length,
					done: ms.filter(e => e.done).length,
				},

				challenges: {
					total: cha.length,
					unlocked: cha.filter(e => e.unlocked).length,
					done: cha.filter(e => player[layerId].challenges[e.id] >= e.completionLimit).length,
					active: !!player[layerId].activeChallenge,
					canComplete: player[layerId].activeChallenge && canCompleteChallenge(layerId, player[layerId].activeChallenge),
				},
			}
		}
		static shortStatus(layerId) {
			let s = this.status(layerId);
			return {
				upgrades: `${s.upgrades.bought}/${s.upgrades.unlocked}/${s.upgrades.total}`,
			}
		}
		static showStars(layerId) {
			function star(color, empty) {
				return elm(`.statusStar[style=color:${color};]`, typeof empty == 'string' ? empty : empty ? '☆' : '★')
			}
			let node = q(`.treeNode[class~="${layerId}"]`);
			if (!node) return

			let s = this.status(layerId);
			let ups = s.upgrades;
			let ms = s.milestones;
			let cha = s.challenges;

			ups = ups.total && star(ups.available ? 'yellowgreen' : ups.unlocked < ups.total ? 'silver' : 'gold', ups.done < ups.unlocked);
			ms = ms.total && star(ms.unlocked < ms.total ? 'silver' : 'gold', ms.done < ms.unlocked);
			cha = cha.total && star(cha.active ? 'red' : cha.unlocked < cha.total ? 'silver' : 'gold', cha.done < cha.unlocked && !cha.canComplete);

			let sel = player.tab == layerId && star('white', '\xa0\xa0^');

            const starElements = isNewTmtVersion
                ? [cha, ms, ups, sel].filter(Boolean)
                : [sel, cha, ms, ups].filter(Boolean);

			let container = elm('.sscon', ...starElements);

			let oldContainer = node.q('.sscon');
			if (!oldContainer) {
				node.append(container);
			} else if (oldContainer.outerHTML != container.outerHTML) {
				oldContainer.replaceWith(container);
			}
		}
		static showAllStars() {
			Object.values(tmp)
            .filter(e => e && e.layerShown === true)
			.map(e => this.showStars(e.layer));
		}
	}

	window.layInt && clearInterval(layInt)
	layInt = setInterval(() => Layer.showAllStars(), 200)

	q('head').append(elm('style', `
		#the-tree-enchanted-font-override { /* 用于强制系统字体的样式ID */ }
		.sscon {
			position:absolute;
			font-size: 33.333%;
			font-family: initial;
			text-shadow: 0 0 4px gray, 0 0 3px black;
			display: flex;
            pointer-events: none;

            ${isNewTmtVersion ? `
                bottom: 0;
                right: 0;
                flex-direction: row;
            ` : `
                flex-direction: row-reverse;
            `}
		}
		.statusStar {
			display: inline-block;
			transform: scale(3);
		}
		.tabButton {
			position: relative;
		}
		.tscon {
			position: relative;
			display: inline-block;
			left: 9px;
		}
		.tabStar {
			display: inline-block;
		}
        #mobile-qol-container {
            position: fixed;
            bottom: 10px;
            left: 50%;
            transform: translateX(-50%);
            z-index: 10001; /* 确保UI在最上层 */
            display: flex;
            flex-direction: column-reverse;
            align-items: center;
            gap: 5px;
        }
        #mobile-controls {
            display: flex;
            flex-wrap: wrap;
            justify-content: center;
            gap: 8px;
            max-width: 95vw;
        }
        #mobile-controls.hidden {
            display: none;
        }
        #mobile-controls button {
            width: 150px;
            height: 70px;
            font-size: 18px; 
            font-weight: bold;
            background-color: rgba(0, 0, 0, 0.7);
            color: white;
            border: 2px solid #888;
            border-radius: 10px;
            cursor: pointer;
            display: flex;
            justify-content: center;
            align-items: center;
            -webkit-user-select: none;
            -moz-user-select: none;
            -ms-user-select: none;
            user-select: none;
            flex-shrink: 0;
            word-break: break-word;
            padding: 2px;
            box-sizing: border-box;
            line-height: 1.1; 
        }
        #toggle-mobile-qol-button {
            padding: 5px 15px;
            font-size: 16px;
            font-weight: bold;
            background-color: rgba(50, 50, 50, 0.8);
            color: white;
            border: 1px solid #aaa;
            border-radius: 8px;
            cursor: pointer;
            width: 100px;
        }
        #desktop-qol-container {
            position: fixed;
            bottom: 15px;
            left: 15px;
            z-index: 10001; /* 确保UI在最上层 */
            display: flex;
            flex-direction: column;
            align-items: flex-start;
            gap: 5px;
        }
        #desktop-controls {
            display: flex;
            flex-direction: column;
            gap: 5px;
        }
        #desktop-controls.hidden {
            display: none;
        }
        #desktop-controls button, #desktop-qol-container > button {
            background-color: rgba(0, 0, 0, 0.6);
            color: white;
            border: 1px solid #888;
            border-radius: 5px;
            padding: 6px 12px;
            font-size: 14px;
            cursor: pointer;
            opacity: 0.7;
            transition: opacity 0.2s ease-in-out;
            width: 150px;
            text-align: center;
        }
        #desktop-controls button:hover, #desktop-qol-container > button:hover {
             opacity: 1;
        }
        #desktop-export-container {
            position: fixed;
            bottom: 15px;
            right: 15px;
            z-index: 10001; /* 确保UI在最上层 */
            display: flex;
            gap: 10px;
        }
        #desktop-export-container button {
            background-color: rgba(0, 0, 0, 0.6);
            color: white;
            border: 1px solid #888;
            border-radius: 5px;
            padding: 8px 15px;
            font-size: 14px;
            cursor: pointer;
            opacity: 0.7;
            transition: opacity 0.2s ease-in-out;
        }
        #desktop-export-container button:hover {
            opacity: 1;
        }
        button.active-auto {
            border-color: #4CAF50; 
            box-shadow: 0 0 8px #4CAF50;
        }
	`))

    ArrowLayerMove = class ArrowLayerMove {
    	static get tree() {
    		if (this._tree) return this._tree;
    		if (typeof Object.values(TREE_LAYERS)[0][0] == 'object') {
    			return this._tree = TREE_LAYERS;
    		}
    		return this._tree = Object.fromEntries(Object.entries(TREE_LAYERS)
    			.map(([k,r]) => {
    				return [k, r.map((layer, position) => ({layer, position}))]
    			}));
    	}
        static get y() {
            return +Object.entries(this.tree).find(([k, r]) => r.find(e=>e.layer==player.tab))[0];
        }
        static get x() {
            return Object.values(this.tree).map(r=>r.find(e=>e.layer==player.tab)).find(Boolean).position;
        }
        static changeLayer(layer) {
            q(`.treeNode.can.${layer}`)?.click();
        }
        static best(a, f) {
            return a.map((e,i,a)=>({e,v:f(e,i,a)})).sort((a,b)=>a.v-b.v)[0].e;
        }
        static moveUp() {
            let row = this.tree[this.y-1];
            if (!row) return;
            let layer = this.best(row, e => Math.abs(this.x - e.position) + 1000*!tmp[e.layer].layerShown).layer;
            this.changeLayer(layer);
        }
        static moveLeft() {
            let row = this.tree[this.y];
            let layer = row.filter(e => e.position < this.x).pop()?.layer;
            if (!layer) return;
            this.changeLayer(layer);
        }
        static moveDown() {
            let row = this.tree[this.y+1];
            if (!row) return;
            let layer = this.best(row, e => Math.abs(this.x - e.position) + 1000*!tmp[e.layer].layerShown).layer;
            this.changeLayer(layer);
        }
        static moveRight() {
            let row = this.tree[this.y];
            let layer = row.filter(e => e.position > this.x)[0]?.layer;
            if (!layer) return;
            this.changeLayer(layer);
        }
    }

	TabStarrer = class {
		static layerContent(layerId) {
			if (!layers[layerId] || !layers[layerId].tabFormat) {
				return {};
			}
			let _tab = player.tab;
			player.tab = layerId;
			let data = Object.fromEntries(Object.entries(layers[layerId].tabFormat).map(([k,v])=>[k, parseTab(k, v)]))
			player.tab = _tab;
			return data;

			function parseTab(id, tab) {
				if (!player.subtabs[layerId]) {
					return {};
				}

				let _mainTabs = player.subtabs[layerId].mainTabs;
				player.subtabs[layerId].mainTabs = id;
				let data = {};
				function parseItem(e) {
					if (typeof e == 'function')
						return parseItem(e());
					if (Array.isArray(e)){
						if (e[0] == 'row' || e[0] == 'column') {
							return e.slice(1).flat().map(parseItem);
						}
						data[e[0]] ??= [];
						if (typeof e[1] != 'object') {
							data[e[0]].push(e[1]);
						} else if (e[0] == 'upgrades') {
							let ups = e[1].flatMap(e => Array.map(10, i => e*10+i)).filter(e => layers[layerId].upgrades[e])
							data.upgrade ??= [];
							data.upgrade.push(...ups);
						} else {
							debugger;
						}
						return;
					}
					data[e] = true;
				}
				tab.content?.map(parseItem);
				player.subtabs[layerId].mainTabs = _mainTabs;
				return data;
			}
		}
		static layerTabStatus(layerId) {
			let content = this.layerContent(layerId);
			return Object.map(content, (tabContent, k) => {
				let layer = tmp[layerId];
				let ups = tabContent.upgrades != true ? (tabContent.upgrade || []).map(e => layer.upgrades[e]) : Object.values(layer.upgrades || {}).filter(e => e.id);
				let ms = !tabContent.milestones ? [] : Object.values(layer.milestones || {}).filter(e => e.id);
				let cha = !tabContent.challenges ? [] : Object.values(layer.challenges || {}).filter(e => e.id);
				return {
					upgrades: {
						total: ups.length,
						unlocked: ups.filter(e => e.unlocked || hasUpgrade(layerId, e.id)).length,
						done: ups.filter(e => hasUpgrade(layerId, e.id)).length,
						available: ups.filter(e => e.unlocked && !hasUpgrade(layerId, e.id) && canAffordUpgrade(layerId, e.id)).length,
					},
					milestones: {
						total: ms.length,
						unlocked: ms.filter(e => e.unlocked).length,
						done: ms.filter(e => e.done).length,
					},
					challenges: {
						total: cha.length,
						unlocked: cha.filter(e => e.unlocked).length,
						done: cha.filter(e => player[layerId].challenges[e.id] >= e.completionLimit).length,
						active: !!player[layerId].activeChallenge,
						canComplete: player[layerId].activeChallenge && canCompleteChallenge(layerId, player[layerId].activeChallenge),
					},
				}
			});
		}
		static starsElement(status) {
			function star(color, empty) {
				return elm(`.tabStar[style=color:${color};]`, typeof empty == 'string' ? empty : empty ? '☆' : '★')
			}
			let ups = status.upgrades;
			let ms = status.milestones;
			let cha = status.challenges;

			ups = ups.total && star(ups.available ? 'yellowgreen' : ups.unlocked < ups.total ? 'silver' : 'gold', ups.done < ups.unlocked);
			ms = ms.total && star(ms.unlocked < ms.total ? 'silver' : 'gold', ms.done < ms.unlocked);
			cha = cha.total && star(cha.active ? 'red' : cha.unlocked < cha.total ? 'silver' : 'gold', cha.done < cha.unlocked && !cha.canComplete);

			return elm('.tscon',...[cha, ms, ups].filter(Boolean));
		}
		static makeTabStars() {
			if (!player) return;

			let layerId = player.tab;
			let layerTabStatus = this.layerTabStatus(layerId);

			qq('.tabButton').map(e => {
				if (!e.q('.tscon')) e.append(elm('.tscon'));
				let tabId = e.childNodes[0].nodeValue;
				let old = e.q('.tscon');
                if (!layerTabStatus[tabId]) return;
				let con = this.starsElement(layerTabStatus[tabId]);
				if (con.outerHTML != old.outerHTML) {
					old.replaceWith(con);
				}
			});
		}
	}

	if(window._tsint) clearInterval(_tsint);
	_tsint = setInterval(() => TabStarrer.makeTabStars(), 200);

    function makeButtonLongPressable(button, actionFn) {
        let intervalId = null;

        function startPress(event) {
            event.preventDefault();
            actionFn();
            if (intervalId) return;
            intervalId = setInterval(actionFn, longPressDelay);
        }

        function endPress() {
            if (intervalId) {
                clearInterval(intervalId);
                intervalId = null;
            }
        }

        button.addEventListener('mousedown', startPress);
        button.addEventListener('touchstart', startPress, { passive: false });
        button.addEventListener('mouseup', endPress);
        button.addEventListener('mouseleave', endPress);
        button.addEventListener('touchend', endPress);
        button.addEventListener('touchcancel', endPress);
    }

    let autoActionsInterval = null;

    function autoActionsLoop() {
        if (autoBuyActive) performBuyAll();
        if (autoResetActive) performReset();
        else if (autoComboActive) performBuyAndReset();
    }

    function startOrUpdateAutoActionsInterval() {
        if (autoActionsInterval) {
            clearInterval(autoActionsInterval);
        }
        const effectiveDelay = Math.max(longPressDelay, 16); 
        autoActionsInterval = setInterval(autoActionsLoop, effectiveDelay);
    }


    const fontStyleElement = elm('style#the-tree-enchanted-font-override');
    q('head').append(fontStyleElement);

    function updateFontOverride() {
        if (forceSystemFont) {
            fontStyleElement.innerHTML = `
                * {
                    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !important;
                }
            `;
        } else {
            fontStyleElement.innerHTML = '';
        }
    }


	function createExtraControls() {
		const isMobile = 'ontouchstart' in window || navigator.maxTouchPoints > 0;

        const buyButton = elm('button', getText('buy'));
        const resetButton = elm('button', getText('reset'));
        const comboButton = elm('button', getText('combo'));
        const speedButton = elm('button', getText('setSpeed'));
        const importButton = elm('button', getText('importSave'));
        const exportButton = elm('button', getText('exportSave'));

        const setDelayButton = elm('button', getText('setDelay', { delay: longPressDelay }));
        const autoBuyToggle = elm('button', getText('autoBuy', { status: autoBuyActive ? getText('on') : getText('off') }));
        const autoResetToggle = elm('button', getText('autoReset', { status: autoResetActive ? getText('on') : getText('off') }));
        const autoComboToggle = elm('button', getText('autoCombo', { status: autoComboActive ? getText('on') : getText('off') }));
        const fontToggleButton = elm('button', getText('fontToggle', { status: forceSystemFont ? getText('on') : getText('off') }));

        makeButtonLongPressable(buyButton, performBuyAll);
        makeButtonLongPressable(resetButton, performReset);
        makeButtonLongPressable(comboButton, performBuyAndReset);
        speedButton.onclick = setGameSpeed;
        importButton.onclick = importSaveFromFile;
        exportButton.onclick = exportSaveAsFile;

        setDelayButton.onclick = () => {
            const input = prompt(getText('delayPrompt'), longPressDelay);
            if (input === null) return;
            const newDelay = parseInt(input, 10);
            if (!isNaN(newDelay) && newDelay >= 0 && newDelay <= 1000) {
                longPressDelay = newDelay;
                setSetting('longPressDelay', longPressDelay);
                setDelayButton.textContent = getText('setDelay', { delay: longPressDelay });
                startOrUpdateAutoActionsInterval(); 
            } else {
                alert(getText('delayInvalid'));
            }
        };

        const setupToggleButton = (button, stateKey, textKey) => {
            let isActive;
            if (stateKey === 'autoBuyActive') isActive = autoBuyActive;
            if (stateKey === 'autoResetActive') isActive = autoResetActive;
            if (stateKey === 'autoComboActive') isActive = autoComboActive;

            if (isActive) button.classList.add('active-auto');
            
            button.onclick = () => {
                let currentState;
                if (stateKey === 'autoBuyActive') {
                    autoBuyActive = !autoBuyActive;
                    currentState = autoBuyActive;
                } else if (stateKey === 'autoResetActive') {
                    autoResetActive = !autoResetActive;
                    currentState = autoResetActive;
                } else if (stateKey === 'autoComboActive') {
                    autoComboActive = !autoComboActive;
                    currentState = autoComboActive;
                }

                setSetting(stateKey, currentState);
                button.textContent = getText(textKey, { status: currentState ? getText('on') : getText('off') });
                button.classList.toggle('active-auto', currentState);
            };
        };

        setupToggleButton(autoBuyToggle, 'autoBuyActive', 'autoBuy');
        setupToggleButton(autoResetToggle, 'autoResetActive', 'autoReset');
        setupToggleButton(autoComboToggle, 'autoComboActive', 'autoCombo');

        fontToggleButton.onclick = () => {
            forceSystemFont = !forceSystemFont;
            setSetting('forceSystemFont', forceSystemFont);
            fontToggleButton.textContent = getText('fontToggle', { status: forceSystemFont ? getText('on') : getText('off') });
            updateFontOverride();
        };


		if (isMobile) {
            const MOBILE_PANEL_VISIBILITY_KEY = SETTINGS_PREFIX + 'mobilePanelVisibility';

			const controlPanel = elm('div#mobile-controls',
				buyButton,
				resetButton,
				comboButton,
				speedButton,
                setDelayButton,
                autoBuyToggle,
                autoResetToggle,
                autoComboToggle,
                fontToggleButton,
				importButton,
				exportButton
			);

            const toggleButton = elm('button#toggle-mobile-qol-button', getText('hidePanel'));

            const savedVisibility = localStorage.getItem(MOBILE_PANEL_VISIBILITY_KEY);
            if (savedVisibility === 'hidden') {
                controlPanel.classList.add('hidden');
                toggleButton.textContent = getText('showPanel');
            }

            toggleButton.onclick = () => {
                controlPanel.classList.toggle('hidden');
                const isHidden = controlPanel.classList.contains('hidden');
                toggleButton.textContent = isHidden ? getText('showPanel') : getText('hidePanel');
                localStorage.setItem(MOBILE_PANEL_VISIBILITY_KEY, isHidden ? 'hidden' : 'visible');
            };

            const qolContainer = elm('div#mobile-qol-container',
                controlPanel,
                toggleButton
            );

			document.body.append(qolContainer);

		} else { 
            const DESKTOP_PANEL_VISIBILITY_KEY = SETTINGS_PREFIX + 'desktopPanelVisibility';

            const toggleShortcutsButton = elm('button', getText('shortcuts', { status: areShortcutsEnabled ? getText('on') : getText('off') }));
            toggleShortcutsButton.onclick = () => {
                areShortcutsEnabled = !areShortcutsEnabled;
                toggleShortcutsButton.textContent = getText('shortcuts', { status: areShortcutsEnabled ? getText('on') : getText('off') });
                setSetting('shortcutsEnabled', areShortcutsEnabled);
            };

			const controlPanel = elm('div#desktop-controls',
				buyButton,
				resetButton,
				comboButton,
				speedButton,
                setDelayButton, 
                autoBuyToggle, 
                autoResetToggle, 
                autoComboToggle, 
                fontToggleButton, 
                toggleShortcutsButton
			);

            const togglePanelButton = elm('button', getText('hidePanel'));
            togglePanelButton.id = 'toggle-desktop-qol-button';

            const savedVisibility = localStorage.getItem(DESKTOP_PANEL_VISIBILITY_KEY);
            if (savedVisibility === 'hidden') {
                controlPanel.classList.add('hidden');
                togglePanelButton.textContent = getText('showPanel');
            }

            togglePanelButton.onclick = () => {
                controlPanel.classList.toggle('hidden');
                const isHidden = controlPanel.classList.contains('hidden');
                togglePanelButton.textContent = isHidden ? getText('showPanel') : getText('hidePanel');
                localStorage.setItem(DESKTOP_PANEL_VISIBILITY_KEY, isHidden ? 'hidden' : 'visible');
            };

            const desktopQolContainer = elm('div#desktop-qol-container',
                controlPanel,
                togglePanelButton
            );
            document.body.append(desktopQolContainer);

            const desktopExportContainer = elm('div#desktop-export-container', importButton, exportButton);
            document.body.append(desktopExportContainer);
        }
	}

	createExtraControls();
    updateFontOverride();
    startOrUpdateAutoActionsInterval();

}();