A set of scripts for an internal Pokemon game - here for archival only.
Created
March 27, 2023 07:09
-
-
Save dcragusa/a86e225d0092cae3b6d80a3ecd8b7d26 to your computer and use it in GitHub Desktop.
This file contains 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
async function find_tms() { | |
let onlySuggestForMultiple = true; | |
/* disable log and debug, used by app and spams console */ | |
console.log = function () {}; | |
console.debug = function () {}; | |
console.clear(); | |
class Converters { | |
/* converts from one representation of pokemon/item to another */ | |
static badgeIdToLabel(badgeId) { | |
/* convert a pokemon badge ID to a long label, e.g. '6s#f00G' -> 'Burmy Plant³ ♀' */ | |
return new Badge(badgeId).toLabel(); | |
} | |
static badgeIdToNonVariantLabel(badgeId) { | |
/* custom label by species, shininess, form & gender excluding variant, e.g. '6s#f00G'->'Burmyfalseplantfemale' */ | |
/* we can't use toLabel() as that also distinguishes by variant, which defeats the point here */ | |
let pokemon = new Badge(badgeId); | |
let personality = pokemon.personality; | |
let datastore = window.datastoreGet(pokemon.toDataStr()); | |
return datastore.species + personality.shiny + (personality.form ?? '') + (personality.gender ?? ''); | |
} | |
} | |
function getVariantCounts() { | |
/* return map of pokemon to owned variants */ | |
let pokemonLabelsSeen = new Set(); | |
let pokemonVariants = new Map(); | |
for (let badgeId of Object.keys(firebase.userData.pokemon)) { | |
let pokemon = new Badge(badgeId); | |
let pokemonLabel = Converters.badgeIdToLabel(badgeId); | |
let personality = pokemon.personality; | |
let nonVariantLabel = Converters.badgeIdToNonVariantLabel(badgeId); | |
/* skip if we have already seen this pokemon (including variant) */ | |
if (pokemonLabelsSeen.has(pokemonLabel)) { | |
continue; | |
} else { | |
pokemonLabelsSeen.add(pokemonLabel); | |
} | |
let variants = pokemonVariants.get(nonVariantLabel) ?? new Set(); | |
variants.add(personality.variant); | |
pokemonVariants.set(nonVariantLabel, variants); | |
} | |
return pokemonVariants; | |
} | |
function getPokemonCounts() { | |
/* return map of pokemon label to count */ | |
let pokemonCounts = new Map(); | |
for (let [badgeId, count] of Object.entries(firebase.userData.pokemon)) { | |
let pokemonLabel = Converters.badgeIdToLabel(badgeId); | |
let existingCount = pokemonCounts.get(pokemonLabel) ?? 0; | |
pokemonCounts.set(pokemonLabel, existingCount + count); | |
} | |
return pokemonCounts; | |
} | |
function getOwnedTMs() { | |
/* return map of owned TMs to count */ | |
let tmCount = new Map(); | |
for (let [item, count] of Object.entries(firebase.userData.items)) { | |
if (item.startsWith('tm-') && count) { | |
tmCount.set(item.slice(3), count); | |
} | |
} | |
return tmCount | |
} | |
function getSortedPokemon() { | |
/* return array of owned pokemon sorted by shininess and ID */ | |
let ownedPokemon = []; | |
for (let badgeId of Object.keys(firebase.userData.pokemon)) { | |
let pokemon = new Badge(badgeId); | |
ownedPokemon.push([badgeId, pokemon.id, pokemon.personality.shiny]); | |
} | |
/* sort by non-shiny -> shiny, then ascending ID */ | |
ownedPokemon.sort(function(a, b){ return a[2]-b[2] }); | |
ownedPokemon.sort(function(a, b){ return a[1]-b[1] }); | |
return ownedPokemon.map(x => x[0]); | |
} | |
let pokemonCounts = getPokemonCounts(); | |
let variantCounts = getVariantCounts(); | |
let tmCount = getOwnedTMs(); | |
let labelsSeen = new Set(); | |
for (let badgeId of getSortedPokemon()) { | |
let pokemon = new Badge(badgeId); | |
let pokemonLabel = Converters.badgeIdToLabel(badgeId); | |
let personality = pokemon.personality; | |
let datastore = window.datastoreGet(pokemon.toDataStr()); | |
let nonVariantLabel = Converters.badgeIdToNonVariantLabel(badgeId); | |
/* skip if we have already seen this pokemon, if it is a variant, or has no variants */ | |
if (labelsSeen.has(pokemonLabel) || personality.variant !== undefined || datastore.novelMoves === undefined) { | |
continue; | |
} else { | |
labelsSeen.add(pokemonLabel); | |
} | |
/* skip if we only have one of this pokemon and we are only suggesting for multiple base pokemon */ | |
if (pokemonCounts.get(pokemonLabel) === 1 && onlySuggestForMultiple) { | |
continue; | |
} | |
let numVariants = datastore.novelMoves.length - 1; | |
let numVariantsStr = `(pokemon has ${numVariants} variant${numVariants > 1 ? 's' : ''})`; | |
for (let [idx, moveArray] of datastore.novelMoves.entries()) { | |
if (idx && !variantCounts.get(nonVariantLabel).has(idx)) { | |
if (idx === 3) { | |
console.info(`Use Move Tutor on ${pokemonLabel} to make it var 3 ${numVariantsStr}`); | |
} | |
for (let move of moveArray) { | |
if (tmCount.has(move)) { | |
console.info( | |
`Use ${move} (${tmCount.get(move)} owned) on ` + | |
`${pokemonLabel} to make it var ${idx} ${numVariantsStr}` | |
); | |
} | |
} | |
} | |
} | |
} | |
} | |
await find_tms(); |
This file contains 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
async function play() { | |
// helpful copypastes: ✨ ¹ ² ³ ⁴ ♀ ♂ | |
const Config = { | |
Catch: { | |
enabled: true, | |
cycleLocations: true, | |
skipCompleteSwarms: true, | |
perLocation: 20, | |
/* banks all pokemon except those fully owned, then cycles through all balls and all locations */ | |
comprehensiveEnabled: true, | |
comprehensiveCooldownMin: 720, | |
/* uses unownReportGreatballs every cooldown if there are any variants missing for the current unown */ | |
unownReportEnabled: false, | |
unownReportGreatballs: 200, | |
unownReportCooldownMin: 60, | |
/* if repeatBallLoop is true, do not run other catches, but only throw repeatballs until the specified */ | |
/* number of shinies is found. */ | |
repeatBallLoop: false, | |
repeatBallLabel: 'Sandshrew', | |
repeatBallNumShinies: 2, | |
}, | |
Release: { | |
/* keeps 10 of each variant (30 if shiny), plus all legendary/mythical pokemon */ | |
enabled: true, | |
numNonShinyVariantsToKeep: 10, | |
releaseExtraNonShinyVariantsIfComplete: true, | |
nonShinyVariantsToKeepIfComplete: 2, | |
numShinyVariantsToKeep: 30, | |
releaseExtraShinyVariantsIfComplete: true, | |
shinyVariantsToKeepIfComplete: 5, | |
/* ignore any legendary/mythical status for below labels */ | |
ignoreLegendaryLabels: ['Zygarde Complete', 'Zygarde Fifty', 'Zygarde Ten'], | |
/* release all pokemon that have the RELEASE tag applied */ | |
releaseManuallyTagged: true, | |
}, | |
Battle: { | |
enabled: true, | |
// type: 'Kalos Cup', | |
// pokemon: ['Snorlax', 'Dragonite'], | |
// items: ['Leftovers', 'Dragon Fang'], | |
type: 'Emerald Cup', | |
pokemon: ['Rayquaza', 'Salamence'], | |
items: ['Dragon Fang', 'Shell Bell'], | |
}, | |
Daycare: { | |
enabled: true, | |
pokemon: ['Starly³ ♂'], | |
item: ['Everstone'], | |
// pokemon: ['Ditto✨', 'Sandshrew'], | |
get isPrivate() { return Config.Daycare.pokemon.length === 2; }, | |
hatchEnabled: true, | |
}, | |
Mart: { | |
enabled: true, | |
cooldownMin: 2, | |
maxCategoryCounts: { | |
'battle': 5, | |
'berry': 50, | |
'fertilizer': 100, | |
'hold': 10, | |
'items': 950, | |
'treasure': 5, | |
'megastone': 5, | |
}, | |
minCategoryCounts: { | |
'balls': 200, | |
'battle': 5, | |
'berry': 50, | |
'hold': 10, | |
'tms': 20, | |
}, | |
minIndividualCounts: { | |
'Fire Stone': 20, | |
'Leaf Stone': 20, | |
'Moon Stone': 20, | |
'Sun Stone': 20, | |
'Thunder Stone': 20, | |
'Water Stone': 20, | |
}, | |
maxIndividualCounts: { | |
'Everstone': 10, | |
'Moon Stone': 50, | |
}, | |
}, | |
Farm: { | |
enabled: true, | |
cooldownMin: 30, | |
fertilizer: 'Boost Mulch', | |
}, | |
Research: { | |
enabled: true, | |
/* always collect rare candies first if available */ | |
prioritiseRareCandy: false, | |
/* wormadam and spheal both give xl exp candy but wormadam isn't catchable */ | |
/* we don't want relic copper as its only use is buying relic song from the bazaar */ | |
questsExcluded: ['WORMADAM', 'UNOVANCOPPER'], | |
}, | |
Lottery: { enabled: true, }, | |
ClaimRaids: { enabled: true, }, | |
Misc: { | |
/* withdraws all banked pokemon at the start of the script */ | |
withdrawBankOnStart: true, | |
sootForFlutes: true, | |
wispsForSpiritomb: true, | |
zygardeCells: true, | |
/* one of 10, 50, or 100 */ | |
zygardeCount: 10, | |
activateFossils: true, | |
}, | |
Bazaar: { | |
enabled: true, | |
/* cynthia and hayley exchange TMs and TRs for heartscales (we're only interested in consumable TMs) */ | |
buyTMs: true, | |
tmCount: 20, | |
/* kurt and arnie exchange specialty balls for pokeballs - kurt max 5 each, arnie max 10. */ | |
buyBalls: true, | |
/* shell merchants sell shell bells for shoal salt and shoal shells */ | |
buyShellBells: true, | |
/* vitamin clerk sells vitamins (for evolving Tyrogue) for pokeballs, max 3 each. */ | |
buyVitamins: true, | |
/* isaac and amber sell incense (for breeding) for pokeballs, max 3 each. */ | |
buyIncense: true, | |
}, | |
} | |
/* disable log and debug, used by app and spams console */ | |
console.log = function () {}; | |
console.debug = function () {}; | |
console.clear(); | |
/* misc functions and constants generally useful */ | |
async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } | |
function now() { return new Date(); } | |
function fmtTime(time) { return time.toLocaleTimeString('en-GB', {timeZone: 'America/Los_Angeles'}); } | |
function fmtDateTime(time) { return time.toLocaleString('en-GB', {timeZone: 'America/Los_Angeles'}); } | |
function info(source = '', text = '') { console.info(`${fmtTime(now())} - ${source} - ${text}`); } | |
async function refreshUserdata() { await sleep(500); await firebase.refreshUser(); await sleep(500); } | |
function getItemCount(key) { return firebase.userData.items[key]; } | |
function getItemsByCategory(category) { | |
let items = Object(); | |
for (let [item, count] of Object.entries(firebase.userData.items)) { | |
if (count > 0 && window.ITEMS[item] && window.ITEMS[item].category === category) { items[item] = count; } | |
} | |
return items; | |
} | |
function getSortedPokemon() { | |
/* return map of owned pokemon to count sorted by ID, form, variant, and shininess */ | |
// unown: 201 | |
let pokemonArray = []; | |
for (let badgeId of Object.keys(firebase.userData.pokemon)) { | |
let pokemon = new Badge(badgeId); | |
let personality = pokemon.personality; | |
pokemonArray.push([badgeId, pokemon.id, personality.form ?? '', personality.variant ?? -1, personality.shiny]); | |
} | |
/* sort by non-shiny -> shiny, variant, then ascending ID */ | |
pokemonArray.sort(function(a, b){ return a[4]-b[4] }); | |
pokemonArray.sort(function(a, b){ return a[3]-b[3] }); | |
pokemonArray.sort(function(a, b){ return a[2].localeCompare(b[2]) }); | |
pokemonArray.sort(function(a, b){ return a[1]-b[1] }); | |
/* save in object form for compatibility with firebaser.userData.pokemon */ | |
let pokemonObject = {}; | |
for (let details of pokemonArray) { | |
pokemonObject[details[0]] = firebase.userData.pokemon[details[0]]; | |
} | |
return pokemonObject; | |
} | |
const HOUR = 1000 * 60 * 60; | |
class Converters { | |
/* converts from one representation of pokemon/item to another */ | |
static itemShortToLong(shortItem) { | |
/* convert short item name to long item name, e.g. 'belue' -> 'Belue Berry' */ | |
if (shortItem in window.ITEMS) { | |
return window.ITEMS[shortItem].label; | |
} | |
} | |
static itemLongToShort(longItem) { | |
/* convert long item name to short item name, e.g. 'Belue Berry' -> 'belue' */ | |
for (let [shortItem, item] of Object.entries(window.ITEMS)) { | |
if (longItem === item.label) { | |
return shortItem; | |
} | |
} | |
} | |
static pokemonBadgeIdToLabel(badgeId) { | |
/* convert a pokemon badge ID to a long label, e.g. '6s#f00G' -> 'Burmy Plant³ ♀' */ | |
return new Badge(badgeId).toLabel(); | |
} | |
static pokemonLabelToBadgeId(longPokemon) { | |
/* convert a long label to a pokemon badge ID, e.g. 'Burmy Plant³ ♀' -> '6s#f00G' */ | |
for (let badgeId of Object.keys(firebase.userData.pokemon)) { | |
if (this.pokemonBadgeIdToLabel(badgeId) === longPokemon) { | |
return badgeId; | |
} | |
} | |
} | |
static pokemonShortToLabel(shortPokemon) { | |
/* convert a short label to a long label, e.g. 'potw-412-plant-female-var3' -> 'Burmy Plant³ ♀' */ | |
return Badge.fromLegacy(shortPokemon).toLabel(); | |
} | |
static pokemonBadgeIdToNonVariantLabel(badgeId) { | |
/* custom label by species, form & gender, excluding variant, e.g. '6s#f00G' -> 'Burmyplantfemale' */ | |
/* we can't use toLabel() as that also distinguishes by variant, which defeats the point here */ | |
let pokemon = new Badge(badgeId); | |
let datastore = window.datastoreGet(pokemon.toDataStr()); | |
return datastore.species + (pokemon.personality.form ?? '') + (pokemon.personality.gender ?? ''); | |
} | |
static pokemonBadgeIdToDetails(badgeId) { | |
let pokemon = new Badge(badgeId); | |
return { | |
pokemon: pokemon, | |
pokemonLabel: pokemon.toLabel(), | |
personality: pokemon.personality, | |
datastore: window.datastoreGet(pokemon.toDataStr()), | |
nonVariantLabel: Converters.pokemonBadgeIdToNonVariantLabel(badgeId), | |
} | |
} | |
} | |
class Schedule { | |
/* sets up and keeps track of when next to run various tasks */ | |
static addMinutes(start, numMinutes) { | |
/* add numMinutes to given time */ | |
return new Date(start + (numMinutes * 60 * 1000)); | |
} | |
static addMinutesToNow(numMinutes) { | |
/* add numMinutes to present time */ | |
return this.addMinutes(Date.now(), numMinutes) | |
} | |
static async setup() { | |
info('Schedule', 'Setting up schedule...'); | |
this.nextCatch = now(); | |
this.nextLocations = now(); | |
if (Config.Mart.enabled) { | |
this.nextMart = now(); | |
info('Schedule', 'Next mart is due'); | |
} | |
if (Config.Farm.enabled) { | |
this.nextFarm = now(); | |
info('Schedule', 'Next farm is due'); | |
} | |
if (Config.Battle.enabled) { | |
this.nextBattle = this.addMinutes(firebase.userData.lastBattleStadiumDate, 60); | |
info('Schedule', 'Next battle ' + (now() > this.nextBattle ? 'is due': `at ${fmtDateTime(this.nextBattle)}`)); | |
} | |
if (Config.Release.enabled) { | |
let releaseLog = await firebase.dbGet(['users', firebase.user.uid, 'adventureLog', 'released']); | |
this.nextRelease = this.addMinutes(releaseLog.releasedTime, 60); | |
info('Schedule', 'Next release ' + (now() > this.nextRelease ? 'is due': `at ${fmtDateTime(this.nextRelease)}`)); | |
} | |
if (Config.Daycare.enabled) { | |
this.nextDaycare = this.addMinutes(firebase.userData.lastDayCareDate, Config.Daycare.isPrivate ? 30 : 60); | |
info('Schedule', 'Next daycare ' + (now() > this.nextDaycare ? 'is due': `at ${fmtDateTime(this.nextDaycare)}`)); | |
} | |
if (Config.Daycare.hatchEnabled) { | |
this.nextHatch = now(); | |
info('Schedule', 'Next hatch is due'); | |
} | |
if (Config.Research.enabled) { | |
this.nextResearch = this.addMinutes(firebase.userData.researchLastClaim, 60); | |
info('Schedule', 'Next research ' + (now() > this.nextResearch ? 'is due': `at ${fmtDateTime(this.nextResearch)}`)); | |
} | |
if (Config.Lottery.enabled) { | |
this.nextLottery = now(); | |
info('Schedule', 'Next lottery is due'); | |
} | |
if (Config.ClaimRaids.enabled) { | |
this.nextClaimRaids = now(); | |
info('Schedule', 'Next raids claim is due'); | |
} | |
if (Config.Catch.comprehensiveEnabled) { | |
this.nextComprehensiveCatch = this.addMinutesToNow(Config.Catch.comprehensiveCooldownMin); | |
info('Schedule', `Next comprehensive catch at ${fmtDateTime(this.nextComprehensiveCatch)}`); | |
} | |
if (Config.Catch.unownReportEnabled) { | |
this.nextUnownReportCatch = this.addMinutesToNow(Config.Catch.unownReportCooldownMin); | |
info('Schedule', `Next unown report catch at ${fmtDateTime(this.nextUnownReportCatch)}`); | |
} | |
if (Config.Misc.sootForFlutes) { | |
this.nextSootForFlutes = now(); | |
info('Schedule', `Next soot for flutes is due`); | |
} | |
if (Config.Misc.wispsForSpiritomb) { | |
this.nextWispsForSpiritomb = now(); | |
info('Schedule', `Next wisps for Spiritomb is due`); | |
} | |
if (Config.Misc.zygardeCells) { | |
this.nextZygardeCells = now(); | |
info('Schedule', `Next Zygarde cells is due`); | |
} | |
if (Config.Misc.activateFossils) { | |
this.nextActivateFossils = now(); | |
info('Schedule', `Next fossil activation is due`); | |
} | |
if (Config.Bazaar.enabled) { | |
this.nextBazaar = now(); | |
info('Schedule', `Next Bazaar is due`); | |
} | |
info('Schedule', 'Finished setting up schedule...'); | |
} | |
} | |
class Wrapper { | |
/* kicks off tasks at appropriate times if enabled, with error handling */ | |
static async Locations() { | |
try { | |
if (now() >= Schedule.nextLocations) { | |
await Locations.main(); | |
} | |
} | |
catch (e) { | |
console.error('Locations failed:'); | |
console.error(e); | |
Schedule.nextLocations = Schedule.addMinutesToNow(60); | |
} | |
} | |
static async Mart() { | |
try { | |
if (Config.Mart.enabled && now() >= Schedule.nextMart) { | |
await Mart.main(); | |
} | |
} | |
catch (e) { | |
console.error('Mart failed:'); | |
console.error(e); | |
Schedule.nextMart = Schedule.addMinutesToNow(60); | |
} | |
} | |
static async Release() { | |
try { | |
if (Config.Release.enabled && now() >= Schedule.nextRelease) { | |
/* release unavailable between 0-5 minutes of the hour, give 1min buffer too */ | |
if (now().getMinutes() >= 59 || now().getMinutes() <= 5) { | |
info('Wrapper', 'Release is in unavailable period, adding 7 minutes'); | |
Schedule.nextRelease = Schedule.addMinutesToNow(7); | |
} | |
else { | |
await Release.main(); | |
} | |
} | |
} | |
catch (e) { | |
console.error('Release failed:'); | |
console.error(e); | |
Schedule.nextRelease = Schedule.addMinutesToNow(60); | |
} | |
} | |
static async Battle() { | |
try { | |
if (Config.Battle.enabled && now() >= Schedule.nextBattle) { | |
await Battle.main(); | |
} | |
} | |
catch (e) { | |
console.error('Battle failed:'); | |
console.error(e); | |
Schedule.nextBattle = Schedule.addMinutesToNow(60); | |
} | |
} | |
static async Daycare() { | |
try { | |
if (Config.Daycare.enabled && now() >= Schedule.nextDaycare) { | |
await Daycare.main(); | |
} | |
} | |
catch (e) { | |
console.error('Daycare failed:'); | |
console.error(e); | |
Schedule.nextDaycare = Schedule.addMinutesToNow(60); | |
} | |
} | |
static async Farm() { | |
try { | |
if (Config.Farm.enabled && now() >= Schedule.nextFarm) { | |
await Farm.main(); | |
} | |
} | |
catch (e) { | |
console.error('Farm failed:'); | |
console.error(e); | |
Schedule.nextFarm = Schedule.addMinutesToNow(60); | |
} | |
} | |
static async Hatch() { | |
try { | |
if (Config.Daycare.hatchEnabled && now() >= Schedule.nextHatch) { | |
await Hatch.main(); | |
} | |
} | |
catch (e) { | |
console.error('Hatch failed:'); | |
console.error(e); | |
Schedule.nextHatch = Schedule.addMinutesToNow(60); | |
} | |
} | |
static async Lottery() { | |
try { | |
if (Config.Lottery.enabled && now() >= Schedule.nextLottery) { | |
await Lottery.main(); | |
} | |
} | |
catch (e) { | |
console.error('Lottery failed:'); | |
console.error(e); | |
Schedule.nextLottery = Schedule.addMinutesToNow(60); | |
} | |
} | |
static async ClaimRaids() { | |
try { | |
if (Config.ClaimRaids.enabled && now() >= Schedule.nextClaimRaids) { | |
await ClaimRaids.main(); | |
} | |
} | |
catch (e) { | |
console.error('Claim raids failed:'); | |
console.error(e); | |
Schedule.nextClaimRaids = Schedule.addMinutesToNow(60); | |
} | |
} | |
static async Research() { | |
try { | |
if (Config.Research.enabled && now() >= Schedule.nextResearch) { | |
await Research.main(); | |
} | |
} | |
catch (e) { | |
console.error('Research failed:'); | |
console.error(e); | |
Schedule.nextResearch = Schedule.addMinutesToNow(60); | |
} | |
} | |
static async SootForFlutes() { | |
try { | |
if (Config.Misc.sootForFlutes && now() >= Schedule.nextSootForFlutes) { | |
await Misc.sootForFlutes(); | |
} | |
} | |
catch (e) { | |
console.error('Soot for flutes failed:'); | |
console.error(e); | |
Schedule.nextSootForFlutes = Schedule.addMinutesToNow(60); | |
} | |
} | |
static async WispsForSpiritomb() { | |
try { | |
if (Config.Misc.wispsForSpiritomb && now() >= Schedule.nextWispsForSpiritomb) { | |
await Misc.wispsForSpiritomb(); | |
} | |
} | |
catch (e) { | |
console.error('Wisps for spiritomb failed:'); | |
console.error(e); | |
Schedule.nextWispsForSpiritomb = Schedule.addMinutesToNow(60); | |
} | |
} | |
static async ZygardeCells() { | |
try { | |
if (Config.Misc.zygardeCells && now() >= Schedule.nextZygardeCells) { | |
await Misc.zygardeCells(); | |
} | |
} | |
catch (e) { | |
console.error('Activating Zygarde cells failed:'); | |
console.error(e); | |
Schedule.nextZygardeCells = Schedule.addMinutesToNow(60); | |
} | |
} | |
static async ActivateFossils() { | |
try { | |
if (Config.Misc.activateFossils && now() >= Schedule.nextActivateFossils) { | |
await Misc.activateFossils(); | |
} | |
} | |
catch (e) { | |
console.error('Activating fossils failed:'); | |
console.error(e); | |
Schedule.nextActivateFossils = Schedule.addMinutesToNow(60); | |
} | |
} | |
static async Bazaar() { | |
try { | |
if (Config.Bazaar.enabled && now() >= Schedule.nextBazaar) { | |
await Bazaar.main(); | |
} | |
} | |
catch (e) { | |
console.error('Bazaar failed:'); | |
console.error(e); | |
Schedule.nextBazaar = Schedule.addMinutesToNow(60); | |
} | |
} | |
static async ComprehensiveCatch() { | |
try { | |
if ( | |
Config.Catch.comprehensiveEnabled && | |
now() >= Schedule.nextComprehensiveCatch && | |
!Config.Catch.repeatBallLoop | |
) { | |
await ComprehensiveCatch.main(); | |
} | |
} | |
catch (e) { | |
console.error('Comprehensive catch failed:'); | |
console.error(e); | |
Schedule.nextComprehensiveCatch = Schedule.addMinutesToNow(60); | |
} | |
} | |
static async UnownReportCatch() { | |
try { | |
if (Config.Catch.unownReportEnabled && | |
now() >= Schedule.nextUnownReportCatch && | |
!Config.Catch.repeatBallLoop | |
) { | |
/* only process if we actually need unown variants */ | |
if (!Locations.allUnownVariantsOwned) { | |
await UnownCatch.main(); | |
} | |
} | |
} | |
catch (e) { | |
console.error('Unown report catch failed:'); | |
console.error(e); | |
Schedule.nextUnownReportCatch = Schedule.addMinutesToNow(60); | |
} | |
} | |
static async RepeatBallCatch() { | |
try { | |
if (Config.Catch.repeatBallLoop && now() >= Schedule.nextCatch) { | |
await RepeatBallCatch.main(); | |
} | |
} | |
catch (e) { | |
console.error('Repeatball catch failed:'); | |
console.error(e); | |
Schedule.nextCatch = Schedule.addMinutesToNow(60); | |
} | |
} | |
static async Catch() { | |
try { | |
if (Config.Catch.enabled && now() >= Schedule.nextCatch && !Config.Catch.repeatBallLoop) { | |
await Catch.main(); | |
} | |
} | |
catch (e) { | |
console.error('Catch failed:'); | |
console.error(e); | |
Schedule.nextCatch = Schedule.addMinutesToNow(60); | |
} | |
} | |
} | |
class Bank { | |
/* functions to deposit and withdraw pokemon from the bank */ | |
static async sendPokemonToBank(depositPokemon) { | |
info('Bank', `Depositing ${depositPokemon.length} pokemon entries to bank...`); | |
if (!depositPokemon.length) { | |
info('Bank', 'No pokemon entries to deposit to bank.'); | |
return; | |
} | |
let chunkSize = 1000; | |
let depositedCount = 0; | |
for (let i = 0; i < depositPokemon.length; i += chunkSize) { | |
const chunkDeposit = depositPokemon.slice(i, i + chunkSize); | |
await firebase.exec('bank_deposit', {operations: chunkDeposit}) | |
.then(res => { | |
depositedCount += chunkDeposit.length; | |
for (let notice of res.data.notices) { | |
if (!notice.startsWith('Deposited')) { | |
console.warn(notice); | |
depositedCount--; | |
} | |
} | |
info('Bank', `Deposited ${depositedCount}...`); | |
}) | |
.catch(err => {console.error(err)}) | |
} | |
info('Bank', 'Finished depositing pokemon entries to bank.'); | |
} | |
static async getAllPokemonFromBank() { | |
let withdrawPokemon = []; | |
await firebase.exec('bank_list', {}) | |
.then(res => { | |
withdrawPokemon = Object.entries(res.data.pokemon); | |
}) | |
.catch(err => {console.error(err)}) | |
if (!withdrawPokemon.length) { | |
info('Bank', 'No pokemon entries to withdraw from bank.'); | |
return; | |
} | |
info('Bank', `Withdrawing all pokemon entries (${withdrawPokemon.length}) from bank...`); | |
let chunkSize = 2000; | |
let withdrawnCount = 0; | |
for (let i = 0; i < withdrawPokemon.length; i += chunkSize) { | |
const chunkWithdraw = withdrawPokemon.slice(i, i + chunkSize); | |
await firebase.exec('bank_withdraw', {operations: chunkWithdraw}) | |
.then(res => { | |
withdrawnCount += chunkWithdraw.length; | |
info('Bank', `Withdrew ${withdrawnCount}...`); | |
}) | |
.catch(err => {console.error(err)}) | |
} | |
info('Bank', 'Finished withrawing pokemon entries from bank.'); | |
} | |
} | |
class Locations { | |
/* maintains list of locations to skip */ | |
static getSwarmLocationsToSkip() { | |
/* return a set of locations to skip because we own all pokemon in evolution line from swarm */ | |
this.swarmLocationsToSkip = new Set(); | |
if (!Config.Catch.skipCompleteSwarms) { | |
return; | |
} | |
let continentsToSkip = new Set(); | |
for (let [continent, pokemonShort] of Object.entries(window.Swarms)) { | |
let pokemonLabel = Converters.pokemonShortToLabel(pokemonShort); | |
if (pokemonLabel === 'Furfrou') { | |
// TODO: fix furfrou forms | |
continue | |
} | |
if (Evolutions.allVariantsOwned(pokemonLabel)) { | |
if (pokemonLabel === 'Luvdisc' && getItemCount('heartscale') < 900) { | |
info('Locations', 'Own all variants of Luvdisc, but catching for Heart Scales'); | |
continue; | |
} | |
info('Locations', `Own all variants of ${pokemonLabel} - skipping ${continent}`); | |
continentsToSkip.add(continent); | |
} | |
} | |
for (let [location, details] of Object.entries(this.locationMap)) { | |
if (continentsToSkip.has(details.region)) { | |
this.swarmLocationsToSkip.add(location); | |
} | |
} | |
for (let [continent, pokemonShort] of Object.entries(window.Swarms)) { | |
if (continentsToSkip.has(continent)) { | |
continue; | |
} | |
info('Locations', `Cycling through ${continent} for ${Converters.pokemonShortToLabel(pokemonShort)}`); | |
} | |
} | |
static getComboLocationsToSkip() { | |
/* return a set of locations to skip because we will have seen the region/terrain/weather combo */ | |
this.comboLocationsToSkip = new Set(); | |
let comboSeen = new Set(); | |
for (let [location, details] of Object.entries(this.locationMap)) { | |
let strDetails = [details.region, details.terrain, details.forecast].toString(); | |
if (comboSeen.has(strDetails)) { | |
this.comboLocationsToSkip.add(location); | |
} | |
else { | |
comboSeen.add(strDetails); | |
} | |
} | |
} | |
static getUnownLocation() { | |
/* return location of unown swarm and catchable form */ | |
for (let [location, details] of Object.entries(this.locationMap)) { | |
if (details.unown !== null) { | |
let pokemonLabel = Converters.pokemonShortToLabel(`potw-201-${details.unown}`); | |
info('Locations', `Today's Unown Report is ${pokemonLabel} in ${location}`); | |
if (Evolutions.allVariantsOwned(`Unown${details.unown}`)) { | |
info('Locations', `All ${pokemonLabel} variants owned!`); | |
} | |
else { | |
this.unownForm = details.unown; | |
this.unownLocation = location; | |
} | |
} | |
} | |
} | |
static async moveTo(location) { | |
info('Locations', `Moving to ${location}`); | |
await firebase.exec('user_location', {location: location}); | |
} | |
static async main() { | |
info('Locations', 'Setting up locations...') | |
let res = await firebase.exec('location_list'); | |
this.locationMap = res.data.locations; | |
this.locations = Object.keys(this.locationMap); | |
this.getComboLocationsToSkip(); | |
Evolutions.refreshVariantsOwned(); | |
this.getSwarmLocationsToSkip(); | |
this.getUnownLocation(); | |
Schedule.nextLocations = Schedule.addMinutesToNow(60); | |
info('Locations', `Finished setting up locations, next refresh at ${fmtDateTime(Schedule.nextLocations)}`); | |
} | |
} | |
class Evolutions { | |
/* functions that deal with evolutions, variants, etc. */ | |
static getEvolutions(datastore) { | |
/* return list of evolutions from datastore */ | |
let evolutions = []; | |
if (Array.isArray(datastore.levelTo)) { | |
evolutions.push(...datastore.levelTo) | |
} else if (datastore.levelTo) { | |
evolutions.push(datastore.levelTo) | |
} | |
if (datastore.evolveTo) { | |
evolutions.push(...datastore.evolveTo) | |
} | |
return evolutions | |
} | |
static getEvolutionLines() { | |
/* return map of pokemon to their evolution line */ | |
let nonVariantLabelsSeen = new Set(); | |
let pokemonEvolutionLines = new Map(); | |
for (let badgeId of Object.keys(getSortedPokemon())) { | |
let details = Converters.pokemonBadgeIdToDetails(badgeId); | |
let nonVariantLabel = details.nonVariantLabel; | |
/* skip if we have already seen this pokemon (including variant) or if shiny (doesn't affect evo line) */ | |
if (nonVariantLabelsSeen.has(details.nonVariantLabel) || details.personality.shiny) { | |
continue; | |
} | |
nonVariantLabelsSeen.add(details.nonVariantLabel); | |
/* set up evolution map */ | |
/* if not present already, add map of base pokemon to itself */ | |
if (!pokemonEvolutionLines.has(nonVariantLabel)) { | |
pokemonEvolutionLines.set(nonVariantLabel, new Set([nonVariantLabel])); | |
} | |
if ((details.datastore.levelTo || details.datastore.evolveTo)) { | |
let preEvolutionLine = pokemonEvolutionLines.get(nonVariantLabel); | |
for (let evolution of Evolutions.getEvolutions(details.datastore)) { | |
/* assume that evolutions have same form and gender as base pokemon */ | |
/* evolutions from datastore do not take this into account */ | |
let evolutionSpecies = window.datastoreGet(Badge.fromLegacy(evolution).toDataStr()).species; | |
/* if not present already, add map of target pokemon to itself */ | |
let targetLabel = evolutionSpecies + (details.personality.form ?? '') + (details.personality.gender ?? ''); | |
if (!pokemonEvolutionLines.has(targetLabel)) { | |
pokemonEvolutionLines.set(targetLabel, new Set([targetLabel])); | |
} | |
let postEvolutionLine = pokemonEvolutionLines.get(targetLabel); | |
/* for each evolution... */ | |
for (let postEvolutionPokemon of Array.from(postEvolutionLine)) { | |
for (let preEvolutionPokemon of Array.from(preEvolutionLine)) { | |
/* add the post-evolution to all pre-evolution sets */ | |
let preEvolutionLineTemp = pokemonEvolutionLines.get(preEvolutionPokemon); | |
preEvolutionLineTemp.add(postEvolutionPokemon); | |
pokemonEvolutionLines.set(preEvolutionPokemon, preEvolutionLineTemp); | |
/* add each pre-evolution to the post-evolution set */ | |
let postEvolutionLineTemp = pokemonEvolutionLines.get(postEvolutionPokemon); | |
postEvolutionLineTemp.add(preEvolutionPokemon); | |
pokemonEvolutionLines.set(postEvolutionPokemon, postEvolutionLineTemp); | |
} | |
} | |
} | |
pokemonEvolutionLines.set(nonVariantLabel, preEvolutionLine); | |
} | |
} | |
return pokemonEvolutionLines; | |
} | |
static getVariantCounts(isShiny) { | |
/* return map of pokemon to total variant count, and map of pokemon to owned variants */ | |
let pokemonLabelsSeen = new Set(); | |
let pokemonVariantCounts = new Map(); | |
let pokemonVariants = new Map(); | |
for (let badgeId of Object.keys(firebase.userData.pokemon)) { | |
let details = Converters.pokemonBadgeIdToDetails(badgeId); | |
let datastore = details.datastore; | |
let nonVariantLabel = Converters.pokemonBadgeIdToNonVariantLabel(badgeId); | |
/* skip if we have already seen this pokemon (including variant) or if not desired shininess */ | |
if (pokemonLabelsSeen.has(details.pokemonLabel) || details.personality.shiny !== isShiny) { | |
continue; | |
} | |
pokemonLabelsSeen.add(details.pokemonLabel); | |
/* get count of total variants available */ | |
pokemonVariantCounts.set(nonVariantLabel, datastore.novelMoves ? datastore.novelMoves.length : 1); | |
/* get owned variants */ | |
let variants = pokemonVariants.get(nonVariantLabel) ?? new Set(); | |
variants.add(details.personality.variant); | |
pokemonVariants.set(nonVariantLabel, variants); | |
} | |
return [pokemonVariantCounts, pokemonVariants] | |
} | |
static getCompleteVariantLabels(isShiny) { | |
/* sets up set of labels that have all variants in evolution line owned */ | |
let variantLabelsToRelease = new Set(); | |
let pokemonEvolutionLines = Evolutions.getEvolutionLines(isShiny); | |
console.info(pokemonEvolutionLines) | |
let [pokemonVariantCounts, pokemonVariants] = Evolutions.getVariantCounts(isShiny); | |
/* now we iterate through the evolution lines to see whether we own all variants of the evolution line */ | |
for (let [pokemonLabel, evolutionLine] of pokemonEvolutionLines) { | |
if (variantLabelsToRelease.has(pokemonLabel)) { | |
continue; | |
} | |
let allVariantsOwned = true; | |
for (let evolution of evolutionLine.values()) { | |
if (!pokemonVariantCounts.has(evolution) || !pokemonVariants.has(evolution)) { | |
allVariantsOwned = false; | |
continue; | |
} | |
if (pokemonVariantCounts.get(evolution) !== pokemonVariants.get(evolution).size) { | |
allVariantsOwned = false; | |
} | |
} | |
if (allVariantsOwned) { | |
variantLabelsToRelease = new Set([...variantLabelsToRelease, ...evolutionLine]); | |
} | |
} | |
info('Evolutions', `${isShiny ? 'Shiny' : 'Non-shiny'} Pokemon that have all evolution variants owned:`); | |
console.info(variantLabelsToRelease.size ? Array.from(variantLabelsToRelease) : 'None'); | |
return variantLabelsToRelease; | |
} | |
static refreshVariantsOwned() { | |
this.nonShinyCompleteVariantsOwned = this.getCompleteVariantLabels(false); | |
this.shinyCompleteVariantsOwned = this.getCompleteVariantLabels(true); | |
} | |
static allNonShinyVariantsOwned(nonVariantLabel) { | |
return this.nonShinyCompleteVariantsOwned.has(nonVariantLabel); | |
} | |
static allShinyVariantsOwned(nonVariantLabel) { | |
return this.shinyCompleteVariantsOwned.has(nonVariantLabel); | |
} | |
static allVariantsOwned(nonVariantLabel) { | |
return this.allNonShinyVariantsOwned(nonVariantLabel) && this.allShinyVariantsOwned(nonVariantLabel); | |
} | |
} | |
class Catch { | |
/* catches using standard pokeball. loops through locations if Catch.cycleLocations is set. */ | |
static hasSetup = false; | |
static async setup() { | |
/* set up location list if cycling */ | |
if (Config.Catch.cycleLocations) { | |
this.catchCount = 0 | |
this.locationIndex = 0; | |
this.locations = Locations.locations; | |
await Locations.moveTo(this.locations[0]); | |
} | |
else { | |
info('Catch', `Catching in ${firebase.userData.location}`); | |
} | |
this.hasSetup = true; | |
} | |
static async reset() { | |
/* reset cycle when reaching the end */ | |
if (this.locationIndex >= this.locations.length) { | |
await this.setup(); | |
return true; | |
} | |
} | |
static async cycleLocations() { | |
/* move to next location every Config.catchPerLocation pokemon caught */ | |
this.locationIndex++ | |
if (await this.reset()) { | |
return; | |
} | |
let location = this.locations[this.locationIndex]; | |
while (Locations.comboLocationsToSkip.has(location)) { | |
info('Catch', `Skipping ${location} - already seen region/terrain/weather combo.`) | |
this.locationIndex++ | |
if (await this.reset()) { | |
return; | |
} | |
location = this.locations[this.locationIndex]; | |
} | |
while (Locations.swarmLocationsToSkip.has(location)) { | |
info('Catch', `Skipping ${location} - already own all variants of swarm evolution line.`) | |
this.locationIndex++ | |
if (await this.reset()) { | |
return; | |
} | |
location = this.locations[this.locationIndex]; | |
} | |
await Locations.moveTo(location); | |
} | |
static async main() { | |
if (!this.hasSetup) { | |
await this.setup(); | |
} | |
for (let i = 0; i < Config.Catch.perLocation; i++) { | |
await firebase.exec('throw', {pokeball: 'pokeball', duplicates: true}) | |
.then(res => { | |
let holdItem = res.data.holdItem ? `holding ${Converters.itemShortToLong(res.data.holdItem)}` : ''; | |
info('Catch', `Caught ${Converters.pokemonBadgeIdToLabel(res.data.badge)} ${holdItem}`); | |
}) | |
.catch(err => { console.error(err) }) | |
} | |
if (Config.Catch.cycleLocations) { | |
await this.cycleLocations(); | |
} | |
} | |
} | |
class ComprehensiveCatch { | |
/* tries every owned ball in every location */ | |
static getPokeballOrder() { | |
/* return preferred order of ball usage - premierballs, pokeballs, buyable by cost, then unbuyable by count */ | |
/* don't use repeatballs - won't work with pokemon in the bank */ | |
let buyable = ['premierball', 'pokeball']; | |
let unBuyable = []; | |
for (let item of Object.keys(getItemsByCategory('balls'))) { | |
if (!['repeatball', 'pokeball', 'premierball'].includes(item)) { | |
if (window.ITEMS[item].buy) { | |
buyable.push(item); | |
} else { | |
unBuyable.push(item); | |
} | |
} | |
} | |
buyable.sort(function(a, b){ return window.ITEMS[a].buy - window.ITEMS[b].buy }); | |
unBuyable.sort(function(a, b){ return getItemCount(b) - getItemCount(a) }); | |
return buyable.concat(unBuyable); | |
} | |
static async sendPokemonToBank() { | |
Evolutions.refreshVariantsOwned(); | |
let depositPokemon = []; | |
for (let [badgeId, count] of Object.entries(firebase.userData.pokemon)) { | |
if (!Evolutions.allVariantsOwned(Converters.pokemonBadgeIdToNonVariantLabel(badgeId))) { | |
depositPokemon.push([badgeId, count]); | |
} | |
} | |
info('Comprehensive Catch', `Depositing all pokemon except fully owned variants to bank...`); | |
await Bank.sendPokemonToBank(depositPokemon); | |
} | |
static async catchWhilePossible(pokeball, lure, lureText) { | |
let pokeballLoop = true; | |
let caughtPokemon = new Set(); | |
while (getItemCount(pokeball) && pokeballLoop) { | |
await firebase.exec('throw', {pokeball: pokeball, duplicates: false, lure: lure}) | |
.then(res => { | |
let holdItem = res.data.holdItem ? ` holding ${Converters.itemShortToLong(res.data.holdItem)}` : ''; | |
let pokemonLabel = Converters.pokemonBadgeIdToLabel(res.data.badge); | |
info( | |
'Comprehensive Catch', | |
`Caught ${pokemonLabel}${holdItem} with ${Converters.itemShortToLong(pokeball)} ${lureText}` | |
); | |
if (caughtPokemon.has(pokemonLabel)) { | |
console.info(`Already caught ${pokemonLabel}, ending loop`); | |
pokeballLoop = false; | |
} else { | |
caughtPokemon.add(pokemonLabel); | |
} | |
}) | |
.catch(() => { pokeballLoop = false; }) | |
} | |
} | |
static async main() { | |
info('Comprehensive Catch', 'Starting comprehensive catch...') | |
let pokeballOrder = this.getPokeballOrder(); | |
await this.sendPokemonToBank(); | |
let originalLocation = firebase.userData.location; | |
info('Comprehensive Catch', `Original location: ${originalLocation}`); | |
for (let location of Locations.locations) { | |
if (Locations.comboLocationsToSkip.has(location)) { | |
info('Comprehensive Catch', `Skipping ${location} - already seen region/terrain/weather combo.`) | |
continue; | |
} | |
await Locations.moveTo(location); | |
/* try every pokeball */ | |
for (let pokeball of pokeballOrder) { | |
await this.catchWhilePossible(pokeball, null, ''); | |
} | |
/* try the trophy garden */ | |
await this.catchWhilePossible('greatball', 'trophygardenkey', 'in the Trophy Garden'); | |
await this.catchWhilePossible('ultraball', 'colressmchn', 'in the Hidden Grotto'); | |
// /* try the friend safari */ | |
// pokeballLoop = true; | |
// while (getItemCount('safariball') && pokeballLoop) { | |
// await firebase.exec('throw', {pokeball: 'safariball', duplicates: false, lure: 'friendsafaripass'}) | |
// .then(res => { | |
// let holdItem = res.data.holdItem ? ` holding ${Converters.itemShortToLong(res.data.holdItem)}` : ''; | |
// info( | |
// 'Comprehensive Catch', | |
// `Caught ${Converters.pokemonBadgeIdToLong(res.data.badge)}` + | |
// `${holdItem} with Safari Ball in the Friend Safari` | |
// ); | |
// }) | |
// .catch(() => { pokeballLoop = false; }) | |
// } | |
/* seeing as comprehensive catch takes ages, try running actions with cooldowns every location change */ | |
/* we also want to run the mart and bazaar to stock up on pokeballs if we can */ | |
/* we don't want to release as this will interfere with the bank */ | |
await Wrapper.Mart(); | |
await Wrapper.Battle(); | |
await Wrapper.Daycare(); | |
await Wrapper.Farm(); | |
await Wrapper.Research(); | |
} | |
await Bank.getAllPokemonFromBank(); | |
info('Comprehensive Catch', 'Moving back to original location...'); | |
await Locations.moveTo(originalLocation); | |
Schedule.nextComprehensiveCatch = Schedule.addMinutesToNow(Config.Catch.comprehensiveCooldownMin); | |
info( | |
'Comprehensive Catch', | |
`Finished comprehensive catch, next at ${fmtDateTime(Schedule.nextComprehensiveCatch)}` | |
); | |
} | |
} | |
class RepeatBallCatch { | |
/* uses repeatballs to catch a specified number of shinies of a given species. */ | |
static async sendPokemonToBank() { | |
let depositPokemon = []; | |
let labelsToKeep = [Config.Catch.repeatBallLabel, `${Config.Catch.repeatBallLabel}✨`]; | |
for (let [badgeId, count] of Object.entries(firebase.userData.pokemon)) { | |
if (!labelsToKeep.includes(Converters.pokemonBadgeIdToLabel(badgeId))) { | |
depositPokemon.push([badgeId, count]); | |
} | |
} | |
info('Repeatball Catch', `Depositing all pokemon except ${Config.Catch.repeatBallLabel} to bank...`); | |
await Bank.sendPokemonToBank(depositPokemon); | |
} | |
static getShinyCounts() { | |
let shinyCounts = 0; | |
for (let badgeId of Object.keys(firebase.userData.pokemon)) { | |
if (Converters.pokemonBadgeIdToLabel(badgeId) === `${Config.Catch.repeatBallLabel}✨`) { | |
shinyCounts++; | |
} | |
} | |
return shinyCounts; | |
} | |
static async main() { | |
info('Repeatball Catch', 'Starting repeatball catch...') | |
await this.sendPokemonToBank(); | |
await refreshUserdata(); | |
while (this.getShinyCounts() < Config.Catch.repeatBallNumShinies) { | |
for (let i = 0; i < Config.Catch.perLocation; i++) { | |
await firebase.exec('throw', {pokeball: 'repeatball', duplicates: true}) | |
.then(res => { | |
let holdItem = res.data.holdItem ? `holding ${Converters.itemShortToLong(res.data.holdItem)}` : ''; | |
let details = Converters.pokemonBadgeIdToDetails(res.data.badge); | |
info('Catch', `Caught ${details.pokemonLabel} ${holdItem}`); | |
if (details.personality.shiny) { | |
shinyCounts++; | |
} | |
console.info(shinyCounts); | |
}) | |
.catch(err => {console.error(err)}) | |
} | |
await refreshUserdata(); | |
/* somehow the repeatball will sometimes catch random pokemon not of the current species? */ | |
await this.sendPokemonToBank(); | |
await Wrapper.Mart(); | |
await Wrapper.Battle(); | |
await Wrapper.Daycare(); | |
await Wrapper.Farm(); | |
await Wrapper.Research(); | |
} | |
info( | |
'Repeatball Catch', | |
`At least ${Config.Catch.repeatBallNumShinies} ${Config.Catch.repeatBallLabel}✨ owned, ` + | |
`ending repeatball catch.` | |
) | |
Config.Catch.repeatBallLoop = false; | |
await Bank.getAllPokemonFromBank(); | |
} | |
} | |
class UnownCatch { | |
/* uses unownReportGreatballs every cooldown if there are any variants missing for the current unown */ | |
static async main() { | |
info('UnownCatch', 'Starting unown report catch...') | |
let originalLocation = firebase.userData.location; | |
info('UnownCatch', `Original location: ${originalLocation}`); | |
await Locations.moveTo(Locations.unownLocation); | |
for (let _ of Array(Config.Catch.unownReportGreatballs).keys()) { | |
await firebase.exec('throw', {pokeball: 'greatball', duplicates: true}) | |
.then(res => { | |
let holdItem = res.data.holdItem ? `holding ${Converters.itemShortToLong(res.data.holdItem)}` : ''; | |
info('UnownCatch', `Caught ${Converters.pokemonBadgeIdToLabel(res.data.badge)} ${holdItem}`); | |
}) | |
.catch(err => {console.error(err)}) | |
} | |
info('UnownCatch', 'Moving back to original location...'); | |
await Locations.moveTo(originalLocation); | |
Schedule.nextUnownReportCatch = Schedule.addMinutesToNow(Config.Catch.unownReportCooldownMin); | |
info('UnownCatch', `Finished unown report catch, next at ${fmtDateTime(Schedule.nextUnownReportCatch)}`); | |
} | |
} | |
class Mart { | |
/* buys and sells items in the mart */ | |
static totalSold = 0; | |
static totalBought = 0; | |
static getItemsToExchange() { | |
/* get items to sell/buy according to max/min amounts in config */ | |
let toSell = []; | |
let toBuy = []; | |
let maxCounts = Config.Mart.maxCategoryCounts; | |
let maxIndCounts = Config.Mart.maxIndividualCounts; | |
let minCounts = Config.Mart.minCategoryCounts; | |
let minIndCounts = Config.Mart.minIndividualCounts; | |
for (let [item, count] of Object.entries(firebase.userData.items)) { | |
let longItem = Converters.itemShortToLong(item); | |
if (window.ITEMS[item] === undefined) { | |
/* skip any corrupted items */ | |
continue; | |
} | |
let category = window.ITEMS[item].category; | |
if (category in maxCounts && count > maxCounts[category] && window.ITEMS[item].sell) { | |
toSell.push([item, count - maxCounts[category]]); | |
} | |
if (longItem in maxIndCounts && count > maxIndCounts[longItem] && window.ITEMS[item].sell) { | |
toSell.push([item, count - maxIndCounts[longItem]]); | |
} | |
if (category in minCounts && count < minCounts[category] && window.ITEMS[item].buy) { | |
toBuy.push([item, minCounts[category] - count]); | |
} | |
if (longItem in minIndCounts && count < minIndCounts[longItem] && window.ITEMS[item].buy) { | |
toBuy.push([item, minIndCounts[longItem] - count]); | |
} | |
} | |
return [toSell, toBuy]; | |
} | |
static async main() { | |
info('Mart', 'Navigating to mart...'); | |
let [itemsToSell, itemsToBuy] = this.getItemsToExchange(); | |
for (let [item, amountToSell] of itemsToSell) { | |
let res = await firebase.exec('exchange_inverse', {type: item, count: amountToSell}); | |
this.totalSold += res.data.price; | |
info( | |
'Mart', | |
`Sold ${res.data.count} ${Converters.itemShortToLong(res.data.sold)} ` + | |
`for ${res.data.price} pokeballs, total sold: ${this.totalSold}` | |
); | |
} | |
for (let [item, amountToBuy] of itemsToBuy) { | |
let res = await firebase.exec('exchange', {type: item, count: amountToBuy}); | |
this.totalBought += res.data.price; | |
info( | |
'Mart', | |
`Bought ${res.data.count} ${Converters.itemShortToLong(res.data.purchased)} ` + | |
`for ${res.data.price} pokeballs, total bought: ${this.totalBought}` | |
); | |
} | |
Schedule.nextMart = Schedule.addMinutesToNow(Config.Mart.cooldownMin); | |
info('Mart', `Finished trading, next at ${fmtDateTime(Schedule.nextMart)}`); | |
} | |
} | |
class Release { | |
/* releases pokemon to obtain pokeballs */ | |
static getPokemonToRelease() { | |
/* return an array of pokemon to release */ | |
let toRelease = new Map(); /* badge id -> count, what is needed for the call */ | |
let toReleaseLabels = new Map(); /* pokemon label -> count, for debugging */ | |
let pokemonSeen = new Map(); | |
Evolutions.refreshVariantsOwned(); | |
for (let [badgeId, count] of Object.entries(firebase.userData.pokemon)) { | |
let details = Converters.pokemonBadgeIdToDetails(badgeId); | |
let pokemonLabel = details.pokemonLabel; | |
let shiny = details.personality.shiny; | |
function releaseShinies(countSeen) { | |
return ( | |
countSeen === Config.Release.numShinyVariantsToKeep || | |
( | |
Config.Release.releaseExtraShinyVariantsIfComplete && | |
countSeen === Config.Release.shinyVariantsToKeepIfComplete && | |
Evolutions.allShinyVariantsOwned(details.nonVariantLabel) | |
) | |
) | |
} | |
function releaseNonShinies(countSeen) { | |
return ( | |
countSeen === Config.Release.numNonShinyVariantsToKeep || | |
( | |
Config.Release.releaseExtraNonShinyVariantsIfComplete && | |
countSeen === Config.Release.nonShinyVariantsToKeepIfComplete && | |
Evolutions.allNonShinyVariantsOwned(details.nonVariantLabel) | |
) | |
) | |
} | |
/* release any manually tagged pokemon without any conditions */ | |
if ( | |
Config.Release.releaseManuallyTagged && | |
details.pokemon.defaultTags && details.pokemon.defaultTags.includes('RELEASE') | |
) { | |
let countRelease = toRelease.get(badgeId) ?? 0; | |
let countReleaseLabels = toReleaseLabels.get(pokemonLabel) ?? 0; | |
toRelease.set(badgeId, countRelease + count); | |
toReleaseLabels.set(pokemonLabel, countReleaseLabels + count); | |
} | |
for (let _ of Array(count).keys()) { | |
/* skip any legendary/mythical pokemon unless in Config.Release.ignoreLegendaryLabels */ | |
/* always keep all legendary/mythical shinies */ | |
if ( | |
['LEGENDARY', 'MYTHICAL'].includes(details.datastore.rarity) && | |
!shiny && | |
!Config.Release.ignoreLegendaryLabels.includes(pokemonLabel) | |
) { | |
continue; | |
} | |
let countSeen = pokemonSeen.get(pokemonLabel) ?? 0; | |
if ((shiny && releaseShinies(countSeen)) || (!shiny && releaseNonShinies(countSeen))) { | |
let countRelease = toRelease.get(badgeId) ?? 0; | |
let countReleaseLabels = toReleaseLabels.get(pokemonLabel) ?? 0; | |
toRelease.set(badgeId, countRelease + 1); | |
toReleaseLabels.set(pokemonLabel, countReleaseLabels + 1); | |
} else { | |
pokemonSeen.set(pokemonLabel, countSeen + 1); | |
} | |
} | |
} | |
info('Release', 'Releasing the following:'); | |
console.info(toReleaseLabels); | |
return Array.from(toRelease); | |
} | |
static async main() { | |
info('Release', 'Navigating to release...'); | |
let toRelease = this.getPokemonToRelease(); | |
if (toRelease.length) { | |
let res = await firebase.exec('release', {operations: toRelease}) | |
let items = res.data.itemMap; | |
let results = `Obtained ${items.pokeball} pokeballs` | |
if (items.greatball) { | |
results += `, ${items.greatball} greatballs` | |
} | |
if (items.ultraball) { | |
results += `, ${items.ultraball} ultraballs` | |
} | |
info('Release', results); | |
} | |
Schedule.nextRelease = Schedule.addMinutesToNow(60); | |
info('Release', `Finished releasing, next at ${fmtDateTime(Schedule.nextRelease)}`); | |
} | |
} | |
class Battle { | |
/* sends pokemon to battle in given stadium */ | |
static async main() { | |
info('Battle', 'Navigating to battle stadium...'); | |
let res = await firebase.exec('battle_stadium', { | |
species: Config.Battle.pokemon.map(longPokemon => Converters.pokemonLabelToBadgeId(longPokemon)), | |
heldItems: Config.Battle.items.map(longItem => Converters.itemLongToShort(longItem)), | |
tier: Config.Battle.type, | |
}) | |
info('Battle', res.data.prize ? `Victory! Won ${Converters.itemShortToLong(res.data.prize)}` : 'Loss :('); | |
Schedule.nextBattle = Schedule.addMinutesToNow(60); | |
info('Battle', `Finished battling, next at ${fmtDateTime(Schedule.nextBattle)}`); | |
} | |
} | |
class Daycare { | |
/* sends pokemon to daycare */ | |
static async main() { | |
info('Daycare', 'Navigating to daycare...'); | |
let res = await firebase.exec('daycare', { | |
species: Config.Daycare.pokemon.map(pokemon => Converters.pokemonLabelToBadgeId(pokemon)), | |
heldItem: Config.Daycare.item.map(longItem => Converters.itemLongToShort(longItem)), | |
isPrivate: Config.Daycare.isPrivate, | |
}) | |
info('Daycare', res.data.egg ? `Found ${Converters.pokemonShortToLabel(res.data.egg.species)} egg` : 'No egg :('); | |
Schedule.nextDaycare = Schedule.addMinutesToNow(Config.Daycare.isPrivate ? 30 : 60); | |
info('Daycare', `Finished daycare, next at ${fmtDateTime(Schedule.nextDaycare)}`); | |
} | |
} | |
class Hatch { | |
/* hatches any eggs that are ready */ | |
static async main() { | |
info('Hatch', 'Navigating to hatchery...'); | |
for (let egg of firebase.userData.eggs) { | |
if ( | |
(egg.laid && ((egg.laid * 1000) + (HOUR * 24 * 7)) <= now().getTime()) || | |
(egg.hatch && (egg.hatch * 1000) <= now().getTime()) | |
) { | |
await firebase.exec('hatch', {key: egg.species}) | |
.then(res => { | |
info('Hatch', `Hatched ${Converters.pokemonShortToLabel(res.data.species)}`); | |
}) | |
.catch(() => {}) | |
} | |
} | |
Schedule.nextHatch = Schedule.addMinutesToNow(60); | |
info('Hatch', `Finished hatching, next at ${fmtDateTime(Schedule.nextHatch)}`); | |
} | |
} | |
class Farm { | |
/* harvests ready plots, seeds with least owned berry, then fertilises */ | |
static numPlots = firebase.userData.berryPlots * 6; | |
static fertilizer = Converters.itemLongToShort(Config.Farm.fertilizer); | |
static fertilizerDueModifier = { | |
growthmulch: (n) => n - HOUR * 24, | |
dampmulch: (n) => n + HOUR * 24, | |
stablemulch: (n) => n, | |
gooeymulch: (n) => n, | |
amazemulch: (n) => n, | |
boostmulch: (n) => n - HOUR * 24, | |
richmulch: (n) => n, | |
surprisemulch: (n) => n, | |
honey: (n) => n, | |
apricorncompost: (n) => n - HOUR * 12, | |
featheredmulch: (n) => n + HOUR * 12, | |
classicmulch: (n) => n - HOUR * 24, | |
pokesnack: (n) => n, | |
berrymulch: (n) => n, | |
pokebeans: (n) => n - HOUR * 24, | |
curryfertilizer: (n) => n + HOUR * 3, | |
undefined: (n) => n, | |
} | |
static isPlotReady(plot) { | |
/* return whether a farm plot is ready for harvest */ | |
let keys = Object.keys(plot); | |
if (!keys.length) { | |
return false; | |
} | |
let berry, plantTime, fertilizer | |
if (keys.length === 1) { | |
berry = keys[0]; | |
plantTime = plot[berry]; | |
} else { | |
fertilizer = plot['fertilizer']; | |
if (keys[0] === 'fertilizer') { | |
berry = keys[1]; | |
plantTime = plot[berry]; | |
} else { | |
berry = keys[0]; | |
plantTime = plot[berry]; | |
} | |
} | |
let dueTimeNoFertiliser = plantTime + (HOUR * window.ITEMS[berry].growTime); | |
let dueTime = this.fertilizerDueModifier[fertilizer](dueTimeNoFertiliser); | |
return dueTime <= now(); | |
} | |
static async harvestBerries() { | |
/* harvest plots that are ready */ | |
for (let [plotIdx, plot] of firebase.userData.berryPlanted.entries()) { | |
if (!this.isPlotReady(plot)) { | |
continue; | |
} | |
await firebase.exec('berry_harvest', {index: plotIdx}) | |
.then(res => { | |
let message = ( | |
`Harvested ${res.data.berryYield[0]} ` + | |
`${Converters.itemShortToLong(res.data.berry[0])} from plot ${plotIdx}` | |
); | |
if (res.data.weed[0]) { | |
message += `, found ${Converters.itemShortToLong(res.data.weed[0])}`; | |
} | |
if (res.data.species[0]) { | |
message += `, found ${Converters.pokemonBadgeIdToLabel(res.data.species[0])}`; | |
} | |
info('Farm', message); | |
}) | |
.catch(() => {}) | |
} | |
} | |
static async getBerryToPlant() { | |
/* get the berry to plant - if we are not full, the one we own least of, otherwise the most valuable */ | |
let berryCounts = []; | |
let berrySellPrices = []; | |
await refreshUserdata(); | |
for (let [item, count] of Object.entries(getItemsByCategory('berry'))) { | |
/* don't plant any berries that we can buy from the mart */ | |
if (!window.ITEMS[item].buy) { | |
berryCounts.push([item, count]); | |
berrySellPrices.push([item, window.ITEMS[item].sell]); | |
} | |
} | |
/* sort by least count and highest sell price */ | |
berryCounts.sort(function(a, b){ return a[1]-b[1] }); | |
berrySellPrices.sort(function(a, b){ return b[1]-a[1] }); | |
return berryCounts[0][1] < Config.Mart.maxCategoryCounts['berry'] ? berryCounts[0][0] : berrySellPrices[0][0]; | |
} | |
static async plantBerries() { | |
/* plant berries in empty plots */ | |
let berries = firebase.userData.berryPlanted; | |
for (let plotIdx of Array(this.numPlots).keys()) { | |
if (berries[plotIdx] === undefined || !Object.keys(berries[plotIdx]).length) { | |
/* a plot is empty if it is either not returned at all, or returned as an empty object */ | |
let berry = await this.getBerryToPlant(); | |
let res = await firebase.exec('berry_plant', {berry: berry, index: plotIdx}); | |
info('Farm', `Planted ${res.data.berry} in plot ${plotIdx}`); | |
} | |
} | |
} | |
static async ensureFertilizerCount() { | |
/* make sure we have sufficient fertilizer for all plots - if not, buy from mart */ | |
let fertCount = getItemCount(this.fertilizer) ?? 0; | |
if (fertCount < this.numPlots) { | |
let res = await firebase.exec('exchange', {type: this.fertilizer, count: this.numPlots - fertCount}); | |
info('Farm', `Bought ${res.data.count} ${Converters.itemShortToLong(res.data.purchased)} for ${res.data.price}`); | |
} | |
} | |
static async fertilizeBerries() { | |
/* fertilize all plots that don't already have fertilizer */ | |
await this.ensureFertilizerCount(); | |
let berries = firebase.userData.berryPlanted; | |
for (let plotIdx of Array(this.numPlots).keys()) { | |
if (berries[plotIdx].fertilizer === undefined) { | |
let res = await firebase.exec('berry_fertilize', {fertilizer: [this.fertilizer], index: [plotIdx]}); | |
info('Farm', `Used ${Converters.itemShortToLong(res.data.dataFertilizer[0])} in plot ${plotIdx}`); | |
} | |
} | |
} | |
static async main() { | |
info('Farm', 'Navigating to farm...'); | |
await this.harvestBerries(); | |
await refreshUserdata(); | |
await this.plantBerries(); | |
await refreshUserdata(); | |
await this.fertilizeBerries(); | |
Schedule.nextFarm = Schedule.addMinutesToNow(60); | |
info('Farm', `Finished farm, next at ${fmtDateTime(Schedule.nextFarm)}`); | |
} | |
} | |
class Lottery { | |
/* claims lottery */ | |
static async main() { | |
info('Lottery', 'Navigating to lottery...'); | |
await firebase.exec('draw_lotto') | |
.then(res => { | |
info('Lottery', res.data.item ? `Won ${Converters.itemShortToLong(res.data.item)}` : 'No item :('); | |
}) | |
.catch(() => {}) | |
Schedule.nextLottery = Schedule.addMinutesToNow(60 * 12); | |
info('Lottery', `Finished lottery, next at ${fmtDateTime(Schedule.nextLottery)}`); | |
} | |
} | |
class ClaimRaids { | |
/* claims any pending raid rewards */ | |
static async main() { | |
info('ClaimRaids', 'Navigating to claim raids...'); | |
let raids = await firebase.exec('raid_list'); | |
for (let raid of raids.data) { | |
if (raid.reason === 'Claim prize') { | |
let res = await firebase.exec('raid_claim', {raidId: raid.id}); | |
let prizes = res.data.prizes.map(shortItem => Converters.itemShortToLong(shortItem)); | |
info('ClaimRaids', `Claiming from ${Converters.pokemonShortToLabel(raid.boss)}: won ${prizes.join(', ')}`); | |
} | |
} | |
Schedule.nextClaimRaids = Schedule.addMinutesToNow(60 * 12); | |
info('ClaimRaids', `Finished claiming raids, next at ${fmtDateTime(Schedule.nextClaimRaids)}`); | |
} | |
} | |
class Research { | |
/* hands in and discards any undesired research. always have a rare candy research going. */ | |
static isResearchComplete(key) { | |
/* return whether a research is complete */ | |
return key in this.currentResearch && this.currentResearch[key] >= window.ACTIVE_RESEARCH[key].steps | |
} | |
static async handInResearch() { | |
/* hand in rare candy research if prioritised and complete */ | |
if (Config.Research.prioritiseRareCandy && this.isResearchComplete('BURMY')) { | |
let res = await firebase.exec('research_claim', {researchId: 'BURMY'}); | |
this.currentResearch = res.data.researchCurrent; | |
info('Research', `Completed research, obtained ${Converters.itemShortToLong(res.data.prize)}`); | |
return true; | |
} | |
/* otherwise hand in any completed research */ | |
for (let key in this.currentResearch) { | |
if (this.isResearchComplete(key)) { | |
let res = await firebase.exec('research_claim', {researchId: key}); | |
this.currentResearch = res.data.researchCurrent; | |
info('Research', `Completed research, obtained ${Converters.itemShortToLong(res.data.prize)}`); | |
return true; | |
} | |
} | |
info('Research', 'No research to hand in :('); | |
} | |
static categoriseResearch() { | |
/* determine, out of available researches, which are desired and undesired */ | |
let unwantedResearch = new Map(); | |
let wantedResearch = new Map(); | |
for (let [key, research] of Object.entries(window.ACTIVE_RESEARCH)) { | |
let prize = research.prize[0]; | |
let category = window.ITEMS[prize].category; | |
let count = getItemCount(prize); | |
if ( | |
/* we don't want anything we can buy at the mart */ | |
window.ITEMS[prize].buy || | |
/* get rid of any manual exclusions */ | |
Config.Research.questsExcluded.includes(key) || | |
/* we don't want any moves, berries, megastones, or fertilizer */ | |
['tms', 'trs', 'berry', 'fertilizer', 'megastone'].includes(category) || | |
/* we dont want hold items if we have more of them than whatever the mart minimum is */ | |
(category === 'hold' && count >= Config.Mart.minCategoryCounts.hold) || | |
/* we dont want balls if we have more of them than whatever the mart minimum is */ | |
(category === 'balls' && count >= Config.Mart.minCategoryCounts.balls) || | |
/* we dont want battle items if we have more of them than whatever the mart minimum is */ | |
(category === 'battle' && count >= Config.Mart.minCategoryCounts.battle) || | |
/* we don't want standard treasure or premierballs */ | |
['prettywing', 'pearl', 'tinymushroom', 'shoalshell', 'premierball'].includes(prize) || | |
/* we don't want most usable items */ | |
['protein', 'iron', 'carbos', 'expcandys', 'expcandym', 'expcandyl'].includes(prize) || | |
/* we don't want other usable items if we have more than 50 of them, but we always want rare candy */ | |
(category === 'items' && count >= 50 && prize !== 'rarecandy') | |
) { | |
unwantedResearch.set(key, Converters.itemShortToLong(prize)); | |
} | |
else { | |
wantedResearch.set(key, Converters.itemShortToLong(prize)); | |
} | |
} | |
info('Research', 'Unwanted research:'); | |
console.info(unwantedResearch); | |
info('Research', 'Wanted Research:'); | |
console.info(wantedResearch); | |
this.unwantedResearchIds = [...unwantedResearch.keys()]; | |
} | |
static countUnwantedResearch() { | |
/* return count of unwanted researches */ | |
return Object.keys(this.currentResearch).filter( | |
k => this.unwantedResearchIds.includes(k) && !this.isResearchComplete(k) | |
).length; | |
} | |
static async changeUnwantedResearch() { | |
/* exchange an unwanted research for a new one */ | |
for (let key in this.currentResearch) { | |
if (this.unwantedResearchIds.includes(key) && !this.isResearchComplete(key)) { | |
let res = await firebase.exec('research_get', {key: key}); | |
this.currentResearch = res.data.researchCurrent; | |
return; | |
} | |
} | |
} | |
static countUnstartedResearch() { | |
/* return count of unstarted researches */ | |
return Object.values(this.currentResearch).filter(v => !v).length; | |
} | |
static async changeUnstartedResearch() { | |
/* exchange an unstarted research for a new one */ | |
for (let [key, count] of Object.entries(this.currentResearch)) { | |
if (!count) { | |
let res = await firebase.exec('research_get', {key: key}); | |
this.currentResearch = res.data.researchCurrent; | |
return; | |
} | |
} | |
} | |
static countUncompletedResearch() { | |
/* return count of uncompleted researches */ | |
return Object.keys(this.currentResearch).filter(k => !this.isResearchComplete(k)).length; | |
} | |
static async changeUncompletedResearch() { | |
/* exchange an uncompleted research for a new one */ | |
for (let key in this.currentResearch) { | |
if (!this.isResearchComplete(key)) { | |
let res = await firebase.exec('research_get', {key: key}); | |
this.currentResearch = res.data.researchCurrent; | |
return; | |
} | |
} | |
} | |
static async getDesiredResearch() { | |
/* cycle all researches until all are desired - one research must be rare candy */ | |
this.categoriseResearch(); | |
if (!('BURMY' in this.currentResearch)) { info('Research', 'Cycling for rare candy...'); } | |
while (!('BURMY' in this.currentResearch)) { | |
if (this.countUnwantedResearch()) { | |
info('Research', 'Changing unwanted research...'); | |
await this.changeUnwantedResearch(); | |
} | |
else if (this.countUnstartedResearch) { | |
info('Research', 'All research is wanted - falling back to changing unstarted research...'); | |
await this.changeUnstartedResearch(); | |
} | |
else if (this.countUncompletedResearch) { | |
info('Research', 'All research is wanted and started - falling back to changing uncompleted research...'); | |
await this.changeUncompletedResearch(); | |
} | |
await Wrapper.Battle() | |
await Wrapper.Daycare() | |
await Wrapper.Farm() | |
} | |
info('Research', 'Cycling unwanted research'); | |
while (this.countUnwantedResearch()) { | |
info('Research', 'Changing unwanted research...'); | |
await this.changeUnwantedResearch(); | |
} | |
} | |
static async main() { | |
info('Research', 'Navigating to research...'); | |
this.currentResearch = firebase.userData.researchCurrent; | |
let handedIn = await this.handInResearch(); | |
/* we set countdown here rather than at end because cycling to get desired research may take quite some time */ | |
/* if we handed research in, then set cooldown of 60 minutes, otherwise retry in 5 minutes */ | |
if (handedIn) { | |
Schedule.nextResearch = Schedule.addMinutesToNow(60); | |
await this.getDesiredResearch(); | |
} | |
else { | |
Schedule.nextResearch = Schedule.addMinutesToNow(5); | |
} | |
info('Research', `Finished research, next at ${fmtDateTime(Schedule.nextResearch)}`); | |
} | |
} | |
class Misc { | |
/* any other miscellaneous functions that don't need their own class */ | |
static async sootForFlutes() { | |
info('sootForFlutes', 'Trading soot for flutes...'); | |
while (getItemCount('soot') >= 400 && window.BAZAAR.hoennFloutist.isOpen(now()) === 0) { | |
let flutes = ['blueflute', 'redflute', 'yellowflute', 'whiteflute', 'blackflute']; | |
flutes.sort(function(a, b){ return getItemCount(a)-getItemCount(b) }); | |
let res = await firebase.exec('exchange_bazaar', {'type': flutes[0], 'count': 1, 'bazaarId': 'hoennFloutist'}); | |
info('sootForFlutes', `Bought a ${Converters.itemShortToLong(res.data.purchased)} for ${res.data.price} soot.`) | |
await refreshUserdata(); | |
} | |
Schedule.nextSootForFlutes = Schedule.addMinutesToNow(60); | |
info('sootForFlutes', `Finished trading soot for flutes, next at ${fmtDateTime(Schedule.nextSootForFlutes)}`); | |
} | |
static async wispsForSpiritomb() { | |
info('wispsForSpiritomb', 'Restoring Spiritombs...'); | |
while (getItemCount('wisp') >= 108) { | |
let res = await firebase.exec('use_item', {'item': 'oddkeystone'}); | |
info('wispsForSpiritomb', `Restored ${res.data.name1} for 108 wisps.`) | |
await refreshUserdata(); | |
} | |
Schedule.nextWispsForSpiritomb = Schedule.addMinutesToNow(60); | |
info('wispsForSpiritomb', `Finished restoring Spiritombs, next at ${fmtDateTime(Schedule.nextWispsForSpiritomb)}`); | |
} | |
static async zygardeCells() { | |
info('zygardeCells', 'Activating Zygarde cells...'); | |
while (getItemCount('zygardecell') >= Config.Misc.zygardeCount) { | |
let res = await firebase.exec('use_item', {'item': 'zygardecube'}); | |
info('zygardeCells', `Activated ${res.data.name1} for ${Config.Misc.zygardeCount} cells.`) | |
await refreshUserdata(); | |
} | |
Schedule.nextZygardeCells = Schedule.addMinutesToNow(60); | |
info('zygardeCells', `Finished activating Zygarde cells, next at ${fmtDateTime(Schedule.nextZygardeCells)}`); | |
} | |
static async activateFossils() { | |
info('activateFossils', 'Activating fossils...'); | |
for (let [item, count] of Object.entries(getItemsByCategory('fossil'))) { | |
for (let _ of Array(count).keys()) { | |
let res = await firebase.exec('use_item', {'item': item}); | |
info('activateFossils', `Activated ${res.data.name1} from ${Converters.itemShortToLong(item)}.`); | |
} | |
} | |
Schedule.nextActivateFossils = Schedule.addMinutesToNow(60); | |
info('activateFossils', `Finished activating fossils, next at ${fmtDateTime(Schedule.nextZygardeCells)}`); | |
} | |
} | |
class Bazaar { | |
/* functions to buy from the bazaar */ | |
static async buyTMs() { | |
/* buy TMs from Cynthia and Hayley */ | |
for (let bazaarId of ['moveTutorCynthia', 'moveTutorHayley']) { | |
let vendor = window.BAZAAR[bazaarId]; | |
if (vendor.isOpen(now()) === 0) { | |
for (let item of vendor.items) { | |
let ownedCount = getItemCount(item.name) | |
if (item.name.startsWith('tm-') && ownedCount < Config.Bazaar.tmCount) { | |
let buyCount = Config.Bazaar.tmCount - ownedCount; | |
let res = await firebase.exec('exchange_bazaar', { | |
'type': item.name, 'count': buyCount, 'bazaarId': bazaarId | |
}); | |
info( | |
'Bazaar', | |
`Bought ${buyCount} ${Converters.itemShortToLong(res.data.purchased)} for ` + | |
`${res.data.price} heartscales from ${vendor.name}.` | |
) | |
} | |
} | |
} | |
} | |
} | |
static async buyBalls() { | |
/* buy balls from Kurt and Arnie */ | |
for (let bazaarId of ['apricornDudeKurt', 'bugcatcher']) { | |
let vendor = window.BAZAAR[bazaarId]; | |
if (vendor.isOpen(now(), firebase.userData.items) === 0) { | |
for (let item of vendor.items) { | |
let count = getItemCount(item.name); | |
if (item.name.endsWith('ball') && count < vendor.maxItems) { | |
let buyCount = vendor.maxItems - count; | |
let res = await firebase.exec('exchange_bazaar', { | |
'type': item.name, 'count': buyCount, 'bazaarId': bazaarId | |
}); | |
info( | |
'Bazaar', | |
`Bought ${buyCount} ${Converters.itemShortToLong(res.data.purchased)} for ` + | |
`${res.data.price} pokeballs from ${vendor.name}.` | |
) | |
} | |
} | |
} | |
} | |
} | |
static async buyShellBells() { | |
/* buy shell bells from the Shell Merchants */ | |
for (let bazaarId of ['shellcollectorLow', 'shellcollectorHigh']) { | |
let vendor = window.BAZAAR[bazaarId]; | |
if (vendor.isOpen(now()) === 0) { | |
for (let item of vendor.items) { | |
if (getItemCount(vendor.currency) >= item.rate) { | |
let buyCount = Math.floor(getItemCount(vendor.currency) / item.rate); | |
let res = await firebase.exec('exchange_bazaar', { | |
'type': item.name, 'count': buyCount, 'bazaarId': bazaarId | |
}); | |
info( | |
'Bazaar', | |
`Bought ${buyCount} ${Converters.itemShortToLong(res.data.purchased)} for ` + | |
`${res.data.price} ${Converters.itemShortToLong(vendor.currency)} from ${vendor.name}.` | |
) | |
} | |
} | |
} | |
} | |
} | |
static async buyVitamins() { | |
/* buy vitamins from the vitamin clerk */ | |
let vendor = window.BAZAAR['vitaminClerk']; | |
if (vendor.isOpen(now(), firebase.userData.items) === 0) { | |
for (let item of vendor.items) { | |
let count = getItemCount(item.name); | |
if (count < vendor.maxItems) { | |
let buyCount = vendor.maxItems - count; | |
let res = await firebase.exec('exchange_bazaar', { | |
'type': item.name, 'count': buyCount, 'bazaarId': 'vitaminClerk' | |
}); | |
info( | |
'Bazaar', | |
`Bought ${buyCount} ${Converters.itemShortToLong(res.data.purchased)} for ` + | |
`${res.data.price} ${Converters.itemShortToLong(vendor.currency)} from ${vendor.name}.` | |
) | |
} | |
} | |
} | |
} | |
static async buyIncense() { | |
/* buy incense from from isaac and amber */ | |
for (let bazaarId of ['incenseClerk', 'incenseClerkDppt']) { | |
let vendor = window.BAZAAR[bazaarId]; | |
if (vendor.isOpen(now(), firebase.userData.items) === 0) { | |
for (let item of vendor.items) { | |
let count = getItemCount(item.name); | |
if (count < vendor.maxItems) { | |
let buyCount = vendor.maxItems - count; | |
let res = await firebase.exec('exchange_bazaar', { | |
'type': item.name, 'count': buyCount, 'bazaarId': bazaarId | |
}); | |
info( | |
'Bazaar', | |
`Bought ${buyCount} ${Converters.itemShortToLong(res.data.purchased)} for ` + | |
`${res.data.price} ${Converters.itemShortToLong(vendor.currency)} from ${vendor.name}.` | |
) | |
} | |
} | |
} | |
} | |
} | |
static async main() { | |
info('Bazaar', 'Visiting the Bazaar...'); | |
if (Config.Bazaar.buyTMs) { | |
await this.buyTMs(); | |
} | |
if (Config.Bazaar.buyBalls) { | |
await this.buyBalls(); | |
} | |
if (Config.Bazaar.buyShellBells) { | |
await this.buyShellBells(); | |
} | |
if (Config.Bazaar.buyVitamins) { | |
await this.buyVitamins(); | |
} | |
if (Config.Bazaar.buyIncense) { | |
await this.buyIncense(); | |
} | |
Schedule.nextBazaar = Schedule.addMinutesToNow(60); | |
info('Bazaar', `Finished with the Bazaar, next visit at ${fmtDateTime(Schedule.nextBazaar)}`); | |
} | |
} | |
if (Config.Misc.withdrawBankOnStart) { | |
await Bank.getAllPokemonFromBank(); | |
} | |
await Schedule.setup(); | |
while (true) { | |
await Wrapper.Locations() | |
await Wrapper.Mart() | |
await Wrapper.Release() | |
await Wrapper.Battle() | |
await Wrapper.Daycare() | |
await Wrapper.Farm() | |
await Wrapper.Hatch() | |
await Wrapper.Lottery() | |
await Wrapper.ClaimRaids() | |
await Wrapper.Research() | |
await Wrapper.SootForFlutes() | |
await Wrapper.WispsForSpiritomb() | |
await Wrapper.ZygardeCells() | |
await Wrapper.ActivateFossils() | |
await Wrapper.Bazaar() | |
await Wrapper.ComprehensiveCatch() | |
await Wrapper.UnownReportCatch() | |
await Wrapper.RepeatBallCatch() | |
await Wrapper.Catch() | |
// return; | |
} | |
} | |
await play(); | |
// helpful copypastes: ✨ ¹ ² ³ ⁴ ♀ ♂ |
This file contains 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
async function move_tutor() { | |
/* disable log and debug, used by app and spams console */ | |
console.log = function () {}; | |
console.debug = function () {}; | |
console.clear(); | |
class Converters { | |
/* converts from one representation of pokemon/item to another */ | |
static badgeIdToLabel(badgeId) { | |
/* convert a pokemon badge ID to a long label, e.g. '6s#f00G' -> 'Burmy Plant³ ♀' */ | |
return new Badge(badgeId).toLabel(); | |
} | |
static badgeIdToNonVariantLabel(badgeId) { | |
/* custom label by species, shininess, form & gender excluding variant, e.g. '6s#f00G'->'Burmyfalseplantfemale' */ | |
/* we can't use toLabel() as that also distinguishes by variant, which defeats the point here */ | |
let pokemon = new Badge(badgeId); | |
let personality = pokemon.personality; | |
let datastore = window.datastoreGet(pokemon.toDataStr()); | |
return datastore.species + personality.shiny + (personality.form ?? '') + (personality.gender ?? ''); | |
} | |
} | |
function getVariantCounts() { | |
/* return map of pokemon to owned variants */ | |
let pokemonLabelsSeen = new Set(); | |
let pokemonVariants = new Map(); | |
for (let badgeId of Object.keys(firebase.userData.pokemon)) { | |
let pokemon = new Badge(badgeId); | |
let pokemonLabel = Converters.badgeIdToLabel(badgeId); | |
let personality = pokemon.personality; | |
let nonVariantLabel = Converters.badgeIdToNonVariantLabel(badgeId); | |
/* skip if we have already seen this pokemon (including variant) */ | |
if (pokemonLabelsSeen.has(pokemonLabel)) { | |
continue; | |
} else { | |
pokemonLabelsSeen.add(pokemonLabel); | |
} | |
let variants = pokemonVariants.get(nonVariantLabel) ?? new Set(); | |
variants.add(personality.variant); | |
pokemonVariants.set(nonVariantLabel, variants); | |
} | |
return pokemonVariants; | |
} | |
function getPokemonCounts() { | |
/* return map of pokemon label to count */ | |
let pokemonCounts = new Map(); | |
for (let [badgeId, count] of Object.entries(firebase.userData.pokemon)) { | |
let pokemonLabel = Converters.badgeIdToLabel(badgeId); | |
let existingCount = pokemonCounts.get(pokemonLabel) ?? 0; | |
pokemonCounts.set(pokemonLabel, existingCount + count); | |
} | |
return pokemonCounts; | |
} | |
function getSortedPokemon() { | |
/* return array of owned pokemon sorted by shininess and ID */ | |
let ownedPokemon = []; | |
for (let badgeId of Object.keys(firebase.userData.pokemon)) { | |
let pokemon = new Badge(badgeId); | |
ownedPokemon.push([badgeId, pokemon.id, pokemon.personality.shiny]); | |
} | |
/* sort by non-shiny -> shiny, then ascending ID */ | |
ownedPokemon.sort(function(a, b){ return a[2]-b[2] }); | |
ownedPokemon.sort(function(a, b){ return a[1]-b[1] }); | |
return ownedPokemon.map(x => x[0]); | |
} | |
let pokemonCounts = getPokemonCounts(); | |
let variantCounts = getVariantCounts(); | |
let labelsTutored = new Set(); | |
for (let badgeId of getSortedPokemon()) { | |
let pokemon = new Badge(badgeId); | |
let pokemonLabel = Converters.badgeIdToLabel(badgeId); | |
let datastore = window.datastoreGet(pokemon.toDataStr()); | |
/* skip if we have already tutored, is a variant, has no variants, does not have var 3, or already has var 3 */ | |
if ( | |
labelsTutored.has(pokemonLabel) || pokemon.personality.variant !== undefined || | |
datastore.novelMoves === undefined || datastore.novelMoves.length < 4 || | |
variantCounts.get(Converters.badgeIdToNonVariantLabel(badgeId)).has(3) | |
) { | |
/* pass */ | |
} else if (pokemonCounts.get(pokemonLabel) > 1) { | |
await firebase.exec('move_tutor', {'species': badgeId, 'tutorId': 3}) | |
.then(res => { | |
console.info(`Tutored ${pokemonLabel} to var 3.`); | |
labelsTutored.add(pokemonLabel); | |
}) | |
.catch(err => {console.error(err)}) | |
} | |
} | |
console.info('Done tutoring.'); | |
} | |
await move_tutor(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment