TheresMoreHelp

Helper for TheresMoreGame

目前為 2022-12-23 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        TheresMoreHelp
// @namespace   TheresMoreGame.com
// @match       https://www.theresmoregame.com/play/
// @grant       none
// @version     1.7
// @description Helper for TheresMoreGame
// @license     MIT
// @run-at      document-idle
// ==/UserScript==

;(async () => {
  const cheatsOff = true

  let scriptPaused = true
  let manualGold = false
  let haveManualResourceButtons = true

  const buildingsList = [
    { name: 'City center', alwaysBuild: true, isSafe: true },
    { name: 'Cathedral', alwaysBuild: true, isSafe: true },
    { name: 'Great fair', alwaysBuild: true, isSafe: true },
    { name: 'Palisade', alwaysBuild: true, isSafe: true },
    { name: 'Wall', alwaysBuild: true, isSafe: true },
    { name: 'City center part', alwaysBuild: true, isSafe: true },
    { name: 'Great fair unit', alwaysBuild: true, isSafe: true },
    { name: 'Cathedral part', alwaysBuild: true, isSafe: true },
    { name: 'Guild of craftsmen', alwaysBuild: true, isSafe: true },
    { name: 'Library of Theresmore', alwaysBuild: true, isSafe: true },
    { name: 'Watchman Outpost', alwaysBuild: true, isSafe: true },
    { name: 'Sawmill', alwaysBuild: true, isSafe: true },
    { name: 'Foundry', alwaysBuild: true, isSafe: true },
    { name: 'Bank', isSafe: true },
    { name: 'Marketplace', isSafe: true },
    { name: 'Artisan Workshop', isSafe: true },
    { name: 'Granary', isSafe: true },
    { name: 'University', isSafe: true },
    { name: 'Fiefdom', isSafe: true },
    { name: 'School', isSafe: true },
    { name: 'Lumberjack Camp', isSafe: true },
    { name: 'Quarry', isSafe: true },
    { name: 'Mine', isSafe: true },
    { name: 'Palisade part', isSafe: true },
    { name: 'Wall part', isSafe: true },
    { name: 'Farm', isSafe: true },
    { name: 'Stable', isSafe: true },
    { name: 'Temple', isSafe: true },
    { name: 'Altar of sacrifices', isSafe: true },
    { name: 'Valley of plenty', isSafe: true },
    { name: 'Magic Circle', isSafe: true },
    { name: 'Carpenter workshop', isSafe: true },
    { name: 'Grocery', isSafe: true },
    { name: 'Steelworks', isSafe: true },
    { name: 'Ballista', isSafe: true },
    { name: 'Recruit training center', isSafe: true },
    { name: 'Large storehouse', isSafe: true },
    { name: 'Store', isSafe: true },
    { name: 'Barracks', isSafe: true },
    { name: 'Fortune grove', isSafe: true },
    { name: 'Mansion', isSafe: false, requires: { resource: 'Food', parameter: 'speed', minValue: 3 } },
    { name: 'City Hall', isSafe: false, requires: { resource: 'Food', parameter: 'speed', minValue: 1.5 } },
    { name: 'Common House', isSafe: false, requires: { resource: 'Food', parameter: 'speed', minValue: 1 } },
  ].filter((building) => building.name)

  const modes = {
    RESEARCH: 'Research',
    GAIN_GOLD: 'Gain Gold',
    BUILD: 'Build',
    DEFAULT: 'Default',
  }
  let mode = modes.DEFAULT
  let marketPlaceAvailable = false

  const pages = {
    BUILD: 'Build',
    RESEARCH: 'Research',
    POPULATION: 'Population',
    ARMY: 'Army',
    MARKETPLACE: 'Marketplace',
    MAGIC: 'Magic',
    UNKNOWN: 'Unknown',
  }

  const sleep = (ms) => new Promise((r) => setTimeout(r, ms))

  // 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,
    }
  }

  window.switchScriptState = () => {
    scriptPaused = !scriptPaused

    if (!scriptPaused) {
      mode = modes.DEFAULT
      switchPage(pages.BUILD)
    }
  }

  window.switchManualGold = () => {
    manualGold = !manualGold
  }

  const getAllButtons = () => {
    let buttonsList = []
    document
      .querySelectorAll('#maintabs-container button.btn.btn-dark:not(.btn-off)')
      .forEach((button) => !button.innerText.includes('Glorious retirement') && buttonsList.push(button))

    return buttonsList
  }

  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.forEach((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())

        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 getAllResources = () => {
    const resources = document.querySelectorAll('#root div > div > div > table > tbody > tr > td:nth-child(1) > span')
    const resourceNames = []

    resources.forEach((resource) => resourceNames.push(resource.textContent.trim()))

    return resourceNames
  }

  const getPage = () => {
    let page = pages.UNKNOWN

    const pageSelectedSelector = document.querySelector('#main-tabs > div > button[aria-selected="true"]')

    if (pageSelectedSelector) {
      const pageSelected = pageSelectedSelector.textContent

      if (pageSelected.includes('Build')) {
        page = pages.BUILD
      } else if (pageSelected.includes('Research')) {
        page = pages.RESEARCH
      } else if (pageSelected.includes('Population')) {
        page = pages.POPULATION
      } else if (pageSelected.includes('Army')) {
        page = pages.ARMY
      } else if (pageSelected.includes('Marketplace')) {
        page = pages.MARKETPLACE
      } else if (pageSelected.includes('Magic')) {
        page = pages.MAGIC
      }
    }

    return page
  }

  const switchPage = async (page) => {
    if (cheatsOff) return
    if (scriptPaused) return

    let switchedPage = false

    const navButtons = document.querySelectorAll('#main-tabs > div > button')
    navButtons.forEach((button) => {
      if (button.innerText.includes(page) && button.getAttribute('aria-selected') !== 'true') {
        button.click()
        switchedPage = true
      }
    })

    if (switchedPage) {
      console.log(`Switched page to ${page}`)
      await sleep(5000)
    }

    setTimeout(takePageAction, 1)
  }

  const takePageAction = async () => {
    if (cheatsOff) return
    if (scriptPaused) return

    let page = getPage()

    if (page !== pages.BUILD && mode === modes.BUILD) {
      switchPage(pages.BUILD)
      return
    } else if (page !== pages.RESEARCH && mode === modes.RESEARCH) {
      switchPage(pages.RESEARCH)
      return
    } else if (page !== pages.MARKETPLACE && mode === modes.GAIN_GOLD) {
      switchPage(pages.MARKETPLACE)
      return
    }

    if (page === pages.BUILD) {
      setTimeout(spendResourcesBuilding, 1)
    } else if (page === pages.RESEARCH) {
      setTimeout(performReserach, 1)
    } else if (page === pages.MARKETPLACE) {
      setTimeout(playMarket, 1)
    } else {
      setTimeout(takePageAction, 1000)
    }
  }

  const spendResourcesBuilding = async () => {
    if (cheatsOff) return
    if (scriptPaused) return

    if (getPage() !== pages.BUILD) {
      setTimeout(takePageAction, 1000)
      return
    }

    const buttonsList = getAllButtons()
    const buildingNames = []

    buttonsList.forEach((button) => buildingNames.push(button.innerText.split('\n').shift()))

    let hasBuilt = false
    let shouldBuild = false

    for (let i = 0; i < buildingsList.length && !hasBuilt; i++) {
      if (hasBuilt) break

      const building = buildingsList[i]

      shouldBuild = buildingNames.includes(building.name)

      if (!building.isSafe) {
        const requiredResource = getResource(building.requires.resource)
        if (!requiredResource) {
          shouldBuild = false
        } else {
          shouldBuild = shouldBuild && requiredResource[building.requires.parameter] > building.requires.minValue
        }
      }

      if (shouldBuild) {
        if (building.alwaysBuild || mode === modes.BUILD) {
          buttonsList.forEach((button) => {
            if (button.innerText.split('\n').shift() === building.name) {
              button.click()
              hasBuilt = true
              mode = modes.DEFAULT
              console.log(`Started building ${building.name}`)
            }
          })
        } else {
          mode = modes.BUILD
        }
      }
    }

    if (mode === modes.BUILD || shouldBuild || hasBuilt) {
      setTimeout(spendResourcesBuilding, 15000)
    } else {
      const gold = getResource('Gold')
      if (marketPlaceAvailable && gold.current + gold.speed * 30 < gold.max) {
        mode = modes.GAIN_GOLD
        switchPage(pages.MARKETPLACE)
      } else {
        mode = modes.RESEARCH
        switchPage(pages.RESEARCH)
      }
    }
  }

  const performReserach = async () => {
    if (cheatsOff) return
    if (scriptPaused) return

    if (getPage() !== pages.RESEARCH) {
      setTimeout(takePageAction, 1000)
      return
    }

    const manualResearches = ['A moonlight night']

    let didResearch = false

    const buttonsList = getAllButtons()
    const researchesList = []

    buttonsList.forEach(
      (button) => !manualResearches.includes(button.innerText.split('\n').shift()) && researchesList.push(button.innerText.split('\n').shift())
    )

    if (researchesList.length) {
      while (!didResearch && researchesList.length) {
        const research = researchesList.pop()

        buttonsList.forEach((button) => {
          if (button.innerText.split('\n').shift() === research) {
            button.click()
            didResearch = true
            console.log(`Researching ${research}`)
          }
        })
      }
    }

    if (didResearch) {
      await sleep(5000)
    }

    mode = modes.DEFAULT
    await switchPage(pages.BUILD)
  }

  const playMarket = async () => {
    if (cheatsOff) return
    if (!marketPlaceAvailable) return

    if (getPage() !== pages.MARKETPLACE) {
      setTimeout(takePageAction, 1000)
      return
    }

    let gold = getResource('Gold')

    if (gold && gold.current < gold.max) {
      const buyHorses = document.querySelector(
        'div > div > div.grid.gap-3.grid-cols-fill-240.min-w-full > div:nth-child(8) > div.grid.gap-2.grid-cols-2 > button:nth-child(4)'
      )

      const sellHorses = document.querySelector(
        'div > div > div.grid.gap-3.grid-cols-fill-240.min-w-full > div:nth-child(8) > div.grid.gap-2.grid-cols-2 > button:nth-child(3)'
      )

      while (gold && gold.current < gold.max) {
        buyHorses.click()
        await sleep(1)
        sellHorses.click()
        await sleep(1)
        gold = getResource('Gold')
      }
    }

    mode = modes.RESEARCH
    await switchPage(pages.RESEARCH)
  }

  const managePanel = () => {
    if (cheatsOff) return

    const controlPanel = document.querySelector('div#theresMoreHelpControlPanel')

    let scriptState = scriptPaused ? `▶️` : `⏸️`
    let manualGoldState = manualGold ? '🤑' : `😅`

    if (!controlPanel) {
      const controlPanelElement = document.createElement('div')
      controlPanelElement.id = 'theresMoreHelpControlPanel'
      controlPanelElement.style.position = 'fixed'
      controlPanelElement.style.bottom = '20px'
      controlPanelElement.style.left = '20px'
      controlPanelElement.style.zIndex = '99999999'
      controlPanelElement.border = '1px black solid'
      controlPanelElement.padding = '5px'

      controlPanelElement.innerHTML = `
          <button onClick="window.switchScriptState()" title="Start/stop script" class="scriptState">${scriptState}</button>
          <button onClick="window.switchManualGold()" title="Always play market" class="manualGold">${manualGoldState}</button>
          <span class="currentPage">Page: ${getPage()}</span>
          <span class="currentAction">Mode: ${mode}</span>
        `

      document.querySelector('div#root').insertAdjacentElement('afterend', controlPanelElement)
    } else {
      controlPanel.querySelector('span.currentPage').textContent = `Page: ${getPage()}`
      controlPanel.querySelector('span.currentAction').textContent = `Mode: ${mode}`
      controlPanel.querySelector('button.scriptState').textContent = scriptState
      controlPanel.querySelector('button.manualGold').textContent = manualGoldState
    }

    if (scriptPaused && manualGold && marketPlaceAvailable && getPage() === pages.MARKETPLACE) {
      playMarket()
    }
  }

  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.timeLong : resource.ttz.timeLong
      }

      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 checkMarketplaceAvailable = async () => {
    const horse = getResource('Horse')
    if (horse && horse.max > 10) {
      const page = getPage()

      if (page === pages.BUILD) {
        const buttonsList = document.querySelectorAll('button.btn')

        buttonsList.forEach((button) => {
          if (button.innerText.includes('Marketplace')) {
            if (button.querySelector('span')) {
              marketPlaceAvailable = true
            }
          }
        })
      }
    }
  }

  const tossACoinToYourClicker = () => {
    if (cheatsOff) return
    if (!haveManualResourceButtons) return
    if (scriptPaused) return

    const manualResources = ['Food', 'Wood', 'Stone']
    const resourcesToClick = []
    const buttonsToClick = []
    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) {
      haveManualResourceButtons = false
      return
    }

    manualResources.forEach((resourceName) => {
      const resource = getResource(resourceName)

      if (resource && resource.current < resource.max && (!resource.speed || resource.speed <= 0)) {
        resourcesToClick.push(resourceName)
      }
    })

    buttons.forEach((button) => {
      if (resourcesToClick.includes(button.innerText.trim())) {
        buttonsToClick.push(button)
      }
    })

    while (buttonsToClick.length) {
      const buttonToClick = buttonsToClick.shift()
      buttonToClick.click()
    }
  }

  const performRoutineTasks = () => {
    if (!marketPlaceAvailable) checkMarketplaceAvailable()
    managePanel()
    calculateTTF()
  }

  const performFastTasks = () => {
    calculateTippyTTF()
    if (haveManualResourceButtons) tossACoinToYourClicker()
  }

  window.setInterval(performRoutineTasks, 1000)
  window.setInterval(performFastTasks, 100)

  if (!cheatsOff) {
    takePageAction()
  }
})()