// ==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();
}();