您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
QOL and helper scripts for FarmRPG
当前为
// ==UserScript== // @name FarmRPG Helper-dev // @namespace https://greasyfork.org/users/1114461 // @version 0.030 // @description QOL and helper scripts for FarmRPG // @author Fewfre // @license GNU GPLv3 // @match https://farmrpg.com/index.php // @icon https://www.google.com/s2/favicons?sz=64&domain=farmrpg.com // @grant none // ==/UserScript== (function($, app) { function randomNumber(min, max) { return Math.random() * (max - min) + min; } // time in seconds async function sleep(time, max) { return new Promise(resolve=>{ setTimeout(resolve, max ? randomNumber(time*1000, max*1000) : time*1000); }); } // https://stackoverflow.com/a/61511955 function waitForElm(selector, options={}) { return new Promise((resolve, reject) => { if ($(selector).is(':visible') && $(selector).css('opacity') > 0.1) { return resolve($(selector+':visible').first()[0]); } const observer = new MutationObserver(mutations => { if ($(selector).is(':visible') && $(selector).css('opacity') > 0.1) { resolve($(selector+':visible').first()[0]); observer.disconnect(); } }); if(options.timeout) { sleep(options.timeout).then(()=>{ observer.disconnect(); reject(new Error("observer timed out")); }); } observer.observe(options.target || document.body, { attributes: true, childList: true, subtree: true, ...options.config }); }); } async function buyItem(id, qty) { return fetch(`worker.php?go=buyitem&id=${id}&qty=${qty}`, { method:'POST' }) .then(r=>r.text()) .then(data=>{ // If number is returned then we bought to much - buy again at specified amount if(!Number.isNaN(Number.parseInt(data))) { return buyItem(id, data); } return data; }); } function refreshPage() { app.mainView.router.refreshPage(); } ///////////////////////////// // Wordle Solver ///////////////////////////// const WordleSolver = (() => { // https://raw.githubusercontent.com/gamescomputersplay/wordle/main/wordle.py // https://www.youtube.com/watch?v=sVCe779YC6A const COLUMNS = 4; const ALPHABET = "0123456789".split(""); //"abcdefghijklmnopqrstuvwxyz"; const ALL_WORDS = Array.from({ length: Math.pow(10, COLUMNS) }).map((_, i) => i.toString().padStart(COLUMNS, "0")); // 0000-9999 const LOC = { WRONG: 0, PARTIAL: 1, CORRECT: 2, }; const arrGen = (len, fill) => Array.from({ length: len }).fill(fill); const randomChoice = (arr) => arr[Math.floor(Math.random() * arr.length)]; const countInstances = (arr, findMe) => Array.from(arr).filter((v) => v == findMe).length; const arrRemove = (arr, delMe) => (i = arr.indexOf(delMe)) > -1 && arr.splice(i, 1)[0]; // https://stackoverflow.com/a/33034768 const difference = (arr1, arr2) => Array.from(arr1).filter((x) => !Array.from(arr2).includes(x)); class WordList { /** * Class to load the list of words from file * Initialized with the file(s) to load words from * @param {()=>string[]} filesCallback */ constructor(filesCallback) { // list of all the words this.word_list = filesCallback?.() ?? []; // letter counts in all words in the list: {"a": 100, "b": 200, ...} this.letter_count = {}; // words' scores: {"apple": 100, "fruit": 200} etc // score is the sum of all letters' frequencies this.word_scores = {}; // Same, but scores account for letter positions this.position_letter_count = arrGen(COLUMNS).map(() => ({})); this.position_word_scores = {}; // Generate the word scores // (both positional and total) this.gen_word_scores(); this.gen_positional_word_scores(); } /** * Copy of existing word list * @returns {WordList} */ copy() { let new_word_list = new WordList(); new_word_list.word_list = this.word_list.slice(); new_word_list.word_scores = { ...this.word_scores }; new_word_list.position_word_scores = { ...this.position_word_scores }; return new_word_list; } /** * Return count of remaining words: len(word_list) * @returns {number} Length of word list */ get length() { return this.word_list.length; } /** * Return random word from the word list * @returns */ get_random_word() { return randomChoice(this.word_list); } /** * Return the word with the highest score * @param {boolean} use_position whether or not use position-based scores * @returns */ get_hiscore_word(use_position = false) { const scores = use_position ? this.position_word_scores : this.word_scores; let best_word = ""; let best_score = 0; for (let word of this.word_list) { if (scores[word] > best_score) { best_score = scores[word]; best_word = word; } } return best_word; } /** * Return the word with maximized number of unique "letters" * @param {string[]} maximized_letters * @returns */ get_maximized_word(maximized_letters) { this.gen_letter_count(); let best_word = ""; let best_score = 0; for (let word of this.word_list) { let this_score = 0; for (let letter of maximized_letters) { if (word.indexOf(letter) > -1) { this_score += 1; } } if (this_score > best_score) { best_score = this_score; best_word = word; } } return best_word; } /** * Calculate counts of all letters in the word_list */ gen_letter_count() { this.letter_count = Object.fromEntries(ALPHABET.map((c) => [c, 0])); for (let word of this.word_list) { for (let letter of new Set(word)) { this.letter_count[letter] += 1; } } } /** * calculate letter count for each letter position */ gen_positional_letter_count() { for (let i = 0; i < COLUMNS; i++) { this.position_letter_count[i] = Object.fromEntries(ALPHABET.map((c) => [c, 0])); } for (let word of this.word_list) { Array.from(word).forEach((letter, i) => { this.position_letter_count[i][letter] += 1; }); } } /** * Calculate scores for each word */ gen_word_scores() { this.gen_letter_count(); this.word_scores = {}; for (let word of this.word_list) { let word_score = 0; for (let letter of new Set(word)) { word_score += this.letter_count[letter]; } this.word_scores[word] = word_score; } } /** * Calculate positional scores for each word */ gen_positional_word_scores() { this.gen_positional_letter_count(); this.position_word_scores = {}; for (let word of this.word_list) { // Sum up scores, but if the letter is twice in the word // use the highest score only let word_score = {}; Array.from(word).forEach((letter, i) => { if (word_score[letter] !== undefined) { word_score[letter] = this.position_letter_count[i][letter]; } else { word_score[letter] = Math.max(word_score[letter], this.position_letter_count[i][letter]); } }); this.position_word_scores[word] = Object.values(word_score).length; } } /** * Removing words from the word list, * by checking with teh three masks * @param {string[][]} yes_mask * @param {string[][]} no_mask * @param {Set<string>[]} allowed_mask */ filter_by_mask(yes_mask, no_mask, allowed_mask) { let new_words = []; for (let word of this.word_list) { // Yes_mask: should have that letter in that place let noLettersMissingFromYesMask = !yes_mask.some( (must_have_letters, n) => !!must_have_letters.length && word[n] != must_have_letters[0] ); if (noLettersMissingFromYesMask) { let noForbiddenLettersFound = true; // No_mask: should NOT have that letter in that place for (let n = 0; n < no_mask.length; n++) { let fail = false; const forbidden_letters = no_mask[n]; for (let forbidden_letter of forbidden_letters) { if (word[n] == forbidden_letter) { fail = true; } } if (fail) { noForbiddenLettersFound = false; break; } } if (noForbiddenLettersFound) { // Allowed mask: should have allowed count of letters if (!ALPHABET.some((letter) => !allowed_mask[countInstances(word, letter)].has(letter))) { new_words.push(word); } } } } this.word_list = new_words; } } class Guess { /** * Class for one guess attempt * Contains the guessed word and list of results * @param {string} guess_word * @param {string?} correct_word * @param {(0|1|2)[]?} guess_result */ constructor(guess_word, correct_word, guess_result) { /** * @type {string} */ this.word = guess_word; // Set to True, but will be switched this.result = guess_result ?? this.get_result(correct_word); this.guessed_correctly = countInstances(this.result, LOC.CORRECT) === COLUMNS; } /** * String representation looks like: ducky: G__Y_ * G, Y, _ is for green / yellow / grey * @returns */ toString() { let out = `${this.word}: `; for (let letter_result of this.result) { if (letter_result == 2) { out += "G"; } else if (letter_result == 1) { out += "Y"; } if (letter_result == 0) { out += "_"; } return out; } } /** * Given the guessed and the right word * generate the list of letter results: * 0/1/2 meaning no/misplaced/correct * @returns {(0|1|2)[]} */ get_result(correct_word) { let result = arrGen(COLUMNS, LOC.WRONG); // we are using a copy to blank guessed green and yellow // letters (to correctly display doubles) let correct_copy = [...correct_word]; Array.from(this.word).forEach((guessed_char, i) => { if (guessed_char == correct_copy[i]) { result[i] = LOC.CORRECT; correct_copy[i] = ""; } }); for (let i = 0; i < this.word.length; i++) { if (correct_copy.includes(this.word[i]) && result[i] != LOC.CORRECT) { result[i] = LOC.PARTIAL; for (let j = 0; j < COLUMNS; j++) { if (correct_copy[j] == this.word[i]) { correct_copy[j] = ""; break; } } } } if (countInstances(result, LOC.CORRECT) === COLUMNS) { this.guessed_correctly = true; } return result; } } class Wordle { /** * Class representing one wordle game. * methods include initiating a secret word, * returning green/yellow/grey results, * keeping track of guessed letters * @param {string|"#random"|null} correct_word */ constructor(correct_word = "#random") { // the word to guess if (correct_word === "#random") { this.correct_word = puzzle_words.get_random_word(); } else if (puzzle_words.word_list.indexOf(correct_word) > -1) { this.correct_word = correct_word; } // else leave as null // list of guesses so far /** * @type {Guess[]} * @public */ this.guesses = []; } toString() { return `::${this.correct_word}::` + this.guesses.map((guess, i) => `\n${i + 1}. ${guess}`).join(""); } /** * One turn of the game * get guessed word, add new Guess in guesses list * if guessed correctly, return True, else False * @param {string} word * @param {(0|1|2)[]?} result */ guess(word, result) { this.guesses.push(new Guess(word, this.correct_word, result)); // Return True/False if you got the word right return this.guesses.at(-1).guessed_correctly; } } class Player { /** * Default player (random) * Guesses a random word from the whole list * @param {WordList} guessing_words */ constructor(guessing_words) { // Mask // Yes mask: this letters should be in these places /** * @type {string[][]} */ this.yes_mask = arrGen(COLUMNS).map(() => []); // No mask: this letters should NOT be in these places /** * @type {string[][]} */ this.no_mask = arrGen(COLUMNS).map(() => []); // Count mask: Word can have (n) such letters // [[letters that can be 0 of], [1 of], [2 of], [3 of]] /** * @type {Set<string>[]} */ // this.allowed_mask = arrGen(COLUMNS - 1).map(() => new Set(ALPHABET)); this.allowed_mask = arrGen(COLUMNS + 1).map(() => new Set(ALPHABET)); // which letter has to be in the word, from green and yellow letters this.must_use = new Set(); // copy of the global word set (we'll be removing unfit words from it) /** * @type {WordList} */ this.remaining_words = guessing_words.copy(); } /** * Removing words from the word list, that don't fit with * what we know about the word (using mask and must_use) */ filter_word_list() { this.remaining_words.filter_by_mask(this.yes_mask, this.no_mask, this.allowed_mask); } /** * Try to re-use "green" space by putting some remaining letters there */ reuse_green() { // Count vowels in teh list of letter // function count_vowels(letters) { // let count = 0; // let vowels = new Set("aoieu"); // for (let letter of letters) { // if (vowels.indexOf(letter) > -1) { // count += 1; // } // } // return count; // } // Temp Yes mask is empty let temp_yes_mask = arrGen(COLUMNS).map(() => []); // Temp No mask is actual Yes mask let temp_no_mask = this.yes_mask; // Prioritize those that are present in all "allowed _mask[1]" // (meaning they have never been grey) minus all yellow and greens let greens_n_yellows = new Set(); this.yes_mask.concat(this.no_mask).forEach((letters) => { for (let letter of letters) { greens_n_yellows.add(letter); } }); // Add vowels if needed let priority_letters = new Set(difference(this.allowed_mask[1], greens_n_yellows)); let letters_for_allowed_mask = priority_letters; // if (count_vowels(priority_letters) == 0) { // letters_for_allowed_mask = new Set([...priority_letters, new Set("aoe")]); // } // Temp Allowed mask: priority letters and some vowels // [0] has all letters - any letter can be missed let temp_allowed_mask = [new Set(ALPHABET), ...arrGen(COLUMNS).map(() => new Set(letters_for_allowed_mask))]; // Find the word to fit temporary mask, with maximized prioritized letters let temp_words = guessing_words.copy(); temp_words.filter_by_mask(temp_yes_mask, temp_no_mask, temp_allowed_mask); if (temp_words.length > 0) { return temp_words.get_maximized_word(Array.from(priority_letters)); } return ""; } /** * Pick the word from the list * @returns */ make_guess() { // Use random word if: // 1. "scored" is no set // 2. "firstrandom" is set and this is the first guess // (word list has not been filtered yet) if ( !params.includes("scored") || (params.includes("firstrandom") && this.remaining_words.length == guessing_words.length) ) { return this.remaining_words.get_random_word(); } // list of masks' lengths let has_greens = COLUMNS - this.yes_mask.filter((y) => y == []).length; // Conditions for "re-use green" logic: // has Green; more than 2 potential answers if (params.includes("easymode") && has_greens > 0 && this.remaining_words.length > 2) { // if reusing green is successful, return that word let reuse_green_word = this.reuse_green(); if (reuse_green_word != "") return reuse_green_word; } // recount / don't recount all scores if (params.includes("recount")) { this.remaining_words.gen_word_scores(); this.remaining_words.gen_positional_word_scores(); } // use / don't use position letter weights if (params.includes("position")) { return this.remaining_words.get_hiscore_word(true); } return this.remaining_words.get_hiscore_word(false); } /** * Track letters that should be in this place (from green) * @param {Guess} guess */ update_yes_mask(guess) { guess.result.forEach((letter_result, i) => { if (letter_result == LOC.CORRECT) { // green: should have this letter here if (!this.yes_mask[i].includes(guess.word[i])) { this.yes_mask[i].push(guess.word[i]); } } }); } /** * Track letters that should not be in this place (from yellow) * @param {Guess} guess */ update_no_mask(guess) { // Delete the letter in the same place in the mask guess.result.forEach((letter_result, i) => { if (letter_result == LOC.PARTIAL) { // yellow: should not have this letter here if (!this.no_mask[i].includes(guess.word[i])) { this.no_mask[i].push(guess.word[i]); } } // This is grey, but not the only letter in the word if (letter_result == LOC.WRONG && countInstances(guess.word, guess.word[i]) > 1) { if (!this.no_mask[i].includes(guess.word[i])) { this.no_mask[i].push(guess.word[i]); } } }); } /** * Track how many which letters should be in the word * @param {Guess} guess * @returns */ update_allowed_mask(guess) { // count colors for each letter, like this // {"a":[2,0], "b":[2,1], "c":[0]} let letter_count = {}; Array.from(guess.word).forEach((letter, i) => { if (letter_count[letter]) { letter_count[letter].push(guess.result[i]); } else { letter_count[letter] = [guess.result[i]]; } }); // Go through each letter count and update count_mask Object.entries(letter_count).forEach(([letter, stats]) => { // Case Grey: // Word should have no more that {count of other numbers except 0} // of this letter. e.g. [0] - none [2,0] - 1, [2,1,0] - 2 if (stats.includes(LOC.WRONG)) { let allowed_count = stats.length - countInstances(stats, LOC.WRONG); // for (let i = allowed_count + 1; i < COLUMNS - 1; i++) { for (let i = allowed_count + 1; i < COLUMNS; i++) { this.allowed_mask[i].delete(letter); } } // Case Yellow / Green // Word should have at leaset {count of 1&2s letters} of these if (stats.includes(LOC.PARTIAL) || stats.includes(LOC.CORRECT)) { let required_count = countInstances(stats, LOC.PARTIAL) + countInstances(stats, LOC.CORRECT); for (let i = 0; i < required_count; i++) { this.allowed_mask[i].delete(letter); } } }); } /** * Combined mask updating functions * @param {Guess} guess */ update_mask_with_guess(guess) { this.update_yes_mask(guess); this.update_no_mask(guess); this.update_allowed_mask(guess); } /** * Update allowed_mask, based on letter freq of remaining words */ update_mask_with_remaining_words() { // Update allow_mask, knowing letter count of remaining words this.remaining_words.gen_letter_count(); Object.entries(this.remaining_words.letter_count).forEach(([letter, count]) => { // If there is no such words in the whole list // remove it from mask if (count == 0) { // for (let i = 1; i < COLUMNS - 2; i++) { for (let i = 1; i < this.allowed_mask.length; i++) { this.allowed_mask[i].delete(letter); } } }); } /** * Remove a word from possible guesses used to remove used words * @param {string} word */ remove_word(word) { arrRemove(this.remaining_words.word_list, word); } } function realtimeGameSolver() { const game = new Wordle(null); const player = new Player(guessing_words); return { getGuess() { return player.make_guess(); }, submitResult(players_guess, result) { // Play the guess, see if we are done const done = game.guess( players_guess, Array.from(result).map((n) => parseInt(n)) ); // Post-guess action: // Remove the words we just played player.remove_word(players_guess); // Update mask with guess results player.update_mask_with_guess(game.guesses.at(-1)); // Filter the word down according to new mask player.filter_word_list(); // Update the mask according to remaining words player.update_mask_with_remaining_words(); return done; }, }; } /** * Playing one round of Wordle using player strategy\n' + ' from PlayerType * @param {boolean} quiet * @param {*} correct_word * @returns */ function play_one_game(quiet = true, correct_word = null) { if (!quiet) console.log("game started"); const game = new Wordle("9876"); const player = new Player(guessing_words); let done = false; // Cycle until we are done let count = 10; while (!done && count > 0) { // Make a guess let players_guess = player.make_guess(); // Play the guess, see if we are done if (game.guess(players_guess)) { done = true; } // Post-guess action: // Remove the words we just played player.remove_word(players_guess); // Update mask with guess results player.update_mask_with_guess(game.guesses.at(-1)); // Filter the word down according to new mask player.filter_word_list(); // Update the mask according to remaining words player.update_mask_with_remaining_words(); count--; } if (!quiet) console.log(game); if (game.guesses.at(-1).guessed_correctly) { return game.guesses; } return -1; // This shouldn't happen } /** * Get couple of main statistics from the list of results * @param {any[]} results */ function parse_results(results) { let frequencies = {}; let lengths = []; let complete = 0; let turns_sum = 0; for (let result of results) { let length = result.length; lengths.push(length); if (frequencies[length] !== undefined) { frequencies[length] += 1; } else { frequencies[length] = 1; } turns_sum += length; if (length <= MAX_TURNS) { complete += 1; } } console.log(`Wins: ${complete}, Losses: ${results.length - complete}`); console.log(`Winrate: ${(complete * 100) / results.length}%`); // console.log(`Winrate: ${complete*100/len(results):.1f}%`); if (complete > 0) { console.log(`Average length: ${turns_sum / results.length}`); // console.log(`Average length: ${turns_sum/len(results):.1f}`); } console.log(`Median length: ${lengths.sort()[Math.floor(results.length / 2)]}`); } /** * launch the simulation */ function main() { let start_time = Date.now(); if (N_GAMES == 1) { play_one_game(false); } else { simulation(N_GAMES); } console.log(`Time: ${Date.now() - start_time}`); } // Word lists to use: // List that wordle game uses as a target word const puzzle_words = new WordList(() => ALL_WORDS); // List that the "player" program uses const guessing_words = new WordList(() => ALL_WORDS); // Game length (the game will go on, but it will affect the % of wins) const MAX_TURNS = 6; // Player's settings: // With everything off uses the naive greedy method (limit the potential // answers and randomly chose a word from the remaining list) // "scored": weight words by the frequency of the words // "recount": recalculate weights for every guess // "firstrandom": random first guess // (worse results but more interesting to watch) // "position": use positional letter weights // "easymode": don't have to use current result (reuse green space) const params = ["scored", "recount", "firstrandom_off", "position", "easymode"]; // Number of games to simulate // if == 1, plays one random game, shows how the game went // if == 2315, runs simulation for all Wordle words (for deterministic methods) // other numbers - play N_GAMES games with random words from puzzle_words const N_GAMES = 1; //2315; // main(); return { realtimeGameSolver, }; })(); ///////////////////////////// // Assets ///////////////////////////// // uri conversion: https://dopiaza.org/tools/datauri/index.php // https://pixabay.com/sound-effects/finished-45049/ const SOUND_FINISHED = new Audio("data:audio/mpeg;base64,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XQDigEAQOE61LX5iKKY6TEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIZAAAAAGkAOAAAAAAA0gBwAAATEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"); ///////////////////////////// // Mailbox Passwords ///////////////////////////// const TEXT_MAILBOX_PWD_BTN = `🤖 CHEAT <abbr title="Passwords fetched from the buddy.farm passwords list: https://buddy.farm/passwords/">ⓘ</abbr>`; async function getMailboxPasswords() { return fetch("https://buddy.farm/page-data/passwords/page-data.json").then(res=>res.json()) .then(json=>json.result.data.farmrpg.passwords.map(pw=>pw.password)); } async function getUsedMailboxPasswords() { return new Promise((resolve)=>{ $('<div/>').load('popwlog.php', function() { resolve( $(this).find(".list-block .item-title span:first-of-type").map((_,el)=>$(el).text()).get() ); }); }); } async function initMailboxPasswords() { let allPasswords, usedPasswords; app.onPageInit("postoffice", ({ container })=>{ $(`<button style="white-space:nowrap;" />`).html(TEXT_MAILBOX_PWD_BTN).appendTo($("#popw").closest(".item-inner")).on("click", async function(){ $(this).html("Loading..."); allPasswords ??= await getMailboxPasswords(); usedPasswords ??= await getUsedMailboxPasswords(); $(this).html(TEXT_MAILBOX_PWD_BTN); const passwords = allPasswords.filter(pw=>!usedPasswords.includes(pw)); if(passwords.length <= 0) { $(this).attr('disabled', 'disabled').html("ALL USED"); return; } $("#popw").val(passwords[0]); }); $(".popwbtn").on("click", function(){ usedPasswords.push( $("#popw").val() ); }); }); } ///////////////////////////// // Vault ///////////////////////////// const vaultTypeMap = { 'G':0, 'Y':1, 'B':2 }; function initVault() { app.onPageInit("crack", ({ container })=>{ $("<button />").html("🤖 GUESS").appendTo($("#vaultcode").closest(".item-inner")).on("click", function(){ const solver = WordleSolver.realtimeGameSolver(); // find current guesses const prevGuesses = $(".col-25[data-type]").closest(".card-content-inner").find(".row").map((_,row) => { return { cellsResult: $(row).find("[data-type]").map((_,g)=>vaultTypeMap[$(g).data('type')]).get(), cellsGuess: $(row).find("[data-type]").map((_,g)=>$(g).text()).get(), }; }).get(); prevGuesses.forEach(prev=>solver.submitResult(prev.cellsGuess.join(""), prev.cellsResult)); $("#vaultcode").val(solver.getGuess()); }) }); } ///////////////////////////// // Fishing ///////////////////////////// const TEXT_FISHING_START = "🤖 AUTO FISH"; const TEXT_FISHING_STOP = "🤖❌ STOP FISHING"; const TEXT_FISHING_STOPPING = "🤖❌ STOPPING"; function getBaitCount() { return parseInt($("#baitarea strong").first().text() || 0); } async function getAllBaitData() { return new Promise((resolve)=>{ $('<div/>').load('changebait.php?from=fishing&id='+$('.zone_id').html(), function() { resolve( $(this).find(".selectbait").map(function(){ return { name: $(this).data('bait'), num: $(this).find(".item-after").text(), imgSrc: $(this).find("img").attr('src'), selected: $(this).find(".item-title i").text() === 'check', } }).get() ); }); }); } async function clickFish() { if(!$("#fishinwater").length) { throw new Error("No fishing area detected") } const fish = await waitForElm(".fish.catch"); fish.click(); /* var triesLeft = 500; while (triesLeft-- > 0) { if($(".fish.catch").length) { $(".fish.catch").click(); return; } await sleep(0.05); } */ /* if(triesLeft <= 0) { throw new Error("Script gave up, couldn't find fish") } */ } async function catchFish() { if(!$(".fishcaught").length) { throw new Error("Cannot find fishing to catch") } $(".fishcaught").trigger("click"); } async function fishOne() { await clickFish(); // We wait for catch modal - but if it doesn't open then we missed the fish and try again try { await waitForElm(".picker-catch.modal-in", { timeout:0.1 }); } catch(err) { if(!fishing) { return; } return await fishOne(); }; await sleep(0.33); // time for modal to animate in + small buffer if(!fishing) { catchFish(); return; } await sleep(0.23, 0.888); await catchFish(); await sleep(0.05, 0.115); // tiny delay before clicking next fish } let fishing = false; async function startFishing() { fishing = true; try { if(!$("#fishinwater").length) { throw new Error("No fishing area detected"); } while (getBaitCount() > 0) { await fishOne(); if(!fishing) { break; } await sleep(0.3, 0.4); } fishing = false; SOUND_FINISHED.play(); $("#pogfishing").html(TEXT_FISHING_START); $("#pogfishing").removeAttr('disabled'); } catch(e) { alert(e.message); } } function initFishing() { app.onPageInit("fishing", ({ container })=>{ $(`<button id="pogfishing">${fishing ? TEXT_FISHING_STOP : TEXT_FISHING_START}</button>`).appendTo(container.querySelector(".buttons-row")).on("click", function(){ if($("#pogfishing").attr('disabled')) { return; } if(!fishing) { $("#pogfishing").html(TEXT_FISHING_STOP); startFishing(); } else { $("#pogfishing").html(TEXT_FISHING_STOPPING); $("#pogfishing").attr('disabled','disabled'); fishing = false; } }); const $contOuter = $(`<div class="card-content-inner" style="padding:5px"></div>`).insertAfter($(container).find('#baitarea')); const $contOuterRow = $(`<div class="row" style="margin-bottom: 0"></div>`).appendTo($contOuter); const $cont = $(`<div style="display:flex;" />`).appendTo($contOuterRow); const $baitList = $(`<div style="display:flex; gap: 10px; margin-right:30px;">Quick Swap: <span class="loadn">Loading...</span></div>`).appendTo($cont) $(`<button id="pogBuyWorms">BUY 200 WORMS</button>`).appendTo($cont).on("click", function(){ if($("#pogBuyWorms").attr('disabled')) { return; } $("#pogBuyWorms").attr('disabled','disabled'); buyItem(18, 200).then(data=>{ if(data=="success" || data==="") { $("#pogBuyWorms").removeAttr('disabled'); refreshPage(); } else { $("#pogBuyWorms").text(data); } }).catch(console.error); }); getAllBaitData().then((baits)=>{ baits = baits.filter(b=>!b.selected); $baitList.find(".loadn").remove(); $baitList.append(baits.length <= 0 ? "No other baits available" : baits.map(bait=>$(`<span title="${bait.name}" style="cursor:pointer;"><img src="${bait.imgSrc}" height="14" /> ${bait.num}</span>`) .on('click', ()=>{ fetch(`worker.php?go=selectbait&bait=${bait.name}`, { method:'POST' }).then(r=>r.text()).then(res=>{ res === 'success' ? refreshPage() : $baitList.html(`ERROR: ${res}`); }); } )) ); }); }); } ///////////////////////////// // Exploration ///////////////////////////// const TEXT_EXPLORING_START = "AUTO EXPLORE"; const TEXT_EXPLORING_STOP = "❌ STOP EXPLORING"; const TEXT_EXPLORING_STOPPING = "❌ STOPPING"; function getStaminaCount() { return parseInt($("#stamina").text() || 0) } async function exploreOne() { if(!$(".explorebtn").length) { throw new Error("No exploration area detected"); } $(".explorebtn").trigger("click"); } let exploring = false; async function startExploring() { exploring = true; try { if(!$(".explorebtn").length) { throw new Error("No exploration area detected"); } while (getStaminaCount() > 0) { await exploreOne(); if(!exploring) { break; } await sleep(0.12, 0.28); } exploring = false; SOUND_FINISHED.play(); $("#pogexploring .item-inner").html(TEXT_EXPLORING_START); $("#pogexploring").removeAttr('disabled'); } catch(e) { alert(e.message); } } function initExploring() { app.onPageInit("area", ({ container })=>{ $(`<li> <div class="item-content" style="cursor:pointer" id="pogexploring"> <div class="item-media">🤖</div> <div class="item-inner">${exploring ? TEXT_EXPLORING_STOP : TEXT_EXPLORING_START}</div> </div> </li>`).insertAfter(container.querySelector("li:has(.explorebtn)")).on("click", function(){ if($("#pogexploring").attr('disabled')) { return; } if(!exploring) { $("#pogexploring .item-inner").html(TEXT_EXPLORING_STOP); startExploring(); } else { $("#pogexploring .item-inner").html(TEXT_EXPLORING_STOPPING); $("#pogexploring").attr('disabled','disabled'); exploring = false; } }); }); } ///////////////////////////// // Navigation ///////////////////////////// function initShortcuts() { $(`<li> <div class="item-content"> <div class="item-inner"> <div style="display:grid; grid-template-columns: 1fr auto;"> <div><i class="fa fa-fw fa-lightbulb-o" /></div> <div> <div class="item-title"> Shortcuts</div> <div style="font-size: 14px;">${[ { links: [ { link:'xfarm', text:'Farm', params:{ id: $('.view-main a[href^="xfarm.php?id="]').attr('href').match(/\?id=(\d*)/)[1] } }, { link:'explore', text:'Explore' }, { link:'fish', text:'Fishing' }, { link:'town', text:'Town' }, { link:'quests', text:'Help' }, { link:'workshop', text:'Workshop' }, ] }, { section: 'Town', links: [ { link:'store', text:'Store' }, { link:'market', text:'Sell' }, { link:'bank', text:'Bank' }, { link:'postoffice', text:'Mail' }, { link:'pets', text:'Pets' }, { link:'supply', text:'Upgrade' }, { link:'locksmith', text:'Locksmith' }, { link:'steakmarket', text:'Steak' }, ] }, { section: 'Daily', links: [ { link:'daily', text:'Chores' }, { link:'well', text:'Well' }, { link:'crack', text:'Vault' }, { link:'comm', text:'Community Center' }, ] }, { section: 'Skills', links: [ { link:'progress', params:{ type:'Farming' }, text:`<img src="/img/items/6137.png" height="12" />` }, { link:'progress', params:{ type:'Fishing' }, text:`<img src="/img/items/7783.png" height="12" />` }, { link:'progress', params:{ type:'Crafting' }, text:`<img src="/img/items/5868.png" height="12" />` }, { link:'progress', params:{ type:'Exploring' }, text:`<img src="/img/items/6075.png" height="12" />` }, { link:'perks', text:'Perks' }, { link:'mastery', text:'Mastery' }, { link:'npclevels', text:'Friendship' }, ] }, ].map(sctn=>`<div> ${sctn.section ? `<div style="margin-top:3px;"><strong>${sctn.section}</strong></div>` : ''} ${sctn.links.map(l=>`<a href='${l.link}.php${l.params ? '?'+new URLSearchParams(l.params).toString() : ''}' data-view=".view-main">${l.text}</a>`).join(` • `)} </div>`).join('')}</div> </div> </div> </div> </div> </li>`).insertAfter('.view.view-left.navbar-through .page-content li:first-of-type'); } ///////////////////////////// // Initialize ///////////////////////////// function init() { initShortcuts(); initFishing(); initExploring(); initVault(); initMailboxPasswords(); // Stop any automation scripts after you change an area app.onPageInit("area", ({ container })=>{ fishing = false; exploring = false; }); } init(); })(window.jQuery, window.myApp);