`;
newSetting.querySelector(`#checkbox-${name}`).checked = isChecked;
});
settingsBody.append(...settingsToAdd);
document.getElementById('auto-gym-select').value = this.autoGymSelect;
document.getElementById('auto-click-start').addEventListener('click', () => { EnhancedAutoClicker.toggleAutoClick(); });
document.getElementById('auto-click-rate').addEventListener('change', (event) => { EnhancedAutoClicker.changeClickMultiplier(event); });
document.getElementById('auto-gym-start').addEventListener('click', () => { EnhancedAutoClicker.toggleAutoGym(); });
document.getElementById('auto-gym-select').addEventListener('change', (event) => { EnhancedAutoClicker.changeSelectedGym(event); });
document.getElementById('auto-dungeon-start').addEventListener('click', () => { EnhancedAutoClicker.toggleAutoDungeon(true); });
document.getElementById('auto-dungeon-encounter-mode').addEventListener('click', () => { EnhancedAutoClicker.toggleAutoDungeonEncounterMode(); });
document.getElementById('auto-dungeon-chest-mode').addEventListener('click', () => { EnhancedAutoClicker.toggleAutoDungeonChestMode(); });
document.getElementById('checkbox-autoDungeonFinishBeforeStopping').addEventListener('change', () => { EnhancedAutoClicker.toggleAutoDungeonFinishBeforeStopping(); });
document.getElementById('checkbox-autoDungeonAlwaysOpenRareChests').addEventListener('change', () => { EnhancedAutoClicker.toggleAutoDungeonAlwaysOpenRareChests(); });
document.getElementById('select-autoClickCalcEfficiencyDisplayMode').addEventListener('change', (event) => { EnhancedAutoClicker.changeCalcEfficiencyDisplayMode(event); });
document.getElementById('select-autoClickCalcDamageDisplayMode').addEventListener('change', (event) => { EnhancedAutoClicker.changeCalcDamageDisplayMode(event); });
document.querySelectorAll('#auto-dungeon-loottier-dropdown > div').forEach((elem) => {
elem.addEventListener('click', () => { EnhancedAutoClicker.changeAutoDungeonLootTier(elem.getAttribute('value')); });
});
addGlobalStyle('#auto-click-info { display: flex; flex-direction: row; justify-content: center; }');
addGlobalStyle('#auto-click-info > div { width: 33.3%; }');
addGlobalStyle('#click-rate-cont { display: flex; flex-direction: column; align-items: stretch; }');
addGlobalStyle('#auto-dungeon-loottier-dropdown img { max-height: 30px; width: auto; }');
if (this.autoClickState()) {
this.toggleAutoClickerLoop();
}
}
/**
* Add extra Knockout data bindings to optionally disable (most) gym and dungeon graphics
*/
static addGraphicsBindings() {
// Add gymView data bindings
var gymContainer = document.querySelector('div[data-bind="if: App.game.gameState === GameConstants.GameState.gym"]');
// Always hide stop button during autoGym, even with graphics enabled
var restartButton = gymContainer.querySelector('button[data-bind="visible: GymRunner.autoRestart()"]');
restartButton.setAttribute('data-bind', 'visible: GymRunner.autoRestart() && !EnhancedAutoClicker.autoGymOn()');
}
/* Settings event handlers */
static toggleAutoClick() {
const element = document.getElementById('auto-click-start');
this.autoClickState(!this.autoClickState());
localStorage.setItem('autoClickState', this.autoClickState());
this.autoClickState() ? element.classList.replace('btn-danger', 'btn-success') : element.classList.replace('btn-success', 'btn-danger');
if (!this.autoClickState()) {
if (this.autoGymState()) {
this.toggleAutoGym();
}
if (this.autoDungeonState()) {
this.toggleAutoDungeon();
}
}
this.toggleAutoClickerLoop();
}
static changeClickMultiplier(event) {
// TODO decide on a better range / function
const multiplier = +event.target.value;
if (Number.isInteger(multiplier) && multiplier > 0) {
this.autoClickMultiplier = multiplier;
localStorage.setItem("autoClickMultiplier", this.autoClickMultiplier);
var displayNum = (this.ticksPerSecond * this.autoClickMultiplier).toLocaleString('en-US', {maximumFractionDigits: 2});
document.getElementById('auto-click-rate-info').innerText = `Click Attack Rate: ${displayNum}/s`;
this.toggleAutoClickerLoop();
}
}
static toggleAutoGym() {
const element = document.getElementById('auto-gym-start');
const newState = !this.autoGymState();
if (newState && !this.canStartAutoGym()) {
// Don't turn on if there's no gym here
return;
} else if (newState && this.autoDungeonState()) {
// Only one mode may be active at a time
return;
}
this.autoGymState(newState);
localStorage.setItem('autoGymState', this.autoGymState());
this.autoGymState() ? element.classList.replace('btn-danger', 'btn-success') : element.classList.replace('btn-success', 'btn-danger');
element.textContent = `Auto Gym [${this.autoGymState() ? 'ON' : 'OFF'}]`;
if (this.autoGymState()) {
if (!this.autoClickState()) {
// Turn on the auto clicker if necessary
this.toggleAutoClick();
}
} else {
// Assume we can't reach this point with only the built-in auto restart running, so it's safe to stop it
GymRunner.autoRestart(false);
}
}
static changeSelectedGym(event) {
const val = +event.target.value;
if ([0, 1, 2, 3, 4].includes(val)) {
this.autoGymSelect = val;
localStorage.setItem("autoGymSelect", this.autoGymSelect);
// In case currently fighting a gym
if (this.autoClickState() && this.autoGymState()) {
// Only break out of this script's auto restart, not the built-in one
GymRunner.autoRestart(false);
}
}
}
static toggleAutoDungeon(allowSlowStop = false) {
const element = document.getElementById('auto-dungeon-start');
let newState = !this.autoDungeonState();
if (DungeonGuides.hired()) {
// Auto Dungeon can't run alongside dungeon guides, force stop
newState = false;
allowSlowStop = false;
Notifier.notify({
type: NotificationConstants.NotificationOption.warning,
title: 'Enhanced Auto Clicker',
message: `Auto Dungeon mode is not compatible with Dungeon Guides.`,
timeout: GameConstants.SECOND * 15,
});
} else if (newState && !this.canStartAutoDungeon()) {
// Don't turn on if there's no dungeon here
return;
} else if (newState && this.autoGymState()) {
// Only one mode may be active at a time
return;
}
localStorage.setItem('autoDungeonState', newState);
if (allowSlowStop && this.autoDungeonFinishBeforeStopping && App.game.gameState === GameConstants.GameState.dungeon &&
(!newState && !this.autoDungeonTracker.stopAfterFinishing)) {
// Instead of stopping immediately, wait and exit after beating this dungeon
this.autoDungeonTracker.stopAfterFinishing = true;
} else {
this.autoDungeonState(newState);
this.autoDungeonTracker.stopAfterFinishing = false;
}
element.classList.remove('btn-success', 'btn-danger', 'btn-warning');
element.classList.add(this.autoDungeonTracker.stopAfterFinishing ? 'btn-warning' : (newState ? 'btn-success' : 'btn-danger'));
element.textContent = `Auto Dungeon [${newState ? 'ON' : 'OFF'}]`;
if (newState) {
// Trigger a dungeon scan
this.autoDungeonTracker.ID = -1;
if (!this.autoClickState()) {
// Turn on the auto clicker if necessary
this.toggleAutoClick();
}
}
}
static toggleAutoDungeonEncounterMode() {
this.autoDungeonEncounterMode = !this.autoDungeonEncounterMode;
$('#auto-dungeon-encounter-mode img').css('filter', `${this.autoDungeonEncounterMode ? '' : 'grayscale(100%)' }`);
localStorage.setItem('autoDungeonEncounterMode', this.autoDungeonEncounterMode);
this.autoDungeonTracker.coords = null;
}
static toggleAutoDungeonChestMode() {
this.autoDungeonChestMode = !this.autoDungeonChestMode;
$('#auto-dungeon-chest-mode img').css('filter', `${this.autoDungeonChestMode ? '' : 'grayscale(100%)' }`);
localStorage.setItem('autoDungeonChestMode', this.autoDungeonChestMode);
this.autoDungeonTracker.coords = null;
}
static changeAutoDungeonLootTier(tier) {
const val = +tier;
if ([...Object.keys(baseLootTierChance).keys()].includes(val)) {
this.autoDungeonLootTier = val;
document.getElementById('auto-dungeon-loottier-img').setAttribute('src', `assets/images/dungeons/chest-${Object.keys(baseLootTierChance)[val]}.png`);
localStorage.setItem("autoDungeonLootTier", this.autoDungeonLootTier);
}
}
static toggleAutoDungeonFinishBeforeStopping() {
this.autoDungeonFinishBeforeStopping = !this.autoDungeonFinishBeforeStopping;
if (!this.autoDungeonFinishBeforeStopping && this.autoDungeonTracker.stopAfterFinishing) {
this.toggleAutoDungeon();
}
localStorage.setItem('autoDungeonFinishBeforeStopping', this.autoDungeonFinishBeforeStopping);
}
static toggleAutoDungeonAlwaysOpenRareChests() {
this.autoDungeonAlwaysOpenRareChests = !this.autoDungeonAlwaysOpenRareChests;
localStorage.setItem('autoDungeonAlwaysOpenRareChests', this.autoDungeonAlwaysOpenRareChests);
}
static changeCalcEfficiencyDisplayMode(event) {
const val = +event.target.value;
if (val != this.autoClickCalcEfficiencyDisplayMode && [0, 1].includes(val)) {
this.autoClickCalcEfficiencyDisplayMode = val;
localStorage.setItem('autoClickCalcEfficiencyDisplayMode', this.autoClickCalcEfficiencyDisplayMode);
this.resetCalculator();
}
}
static changeCalcDamageDisplayMode(event) {
const val = +event.target.value;
if (val != this.autoClickCalcDamageDisplayMode && [0, 1].includes(val)) {
this.autoClickCalcDamageDisplayMode = val;
localStorage.setItem('autoClickCalcDamageDisplayMode', this.autoClickCalcDamageDisplayMode);
this.resetCalculator();
}
}
/* Auto Clicker */
/**
* Resets and, if enabled, restarts autoclicker
* -While enabled, runs times per second in active battle
*/
static toggleAutoClickerLoop() {
var delay = Math.ceil(1000 / this.ticksPerSecond);
clearInterval(this.autoClickerLoop);
// Restart stats calculator
this.resetCalculator();
// Only use click multiplier while autoclicking
this.overrideClickAttack(this.autoClickState() ? this.autoClickMultiplier : 1);
if (this.autoClickState()) {
// Start autoclicker loop
this.autoClickerLoop = setInterval(() => EnhancedAutoClicker.autoClicker(), delay);
} else {
if (this.autoGymState()) {
GymRunner.autoRestart(false);
}
}
}
/**
* One tick of the autoclicker
* -Performs click attacks while in an active battle
* -Outside of battle, runs Auto Dungeon and Auto Gym
*/
static autoClicker() {
// Click while in a normal battle
if (App.game.gameState === GameConstants.GameState.fighting) {
Battle.clickAttack(this.autoClickMultiplier);
}
// ...or gym battle
else if (App.game.gameState === GameConstants.GameState.gym) {
GymBattle.clickAttack(this.autoClickMultiplier);
}
// ...or dungeon battle
else if (App.game.gameState === GameConstants.GameState.dungeon && DungeonRunner.fighting()) {
DungeonBattle.clickAttack(this.autoClickMultiplier);
}
// ...or temporary battle
else if (App.game.gameState === GameConstants.GameState.temporaryBattle) {
TemporaryBattleBattle.clickAttack(this.autoClickMultiplier);
}
// If not battling, progress through dungeon
else if (this.autoDungeonState()) {
this.autoDungeon();
}
// If not battling gym, start battling
else if (this.autoGymState()) {
this.autoGym();
}
// Turn off autoclicker in certain game states to avoid lag
else if (App.game.gameState === GameConstants.GameState.battleFrontier || App.game.gameState === GameConstants.GameState.safari) {
this.toggleAutoClick();
}
this.autoClickCalcTracker.ticks[0]++;
}
/**
* Override the game's function for Click Attack to:
* - make multiple clicks at once via multiplier
* - support changing the attack speed cap for higher tick speeds
*/
static overrideClickAttack(clickMultiplier = 1) {
// Set delay based on the autoclicker's tick rate
// (lower to give setInterval some wiggle room)
const delay = Math.min(Math.ceil(1000 / this.ticksPerSecond) - 10, 50);
Battle.clickAttack = function () {
// click attacks disabled and we already beat the starter
if (App.game.challenges.list.disableClickAttack.active() && player.regionStarters[GameConstants.Region.kanto]() != GameConstants.Starter.None) {
return;
}
const now = Date.now();
if (this.lastClickAttack > now - delay) {
return;
}
if (!this.enemyPokemon()?.isAlive()) {
return;
}
this.lastClickAttack = now;
// Don't autoclick more than needed for lethal
const clickDamage = App.game.party.calculateClickAttack(true);
var clicks = Math.min(clickMultiplier, Math.ceil(this.enemyPokemon().health() / clickDamage));
GameHelper.incrementObservable(App.game.statistics.clickAttacks, clicks);
this.enemyPokemon().damage(clickDamage * clicks);
if (!this.enemyPokemon().isAlive()) {
this.defeatPokemon();
}
}
}
/* Auto Gym */
/**
* Starts selected gym with auto restart enabled
*/
static autoGym() {
if (this.canStartAutoGym()) {
// Find all unlocked gyms in the current town
var gymList = player.town.content.filter((c) => (c.constructor.name == "Gym" && c.isUnlocked()));
if (gymList.length > 0) {
var gymIndex = Math.min(this.autoGymSelect, gymList.length - 1);
// Start in auto restart mode
GymRunner.startGym(gymList[gymIndex], true);
return;
}
}
// Disable if we aren't in a location with unlocked gyms
this.toggleAutoGym();
}
static canStartAutoGym() {
return App.game.gameState === GameConstants.GameState.gym || (App.game.gameState === GameConstants.GameState.town &&
player.town.content.some((c) => c.constructor.name == "Gym" && c.isUnlocked())
);
}
/**
* Override GymRunner built-in functions:
* -Add auto gym equivalent of gymWon() to save on performance by not loading town between
*/
static overrideGymRunner() {
GymRunner.gymWonNormal = GymRunner.gymWon;
// Version with free auto restart
GymRunner.gymWonAuto = function(gym) {
if (GymRunner.running()) {
GymRunner.running(false);
// First time defeating this gym
if (!App.game.badgeCase.hasBadge(gym.badgeReward)) {
gym.firstWinReward();
}
GameHelper.incrementObservable(App.game.statistics.gymsDefeated[GameConstants.getGymIndex(gym.town)]);
// Award money for defeating gym as we're auto clicking
App.game.wallet.gainMoney(gym.moneyReward);
if (GymRunner.autoRestart()) {
// Unlike the original function, autoclicker doesn't charge the player money
GymRunner.startGym(GymRunner.gymObservable(), GymRunner.autoRestart(), false);
return;
}
// Send the player back to the town they were in
player.town = gym.parent;
App.game.gameState = GameConstants.GameState.town;
}
}
// Only use our version when auto gym is running
GymRunner.gymWon = function(...args) {
if (EnhancedAutoClicker.autoClickState() && EnhancedAutoClicker.autoGymState()) {
GymRunner.gymWonAuto(...args);
} else {
GymRunner.gymWonNormal(...args);
}
}
}
/* Auto Dungeon */
/**
* Automatically begins and progresses through dungeons with multiple pathfinding options
*/
static autoDungeon() { // TODO more thoroughly test switching between modes and enabling/disabling within a dungeon
// Progress through dungeon
if (App.game.gameState === GameConstants.GameState.dungeon) {
if (DungeonRunner.fighting() || DungeonBattle.catching()) {
// Can't do anything while in a battle
return;
}
// Scan each new dungeon floor
if (this.autoDungeonTracker.ID !== DungeonRunner.dungeonID || this.autoDungeonTracker.floor !== DungeonRunner.map.playerPosition().floor) {
this.scanDungeon();
}
// If boss has been defeated, wait for the dungeon restart (delayed to allow quest updates)
if (this.autoDungeonTracker.dungeonFinished) {
return;
}
// Reset pathfinding coordinates to entrance
if (this.autoDungeonTracker.coords == null) {
this.autoDungeonTracker.coords = new Point(Math.floor(this.autoDungeonTracker.floorSize / 2), this.autoDungeonTracker.floorSize - 1, this.autoDungeonTracker.floor);
}
const floorMap = DungeonRunner.map.board()[this.autoDungeonTracker.floor];
// All targets visible, fight enemies / open chests then finish floor
if (floorMap[this.autoDungeonTracker.bossCoords.y][this.autoDungeonTracker.bossCoords.x].isVisible &&
!((this.autoDungeonChestMode || this.autoDungeonEncounterMode) && !this.autoDungeonTracker.floorExplored)) {
this.clearDungeon();
}
// Explore dungeon to reveal boss + any target tiles
else {
this.exploreDungeon();
}
}
// Begin dungeon
else if (this.canStartAutoDungeon()) {
// Enter dungeon if unlocked and affordable
DungeonRunner.initializeDungeon(player.town.dungeon);
} else {
// Disable if locked, can't afford entry cost, or there's no dungeon here
this.toggleAutoDungeon();
}
}
static canStartAutoDungeon() {
if (!(App.game.gameState === GameConstants.GameState.dungeon || (App.game.gameState === GameConstants.GameState.town && player.town instanceof DungeonTown))) {
return false;
}
if (DungeonGuides.hired()) {
return false;
}
const dungeon = player.town.dungeon;
return dungeon?.isUnlocked() && App.game.wallet.hasAmount(new Amount(dungeon.tokenCost, GameConstants.Currency.dungeonToken));
}
/**
* Scans current dungeon floor for relevant locations and pathfinding data
*/
static scanDungeon() {
// Reset / update tracker values
this.autoDungeonTracker.ID = DungeonRunner.dungeonID;
this.autoDungeonTracker.floor = DungeonRunner.map.playerPosition().floor;
this.autoDungeonTracker.floorSize = DungeonRunner.map.floorSizes[DungeonRunner.map.playerPosition().floor];
this.autoDungeonTracker.encounterCoords = [];
this.autoDungeonTracker.chestCoords = [];
this.autoDungeonTracker.coords = null;
this.autoDungeonTracker.targetCoords = null;
this.autoDungeonTracker.floorExplored = false;
this.autoDungeonTracker.floorFinished = false;
this.autoDungeonTracker.dungeonFinished = false;
// Scan for chest and boss coordinates
var dungeonBoard = DungeonRunner.map.board()[this.autoDungeonTracker.floor];
for (var y = 0; y < dungeonBoard.length; y++) {
for (var x = 0; x < dungeonBoard[y].length; x++) {
let tile = dungeonBoard[y][x];
if (tile.type() == GameConstants.DungeonTileType.enemy) {
this.autoDungeonTracker.encounterCoords.push(new Point(x, y, this.autoDungeonTracker.floor));
} else if (tile.type() == GameConstants.DungeonTileType.chest) {
let lootTier = Object.keys(baseLootTierChance).indexOf(tile.metadata.tier);
this.autoDungeonTracker.chestCoords.push({'xy': new Point(x, y, this.autoDungeonTracker.floor), 'tier': lootTier});
} else if (tile.type() == GameConstants.DungeonTileType.boss || tile.type() == GameConstants.DungeonTileType.ladder) {
this.autoDungeonTracker.bossCoords = new Point(x, y, this.autoDungeonTracker.floor);
}
}
}
// Sort chests by descending rarity
this.autoDungeonTracker.chestCoords.sort((a, b) => b.tier - a.tier);
// TODO find a more future-proof way to get flash distance
this.autoDungeonTracker.flashTier = DungeonFlash.tiers.findIndex(tier => tier === DungeonRunner.map.flash);
this.autoDungeonTracker.flashCols = [];
let flashRadius = DungeonRunner.map.flash?.playerOffset[0] ?? 0;
// Calculate minimum columns to fully reveal dungeon with Flash
if (flashRadius > 0) {
let cols = new Set();
cols.add(flashRadius);
let i = this.autoDungeonTracker.floorSize - flashRadius - 1;
while (i > flashRadius) {
cols.add(i);
i -= flashRadius * 2 + 1;
}
this.autoDungeonTracker.flashCols = [...cols].sort();
}
}
/**
* Explores dungeon to reveal tiles, skipping columns that can be efficiently revealed by Flash
*/
static exploreDungeon() {
const dungeonBoard = DungeonRunner.map.board()[this.autoDungeonTracker.floor];
var hasMoved = false;
var stuckInLoopCounter = 0;
while (!hasMoved) {
// End of column
if (this.autoDungeonTracker.coords.y == 0) {
if (this.autoDungeonTracker.coords.x <= 0 || this.autoDungeonTracker.coords.x === this.autoDungeonTracker.flashCols[0]) {
// Done exploring, clearDungeon() will take over from here
this.autoDungeonTracker.floorExplored = true;
return;
}
// Move to start of next column
this.autoDungeonTracker.coords.y = this.autoDungeonTracker.floorSize - 1;
if (this.autoDungeonTracker.coords.x >= this.autoDungeonTracker.floorSize - 1 || this.autoDungeonTracker.coords.x === this.autoDungeonTracker.flashCols.at(-1)) {
// Done with this side, move to other side of the entrance
this.autoDungeonTracker.coords.x = Math.floor(this.autoDungeonTracker.floorSize / 2) - 1;
} else {
// Move away from the entrance
this.autoDungeonTracker.coords.x += (this.autoDungeonTracker.coords.x >= Math.floor(this.autoDungeonTracker.floorSize / 2) ? 1 : -1);
}
// Dungeon has Flash unlocked, skip columns not in optimal flash pathing
} else if (this.autoDungeonTracker.flashTier > -1
&& this.autoDungeonTracker.coords.y == (this.autoDungeonTracker.floorSize - 1)
&& !this.autoDungeonTracker.flashCols.includes(this.autoDungeonTracker.coords.x)) {
// Move one column further from the entrance
this.autoDungeonTracker.coords.x += (this.autoDungeonTracker.coords.x >= Math.floor(this.autoDungeonTracker.floorSize / 2) ? 1 : -1);
}
// Move through current column
else {
this.autoDungeonTracker.coords.y -= 1;
}
// One move per tick to look more natural
if (!dungeonBoard[this.autoDungeonTracker.coords.y][this.autoDungeonTracker.coords.x].isVisited) {
DungeonRunner.map.moveToCoordinates(this.autoDungeonTracker.coords.x, this.autoDungeonTracker.coords.y);
hasMoved = true;
}
stuckInLoopCounter++;
if (stuckInLoopCounter > 100) {
console.warn(`Auto Dungeon got stuck in a loop while moving to tile \'${GameConstants.DungeonTileType[dungeonBoard[this.autoDungeonTracker.targetCoords.y][this.autoDungeonTracker.targetCoords.x].type()]}\' (${this.autoDungeonTracker.targetCoords.x}, ${this.autoDungeonTracker.targetCoords.y})`);
this.toggleAutoDungeon();
return;
}
}
}
/**
* Clears dungeon, visiting all desired tile types. Assumes dungeon has already been explored to reveal desired tiles.
*/
static clearDungeon() {
const dungeonBoard = DungeonRunner.map.board()[this.autoDungeonTracker.floor];
var hasMoved = false;
var stuckInLoopCounter = 0;
while (!hasMoved) {
// Choose a tile to move towards
if (!this.autoDungeonTracker.targetCoords) {
this.autoDungeonTracker.targetCoords = this.chooseDungeonTargetTile();
}
this.autoDungeonTracker.coords = this.pathfindTowardDungeonTarget();
if (!this.autoDungeonTracker.coords) {
console.warn(`Auto Dungeon could not find path to target tile \'${GameConstants.DungeonTileType[dungeonBoard[this.autoDungeonTracker.targetCoords.y][this.autoDungeonTracker.targetCoords.x].type()]}\' (${this.autoDungeonTracker.targetCoords.x}, ${this.autoDungeonTracker.targetCoords.y})`);
this.toggleAutoDungeon();
return;
}
// One move per tick to look more natural
if (!(dungeonBoard[this.autoDungeonTracker.coords.y][this.autoDungeonTracker.coords.x] === DungeonRunner.map.currentTile())) {
DungeonRunner.map.moveToCoordinates(this.autoDungeonTracker.coords.x, this.autoDungeonTracker.coords.y);
hasMoved = true;
}
// Target tile reached
if (this.autoDungeonTracker.coords.x === this.autoDungeonTracker.targetCoords.x && this.autoDungeonTracker.coords.y === this.autoDungeonTracker.targetCoords.y) {
this.autoDungeonTracker.targetCoords = null;
hasMoved = true;
// Take corresponding action
const tileType = DungeonRunner.map.currentTile().type();
if (tileType === GameConstants.DungeonTileType.enemy) {
// Do nothing, fights begin automatically
} else if (tileType === GameConstants.DungeonTileType.chest) {
DungeonRunner.openChest();
} else if (tileType === GameConstants.DungeonTileType.boss) {
if (this.autoDungeonTracker.floorFinished) {
DungeonRunner.startBossFight();
}
} else if (tileType === GameConstants.DungeonTileType.ladder) {
if (this.autoDungeonTracker.floorFinished) {
DungeonRunner.nextFloor();
}
} else {
console.warn(`Auto Dungeon targeted tile type ${GameConstants.DungeonTileType[tileType]}`);
}
}
stuckInLoopCounter++;
if (stuckInLoopCounter > 5) {
console.warn(`Auto Dungeon got stuck in a loop while moving to tile \'${GameConstants.DungeonTileType[dungeonBoard[this.autoDungeonTracker.targetCoords.y][this.autoDungeonTracker.targetCoords.x].type()]}\' (${this.autoDungeonTracker.targetCoords.x}, ${this.autoDungeonTracker.targetCoords.y})`);
this.toggleAutoDungeon();
return;
}
}
}
/**
* Chooses a target tile based on auto dungeon modes and dungeon progress
*/
static chooseDungeonTargetTile() {
const dungeonBoard = DungeonRunner.map.board()[this.autoDungeonTracker.floor];
let target = null;
while (!target) {
// Boss tile not yet unlocked
if (!dungeonBoard[this.autoDungeonTracker.bossCoords.y][this.autoDungeonTracker.bossCoords.x].isVisited) {
target = this.autoDungeonTracker.bossCoords;
}
// Encounters to fight
else if (this.autoDungeonEncounterMode && this.autoDungeonTracker.encounterCoords.length) {
// Skip already-fought encounters
let encounter = this.autoDungeonTracker.encounterCoords.pop();
if (dungeonBoard[encounter.y][encounter.x].type() == GameConstants.DungeonTileType.enemy) {
target = encounter;
} else {
continue;
}
}
// Chests to open
else if (this.autoDungeonChestMode && this.autoDungeonTracker.chestCoords.length && this.autoDungeonTracker.chestCoords[0].tier >= this.autoDungeonLootTier) {
target = this.autoDungeonTracker.chestCoords.shift().xy;
}
// Open visible chests of sufficient rarity
else if (this.autoDungeonAlwaysOpenRareChests && this.autoDungeonTracker.chestCoords.some((c) => c.tier >= this.autoDungeonLootTier && dungeonBoard[c.xy.y][c.xy.x].isVisible)) {
let index = this.autoDungeonTracker.chestCoords.findIndex((c) => c.tier >= this.autoDungeonLootTier && dungeonBoard[c.xy.y][c.xy.x].isVisible);
target = this.autoDungeonTracker.chestCoords[index].xy;
this.autoDungeonTracker.chestCoords.splice(index, 1);
}
// Time to fight the boss
else {
target = this.autoDungeonTracker.bossCoords;
this.autoDungeonTracker.floorFinished = true;
}
}
return target;
}
/**
* Find next tile on the shortest path towards target via breadth-first search
*/
static pathfindTowardDungeonTarget() {
const target = this.autoDungeonTracker.targetCoords;
let result = null;
if (!target) {
return result;
}
const queue = [target];
const visited = new Set(`${target.x}-${target.y}`);
while (queue.length) {
const p = queue.shift();
if (DungeonRunner.map.hasAccessToTile(p)) {
result = p;
break;
}
const adjTiles = [[p.x - 1, p.y], [p.x + 1, p.y], [p.x, p.y - 1], [p.x, p.y + 1]];
for (let [nx, ny] of adjTiles) {
// Enqueue valid tiles not yet considered
let xy = `${nx}-${ny}`;
if (0 <= nx && nx < this.autoDungeonTracker.floorSize && 0 <= ny && ny < this.autoDungeonTracker.floorSize && !visited.has(xy)) {
queue.push(new Point(nx, ny, target.floor));
visited.add(xy);
}
}
}
return result;
}
/**
* Restart dungeon, separate from dungeonWon function so it can be called with a delay
*/
static restartDungeon() {
if (App.game.gameState !== GameConstants.GameState.dungeon) {
return;
} else if (!EnhancedAutoClicker.canStartAutoDungeon()) {
MapHelper.moveToTown(DungeonRunner.dungeon.name);
return;
}
this.autoDungeonTracker.dungeonFinished = false;
// Clear old board to force map visuals refresh
DungeonRunner.map.board([]);
DungeonRunner.initializeDungeon(DungeonRunner.dungeon);
}
/**
* Override DungeonRunner built-in functions:
* -Add dungeon ID tracking to initializeDungeon() for easier mapping
* -Add auto dungeon equivalent of dungeonWon() to save on performance by restarting without loading town
*/
static overrideDungeonRunner() {
// Differentiate between dungeons for mapping
DungeonRunner.dungeonID = 0;
const oldInit = DungeonRunner.initializeDungeon.bind(DungeonRunner);
DungeonRunner.initializeDungeon = function (...args) {
DungeonRunner.dungeonID++;
return oldInit(...args);
}
DungeonRunner.dungeonWonNormal = DungeonRunner.dungeonWon;
// Version with integrated auto-restart to avoid loading town in between dungeons
DungeonRunner.dungeonWonAuto = function () {
if (!DungeonRunner.dungeonFinished()) {
DungeonRunner.dungeonFinished(true);
// First time clearing dungeon
if (!App.game.statistics.dungeonsCleared[GameConstants.getDungeonIndex(DungeonRunner.dungeon.name)]()) {
DungeonRunner.dungeon.rewardFunction();
}
GameHelper.incrementObservable(App.game.statistics.dungeonsCleared[GameConstants.getDungeonIndex(DungeonRunner.dungeon.name)]);
if (EnhancedAutoClicker.autoDungeonTracker.stopAfterFinishing) {
EnhancedAutoClicker.toggleAutoDungeon();
}
if (EnhancedAutoClicker.autoDungeonState() && EnhancedAutoClicker.canStartAutoDungeon()) {
// Restart the dungeon with a delay, giving Defeat Dungeon Boss quests time to update
EnhancedAutoClicker.autoDungeonTracker.dungeonFinished = true;
setTimeout(() => EnhancedAutoClicker.restartDungeon(), 50);
} else {
if (!DungeonRunner.hasEnoughTokens()) {
// Notify player if they've run out of tokens
Notifier.notify({
type: NotificationConstants.NotificationOption.warning,
title: 'Enhanced Auto Clicker',
message: `Auto Dungeon mode ran out of dungeon tokens.`,
timeout: GameConstants.DAY,
});
}
if (EnhancedAutoClicker.autoDungeonState()) {
EnhancedAutoClicker.toggleAutoDungeon();
}
// Can't continue, exit auto dungeon mode
MapHelper.moveToTown(DungeonRunner.dungeon.name);
}
}
}
// Only use our version when auto dungeon is running
DungeonRunner.dungeonWon = function (...args) {
if (EnhancedAutoClicker.autoClickState() && EnhancedAutoClicker.autoDungeonState()) {
DungeonRunner.dungeonWonAuto(...args);
} else {
DungeonRunner.dungeonWonNormal(...args);
}
}
}
/* Clicker statistics calculator */
/**
* Resets calculator loop, stats trackers, and calculator display
* -If auto clicker is running, restarts calculator
*/
static resetCalculator() {
clearInterval(this.autoClickCalcLoop);
this.autoClickCalcTracker.lastUpdate = [Date.now()];
this.autoClickCalcTracker.ticks = [0];
this.autoClickCalcTracker.clicks = [App.game.statistics.clickAttacks()];
this.autoClickCalcTracker.enemies = [App.game.statistics.totalPokemonDefeated()];
this.calculateAreaHealth();
document.getElementById('auto-click-info').innerHTML = `