// ==UserScript==
// @name [Pokeclicker] Idle Battle Frontier
// @namespace Pokeclicker Scripts
// @author wizanyx
// @description Runs the Battle Frontier in background, allowing you to do other things while progressing through the Battle Frontier.
// @copyright https://github.com/wizanyx
// @license GPL-3.0 License
// @version 0.0.1
// @homepageURL https://github.com/wizanyx/Pokeclicker-Scripts/
// @supportURL https://github.com/wizanyx/Pokeclicker-Scripts/issues
// @downloadURL https://raw.githubusercontent.com/wizanyx/Pokeclicker-Scripts/master/idleBattleFrontier.user.js
// @updateURL https://raw.githubusercontent.com/wizanyx/Pokeclicker-Scripts/master/idleBattleFrontier.user.js
// @match https://www.pokeclicker.com/
// @icon https://www.google.com/s2/favicons?domain=pokeclicker.com
// @grant unsafeWindow
// @run-at document-idle
// ==/UserScript==
const SETTINGS = {
TICK_RATE: 500,
UI_CONTAINER_ID: "customScriptsContainer",
STORAGE_KEY: "pokeclicker_idleBattleFrontier_settings",
};
/**
* Manages the main execution loop for the Idle Battle Frontier.
*/
const IdleFrontierManager = {
running: ko.observable(false),
autoRestart: ko.observable(false),
loopInterval: null,
/**
* Loads settings from local storage
*/
load() {
try {
const saved = localStorage.getItem(SETTINGS.STORAGE_KEY);
if (saved) {
const data = JSON.parse(saved);
this.running(data.running || false);
this.autoRestart(data.autoRestart || false);
}
} catch (e) {
console.error("Failed to load Idle Battle Frontier settings", e);
}
},
/**
* Saves settings to local storage
*/
save() {
const data = {
running: this.running(),
autoRestart: this.autoRestart(),
};
localStorage.setItem(SETTINGS.STORAGE_KEY, JSON.stringify(data));
},
/**
* Starts the idle runner.
* @param {boolean} useCheckpoint - Whether to start from the highest checkpoint.
*/
start(useCheckpoint) {
IdleRunner.start(useCheckpoint);
this.stopLoop(); // Ensure no duplicate loops
this.loopInterval = setInterval(() => {
if (this.running()) {
BattleMechanics.tick();
IdleRunner.tick();
} else {
this.end();
}
}, SETTINGS.TICK_RATE);
this.save();
},
/**
* Stops the idle runner and clears the loop.
*/
end() {
this.running(false);
this.stopLoop();
this.save();
},
/**
* Clears the interval timer.
*/
stopLoop() {
if (this.loopInterval) {
clearInterval(this.loopInterval);
this.loopInterval = null;
}
},
/**
* Initializes the manager, restoring state if needed.
*/
init() {
IdleFrontierManager.autoRestart.subscribe(() => {
IdleFrontierManager.save();
});
// Check if we should auto-start
if (IdleFrontierManager.running()) {
IdleFrontierManager.start(true);
}
},
};
/**
* Handles battle simulation mechanics, including enemy generation and attacking.
*/
class BattleMechanics {
static enemyPokemon = ko.observable(null);
static alternateAttack = false;
static pokemonIndex = ko.observable(0);
/**
* Performed every tick of the idle loop.
*/
static tick() {
this.pokemonAttack();
}
/**
* Simulates player pokemon attacking the enemy.
*/
static pokemonAttack() {
// attack twice as fast if we have defeated this stage
this.alternateAttack = !this.alternateAttack;
if (
this.alternateAttack &&
IdleRunner.stage() >
App.game.statistics.battleFrontierHighestStageCompleted()
) {
return;
}
const enemy = this.enemyPokemon();
if (!enemy?.isAlive()) return;
// Calculate and apply damage
const damage = App.game.party.calculatePokemonAttack(
enemy.type1,
enemy.type2,
true, // ignoreRegionMultiplier
GameConstants.Region.none,
false, // includeBreeding
false, // useBaseAttack
WeatherType.Clear,
);
enemy.damage(damage);
if (!enemy.isAlive()) {
this.defeatPokemon();
}
}
/**
* Handles logic when an enemy is defeated (rewards, progression).
*/
static defeatPokemon() {
this.enemyPokemon().defeat(true);
// Progress Eggs
App.game.breeding.progressEggsBattle(
IdleRunner.stage(),
GameConstants.Region.none,
);
// Advance internal counter
GameHelper.incrementObservable(this.pokemonIndex);
if (this.pokemonIndex() >= 3) {
IdleRunner.nextStage();
this.pokemonIndex(0);
}
if (IdleRunner.started()) {
this.generateNewEnemy();
} else {
this.enemyPokemon(null);
}
}
/**
* Generates a new enemy based on the current stage and highest region.
*/
static generateNewEnemy() {
const enemy = pokemonMap.randomRegion(player.highestRegion());
const stage = IdleRunner.stage();
const health = PokemonFactory.routeHealth(
stage + 10,
GameConstants.Region.none,
);
const level = Math.min(100, stage);
const money = 0; // Money is awarded at the end of the run
const shiny = PokemonFactory.generateShiny(
GameConstants.SHINY_CHANCE_BATTLE,
);
const gems = Math.ceil(stage / 80);
const gender = PokemonFactory.generateGender(
enemy.gender.femaleRatio,
enemy.gender.type,
);
if (shiny) {
GameHelper.incrementObservable(
App.game.statistics.totalShinyTrainerPokemonSeen,
);
}
const enemyPokemon = new BattlePokemon(
enemy.name,
enemy.id,
enemy.type[0],
enemy.type[1],
health,
level,
0, // catchRate
enemy.exp,
new Amount(money, GameConstants.Currency.money),
shiny,
gems,
gender,
GameConstants.ShadowStatus.None,
EncounterType.trainer,
);
this.enemyPokemon(enemyPokemon);
}
}
/**
* Extends the game's BattleFrontierRunner to manage the idle run state.
*/
class IdleRunner extends BattleFrontierRunner {
static timeLeft = ko.observable(GameConstants.GYM_TIME);
static timeLeftPercentage = ko.observable(100);
static started = ko.observable(false);
/**
* Updates time tracking for the current stage.
*/
static tick() {
if (!this.started()) return;
if (this.timeLeft() < 0) {
this.battleLost();
}
this.timeLeft(this.timeLeft() - SETTINGS.TICK_RATE);
this.timeLeftPercentage(
Math.floor((this.timeLeft() / GameConstants.GYM_TIME) * 100),
);
}
/**
* Initializes the idle run.
* @param {boolean} useCheckpoint
*/
static async start(useCheckpoint) {
if (!useCheckpoint && this.hasCheckpoint()) {
const confirmed = await Notifier.confirm({
title: "Restart Battle Frontier?",
message:
"Current progress will be lost and you will restart from the first stage.",
type: NotificationConstants.NotificationOption.warning,
confirm: "OK",
});
if (!confirmed) return;
}
this.started(true);
this.stage(useCheckpoint ? BattleFrontierRunner.checkpoint() : 1);
BattleFrontierRunner.highest(
App.game.statistics.battleFrontierHighestStageCompleted(),
);
BattleMechanics.pokemonIndex(0);
BattleMechanics.generateNewEnemy();
this.timeLeft(GameConstants.GYM_TIME);
this.timeLeftPercentage(100);
IdleFrontierManager.running(true);
}
/**
* Advances to the next stage.
*/
static nextStage() {
BattleFrontierMilestones.gainReward(this.stage());
if (
App.game.statistics.battleFrontierHighestStageCompleted() <
this.stage()
) {
App.game.statistics.battleFrontierHighestStageCompleted(
this.stage(),
);
}
GameHelper.incrementObservable(this.stage);
GameHelper.incrementObservable(
App.game.statistics.battleFrontierTotalStagesCompleted,
);
this.timeLeft(GameConstants.GYM_TIME);
this.timeLeftPercentage(100);
BattleFrontierRunner.checkpoint(this.stage());
}
/**
* Stops the run normally.
*/
static end() {
BattleMechanics.enemyPokemon(null);
this.stage(1);
this.started(false);
IdleFrontierManager.running(false);
}
/**
* Handles loss condition (time ran out).
*/
static battleLost() {
const stageBeaten = this.stage() - 1;
const battleMultiplier = Math.max(stageBeaten / 100, 1);
const battlePointsEarned = App.game.wallet.gainBattlePoints(
Math.round(stageBeaten * battleMultiplier),
).amount;
const moneyEarned = App.game.wallet.gainMoney(
stageBeaten * 100 * battleMultiplier,
true,
).amount;
Notifier.notify({
title: "Battle Frontier",
message: `You managed to beat stage ${stageBeaten.toLocaleString("en-US")}.\nYou received ${battlePointsEarned.toLocaleString("en-US")}.\nYou received
${moneyEarned.toLocaleString("en-US")}.`,
strippedMessage: `You managed to beat stage ${stageBeaten.toLocaleString("en-US")}.\nYou received ${battlePointsEarned.toLocaleString("en-US")} Battle Points.\nYou received ${moneyEarned.toLocaleString("en-US")} Pokédollars.`,
type: NotificationConstants.NotificationOption.success,
setting:
NotificationConstants.NotificationSetting.General
.battle_frontier,
sound: NotificationConstants.NotificationSound.General
.battle_frontier,
timeout: 30 * GameConstants.MINUTE,
});
App.game.logbook.newLog(
LogBookTypes.FRONTIER,
createLogContent.gainBattleFrontierPoints({
stage: stageBeaten.toLocaleString("en-US"),
points: battlePointsEarned.toLocaleString("en-US"),
}),
);
BattleFrontierRunner.checkpoint(1);
this.end();
if (IdleFrontierManager.autoRestart()) {
setTimeout(() => {
IdleFrontierManager.start(true);
}, 1000);
}
}
/**
* Stops the idle run.
*/
static stopIdle() {
this.started(false);
BattleMechanics.enemyPokemon(null);
IdleFrontierManager.running(false);
}
static hasCheckpoint = ko.computed(() => {
return BattleFrontierRunner.checkpoint() > 1;
});
/**
* Patches the native BattleFrontierRunner.start to ensure idle mode stops when manual mode starts.
*/
static overrideStart() {
const originalStart = BattleFrontierRunner.start;
BattleFrontierRunner.start = async function (useCheckpoint) {
IdleRunner.stopIdle();
await originalStart.call(BattleFrontierRunner, useCheckpoint);
};
}
}
/**
* Handles User Interface Injection and Customizations.
*/
const UserInterface = {
/**
* Injects the shared scripts container into the Left Column.
*/
createContainer() {
if (document.getElementById(SETTINGS.UI_CONTAINER_ID)) return;
const leftColumn = document.getElementById("left-column");
if (leftColumn) {
const div = document.createElement("div");
div.id = SETTINGS.UI_CONTAINER_ID;
div.className = "card sortable border-secondary mb-3";
div.innerHTML = `
| Checkpoint: | |
| Highest: | |
| Current Stage: | |
|
|
|
|
|
|