// ==UserScript== // @name The Tree Enchanted丨被附魔的树 // @namespace http://tampermonkey.net/ // @version 2.2.2 // @description 针对 TMT 的自定义增强功能,为移动端和桌面端分别添加了超级便捷的QOL按钮! // @author LinLei_Baruch & Google Gemini 2.5 Pro & Claude 3 Opus // @original-author Dimava & Assistant // @original-license 暂无 // @original-script https://greasyfork.org/zh-CN/scripts/425404-the-tree-enchanted // @match *://*/* // @grant none // ==/UserScript== if (globalThis.TREE_LAYERS) void function() { 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__(); 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(); performReset(); } function setGameSpeed() { const currentSpeed = (typeof player.devSpeed === 'number') ? player.devSpeed : 1; const input = prompt("请输入游戏速度 (例如: 10,不建议设定过快的游戏速度,否则可能会出bug,部分模组树存在无法生效的情况,那就是有防作弊):", currentSpeed); if (input === null) { return; } const newSpeed = parseFloat(input); if (isNaN(newSpeed)) { alert("输入无效,请输入一个数字!"); return; } player.devSpeed = newSpeed; console.log(`游戏速度已设置为: ${player.devSpeed}`); } function exportSaveAsFile() { if (typeof NaNcheck === 'function') { NaNcheck(player); if (window.NaNalert) { alert("存档数据异常 (NaN),无法导出!请刷新页面后再试。"); 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("导出存档时发生错误!"); 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("文件为空或无法读取。"); } if (saveDataString.startsWith("N4IgLghg")) { console.log("检测到 LZString 格式存档,使用游戏原生 importSave 函数导入。"); if (typeof importSave === 'function') { importSave(saveDataString); return; } else { throw new Error("检测到标准游戏存档,但未找到游戏内的 importSave 函数!"); } } 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("解码成功,但缺少手动加载所需的游戏核心函数。"); } } else { throw new Error("文件内容不是有效的存档格式。"); } } catch (error) { alert("导入失败!文件可能已损坏或格式不正确。\n错误详情: " + error.message); console.error("存档导入失败:", error); } }; reader.onerror = function() { alert("读取文件时发生错误!"); console.error("FileReader error:", reader.error); }; reader.readAsText(file); }; document.body.appendChild(fileInput); fileInput.click(); document.body.removeChild(fileInput); } let loopReset = false; let keyzloop = 0; let keysdown = {}; function onraf() { if (keysdown.x) { } } void async function() { while(true) { await Promise.frame(); onraf(); } } addEventListener('keydown', async event=>{ if (event.key == 'z' || event.key == 'X') { performReset(); } if (event.key == 'x' || event.key == 'X') { performBuyAll(); } 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; } }); onkeyup = event => { if (event.key == 'c') { loopReset = false; } } 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.${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^'); let container = elm('.sscon',...[sel, cha, ms, ups].filter(Boolean)); 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.layerShown==true) .map(e=>this.showStars(e.layer)); } } window.layInt && clearInterval(layInt) layInt=setInterval(()=>Layer.showAllStars(), 200) q('head').append(elm('style', ` .sscon { position:absolute; font-size: 33.333%; font-family: initial; text-shadow: 0 0 4px gray, 0 0 3px black; display: flex; 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; } #qol-container { position: fixed; bottom: 10px; left: 50%; transform: translateX(-50%); z-index: 10000; 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: 20px; 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; } #toggle-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-export-container { position: fixed; bottom: 15px; right: 15px; z-index: 10000; 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; } `)) 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].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) { let _mainTabs = player.subtabs.e.mainTabs; player.subtabs.e.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.e.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() { 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; const REPEAT_DELAY = 100; function startPress(event) { event.preventDefault(); actionFn(); if (intervalId) return; intervalId = setInterval(actionFn, REPEAT_DELAY); } function endPress() { if (intervalId) { clearInterval(intervalId); intervalId = null; } } button.addEventListener('mousedown', startPress); button.addEventListener('touchstart', startPress); button.addEventListener('mouseup', endPress); button.addEventListener('mouseleave', endPress); button.addEventListener('touchend', endPress); button.addEventListener('touchcancel', endPress); } function createExtraControls() { const isMobile = 'ontouchstart' in window || navigator.maxTouchPoints > 0; const importButton = elm('button', '导入存档文件'); importButton.onclick = importSaveFromFile; const exportButton = elm('button', '导出存档文件'); exportButton.onclick = exportSaveAsFile; if (isMobile) { const buyButton = elm('button', '购买'); const resetButton = elm('button', '重置'); const comboButton = elm('button', '购买&重置'); const speedButton = elm('button', '设定速度'); makeButtonLongPressable(buyButton, performBuyAll); makeButtonLongPressable(resetButton, performReset); makeButtonLongPressable(comboButton, performBuyAndReset); speedButton.onclick = setGameSpeed; const controlPanel = elm('div#mobile-controls', buyButton, resetButton, comboButton, speedButton, importButton, exportButton ); const toggleButton = elm('button#toggle-qol-button', '隐藏面板'); toggleButton.onclick = () => { controlPanel.classList.toggle('hidden'); toggleButton.textContent = controlPanel.classList.contains('hidden') ? '显示面板' : '隐藏面板'; }; const qolContainer = elm('div#qol-container', controlPanel, toggleButton ); document.body.append(qolContainer); } else { const desktopContainer = elm('div#desktop-export-container', importButton, exportButton); document.body.append(desktopContainer); } } createExtraControls(); }();