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