Last active
October 7, 2025 16:43
-
-
Save anuragl94/5055b966ce2e6f848c721271c7434ec1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name AutoFarm RPG | |
| // @namespace automatron | |
| // @version 0.1.0 | |
| // @description Farm RPG shortcuts to make your life easy | |
| // @author Automatron | |
| // @match https://farmrpg.com/* | |
| // @match https://alpha.farmrpg.com/* | |
| // @icon https://www.google.com/s2/favicons?sz=64&domain=farmrpg.com | |
| // @license MIT | |
| // @grant none | |
| // ==/UserScript== | |
| (function(window) { | |
| 'use strict'; | |
| const DEBUG_MODE = true; | |
| const STYLE = ` | |
| @scope { | |
| :scope { | |
| --border-color: hsl(0 0% 40% / 1); | |
| --bg-color: hsl(220deg 5% 8% / 80%); | |
| position: fixed; | |
| border: 1px solid var(--border-color); | |
| border-radius: 2px; | |
| background-color: var(--bg-color); | |
| backdrop-filter: blur(4px); | |
| min-height: 240px; | |
| min-width: 320px; | |
| color: #fff; | |
| padding: 32px 16px; | |
| &.movable { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| z-index: 999999; | |
| } | |
| .tray-handle { | |
| cursor: move; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| padding: 4px; | |
| &::after { | |
| display: block; | |
| content: ""; | |
| width: 16px; | |
| aspect-ratio: 1/1; | |
| background-image: linear-gradient(to bottom right, var(--border-color) 25%, transparent 25%, transparent 50%, var(--border-color) 50%, var(--border-color) 55%, transparent 55%); | |
| } | |
| } | |
| .tray-actions { | |
| margin-bottom: 32px; | |
| } | |
| .tray-abort { | |
| margin-top: 8px; | |
| } | |
| button { | |
| cursor: pointer; | |
| } | |
| }}`; | |
| // Dataset | |
| const TOWNSFOLK = { | |
| "Baba Gec": { | |
| "likes": [ | |
| "Leek", | |
| "Onion", | |
| "Rope", | |
| "Snail" | |
| ], | |
| "loves": [ | |
| "Cabbage Stew", | |
| "Peach Juice", | |
| "Wooden Button" | |
| ], | |
| "mailbox": "267531", | |
| "img": "https://farmrpg.com/img/items/merchant.png" | |
| }, | |
| "Beatrix": { | |
| "likes": [ | |
| "Bird Egg", | |
| "Carbon Sphere", | |
| "Coal", | |
| "Hammer", | |
| "Hops", | |
| "Oak" | |
| ], | |
| "loves": [ | |
| "Black Powder", | |
| "Explosive", | |
| "Fireworks", | |
| "Iced Tea" | |
| ], | |
| "mailbox": "22440", | |
| "img": "https://farmrpg.com/img/items/a_011.png" | |
| }, | |
| "Borgen": { | |
| "likes": [ | |
| "Glass Orb", | |
| "Gold Carrot", | |
| "Gold Cucumber", | |
| "Gold Peas", | |
| "Milk", | |
| "Slimestone" | |
| ], | |
| "loves": [ | |
| "Cheese", | |
| "Gold Catfish", | |
| "Wooden Box" | |
| ], | |
| "mailbox": "53900", | |
| "img": "https://farmrpg.com/img/items/borgen.png" | |
| }, | |
| "Buddy": { | |
| "likes": [ | |
| "Bone", | |
| "Bucket", | |
| "Giant Centipede", | |
| "Gold Peppers", | |
| "Gummy Worms", | |
| "Mushroom", | |
| "Snail", | |
| "Spider" | |
| ], | |
| "loves": [ | |
| "Pirate Bandana", | |
| "Pirate Flag", | |
| "Purple Flower", | |
| "Valentines Card" | |
| ], | |
| "mailbox": "22447", | |
| "img": "https://farmrpg.com/img/items/buddy.png" | |
| }, | |
| "Cpt Thomas": { | |
| "likes": [ | |
| "Blue Crab", | |
| "Minnows" | |
| ], | |
| "loves": [ | |
| "Fishing Net", | |
| "Gold Catfish", | |
| "Gold Drum", | |
| "Gold Trout", | |
| "Large Net" | |
| ], | |
| "mailbox": "71805", | |
| "img": "https://farmrpg.com/img/items/MustacheTom96.png" | |
| }, | |
| "Cecil": { | |
| "likes": [ | |
| "Aquamarine", | |
| "Giant Centipede", | |
| "Grapes", | |
| "Ladder", | |
| "Slimestone", | |
| "Snail" | |
| ], | |
| "loves": [ | |
| "Grasshopper", | |
| "Horned Beetle", | |
| "Leather", | |
| "MIAB", | |
| "Old Boot", | |
| "Shiny Beetle", | |
| "Yarn" | |
| ], | |
| "mailbox": "22442", | |
| "img": "https://farmrpg.com/img/items/a_027.png" | |
| }, | |
| "Charles": { | |
| "likes": [ | |
| "3-leaf Clover", | |
| "Carrot", | |
| "Grasshopper", | |
| "Twine" | |
| ], | |
| "loves": [ | |
| "Apple", | |
| "Apple Cider", | |
| "Box of Chocolate 01", | |
| "Gold Carrot", | |
| "Peach", | |
| "Valentines Card" | |
| ], | |
| "mailbox": "71760", | |
| "img": "https://farmrpg.com/img/items/npc_horse.png" | |
| }, | |
| "Cid": { | |
| "likes": [ | |
| "Black Powder", | |
| "Blue Feathers", | |
| "Shimmer Stone", | |
| "Stone" | |
| ], | |
| "loves": [ | |
| "Bomb", | |
| "Diamonds", | |
| "Explosive", | |
| "Mushroom Stew", | |
| "Safety Goggles", | |
| "Spider" | |
| ], | |
| "mailbox": "16", | |
| "img": "https://farmrpg.com/img/items/cid.png" | |
| }, | |
| "frank": { | |
| "likes": [ | |
| "Blue Dye", | |
| "Blue Feathers", | |
| "Bucket", | |
| "Caterpillar", | |
| "Feathers", | |
| "Grasshopper" | |
| ], | |
| "loves": [ | |
| "Carrot", | |
| "Gold Carrot" | |
| ], | |
| "mailbox": "84518", | |
| "img": "https://farmrpg.com/img/items/npc_bunny1.png" | |
| }, | |
| "Gary Bearson V": { | |
| "likes": [ | |
| "Feathers", | |
| "Oak", | |
| "Trout" | |
| ], | |
| "loves": [ | |
| "Apple Cider", | |
| "Gold Trout", | |
| "Yarn", | |
| "You Rock Card" | |
| ], | |
| "mailbox": "38", | |
| "img": "https://farmrpg.com/img/items/bear_01.png" | |
| }, | |
| "Geist": { | |
| "likes": [ | |
| "Blue Crab", | |
| "Green Chromis", | |
| "Stingray", | |
| "Yellow Perch" | |
| ], | |
| "loves": [ | |
| "Gold Catfish", | |
| "Goldgill", | |
| "Sea Pincher Special", | |
| "Shrimp-a-Plenty" | |
| ], | |
| "mailbox": "118065", | |
| "img": "https://farmrpg.com/img/items/npc_beast.png" | |
| }, | |
| "George": { | |
| "likes": [ | |
| "Arrowhead", | |
| "Bird Egg", | |
| "Glass Orb", | |
| "Hops", | |
| "Mushroom Stew", | |
| "Orange Juice" | |
| ], | |
| "loves": [ | |
| "Apple Cider", | |
| "Carbon Sphere", | |
| "Hide", | |
| "Mug of Beer", | |
| "Spider" | |
| ], | |
| "mailbox": "22443", | |
| "img": "https://farmrpg.com/img/items/a_034.png" | |
| }, | |
| "Holger": { | |
| "likes": [ | |
| "Apple Cider", | |
| "Arrowhead", | |
| "Bluegill", | |
| "Carp", | |
| "Cheese", | |
| "Horn", | |
| "Largemouth Bass", | |
| "Mushroom Stew", | |
| "Peach", | |
| "Peas", | |
| "Trout" | |
| ], | |
| "loves": [ | |
| "Gold Trout", | |
| "Mug of Beer", | |
| "Potato", | |
| "Wooden Table" | |
| ], | |
| "mailbox": "22439", | |
| "img": "https://farmrpg.com/img/items/a_028.png" | |
| }, | |
| "Jill": { | |
| "likes": [ | |
| "Cheese", | |
| "Grapes", | |
| "Milk", | |
| "Old Boot", | |
| "Scrap Metal", | |
| "Tomato" | |
| ], | |
| "loves": [ | |
| "Leather", | |
| "MIAB", | |
| "Mushroom Paste", | |
| "Peach", | |
| "Yellow Perch" | |
| ], | |
| "mailbox": "22444", | |
| "img": "https://farmrpg.com/img/items/a_024.png" | |
| }, | |
| "Lorn": { | |
| "likes": [ | |
| "3-leaf Clover", | |
| "Apple Cider", | |
| "Bucket", | |
| "Green Parchment", | |
| "Iced Tea", | |
| "Iron Cup", | |
| "Peas", | |
| "Purple Parchment" | |
| ], | |
| "loves": [ | |
| "Glass Orb", | |
| "Gold Peas", | |
| "Milk", | |
| "Shrimp", | |
| "Small Prawn" | |
| ], | |
| "mailbox": "22446", | |
| "img": "https://farmrpg.com/img/items/a_088.png" | |
| }, | |
| "Mariya": { | |
| "likes": [ | |
| "Cucumber", | |
| "Eggplant", | |
| "Eggs", | |
| "Iced Tea", | |
| "Milk", | |
| "Peach", | |
| "Radish" | |
| ], | |
| "loves": [ | |
| "Cat's Meow", | |
| "Leather Diary", | |
| "Mushroom Stew", | |
| "Onion Soup", | |
| "Over The Moon", | |
| "Quandary Chowder", | |
| "Sea Pincher Special", | |
| "Shrimp-a-Plenty" | |
| ], | |
| "mailbox": "178572", | |
| "img": "https://farmrpg.com/img/items/mariya.png" | |
| }, | |
| "Mummy": { | |
| "likes": [ | |
| "Fish Bones", | |
| "Hammer", | |
| "Treat Bag 02", | |
| "Yarn" | |
| ], | |
| "loves": [ | |
| "Bone", | |
| "Spider", | |
| "Valentines Card" | |
| ], | |
| "mailbox": "70604", | |
| "img": "https://farmrpg.com/img/items/mummy_t_01.png" | |
| }, | |
| "Ric Ryph": { | |
| "likes": [ | |
| "Arrowhead", | |
| "Black Powder", | |
| "Bucket", | |
| "Carbon Sphere", | |
| "Coal", | |
| "Green Parchment", | |
| "Old Boot", | |
| "Unpolished Shimmer Stone" | |
| ], | |
| "loves": [ | |
| "5 Gold", | |
| "Hammer", | |
| "Mushroom Paste", | |
| "Shovel" | |
| ], | |
| "mailbox": "59421", | |
| "img": "https://farmrpg.com/img/items/npc_figure2.png" | |
| }, | |
| "ROOMBA": { | |
| "likes": [ | |
| "Glass Orb", | |
| "Hammer", | |
| "Scrap Wire" | |
| ], | |
| "loves": [ | |
| "Carbon Sphere", | |
| "Scrap Metal" | |
| ], | |
| "mailbox": "71761", | |
| "img": "https://farmrpg.com/img/items/robot_02.png" | |
| }, | |
| "Rosalie": { | |
| "likes": [ | |
| "Apple", | |
| "Apple Cider", | |
| "Aquamarine", | |
| "Carrot", | |
| "Caterpillar", | |
| "Fireworks", | |
| "Iced Tea", | |
| "Purple Flower" | |
| ], | |
| "loves": [ | |
| "Blue Dye", | |
| "Box of Chocolate 01", | |
| "Gold Carrot", | |
| "Green Dye", | |
| "Purple Dye", | |
| "Red Dye", | |
| "Valentines Card" | |
| ], | |
| "mailbox": "22438", | |
| "img": "https://farmrpg.com/img/items/a_098.png" | |
| }, | |
| "Star Meerif": { | |
| "likes": [ | |
| "Eggs", | |
| "Feathers" | |
| ], | |
| "loves": [ | |
| "Blue Feathers", | |
| "Gold Feather" | |
| ], | |
| "mailbox": "46158", | |
| "img": "https://farmrpg.com/img/items/npc_figure.png" | |
| }, | |
| "Thomas": { | |
| "likes": [ | |
| "Carp", | |
| "Drum", | |
| "Gummy Worms", | |
| "Iced Tea", | |
| "Largemouth Bass", | |
| "Mealworms", | |
| "Minnows" | |
| ], | |
| "loves": [ | |
| "Fishing Net", | |
| "Flier", | |
| "Gold Catfish", | |
| "Gold Trout", | |
| "Goldgill" | |
| ], | |
| "mailbox": "22441", | |
| "img": "https://farmrpg.com/img/items/a_048.png" | |
| }, | |
| "Vincent": { | |
| "likes": [ | |
| "Acorn", | |
| "Apple", | |
| "Cheese", | |
| "Hops", | |
| "Horn", | |
| "Leather Diary", | |
| "Shovel", | |
| "Wooden Box" | |
| ], | |
| "loves": [ | |
| "5 Gold", | |
| "Apple Cider", | |
| "Axe", | |
| "Lemonade", | |
| "Mushroom Paste", | |
| "Onion Soup", | |
| "Orange Juice" | |
| ], | |
| "mailbox": "22445", | |
| "img": "https://farmrpg.com/img/items/a_047.png" | |
| } | |
| }; | |
| // { crop_id: seed_id } | |
| const SEEDS_MAP = { | |
| 11: 12, // pepper | |
| }; | |
| // Utils | |
| function newEl(tag, classes, content) { | |
| const el = document.createElement(tag); | |
| if (classes) { | |
| el.classList.add(...classes.split(" ").map(s => s.trim()).filter(Boolean)); | |
| } | |
| if (content) { | |
| el.innerHTML = content; | |
| } | |
| return el; | |
| } | |
| async function addDelay(minDelay, maxDelay = minDelay, abortSignal) { | |
| if (maxDelay < minDelay) { | |
| maxDelay = minDelay; | |
| } | |
| const delay = Math.floor(minDelay + Math.random() * (maxDelay - minDelay)); | |
| return new Promise((resolve, reject) => { | |
| const timer = window.setTimeout(() => resolve(delay), delay); | |
| abortSignal?.addEventListener("abort", () => { | |
| window.clearTimeout(timer); | |
| reject("Request aborted"); | |
| }); | |
| }); | |
| } | |
| async function findElement(selector, subtree = document.body, abortSignal) { | |
| const elementInExistence = document.querySelector(selector); | |
| if (elementInExistence) { | |
| return elementInExistence; | |
| } | |
| return new Promise((resolve, reject) => { | |
| const observer = new MutationObserver((mutations, observer) => { | |
| const element = document.querySelector(selector); | |
| if (element) { | |
| observer.disconnect(); | |
| resolve(element); | |
| } | |
| }); | |
| observer.observe(subtree, { | |
| childList: true, | |
| subtree: true, | |
| attributes : true, | |
| attributeFilter : ["style", "class"] | |
| }); | |
| abortSignal?.addEventListener("abort", () => { | |
| reject("Request aborted"); | |
| }); | |
| }); | |
| } | |
| // give it a name for the script to remember the element's position even after browser reload | |
| function makeElementMovable(element, handler, syncName) { | |
| element.classList.add("movable"); | |
| let prevX = 0, prevY = 0, isDragging = false; | |
| const moveEl = (dx, dy) => { | |
| const rect = element.getBoundingClientRect(); | |
| const x = rect.left + dx; | |
| const y = rect.top + dy; | |
| element.style.position = "absolute"; | |
| element.style.left = x + "px"; | |
| element.style.top = y + "px"; | |
| if (syncName) { | |
| window.localStorage.setItem(`__autofarm_pos_${syncName}`, `${x},${y}`); | |
| } | |
| }; | |
| const syncedValue = window.localStorage.getItem(`__autofarm_pos_${syncName}`); | |
| if (syncedValue) { | |
| const coords = syncedValue.split(",").map(v => Math.min(Number(v), 1000)); | |
| moveEl(...coords.map(Number)); | |
| } | |
| handler.addEventListener("mousedown", function (e) { | |
| isDragging = true; | |
| prevX = e.clientX; | |
| prevY = e.clientY; | |
| e.preventDefault(); | |
| }); | |
| window.addEventListener("mouseup", function () { | |
| isDragging = false; | |
| }); | |
| window.addEventListener("mousemove", function (e) { | |
| if (!isDragging) return; | |
| const dx = e.clientX - prevX; | |
| const dy = e.clientY - prevY; | |
| moveEl(dx, dy); | |
| prevX = e.clientX; | |
| prevY = e.clientY; | |
| }); | |
| } | |
| class Overseer { | |
| // This is a singleton class. Do not instantiate multiple times. | |
| #tray; | |
| #currentPage; | |
| #flags = {}; // any page should be able to access flags - toggles may be selectively shown | |
| #actionButtons = []; | |
| #actionControllers = []; | |
| #injectorScripts = []; | |
| #abortController = new AbortController(); | |
| utils = { | |
| findElement: async (query, subtree) => findElement(query, subtree, this.abortController.signal), | |
| addDelay: async (min, max) => addDelay(min, max, this.abortController.signal), | |
| abort: () => { this.#abortController.abort(); } | |
| }; | |
| get abortController() { | |
| if (this.#abortController.signal.aborted) { | |
| this.#abortController = new AbortController(); | |
| } | |
| return this.#abortController; | |
| } | |
| constructor() { | |
| this.#createTray(); | |
| this.#attachHashStateListener(); | |
| } | |
| // DOM actions | |
| #createTray = () => { | |
| const styleTag = newEl("style", "", STYLE); | |
| this.#tray = newEl("div", "autofarm-tray"); | |
| this.#tray.appendChild(styleTag); | |
| const trayHandle = newEl("div", "tray-handle"); | |
| this.#tray.appendChild(trayHandle); | |
| const trayActionsContainer = newEl("div", "tray-actions"); | |
| this.#tray.appendChild(trayActionsContainer); | |
| const abortButton = newEl("button", "tray-abort button btnred", "Abort All"); | |
| this.#tray.appendChild(abortButton); | |
| abortButton.addEventListener("click", (e) => { | |
| this.#abortController.abort(); | |
| }); | |
| document.body.appendChild(this.#tray); | |
| makeElementMovable(this.#tray, trayHandle, "tray"); | |
| } | |
| #attachHashStateListener = () => { | |
| this.page = window.history.state?.url || "index.php"; // game is inconsistent with setting its own hashstate | |
| window.history.pushState = new Proxy(window.history.pushState, { | |
| apply: (target, thisArg, argArray) => { | |
| // trigger here what you need | |
| this.page = argArray[0].url; | |
| return target.apply(thisArg, argArray); | |
| }, | |
| }); | |
| window.history.back = new Proxy(window.history.back, { | |
| apply: (target, thisArg, argArray) => { | |
| // trigger here what you need | |
| this.page = argArray[0].url; | |
| return target.apply(thisArg, argArray); | |
| }, | |
| }); | |
| window.history.replaceState = new Proxy(window.history.replaceState, { | |
| apply: (target, thisArg, argArray) => { | |
| // trigger here what you need | |
| this.page = argArray[0].url; | |
| return target.apply(thisArg, argArray); | |
| }, | |
| }); | |
| window.history.go = new Proxy(window.history.go, { | |
| apply: (target, thisArg, argArray) => { | |
| // trigger here what you need | |
| this.page = argArray[0].url; | |
| return target.apply(thisArg, argArray); | |
| }, | |
| }); | |
| } | |
| createActionButton({ | |
| page = ".*", | |
| icon = "Button", | |
| flags, | |
| actions, | |
| condition | |
| }) { | |
| const button = newEl("button", "action-button button btngreen", icon); | |
| const config = { | |
| page, | |
| button, | |
| flagButtons: [] | |
| }; | |
| const actionQueue = new ActionQueue(actions.map(action => action.bind(null, this.utils, this.#flags)), condition); | |
| actionQueue.abortController = this.#abortController; | |
| this.#actionControllers.push(actionQueue); | |
| button.addEventListener("click", function(e) { | |
| const btn = e.currentTarget; | |
| btn.setAttribute("disabled", true); | |
| actionQueue.start().finally(() => btn.removeAttribute("disabled")); | |
| }); | |
| if (flags?.length) { | |
| flags.forEach(({ | |
| key, text, defaultValue | |
| }) => { | |
| this.#flags[key] = Boolean(defaultValue); | |
| const containerEl = newEl("label"); | |
| const checkboxEl = newEl("input"); | |
| checkboxEl.type = "checkbox"; | |
| checkboxEl.checked = this.#flags[key]; | |
| checkboxEl.addEventListener("change", (e) => { | |
| this.#flags[key] = e.target.checked; | |
| }); | |
| containerEl.appendChild(checkboxEl); | |
| const textEl = newEl("span"); | |
| textEl.innerText = text; | |
| containerEl.appendChild(textEl); | |
| config.flagButtons.push(containerEl); | |
| }); | |
| } | |
| this.#actionButtons.push(config); | |
| this.redraw(); | |
| } | |
| createScriptInjector({ | |
| page = "^$", | |
| name = "Injector script", | |
| actions | |
| }) { | |
| const actionQueue = new ActionQueue(actions.map(action => action.bind(null, this.utils, this.#flags))); | |
| this.#injectorScripts.push({ | |
| page, | |
| actions: actionQueue | |
| }); | |
| // Run it now if the user is already on the target page | |
| if ((new RegExp(page)).test(this.#currentPage)) { | |
| actionQueue.start(); | |
| } | |
| } | |
| // Utils | |
| redraw = () => { | |
| // render buttons relevant to the current page only | |
| const applicableConfigs = this.#actionButtons.filter(({ page }) => (new RegExp(page)).test(this.#currentPage)); | |
| const buttons = this.#actionButtons.filter(({ page }) => (new RegExp(page)).test(this.#currentPage)).map(({ button }) => button); | |
| const container = this.#tray.querySelector(".tray-actions"); | |
| container.childNodes.forEach(function(item) { | |
| container.removeChild(item); | |
| }); | |
| const innerContainer = newEl("div"); | |
| container.appendChild(innerContainer); | |
| applicableConfigs.forEach(({ button, flagButtons }) => { | |
| flagButtons.forEach(flagButton => { | |
| innerContainer.appendChild(flagButton); | |
| }); | |
| innerContainer.appendChild(newEl("br")); | |
| innerContainer.appendChild(button); | |
| }); | |
| } | |
| // Automation | |
| get page() { | |
| return this.#currentPage; | |
| } | |
| set page(value) { | |
| this.#currentPage = value; | |
| console.log("Page changed. Redrawing @", value); | |
| this.redraw(); | |
| const injectorActions = this.#injectorScripts.filter(({ page }) => (new RegExp(page)).test(this.#currentPage)).map(({ actions }) => actions); | |
| injectorActions.forEach(actionQueue => actionQueue.start()); | |
| } | |
| } | |
| class ActionQueue { | |
| #queue = []; | |
| #repeatCondition; | |
| #aborted = false; | |
| abortController; | |
| constructor(tasks = [], repeatCondition = () => false) { | |
| this.#queue = tasks; | |
| this.#repeatCondition = repeatCondition; | |
| } | |
| start = async () => { | |
| this.#aborted = false; | |
| const abortSignal = () => this.abort(); | |
| do { | |
| for (const task of this.#queue) { | |
| if (this.#aborted) return; | |
| try { | |
| await task(abortSignal); | |
| } catch (err) { | |
| this.abort(); | |
| console.log("Autofarm error:", err); | |
| break; | |
| } | |
| } | |
| } while (!this.#aborted && this.#repeatCondition()) | |
| }; | |
| abort = () => { | |
| this.#aborted = true; | |
| }; | |
| } | |
| class InventoryManager { | |
| #inventory = {}; | |
| #stale = true; | |
| constructor() {} | |
| refresh = (checkSellables = false) => { | |
| const url = checkSellables ? "https://farmrpg.com/store.php" : "https://farmrpg.com/workshop.php"; | |
| window.fetch(url) | |
| .then(r => r.text()) | |
| .then(html => { | |
| const parser = new DOMParser(); | |
| const doc = parser.parseFromString(html, "text/html"); | |
| this.#parseAndSave(doc); | |
| this.#stale = false; | |
| }).catch(error => { | |
| console.error("Failed to fetch Inventory page: ", error); | |
| }) | |
| } | |
| purchase(itemName, qty = 1) { | |
| if (!this.#inventory.hasOwnProperty(itemName)) { | |
| return; | |
| } | |
| const { id, maxPurchasable } = this.#inventory[itemName]; | |
| qty = Math.min(qty, maxPurchasable); | |
| return GlobalUtils.purchaseItem(id, qty).then(() => { | |
| this.#stale = true; | |
| }); | |
| } | |
| sell(itemName, qty = 1) { | |
| if (!this.#inventory.hasOwnProperty(itemName)) { | |
| return; | |
| } | |
| const { id, maxPurchasable } = this.#inventory[itemName]; | |
| qty = Math.min(qty, maxPurchasable); | |
| return GlobalUtils.sellItem(id, qty).then(() => { | |
| this.#stale = true; | |
| }); | |
| } | |
| #parseAndSave(doc) { | |
| this.#inventory = Array.from(doc.querySelectorAll("li.close-panel")).map(el => ({ | |
| id: Number(el.dataset.id), | |
| name: el.dataset.name, | |
| maxPurchasable: Number(el.querySelector("input.qty").dataset.max), | |
| requirements: Array.from(document.querySelectorAll(`.res${el.dataset.id}`)).map(span => ({ | |
| item: span.nextSibling.textContent.trim(), | |
| qty: parseInt(span.dataset.amt, 10) | |
| })).concat(Array.from(el.querySelectorAll("span[style='color:red']")).map(n => { | |
| const text = n.innerText.split(" "); | |
| return { | |
| item: text.slice(3).join(" "), | |
| qty: Number(text[2].replaceAll(",", "")) | |
| }; | |
| })) | |
| })).reduce(function(acc, { name, ...rest }) { | |
| acc[name] = { ...rest }; | |
| return acc; | |
| }, {}); | |
| } | |
| get stale() { | |
| return this.stale; | |
| } | |
| } | |
| const GlobalUtils = { | |
| addDelay: addDelay, | |
| findElement: findElement, | |
| isCropInventoryFull: function() { | |
| return /[a-zA-Z ]+\*/.test(document.querySelector(".seedid").selectedOptions[0].innerText); | |
| }, | |
| getSeedCount: function() { | |
| return Number(document.querySelector(".seedid").selectedOptions[0].innerText.match(/[0-9]/g).join("")); | |
| }, | |
| getPlotCount: function() { | |
| return document.querySelector("#crops").childElementCount * 4; | |
| }, | |
| getBaitCount: function() { | |
| return Number(document.getElementById("baitleft").innerText.match(/[0-9]/g).join("")); | |
| }, | |
| getBaitType: function() { | |
| return document.getElementById("baitleft").closest("div").innerText.split(":")[0]; | |
| }, | |
| getCookableMealCount: function() { | |
| return Number(document.querySelector(".mealloadid").selectedOptions[0].innerText.match(/[0-9]/g).join("")); | |
| }, | |
| getOvenCount: function() { | |
| return document.querySelectorAll(`a[href^=oven]`).length; | |
| }, | |
| purchaseItem: async function(id, qty = 1) { | |
| const apiUrl = `worker.php?go=buyitem&id=${id}&qty=${qty}`; | |
| return fetch(apiUrl, { | |
| method: "POST" | |
| }); | |
| }, | |
| sellItem: async function(id, qty = 1) { | |
| const apiUrl = `worker.php?go=sellitem&id=${id}&qty=${qty}`; | |
| return fetch(apiUrl, { | |
| method: "POST" | |
| }); | |
| }, | |
| }; | |
| const overseer = new Overseer(); | |
| const inventory = new InventoryManager(); | |
| window.inventory = inventory; | |
| // dummy button / template | |
| if (DEBUG_MODE) { | |
| overseer.createActionButton({ | |
| page: ".*", | |
| icon: `Test button`, | |
| condition: () => true, | |
| actions: [ | |
| async () => { console.log("Tick"); await GlobalUtils.addDelay(1000); }, | |
| async () => { console.log("Tock"); await GlobalUtils.addDelay(1000); }, | |
| ] | |
| }); | |
| } | |
| // Farm page actions | |
| overseer.createActionButton({ | |
| page: "xfarm", | |
| icon: `Manage Crops`, | |
| flags: [ | |
| { | |
| key: "autoBuySell", | |
| text: "Buy & Sell", | |
| defaultValue: false | |
| } | |
| ], | |
| condition: () => !GlobalUtils.isCropInventoryFull() && (GlobalUtils.getSeedCount() > GlobalUtils.getPlotCount()), | |
| actions: [ | |
| async (utils) => { | |
| if (document.querySelector(".harvest") || document.querySelector(".cropitem")) { | |
| console.log("Waiting for crops to be harvested first"); | |
| return; // if the last batch is waiting for a harvest, skip to the "harvest" step first | |
| } | |
| const canPlant = await utils.findElement(".plantseed"); | |
| if (canPlant) { | |
| const plantBtn = await utils.findElement(".plantallbtn"); | |
| plantBtn.click(); | |
| await utils.addDelay(2000); | |
| } | |
| }, | |
| async (utils) => { | |
| const canHarvest = await utils.findElement(".harvest"); | |
| if (canHarvest) { | |
| const harvestBtn = await utils.findElement(".harvestallbtn"); | |
| harvestBtn.click(); | |
| await utils.addDelay(2000); | |
| } | |
| }, | |
| async (utils, flags) => { | |
| if (!flags.autoBuySell) { | |
| return; | |
| } | |
| const seedId = Number(document.querySelector("select.seedid [selected]").getAttribute("value")); | |
| const seedMap = Object.entries(SEEDS_MAP).find(x => x[1] === seedId); | |
| if (!seedMap || !GlobalUtils.isCropInventoryFull()) { | |
| return; | |
| } | |
| const cropId = Number(seedMap[0]); | |
| // sell | |
| (await utils.findElement(`a[href="town.php"]`)).click(); | |
| await utils.addDelay(2000); | |
| (await utils.findElement(`a[href="market.php"]`)).click(); | |
| await utils.addDelay(2000); | |
| (await utils.findElement(`button.sellbtn[data-id="${cropId}"]`)).click(); | |
| await utils.addDelay(2000); | |
| (await utils.findElement(`.actions-modal .actions-modal-button`)).click(); | |
| await utils.addDelay(1000); | |
| (await utils.findElement(`.modal .modal-button-bold`)).click(); | |
| await utils.addDelay(1000); | |
| // buy | |
| (await utils.findElement(`a[href="town.php"]`)).click(); | |
| await utils.addDelay(2000); | |
| (await utils.findElement(`a[href="store.php"]`)).click(); | |
| await utils.addDelay(2000); | |
| (await utils.findElement(`button.cmaxbtn[data-id="${seedId}"]`)).click(); | |
| await utils.addDelay(200); | |
| (await utils.findElement(`button.buybtnnc[data-id="${seedId}"]`)).click(); | |
| await utils.addDelay(1000); | |
| // return | |
| (await utils.findElement(`a[href="index.php"]`)).click(); | |
| await utils.addDelay(2000); | |
| (await utils.findElement(`a[href^="xfarm.php"]`)).click(); | |
| await utils.addDelay(2000); | |
| }, | |
| ] | |
| }); | |
| // Daily Farm actions | |
| const farmAction = async (utils, pagequery, actionquery) => { | |
| const pageLink = await utils.findElement(pagequery); | |
| if (!pageLink.querySelector(".f7-icons")) { return; } | |
| pageLink.click(); | |
| await utils.addDelay(2000); | |
| const actionButton = await utils.findElement(actionquery); | |
| actionButton.click(); | |
| await utils.addDelay(1000); | |
| const backButton = await utils.findElement(".modal .modal-button"); | |
| backButton.click(); | |
| await utils.addDelay(2000); | |
| }; | |
| overseer.createActionButton({ | |
| page: "xfarm|coop|pasture|pigpen|storehouse|farmhouse|pen", | |
| icon: `Daily farmwork`, | |
| //condition: () => document.querySelectorAll(".item-link:has(.f7-icons)").length > 0, | |
| actions: [ | |
| async (utils) => await farmAction(utils, `.page-on-center a[href^="coop.php"]`, `.petallbtn`), | |
| async (utils) => await farmAction(utils, `.page-on-center a[href^="pasture.php"]`, `.petallbtn`), | |
| async (utils) => await farmAction(utils, `.page-on-center a[href^="pigpen.php"]`, `.feedallbtn`), | |
| async (utils) => await farmAction(utils, `.page-on-center a[href^="storehouse.php"]`, `.workbtnnc`), | |
| async (utils) => await farmAction(utils, `.page-on-center a[href^="farmhouse.php"]`, `.restbtnnc`), | |
| async (utils) => await farmAction(utils, `.page-on-center a[href^="pen.php"]`, `.incubateallbtn`), | |
| async (utils) => { | |
| // guard to avoid infinite loop if there are no chores | |
| await utils.addDelay(1000); | |
| }, | |
| ] | |
| }); | |
| // Fish page actions | |
| overseer.createActionButton({ | |
| page: "fishing", | |
| icon: `Manual fishing`, | |
| condition: () => (GlobalUtils.getBaitCount() > 0), | |
| actions: [ | |
| async (utils) => { | |
| if (GlobalUtils.getBaitType() === "Mealworms") { | |
| const fish = await utils.findElement(`.fishcaught`); | |
| fish.click(); | |
| await utils.addDelay(100, 400); | |
| } else { | |
| const fish = await utils.findElement(".fish.catch", document.getElementById("water")); | |
| fish.click(); | |
| await utils.addDelay(1000, 1200); | |
| const confirmationButton = await utils.findElement(".fishcaught"); | |
| if (confirmationButton.classList.length === 1) { | |
| // some dumb error handling | |
| return; | |
| } | |
| confirmationButton.click(); | |
| await utils.addDelay(1000); | |
| } | |
| } | |
| ] | |
| }); | |
| // Kitchen page actions | |
| overseer.createActionButton({ | |
| page: "kitchen|oven", | |
| icon: `Auto cook`, | |
| flags: [ | |
| { | |
| key: "enableStir", | |
| text: "Stir meals", | |
| defaultValue: false | |
| } | |
| ], | |
| condition: () => (GlobalUtils.getCookableMealCount() > GlobalUtils.getOvenCount()), | |
| actions: [ | |
| async (utils) => { | |
| // Add minimum 1s duration to our loop | |
| const kitchenLink = await utils.findElement(`a[href="kitchen.php"]`); | |
| kitchenLink?.click(); | |
| await utils.addDelay(1000); | |
| }, | |
| async (utils) => { | |
| // start cooking | |
| if (document.querySelector(`a[href^=oven] .item-title`).innerText.split("\n")[0] !== "Empty") { | |
| // ovens are not empty | |
| return; | |
| } | |
| console.log("Start cooking"); | |
| const cookAllButton = await utils.findElement(".cookallbtn"); | |
| cookAllButton.click(); | |
| await utils.addDelay(5000); | |
| }, | |
| async (utils, flags) => { | |
| // meal action | |
| const optionalButtonMapping = flags.enableStir ? { | |
| ".stirbtn": ".stirmealall", | |
| } : {}; | |
| const actionButtonMapping = { | |
| ...optionalButtonMapping, | |
| ".tastebtn": ".tastemealall", | |
| ".seasonbtn": ".seasonmealall", | |
| ".cookreadybtn": ".cookreadyallbtn" | |
| }; | |
| // we will keep track of cooking progress via the oven page - because the kitchen page doesn't have live updates on the progress (probably an oversight) | |
| document.querySelector(`a[href^="oven.php"]`).click(); | |
| const mealActionButton = await utils.findElement(Object.keys(actionButtonMapping).join(", ")); | |
| // go back to kitchen | |
| document.querySelector(`a[href="x"]`).click(); | |
| await utils.addDelay(2000); | |
| const correspondingButtonSelector = Object.entries(actionButtonMapping).find(([k, v]) => mealActionButton.classList.contains(k.replace(".", "")))[1]; | |
| const mealsBulkActionButton = await utils.findElement(correspondingButtonSelector); | |
| await utils.addDelay(500, 1000); // humanized delay | |
| mealsBulkActionButton.click(); | |
| await utils.addDelay(5000); | |
| } | |
| ] | |
| }); | |
| // --------- Injector scripts --------- | |
| overseer.createScriptInjector({ | |
| page: "location\.php\\?type=explore", | |
| name: `Exploration Page Utils`, | |
| actions: [ | |
| async (utils) => { | |
| // Add gifting shortcuts | |
| let explorationArea = (await utils.findElement(".navbar-on-center .center", document.querySelector(".view-main"))).innerText; | |
| explorationArea = explorationArea.slice(-4) === "info" ? explorationArea.slice(0, -6) : explorationArea; | |
| console.log("You are looking at items that can be found in", explorationArea); | |
| } | |
| ] | |
| }); | |
| console.log( | |
| "%cAutofarm running!", | |
| "color:green;font-family:system-ui;font-size:2rem;font-weight:bold" | |
| ); | |
| })(window); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment