Skip to content

Instantly share code, notes, and snippets.

@koutoftimer
Last active March 4, 2024 06:38
Show Gist options
  • Save koutoftimer/c497259d270640c00f4bd637b82eeb3a to your computer and use it in GitHub Desktop.
Save koutoftimer/c497259d270640c00f4bd637b82eeb3a to your computer and use it in GitHub Desktop.
Clickpocalypse 2 automation script
// Usage: run this script in your browser.
//
// By default it doesn't starts automatically. You have to run `turn_on()` function
// or start each module indivitually `auto_<name>.start()`.
// `turn_off()` and `auto_<name>.stop()` does the oposite.
// `modules_preset` defines set of modules to be handled by `turn_*` functions.
//
// ------------------------------------------
// -- Utility
// ------------------------------------------
/** @param {number} msec */
async function sleep(msec) {
return new Promise(resolve => setTimeout(resolve, msec))
}
class Mutex {
constructor() {
this.current = Promise.resolve()
}
async lock() {
/** @type {() => void} */
let unlock
const prev = this.current
this.current = new Promise(resolve => { unlock = () => resolve(undefined) })
return prev.then(() => unlock)
}
}
const mutex = new Mutex()
/**
* Manages event queue to let Clickpocalypse 2 engine handle them properly.
* @param {Element} button
*/
async function button_click(button) {
const unlock = await mutex.lock()
button.dispatchEvent(new MouseEvent('mouseup'))
await sleep(150)
unlock()
}
// Decorator class that allows to turn on/off separate modules
class WorkerDecorator {
#job
#interval
#timer
/**
* @param {number} interval
* @param {() => void} job
*/
constructor(job, interval) {
this.#job = job
this.#interval = interval
this.#timer = 0
}
start() {
this.#timer = setInterval(this.#job, this.#interval)
}
stop() {
clearInterval(this.#timer)
}
}
// ------------------------------------------
// -- Set auto looter (1 second interval)
// ------------------------------------------
// Presses loot button.
const auto_loot = new WorkerDecorator(
async function () {
const loot_button = document.querySelector('#treasureChestLootButtonPanel.lootButton')
if (loot_button) button_click(loot_button)
}
, 1000
)
// ------------------------------------------
// -- Set auto upgrader (30 seconds interval)
// ------------------------------------------
// Uses gold, kills and experience to buy upgrades.
const auto_upgrader = new WorkerDecorator(
async function () {
Array.prototype.filter.call(
document.querySelectorAll('#upgradeButtonContainer .upgradeButton'),
/** @param {HTMLElement} e */
e => e.style.display == 'block'
).forEach(/** @param {Element} e */ async (e) => await button_click(e))
}
, 30 * 1000
)
// ------------------------------------------
// -- Set auto potion user (30 seconds interval)
// ------------------------------------------
// Waits for all avaliable potion slots to fill up
// before activation.
const auto_potions = new WorkerDecorator(
async function () {
const potions = document.querySelectorAll('#potionButtonContainer .potionButton')
const locked_potions = document.querySelectorAll('.potionButtonLocked').length
if (potions.length === 8 - locked_potions) {
potions.forEach(async (e) => {
if (e.parentElement) {
await button_click(e.parentElement)
}
})
}
}
, 30 * 1000
)
// ------------------------------------------
// -- Set auto skill unlocker (30 seconds interval)
// ------------------------------------------
// Unlocks skills for each hero in order specified
// by strategies.
var auto_skills = function() {
// Character names - Role:
// * Hugo - Fighter
// * Drago - Druid
// * Meiji - Ninja
// * Lord Volaille - King
// * Casey - Rogue
/** @param {number} id */
function get_character_name(id) {
return document.querySelector(`#characterLevelUpButtonContainer${id}_0 span`)
?.textContent
?.replace('Level Up ', '')
}
// List of all skills, top to bottom, left to right.
const full_skillset = [...Array(4).keys()].map(col => [...Array(9).keys()].map(row => [row, col])).flat()
// List of skill learning strategies per hero
// TODO: add more strategies for ommited heroes
const strategies = new Map([
[
'Hugo',
[
// taunt
[...Array(9).keys()].map(row => [row, 0]),
// health regen
[...Array(9).keys()].map(row => [row, 2]),
// AOE
[...Array(9).keys()].map(row => [row, 3]),
// last skills
[...Array(9).keys()].map(row => [row, 1]),
].flat(),
],
[
'Drago',
[
// Minor Heal
[...Array(9).keys()].map(row => [row, 0]),
// Guard Dog
[...Array(9).keys()].map(row => [row, 3]),
// max wolf pack
[...Array(9).keys()].map(row => [row, 2]),
// Sleep
[...Array(9).keys()].map(row => [row, 1]),
].flat(),
],
[
'Meiji',
[
// Swift Strike
[...Array(9).keys()].map(row => [row, 0]),
// HP regen
[...Array(9).keys()].map(row => [row, 2]),
// Damage
[...Array(9).keys()].map(row => [row, 1]),
// Attack speed
[...Array(9).keys()].map(row => [row, 3]),
].flat(),
],
[
'Lord Volaille',
[
// Very first chicken
[[0, 0]],
// Guard chicken
[...Array(9).keys()].map(row => [row, 1]),
// Chicken chance: Rouge
[...Array(9).keys()].map(row => [row, 0]).slice(1),
// Chicken chance: Ninja
[...Array(9).keys()].map(row => [row, 2]),
// Chicken chance: Barbarian
[...Array(9).keys()].map(row => [row, 3]),
].flat(),
],
[
'Casey',
[
// Detect Treasure Chest
[...Array(9).keys()].map(row => [row, 3]),
// Loot instantly
[...Array(9).keys()].map(row => [row, 2]),
// Improve health
[...Array(9).keys()].map(row => [row, 1]),
// Skip stealth
[...Array(4).keys()].map(row => [row, 0]),
].flat(),
],
])
async function worker() {
// `id` is a positional index of the hero in the party composition
const locked_hero_slots = document.querySelectorAll('.gameTabLockedAdventurerInfo').length
for (let id of Array(5 - locked_hero_slots).keys()) {
// skip hero if he has no free skill points
if (!document.querySelector(`#gameTabMenu li:nth-child(${id + 4}).tabHighlighted`)) {
continue
}
// select skillset tab for current hero
/** @type {HTMLElement|null} */
const link = document.querySelector(`#gameTabMenu li:nth-child(${id + 4}) > a`)
link?.click()
// let Clickpocalypse 2 engine update skillset DOM
await sleep(200)
// TODO: cache this value over id
let strategy
const name = get_character_name(id)
if (name === undefined || !strategies.has(name)) {
console.warn(`Custom strategy for [${name}] not found. Using default one.`)
} else {
strategy = strategies.get(name)
}
for (let [row, col] of strategy || full_skillset) {
const skill = document.getElementById(`characterSkillsContainer${id}_${row}_${col}_${row}`)
// some characters have incompleate skill table
// ensure we have met requirements to learn this skill
if (skill?.classList.contains('upgradeButton')) {
await button_click(skill) // dead-lock
break
}
}
}
// go back to Game screen
/** @type {HTMLElement|null} */
const link = document.querySelector('#gameTabMenu li:nth-child(3) > a')
link?.click()
}
return new WorkerDecorator(worker, 30 * 1000)
}()
// auto_loot helpfull for early game or if you have no Rouge in the party
const modules_preset = [auto_skills, auto_potions, auto_upgrader]
// Bot (automation) switch handlers
function turn_on() {
for (const worker of modules_preset) {
worker.start()
}
}
function turn_off() {
for (const worker of modules_preset) {
worker.stop()
}
}
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Language and Environment */
"target": "es6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"lib": ["es2019", "dom"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
"useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
/* JavaScript Support */
"allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
"checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
/* Interop Constraints */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
"alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment