您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Helper for TheresMoreGame
当前为
// ==UserScript== // @name TheresMoreHelp // @namespace TheresMoreGame.com // @match https://www.theresmoregame.com/play/ // @grant none // @version 2.0.23 // @description Helper for TheresMoreGame // @license MIT // @run-at document-idle // ==/UserScript== ;(async () => { let cheatsOff = true let scriptPaused = true let haveManualResourceButtons = true let isClicking = false let mainLoopRunning = false const buildingsList = [ { id: 'Mausoleum of gods', alwaysBuild: true, isSafe: true }, { id: 'Tower of mana', alwaysBuild: true, isSafe: true }, { id: 'City of Lights', alwaysBuild: true, isSafe: true }, { id: 'Harbor district', alwaysBuild: true, isSafe: true }, { id: 'Stock exchange', alwaysBuild: true, isSafe: true }, { id: 'Mana pit', alwaysBuild: true, isSafe: true }, { id: 'Great bombard', alwaysBuild: true, isSafe: true }, { id: 'Refugees district', alwaysBuild: true, isSafe: true }, { id: 'Academy of Freethinkers', alwaysBuild: true, isSafe: true }, { id: 'City center', alwaysBuild: true, isSafe: true }, { id: 'Cathedral', alwaysBuild: true, isSafe: true }, { id: 'Great fair', alwaysBuild: true, isSafe: true }, { id: 'Palisade', alwaysBuild: true, isSafe: true }, { id: 'Wall', alwaysBuild: true, isSafe: true }, { id: 'Tower of mana part', alwaysBuild: true, isSafe: true }, { id: 'City of Lights part', alwaysBuild: true, isSafe: true }, { id: 'Harbor district part', alwaysBuild: true, isSafe: true }, { id: 'Stock exchange part', alwaysBuild: true, isSafe: true }, { id: 'Mana pit part', alwaysBuild: true, isSafe: true }, { id: 'Great bombard part', alwaysBuild: true, isSafe: true }, { id: 'Refugees district part', alwaysBuild: true, isSafe: true }, { id: 'A. of Freethinkers Part', alwaysBuild: true, isSafe: true }, { id: 'City center part', alwaysBuild: true, isSafe: true }, { id: 'Great fair unit', alwaysBuild: true, isSafe: true }, { id: 'Cathedral part', alwaysBuild: true, isSafe: true }, { id: 'Guild of craftsmen', alwaysBuild: true, isSafe: true }, { id: 'Machines of gods', alwaysBuild: true, isSafe: true }, { id: 'Library of Theresmore', alwaysBuild: true, isSafe: true }, { id: 'Monastery', alwaysBuild: true, isSafe: true }, { id: 'Watchman Outpost', alwaysBuild: true, isSafe: true }, { id: 'Hall of the dead', alwaysBuild: true, isSafe: true }, { id: 'Sawmill', alwaysBuild: true, isSafe: true }, { id: 'Monument', alwaysBuild: true, isSafe: true }, { id: 'Foundry', alwaysBuild: true, isSafe: true }, { id: 'Builder district', alwaysBuild: true, isSafe: true }, { id: 'Gan Eden', alwaysBuild: true, isSafe: true }, { id: 'Portal of the dead', alwaysBuild: true, isSafe: true }, { id: 'Library of SouLs', alwaysBuild: true, isSafe: true }, { id: 'Souls', isSafe: true }, { id: 'Books', isSafe: true }, { id: 'Observatory', isSafe: true }, { id: 'The Vaults', isSafe: true }, { id: 'Credit union', isSafe: true }, { id: 'Canava trading post', isSafe: true }, { id: 'Bank', isSafe: true }, { id: 'Marketplace', isSafe: true }, { id: 'Artisan Workshop', isSafe: true }, { id: 'Granary', isSafe: true }, { id: 'Hall of wisdom', isSafe: true }, { id: 'School', isSafe: true }, { id: 'University', isSafe: true }, { id: 'Research plant', isSafe: true }, { id: 'Undead Herds', isSafe: true }, { id: 'Guarded warehouse', isSafe: true }, { id: 'Fiefdom', isSafe: true }, { id: 'Natronite refinery', isSafe: true }, { id: 'Alchemical laboratory', isSafe: true }, { id: 'Lumberjack Camp', isSafe: true }, { id: 'Quarry', isSafe: true }, { id: 'Mine', isSafe: true }, { id: 'Palisade part', isSafe: true }, { id: 'Wall part', isSafe: true }, { id: 'Rampart', alwaysBuild: true, isSafe: true }, { id: 'Rampart part', isSafe: true }, { id: 'Farm', isSafe: true }, { id: 'Matter transmuter', isSafe: true }, { id: 'Stable', isSafe: true }, { id: 'Spiritual garden', isSafe: true }, { id: 'Conclave', isSafe: true }, { id: 'Magical tower', isSafe: true }, { id: 'Temple', isSafe: true }, { id: 'Altar of sacrifices', isSafe: true }, { id: 'Fountain of Prosperity', isSafe: true }, { id: 'Valley of plenty', isSafe: true }, { id: 'Tax revenue checkpoints', isSafe: true }, { id: 'Industrial plant', isSafe: true }, { id: 'Magic Circle', isSafe: true }, { id: 'Carpenter workshop', isSafe: true }, { id: 'Grocery', isSafe: true }, { id: 'Steelworks', isSafe: true }, { id: 'Military academy', isSafe: true }, { id: 'Ancient vault', isSafe: true }, { id: 'Recruit training center', isSafe: true }, { id: 'Officer training ground', isSafe: true }, { id: 'Castrum Militia', isSafe: true }, { id: 'Mansion', isSafe: false, requires: { resource: 'Food', parameter: 'speed', minValue: 3 } }, { id: 'City Hall', isSafe: false, requires: { resource: 'Food', parameter: 'speed', minValue: 1.5 } }, { id: 'Residential block', isSafe: false, requires: { resource: 'Food', parameter: 'speed', minValue: 5 } }, { id: 'Common House', isSafe: false, requires: { resource: 'Food', parameter: 'speed', minValue: 1 } }, { id: 'Storage facility', isSafe: true }, { id: 'Ballista', isSafe: true }, { id: 'Large warehouse', isSafe: true }, { id: 'Large storehouse', isSafe: true }, { id: 'Store', isSafe: true }, { id: 'Natronite depot', isSafe: true }, { id: 'Barracks', isSafe: true }, { id: 'Minefield', isSafe: true }, { id: 'Natronite balloon', isSafe: true }, { id: 'Decryption of the portal', isSafe: true }, { id: 'Fortune grove', isSafe: true }, { id: 'Pillars of mana', isSafe: false, requires: { resource: 'Gold', parameter: 'speed', minValue: 100 } }, ] .filter((building) => building.id) .map((building, index) => { return { ...building, order: index, } }) const lang = { pop_artisan: 'Artisan', pop_breeder: 'Breeder', pop_farmer: 'Farmer', pop_lumberjack: 'Lumberjack', pop_merchant: 'Merchant', pop_trader: 'Trader', pop_miner: 'Miner', pop_quarryman: 'Quarryman', pop_priest: 'Priest', pop_carpenter: 'Carpenter', pop_steelworker: 'Steelworker', pop_professor: 'Professor', pop_skymancer: 'Skymancer', pop_supplier: 'Supplier', pop_alchemist: 'Alchemist', pop_unemployed: 'Unemployed', pop_natro_refiner: 'Nat-Refiner', pop_researcher: 'Researcher', res_army: 'Army', res_coin: 'Coin', res_copper: 'Copper', res_cow: 'Cow', res_crystal: 'Crystal', res_faith: 'Faith', res_fame: 'Fame', res_food: 'Food', res_gold: 'Gold', res_horse: 'Horse', res_iron: 'Iron', res_legacy: 'Legacy', res_luck: 'Luck', res_mana: 'Mana', res_natronite: 'Natronite', res_population: 'Population', res_stone: 'Stone', res_relic: 'Relic', res_research: 'Research', res_tools: 'Tools', res_wood: 'Wood', res_building_material: 'Materials', res_steel: 'Steel', res_supplies: 'Supplies', res_saltpetre: 'Saltpetre', res_tome_wisdom: 'Tome of Wisdom', res_gem: 'Gem', } const allJobs = [ { id: 'unemployed', }, { id: 'farmer', req: [ { type: 'building', id: 'farm', value: 1, }, ], gen: [ { type: 'resource', id: 'food', value: 1.6, }, ], }, { id: 'lumberjack', req: [ { type: 'building', id: 'lumberjack_camp', value: 1, }, ], gen: [ { type: 'resource', id: 'wood', value: 0.7, }, ], }, { id: 'quarryman', req: [ { type: 'building', id: 'quarry', value: 1, }, ], gen: [ { type: 'resource', id: 'stone', value: 0.6, }, ], }, { id: 'miner', req: [ { type: 'building', id: 'mine', value: 1, }, ], gen: [ { type: 'resource', id: 'copper', value: 0.5, }, { type: 'resource', id: 'iron', value: 0.3, }, ], }, { id: 'artisan', req: [ { type: 'building', id: 'artisan_workshop', value: 1, }, ], gen: [ { type: 'resource', id: 'gold', value: 0.5, }, { type: 'resource', id: 'tools', value: 0.3, }, ], }, { id: 'merchant', req: [ { type: 'building', id: 'marketplace', value: 1, }, ], gen: [ { type: 'resource', id: 'gold', value: 3, }, ], }, { id: 'trader', req: [ { type: 'building', id: 'credit_union', value: 1, }, ], gen: [ { type: 'resource', id: 'gold', value: 6, }, ], }, { id: 'breeder', req: [ { type: 'building', id: 'stable', value: 1, }, ], gen: [ { type: 'resource', id: 'cow', value: 0.2, }, { type: 'resource', id: 'horse', value: 0.1, }, ], }, { id: 'carpenter', req: [ { type: 'building', id: 'carpenter_workshop', value: 1, }, ], gen: [ { type: 'resource', id: 'building_material', value: 0.3, }, { type: 'resource', id: 'wood', value: -3, }, { type: 'resource', id: 'stone', value: -1.5, }, { type: 'resource', id: 'tools', value: -0.5, }, ], }, { id: 'steelworker', req: [ { type: 'building', id: 'steelworks', value: 1, }, ], gen: [ { type: 'resource', id: 'steel', value: 0.4, }, { type: 'resource', id: 'copper', value: -1, }, { type: 'resource', id: 'iron', value: -0.5, }, ], }, { id: 'professor', req: [ { type: 'building', id: 'university', value: 1, }, ], gen: [ { type: 'resource', id: 'crystal', value: 0.06, }, { type: 'resource', id: 'research', value: 1, }, ], }, { id: 'researcher', req: [ { type: 'building', id: 'research_plant', value: 1, }, ], gen: [ { type: 'resource', id: 'research', value: 3, }, ], }, { id: 'supplier', req: [ { type: 'building', id: 'grocery', value: 1, }, ], gen: [ { type: 'resource', id: 'supplies', value: 0.4, }, { type: 'resource', id: 'food', value: -2, }, { type: 'resource', id: 'cow', value: -0.2, }, ], }, { id: 'skymancer', req: [ { type: 'building', id: 'observatory', value: 1, }, ], gen: [ { type: 'resource', id: 'faith', value: 3, }, { type: 'resource', id: 'mana', value: 3, }, ], }, { id: 'alchemist', req: [ { type: 'building', id: 'alchemic_laboratory', value: 1, }, ], gen: [ { type: 'resource', id: 'saltpetre', value: 0.7, }, ], }, { id: 'natro_refiner', req: [ { type: 'building', id: 'natronite_refinery', value: 1, }, ], gen: [ { type: 'resource', id: 'natronite', value: 1, }, { type: 'resource', id: 'mana', value: -5, }, { type: 'resource', id: 'saltpetre', value: -0.5, }, ], }, ] .filter((job) => job.gen && job.gen.length) .map((job) => { return { id: lang[`pop_${job.id}`], gen: job.gen .filter((gen) => gen.type === 'resource') .map((gen) => { return { id: lang[`res_${gen.id}`], value: gen.value, } }), } }) .map((job) => { return { id: job.id, isSafe: !job.gen.find((gen) => gen.value < 0), resourcesGenerated: job.gen .filter((gen) => gen.value > 0) .map((gen) => { return { id: gen.id, value: gen.value } }), resourcesUsed: job.gen .filter((gen) => gen.value < 0) .map((gen) => { return { id: gen.id, value: gen.value } }), } }) const resourcesToTrade = ['Cow', 'Horse', 'Food', 'Copper', 'Wood', 'Stone', 'Iron', 'Tools'] const timeToWaitUntilFullGold = 60 const minFarmers = 5 const sleep = (miliseconds) => new Promise((resolve) => setTimeout(resolve, miliseconds)) // https://stackoverflow.com/a/55366435 class NumberParser { constructor(locale) { const format = new Intl.NumberFormat(locale) const parts = format.formatToParts(12345.6) const numerals = Array.from({ length: 10 }).map((_, i) => format.format(i)) const index = new Map(numerals.map((d, i) => [d, i])) this._group = new RegExp(`[${parts.find((d) => d.type === 'group').value}]`, 'g') this._decimal = new RegExp(`[${parts.find((d) => d.type === 'decimal').value}]`) this._numeral = new RegExp(`[${numerals.join('')}]`, 'g') this._index = (d) => index.get(d) } parse(string) { let multiplier = 1 if (string.includes('K')) { multiplier = 1000 } else if (string.includes('M')) { multiplier = 1000000 } return (string = string.replace('K', '').replace('M', '').trim().replace(this._group, '').replace(this._decimal, '.').replace(this._numeral, this._index)) ? +string * multiplier : NaN } } const numberParser = new NumberParser() const formatTime = (timeToFormat) => { const timeValues = { seconds: 0, minutes: 0, hours: 0, days: 0, } let timeShort = '' let timeLong = '' timeValues.seconds = timeToFormat % 60 timeToFormat = (timeToFormat - (timeToFormat % 60)) / 60 timeValues.minutes = timeToFormat % 60 timeToFormat = (timeToFormat - (timeToFormat % 60)) / 60 timeValues.hours = timeToFormat % 24 timeToFormat = (timeToFormat - (timeToFormat % 24)) / 24 timeValues.days = timeToFormat if (timeValues.days) { timeShort += `${timeValues.days}d ` timeLong += `${timeValues.days} days ` } if (timeValues.hours) { timeShort += `${timeValues.hours}h ` timeLong += `${timeValues.hours} hrs ` } if (timeValues.minutes) { timeShort += `${timeValues.minutes}m ` timeLong += `${timeValues.minutes} min ` } if (timeValues.seconds) { timeShort += `${timeValues.seconds}s ` timeLong += `${timeValues.seconds} sec ` } timeShort = timeShort.trim() timeLong = timeLong.trim() return { timeShort, timeLong, timeValues, } } const logger = ({ msgLevel, msg }) => { const logText = `[TMH][${new Date().toLocaleTimeString()}] ${msg}` const levelsToLog = ['log', 'warn', 'error'] if (levelsToLog.includes(msgLevel)) { const logHolder = document.querySelector('#root > div > div > div > div.w-full.order-2.flex-grow.overflow-x-hidden.overflow-y-auto.pr-4') const tmhLogs = [...logHolder.querySelectorAll('.tmh-log')] if (tmhLogs.length > 10) { for (let i = 10; i < tmhLogs.length; i++) { tmhLogs[i].remove() } } const p = document.createElement('p') p.classList.add('text-xs', 'mb-2', 'text-green-600', 'tmh-log') p.innerText = logText logHolder.insertAdjacentElement('afterbegin', p) } console[msgLevel](logText) } const getAllButtons = () => { return [...document.querySelectorAll('#maintabs-container > div > div[role=tabpanel] button.btn.btn-dark:not(.btn-off)')] } const getResource = (resourceName = 'Gold') => { let resourceFound = false let resourceToFind = { name: resourceName, current: 0, max: 0, speed: 0, ttf: null, ttz: null } const resources = [...document.querySelectorAll('#root div > div > div > table > tbody > tr > td:nth-child(1) > span')] resources.map((resource) => { if (resource.textContent.includes(resourceName)) { resourceFound = true const values = resource.parentNode.parentNode.childNodes[1].textContent .split('/') .map((x) => numberParser.parse(x.replace(/[^0-9KM\-,\.]/g, '').trim())) resourceToFind.current = values[0] resourceToFind.max = values[1] resourceToFind.speed = numberParser.parse(resource.parentNode.parentNode.childNodes[2].textContent.replace(/[^0-9KM\-,\.]/g, '').trim()) || 0 resourceToFind.ttf = resourceToFind.speed > 0 && resourceToFind.max !== resourceToFind.current ? formatTime(Math.ceil((resourceToFind.max - resourceToFind.current) / resourceToFind.speed)) : null resourceToFind.ttz = resourceToFind.speed < 0 && resourceToFind.current ? formatTime(Math.ceil(resourceToFind.current / (resourceToFind.speed * -1))) : null } }) return resourceFound ? resourceToFind : null } const hasUnassignedPopulation = () => { let unassignedPopulation = false const navButtons = document.querySelectorAll('#main-tabs > div > button') navButtons.forEach((button) => { if (button.innerText.includes(KEYS.PAGES.POPULATION)) { unassignedPopulation = !!button.querySelector('span') } }) return unassignedPopulation } const canAffordBA = () => { const faith = getResource('Faith') const mana = getResource('Mana') if (faith && mana) { if (faith.current >= 2000 && mana.current >= 600) { return true } } return false } const shouldBuyBA = () => { const faith = getResource('Faith') const mana = getResource('Mana') if (faith && mana) { if ( faith.current + faith.speed * timeToWaitUntilFullGold >= faith.max && mana.current + mana.speed * timeToWaitUntilFullGold >= mana.max && mana.speed > 20 ) { return true } } return false } const lastSell = {} const shouldSell = () => { return !!resourcesToTrade.find((resName) => { if (!lastSell[resName]) lastSell[resName] = 0 const res = getResource(resName) if ( res && (res.current === res.max || res.current + res.speed * timeToWaitUntilFullGold >= res.max) && lastSell[resName] + 90 * 1000 < new Date().getTime() ) return true }) } const KEYS = { PAGES: { BUILD: 'Build', RESEARCH: 'Research', POPULATION: 'Population', ARMY: 'Army', MARKETPLACE: 'Marketplace', MAGIC: 'Magic', }, } const hasPage = (page) => { const navButtons = [...document.querySelectorAll('#main-tabs > div > button')] return !!navButtons.find((button) => button.innerText.includes(page)) } const switchPage = async (page) => { let foundPage = hasPage(page) if (!foundPage) { await switchPage(KEYS.PAGES.BUILD) return } let pageButton let switchedPage = false const navButtons = document.querySelectorAll('#main-tabs > div > button') navButtons.forEach((button) => { if (button.innerText.includes(page) && button.getAttribute('aria-selected') !== 'true') { pageButton = button } }) if (pageButton) { pageButton.click() switchedPage = true } await sleep(2000) if (switchedPage) { logger({ msgLevel: 'debug', msg: `Switched page to ${page}` }) } } const pages = [ { id: KEYS.PAGES.BUILD, action: async () => { await switchPage(KEYS.PAGES.BUILD) let buttons = getAllButtons() .map((button) => { const id = button.innerText.split('\n').shift() return { id: id, element: button, building: buildingsList.find((building) => building.id === id) } }) .filter((button) => button.building) .sort((a, b) => a.building.order - b.building.order) if (buttons.length) { while (!scriptPaused && buttons.length) { let shouldBuild = true const button = buttons.shift() if (!button.building.isSafe) { const requiredResource = getResource(button.building.requires.resource) if (!requiredResource) { shouldBuild = false } else { if ( button.id === 'Common House' && (!button.element.querySelector('span') || numberParser.parse(button.element.querySelector('span').innerText) < 2) ) { shouldBuild = true } else { shouldBuild = shouldBuild && requiredResource[button.building.requires.parameter] > button.building.requires.minValue } } } if (shouldBuild) { button.element.click() logger({ msgLevel: 'log', msg: `Building ${button.building.id}` }) await sleep(6000) buttons = getAllButtons() .map((button) => { const id = button.innerText.split('\n').shift() return { id: id, element: button, building: buildingsList.find((building) => building.id === id) } }) .filter((button) => button.building) .sort((a, b) => a.building.order - b.building.order) } } } await sleep(5000) }, }, { id: KEYS.PAGES.RESEARCH, action: async () => { await switchPage(KEYS.PAGES.RESEARCH) const manualResearches = [ 'A moonlight night', 'Dragon assault', 'Mysterious robbery', 'The Fallen Angel reveal', 'Persuade the nobility', 'Persuade the people', ] let buttonsList = getAllButtons().filter((button) => !manualResearches.includes(button.innerText.split('\n').shift())) if (buttonsList.length) { while (!scriptPaused && buttonsList.length) { const button = buttonsList.shift() button.click() logger({ msgLevel: 'log', msg: `Researching ${button.innerText.split('\n').shift()}` }) await sleep(6000) buttonsList = getAllButtons().filter((button) => !manualResearches.includes(button.innerText.split('\n').shift())) } } await sleep(5000) }, }, { id: KEYS.PAGES.POPULATION, action: async () => { await switchPage(KEYS.PAGES.POPULATION) let canAssignJobs = true const container = document.querySelector('#maintabs-container > div > div[role=tabpanel]') let availablePop = container .querySelector('div > span.ml-2') .textContent.split('/') .map((pop) => numberParser.parse(pop.trim())) const availableJobsQSA = container.querySelectorAll('h5') const availableJobs = [] availableJobsQSA.forEach((job) => { const jobTitle = job.textContent.trim() availableJobs.push({ ...allJobs.find((allJob) => allJob.id === jobTitle), container: job.parentElement.parentElement, current: +job.parentElement.parentElement.querySelector('input').value.split('/').shift().trim(), max: +job.parentElement.parentElement.querySelector('input').value.split('/').pop().trim(), }) }) if (availablePop[0] > 0) { while (!scriptPaused && canAssignJobs) { const jobsWithSpace = availableJobs.filter((job) => !!job.container.querySelector('button.btn-green')) canAssignJobs = false if (jobsWithSpace.length) { const foodJob = availableJobs.find((job) => job.resourcesGenerated.find((res) => res.id === 'Food')) if (foodJob && (getResource('Food').speed <= 0 || foodJob.current < Math.min(minFarmers, foodJob.max))) { const foodJob = jobsWithSpace.find((job) => job.resourcesGenerated.find((res) => res.id === 'Food')) if (foodJob) { const addJobButton = foodJob.container.querySelector('button.btn-green') if (addJobButton) { logger({ msgLevel: 'log', msg: `Assigning worker as ${foodJob.id}` }) addJobButton.click() canAssignJobs = true await sleep(1000) } } } else { let unassigned = container .querySelector('div > span.ml-2') .textContent.split('/') .map((pop) => numberParser.parse(pop.trim())) .shift() if (unassigned > 0) { const resources = [ 'Natronite', 'Saltpetre', 'Tools', 'Wood', 'Stone', 'Iron', // 'Copper', // Same as Iron 'Mana', // 'Faith', // Same as Mana 'Research', 'Materials', 'Steel', 'Supplies', 'Gold', 'Crystal', 'Horse', // 'Cow', // Same as Horse ] .filter((res) => getResource(res)) .filter((res) => jobsWithSpace.find((job) => job.resourcesGenerated.find((resGen) => resGen.id === res))) const resourcesWithNegativeGen = resources.filter((res) => getResource(res) && res.speed < 0) const resourcesWithNoGen = resources.filter((res) => !resourcesWithNegativeGen.includes(res) && getResource(res) && !res.speed) const resourcesLeft = resources.filter((res) => !resourcesWithNegativeGen.includes(res) && !resourcesWithNoGen.includes(res)) const resourcesSorted = resourcesWithNegativeGen.concat(resourcesWithNoGen).concat(resourcesLeft) if (resourcesSorted.length) { for (let i = 0; i < resourcesSorted.length && !scriptPaused; i++) { if (unassigned === 0) break const resourceName = resourcesSorted[i] const jobsForResource = jobsWithSpace .filter((job) => job.resourcesGenerated.find((resGen) => resGen.id === resourceName)) .sort( (a, b) => b.resourcesGenerated.find((resGen) => resGen.id === resourceName).value - a.resourcesGenerated.find((resGen) => resGen.id === resourceName).value ) if (jobsForResource.length) { for (let i = 0; i < jobsForResource.length && !scriptPaused; i++) { if (unassigned === 0) break const job = jobsForResource[i] let isSafeToAdd = true if (!job.isSafe) { job.resourcesUsed.forEach((resUsed) => { const res = getResource(resUsed.id) if (!res || res.speed < Math.abs(resUsed.value * 2)) { isSafeToAdd = false } }) } if (isSafeToAdd) { const addJobButton = job.container.querySelector('button.btn-green') if (addJobButton) { logger({ msgLevel: 'log', msg: `Assigning worker as ${job.id}` }) addJobButton.click() unassigned -= 1 canAssignJobs = !!unassigned await sleep(1000) } } } } } } } } } const unassigned = container .querySelector('div > span.ml-2') .textContent.split('/') .map((pop) => numberParser.parse(pop.trim())) .shift() if (unassigned === 0) { canAssignJobs = false } await sleep(10) } } await sleep(5000) }, }, { id: KEYS.PAGES.ARMY, action: async () => { await switchPage(KEYS.PAGES.ARMY) if (canAffordBA() && shouldBuyBA()) { const allButtonsQSA = document.querySelectorAll('div > div > div > div > div > span > button:not(.btn-off)') let buyBAButton = null allButtonsQSA.forEach((button) => { if (button.innerText.includes('Battle Angel')) { buyBAButton = button } }) if (buyBAButton) { buyBAButton.click() logger({ msgLevel: 'log', msg: `Buying Battle Angel(s)` }) await sleep(5000) } } await sleep(5000) }, }, { id: KEYS.PAGES.MARKETPLACE, action: async () => { await switchPage(KEYS.PAGES.MARKETPLACE) let gold = getResource('Gold') if (gold && gold.current < gold.max && shouldSell()) { const resourceHoldersQSA = document.querySelectorAll('div > div.tab-container > div > div > div') const resourceHolders = [] if (resourceHoldersQSA) { resourceHoldersQSA.forEach((resourceHolder) => { const resNameElem = resourceHolder.querySelector('h5') if (resNameElem) { const resName = resNameElem.innerText const res = getResource(resName) if (resourcesToTrade.includes(resName) && res && (res.current === res.max || res.current + res.speed * timeToWaitUntilFullGold >= res.max)) { resourceHolders.push(resourceHolder) } } }) } let goldEarned = 0 let soldTotals = {} for (let i = 0; i < resourceHolders.length && !scriptPaused; i++) { gold = getResource('Gold') const resourceHolder = resourceHolders[i] const resName = resourceHolder.querySelector('h5').innerText let res = getResource(resName) const initialPrice = numberParser.parse(resourceHolder.querySelector('div:nth-child(2) > div > table > tbody > tr > td:nth-child(2)').innerText) let price = initialPrice let sellButtons = resourceHolder.querySelectorAll('div > div.grid.gap-3 button.btn-red:not(.btn-dark)') while ( !scriptPaused && sellButtons && sellButtons.length && gold.current < gold.max && res.current + res.speed * timeToWaitUntilFullGold * 2 >= res.max ) { let maxSellButton = 2 const missingResToSell = Math.ceil((gold.max - gold.current) / price) if (missingResToSell < 80) { maxSellButton = 0 } else if (missingResToSell < 800) { maxSellButton = 1 } maxSellButton = Math.min(maxSellButton, sellButtons.length - 1) sellButtons[maxSellButton].click() lastSell[resName] = new Date().getTime() soldTotals[resName] = soldTotals[resName] ? soldTotals[resName] : { amount: 0, gold: 0 } soldTotals[resName].amount += +sellButtons[maxSellButton].innerText soldTotals[resName].gold += +sellButtons[maxSellButton].innerText * price logger({ msgLevel: 'debug', msg: `Selling ${sellButtons[maxSellButton].innerText} of ${res.name} for ${price}` }) goldEarned += numberParser.parse(sellButtons[maxSellButton].innerText) * price await sleep(10) sellButtons = resourceHolder.querySelectorAll('div:nth-child(2) > div.grid.gap-3 button:not(.btn-dark)') gold = getResource('Gold') res = getResource(resName) price = numberParser.parse(resourceHolder.querySelector('div:nth-child(2) > div > table > tbody > tr > td:nth-child(2)').innerText) await sleep(1) if (price / initialPrice < 0.1) { break } } } if (goldEarned) { const totals = Object.keys(soldTotals).map( (resName) => `${resName}: ${new Intl.NumberFormat().format(soldTotals[resName].amount)} units for ${new Intl.NumberFormat() .format(soldTotals[resName].gold) .toFixed(2)} gold (avg price: ${(soldTotals[resName].gold / soldTotals[resName].amount).toFixed(2)})` ) logger({ msgLevel: 'log', msg: `Earned ${new Intl.NumberFormat().format(goldEarned)} gold on Marketplace [${totals.join(', ')}]` }) } } await sleep(5000) }, }, ] window.switchScriptState = () => { scriptPaused = !scriptPaused window.localStorage.setItem('TMH_cheatsOff', JSON.stringify(false)) window.localStorage.setItem('TMH_scriptPaused', JSON.stringify(scriptPaused)) if (!scriptPaused) { setTimeout(() => { window.location.reload() }, 60 * 60 * 1000) mainLoop() } } const lastVisited = { [KEYS.PAGES.BUILD]: 1, [KEYS.PAGES.RESEARCH]: 0, [KEYS.PAGES.POPULATION]: 0, [KEYS.PAGES.ARMY]: 0, [KEYS.PAGES.MARKETPLACE]: 0, } const mainLoop = async () => { if (cheatsOff) return if (mainLoopRunning) { setTimeout(mainLoop, 1000) return } mainLoopRunning = true while (!scriptPaused) { const should = { [KEYS.PAGES.BUILD]: () => { return hasPage(KEYS.PAGES.BUILD) && lastVisited[KEYS.PAGES.BUILD] < lastVisited[KEYS.PAGES.RESEARCH] }, [KEYS.PAGES.RESEARCH]: () => { return hasPage(KEYS.PAGES.RESEARCH) && lastVisited[KEYS.PAGES.RESEARCH] < lastVisited[KEYS.PAGES.BUILD] }, [KEYS.PAGES.POPULATION]: () => { return hasPage(KEYS.PAGES.POPULATION) && hasUnassignedPopulation() }, [KEYS.PAGES.ARMY]: () => { const timeout = lastVisited[KEYS.PAGES.ARMY] + 2 * 60 * 1000 < new Date().getTime() return hasPage(KEYS.PAGES.ARMY) && canAffordBA() && shouldBuyBA() && timeout }, [KEYS.PAGES.MARKETPLACE]: () => { const gold = getResource('Gold') return hasPage(KEYS.PAGES.MARKETPLACE) && gold.current + gold.speed * timeToWaitUntilFullGold < gold.max && shouldSell() }, } const pagesToCheck = [KEYS.PAGES.POPULATION, KEYS.PAGES.MARKETPLACE, KEYS.PAGES.ARMY, KEYS.PAGES.RESEARCH, KEYS.PAGES.BUILD] while (!scriptPaused && pagesToCheck.length) { const pageToCheck = pagesToCheck.shift() if (should[pageToCheck] && should[pageToCheck]()) { const page = pages.find((page) => page.id === pageToCheck) if (page) { logger({ msgLevel: 'debug', msg: `Executing ${page.id} action` }) lastVisited[page.id] = new Date().getTime() await page.action() await sleep(1000) } } } await sleep(1000) } mainLoopRunning = false } const managePanel = () => { if (cheatsOff) return const controlPanel = document.querySelector('div#theresMoreHelpControlPanel') let scriptState = scriptPaused ? `▶️` : `⏸️` if (!controlPanel) { const controlPanelElement = document.createElement('div') controlPanelElement.id = 'theresMoreHelpControlPanel' controlPanelElement.classList.add('dark') controlPanelElement.classList.add('dark:bg-mydark-300') controlPanelElement.style.position = 'fixed' controlPanelElement.style.bottom = '10px' controlPanelElement.style.left = '10px' controlPanelElement.style.zIndex = '99999999' controlPanelElement.style.border = '1px black solid' controlPanelElement.style.padding = '10px' controlPanelElement.innerHTML = ` <p class="mb-2">TheresMoreHelp: <button onClick="window.switchScriptState()" title="Start/stop script" class="scriptState">${scriptState}</button> </p> ` document.querySelector('div#root').insertAdjacentElement('afterend', controlPanelElement) } else { controlPanel.querySelector('.scriptState').textContent = scriptState } if (!scriptPaused) { const fullPageOverlay = document.querySelector('div > div.absolute.top-0.right-0.z-20.pt-4.pr-4 > button') if (fullPageOverlay) { fullPageOverlay.click() } } } const calculateTTF = () => { const resourceTrNodes = document.querySelectorAll('#root > div > div:not(#maintabs-container) > div > div > div > table:not(.hidden) > tbody > tr') resourceTrNodes.forEach((row) => { const cells = row.querySelectorAll('td') const resourceName = cells[0].textContent.trim() const resource = getResource(resourceName) let ttf = '' if (resource && resource.current < resource.max && resource.speed) { ttf = resource.ttf ? resource.ttf.timeShort : resource.ttz ? resource.ttz.timeShort : '' } if (!cells[3]) { const ttfElement = document.createElement('td') ttfElement.className = 'px-3 3xl:px-5 py-3 lg:py-2 3xl:py-3 whitespace-nowrap w-1/3 text-right' ttfElement.textContent = ttf row.appendChild(ttfElement) } else { cells[3].textContent = ttf } }) } const calculateTippyTTF = () => { let potentialResourcesToFillTable = document.querySelectorAll('div.tippy-box > div.tippy-content > div > div > table') if (potentialResourcesToFillTable.length) { potentialResourcesToFillTable = potentialResourcesToFillTable[0] const rows = potentialResourcesToFillTable.querySelectorAll('tr') rows.forEach((row) => { const cells = row.querySelectorAll('td') const resourceName = cells[0].textContent.trim() const resource = getResource(resourceName) if (resource) { let ttf = '✅' const target = numberParser.parse( cells[1].textContent .split(' ') .shift() .replace(/[^0-9KM\-,\.]/g, '') .trim() ) if (target > resource.max || resource.speed <= 0) { ttf = 'never' } else if (target > resource.current) { ttf = formatTime(Math.ceil((target - resource.current) / resource.speed)).timeShort } if (!cells[2]) { const ttfElement = document.createElement('td') ttfElement.className = 'px-4 3xl:py-1 text-right' ttfElement.textContent = ttf row.appendChild(ttfElement) } else { cells[2].textContent = ttf } } }) } } const tossACoinToYourClicker = async () => { if (cheatsOff) return if (!haveManualResourceButtons) return if (scriptPaused) return if (isClicking) return isClicking = true const manualResources = ['Food', 'Wood', 'Stone'].filter((resourceName) => { const resource = getResource(resourceName) if (resource && resource.current < Math.min(200, resource.max) && (!resource.speed || resource.speed <= 0)) { return true } }) const buttons = [ ...document.querySelectorAll('#root > div.flex.flex-wrap.w-full.mx-auto.p-2 > div.w-full.lg\\:pl-2 > div > div.order-2.flex.flex-wrap.gap-3 > button'), ] if (!buttons.length) { haveManualResourceButtons = false return } const buttonsToClick = buttons.filter((button) => manualResources.includes(button.innerText.trim())) while (!scriptPaused && buttonsToClick.length) { const buttonToClick = buttonsToClick.shift() buttonToClick.click() await sleep(250) } isClicking = false } const performRoutineTasks = async () => { calculateTTF() if (!cheatsOff) { managePanel() if (haveManualResourceButtons) tossACoinToYourClicker() } } const performFastTasks = async () => { calculateTippyTTF() } window.setInterval(performRoutineTasks, 1000) window.setInterval(performFastTasks, 100) const loadSettingsFromLocalStorage = () => { const TMH_scriptPaused = window.localStorage.getItem('TMH_scriptPaused') const TMH_cheatsOff = window.localStorage.getItem('TMH_cheatsOff') if (TMH_cheatsOff) { cheatsOff = JSON.parse(TMH_cheatsOff) } if (TMH_scriptPaused) { scriptPaused = JSON.parse(TMH_scriptPaused) } } loadSettingsFromLocalStorage() if (!cheatsOff) { await sleep(5000) if (!scriptPaused) { setTimeout(() => { window.location.reload() }, 60 * 60 * 1000) mainLoop() } } else { logger({ msgLevel: 'log', msg: 'Welcome to TheresMoreHelp script!' }) logger({ msgLevel: 'log', msg: 'Please execute the following in the console if you want to enable cheats:' }) logger({ msgLevel: 'log', msg: ' ' }) logger({ msgLevel: 'log', msg: 'window.localStorage.setItem("TMH_cheatsOff", JSON.stringify(false))' }) logger({ msgLevel: 'log', msg: ' ' }) logger({ msgLevel: 'log', msg: 'and then refresh the page. Have fun!' }) } })()