Script Archived
This script has been archived by the author. The script may be no longer functional, and the author no longer maintains it. You cannot provide feedback for this script.
// ==UserScript==
// @name 宝可梦点击(Poke Clicker)辅助脚本 自动地牢/道馆模块
// @namespace PokeClickerHelper
// @version 0.1.7
// @description 船新版本的策略算法,极大提高自动地牢/道馆效率
// @author DreamNya
// @match https://www.pokeclicker.com
// @match https://g8hh.github.io/pokeclicker/
// @match https://pokeclicker.g8hh.com
// @match https://yx.g8hh.com/pokeclicker/
// @match https://dreamnya.github.io/pokeclicker/
// @icon data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAP8AAAD/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAA/319hf99fYX/fX2F/319hf8AAAD/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///////////////////////////99fYX/fX2F/319hf8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////////////////wAAAP8AAAD/fX2F/319hf99fYX/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//////8AAAD/AAAA/wAAAP99fYX/fX2F/wAAAP8AAAD/AAAA/319hf8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAP8AAAD/HBT//xwU//8AAAD//////319hf8AAAD/Dgim/w4Ipv8AAAD/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAD/HBT//xwU//8cFP//HBT//wAAAP8AAAD/Dgim/w4Ipv8OCKb/Dgim/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAA/xwU//8cFP//HBT//xwU/44cFP//HBT//xwU//8cFP//Dgim/w4Ipv8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/HBT//xwU/47/////HBT/jhwU//8cFP//HBT//w4Ipv8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/xwU//8cFP//HBT/jhwU//8OCKb/Dgim/w4Ipv8OCKb/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAA/xwU//8OCKb/Dgim/w4Ipv8AAAD/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAA/wAAAP8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//8AAPw/AADwDwAA4AcAAOAHAADAAwAAwAMAAMADAADAAwAA4AcAAOAHAADwDwAA/D8AAP//AAD//wAA//8AAA==
// @grant none
// @license MIT
// @run-at document-end
// ==/UserScript==
/* global $, PokeClickerHelper, MapHelper, player, App, GameConstants, DungeonRunner, DungeonBattle, GymRunner, Amount, RouteHelper */
if (typeof PokeClickerHelper == typeof void 0) {
alert('宝可梦点击(Poke Clicker)辅助脚本 自动地牢/道馆模块加载失败\n\n未找到核心模块,需要先安装核心模块才可正常使用\n\n论坛主页:https://bbs.tampermonkey.net.cn/forum.php?mod=viewthread&tid=3842')
window.open("https://bbs.tampermonkey.net.cn/forum.php?mod=viewthread&tid=3842")
return
}
// UI相关
PokeClickerHelper.UIDOM.push(`
<div id="PokeClickerHelperDungeonGYM" class="custom-row">
<div class="labelContainer">
<label>自动地牢/道馆(-1为无限次):</label>
</div>
<div class="contentContainer ml-2">
<div class="form-row">
<select id="PokeClickerHelperDungeonGYMType" class="custom-select col-3" disabled data-save="false" title="脚本自动读取当前区域类型 无需手动切换">
<option value="Dungeon">地牢</option>
<option value="GYM">道馆</option>
<option value="Route">野外</option>
</select>
<select id="PokeClickerHelperDungeonGYMClearType" class="custom-select col-4 ml-1" title="切换地牢不同挑战策略">
<option value="OnlyBoss" title="自动计算避免普通战斗及不开宝箱的最短直奔Boss路线">速通不开</option>
<option value="BossOpen" title="自动计算避免普通战斗及顺路开宝箱的最短直奔Boss路线">速通开箱</option>
<option value="ClearOpen" title="清完所有普通战斗后先开宝箱再开始Boss战">全清开箱</option>
<option value="OnlyClear" title="清完所有普通战斗后不开宝箱直接开始Boss战">全清不开</option>
</select>
<label class="form-check-label m-auto" title="直观显示脚本自动寻路效果"><input id="PokeClickerHelperShowMap" type="checkbox" value="false">显示地图</label>
</div>
<div class="form-row mt-1 mb-2">
<input id="PokeClickerHelperDungeonGYMTimes" type="number" class="outline-dark form-control form-control-number col-4" placeholder="循环次数" title="地牢/道馆重复挑战次数(负数为无限次)">
<select id="PokeClickerHelperDungeonGYMIndex" class="custom-select col-6 ml-1 opacity-25" data-save="false" title="自动读取当前地区可用道馆列表,如有多个道馆需要手动切换"><option value="0">道馆列表</option><option value="1" class="d-none"></option><option value="2" class="d-none"></option><option value="3" class="d-none"></option><option value="4" class="d-none"></option></select>
</div>
<button id="PokeClickerHelperToggleDungeonGYM" class="btn btn-sm btn-primary d-inline" data-save="false">开始</button>
<select id="PokeClickerHelperDungeonCaughtType" class="custom-select col-5 ml-3" title="重复地牢直至当前所有可抓宝可梦全部符合条件(循环次数不能为0)" style="max-width: 43%;" data-save="false">
<option value="none">无特殊选项</option>
<option value="CaughtAllPokemon">抓齐普通</option>
<option value="CaughtAllShinyPokemon">抓齐闪光</option>
<option value="CaughtAllResistantPokemon">抓齐治愈</option>
</select>
</div>
</div>
<div class="mt-2 mb-1 border-top border-secondary"></div>
`)
const listener = () => {
$("#PokeClickerHelperDungeonGYMType").on('change', changeListener)
$("#PokeClickerHelperToggleDungeonGYM").on('click', DungeonGYMHelper.ToggleDungeonGYM)
}
PokeClickerHelper.UIlistener.push(listener);
//暴露对象方法到全局
const DungeonGYMHelper = {};
PokeClickerHelper.DungeonGYMHelper = DungeonGYMHelper;
//野外道路检测hook
const routeRep = [['App.game.gameState = GameConstants.GameState.fighting;', 'App.game.gameState = GameConstants.GameState.fighting;PokeClickerHelper.DungeonGYMHelper.mapMove();']];
PokeClickerHelper.HookFuc(MapHelper, 'moveToRoute', routeRep, 'route,region');
//城镇检测hook
const townRep = [['App.game.gameState = GameConstants.GameState.town;', 'App.game.gameState = GameConstants.GameState.town;PokeClickerHelper.DungeonGYMHelper.mapMove();']];
PokeClickerHelper.HookFuc(MapHelper, 'moveToTown', townRep, 'townName');
//道馆hook
const restartGYMBody = PokeClickerHelper.HookFucBody(restartGYM);
const gymLostRep = [['App.game.gameState = GameConstants.GameState.town;', restartGYMBody + 'App.game.gameState = GameConstants.GameState.town;']];
const gymWonRep = [['player.town(gym.parent);\n App.game.gameState = GameConstants.GameState.town;\n', restartGYMBody + 'player.town(gym.parent);\n App.game.gameState = GameConstants.GameState.town;\n']];
PokeClickerHelper.HookFuc(GymRunner, 'gymLost', gymLostRep, '');
PokeClickerHelper.HookFuc(GymRunner, 'gymWon', gymWonRep, 'gym');
function restartGYM() {
if ($('#PokeClickerHelperToggleDungeonGYM').text() == '结束') {
if ($('#PokeClickerHelperDungeonGYMTimes').val() != 0) {
if (document.querySelector('#PokeClickerHelperDungeonGYMTimes').value > 0) document.querySelector('#PokeClickerHelperDungeonGYMTimes').value--
this.startGym(this.gymObservable(), false, false)
return
}
PokeClickerHelper.DungeonGYMHelper.ToggleDungeonGYM('', '剩余挑战次数为0')
};
}
//移动hook
DungeonGYMHelper.mapMove = function () {
const Type = document.querySelector("#PokeClickerHelperDungeonGYMType")
const Helper = document.querySelector("#PokeClickerHelperToggleDungeonGYM").innerText
const Times = document.querySelector("#PokeClickerHelperDungeonGYMTimes").value
if (Helper == '开始') { //野外道路、城镇切换自动改变#PokeClickerHelperDungeonGYMType
if (player.route() > 0) {
changeValue(Type, "Route")
} else {
if (player.town().dungeon?.isUnlocked()) {
changeValue(Type, "Dungeon")
} else if (player.town().content.find(i => i.constructor.name == 'Gym' && i.isUnlocked())) {
changeValue(Type, "GYM")
} else {
changeValue(Type, "Route")
}
}
} else {
if (Type.value == 'Dungeon') {//离开地牢
DungeonGYMHelper.generator = void 0
DungeonGYMHelper.isDungeon = false
//dungeonEnd = new Date().getTime()
//console.log('自动地牢耗时 ' + (dungeonEnd - dungeonStart))
const DungeonCaughtType = document.querySelector('#PokeClickerHelperDungeonCaughtType').value
if (Times > 0 && DungeonCaughtType == 'none') document.querySelector("#PokeClickerHelperDungeonGYMTimes").value--
}
if (Type.value == 'GYM') {
DungeonGYMHelper.ToggleDungeonGYM('', '手动离开道馆')
DungeonGYMHelper.mapMove()
}
}
};
PokeClickerHelper.initAfterList.add(() => { document.querySelector("#PokeClickerHelperDungeonGYMType").value = null })
PokeClickerHelper.initAfterList.add(DungeonGYMHelper.mapMove)
//赋值并触发事件监听
const changeValue = function (element, newValue) {
const oldValue = element.value
element.value = newValue
if (oldValue != newValue || newValue == 'GYM') changeListener.call(element)
};
let init
//按钮点击事件
DungeonGYMHelper.ToggleDungeonGYM = (e, message = '') => {
const type = $("#PokeClickerHelperDungeonGYMType").val()
if ($('#PokeClickerHelperToggleDungeonGYM').text() == '开始') {
$('#PokeClickerHelperToggleDungeonGYM').text('结束')
if (type == 'Dungeon') (init = true, PokeClickerHelper.Worker.setInterval(DungeonGYMHelper.AutoDungeon, 50))
if (type == 'GYM') DungeonGYMHelper.AutoGYM()
} else {
$('#PokeClickerHelperToggleDungeonGYM').text('开始')
PokeClickerHelper.Worker.clearInterval(DungeonGYMHelper.AutoDungeon, 50)
DungeonGYMHelper.generator = void 0
DungeonGYMHelper.isDungeon = false
PokeClickerHelper.Notify({ message: "自动地牢/道馆结束 " + message, timeout: 5000 }, true);
}
}
//类型改变事件
const changeListener = function () {
$("#PokeClickerHelperDungeonCaughtType").toggleClass('invisible', this.value != 'Dungeon')
$("label:has(#PokeClickerHelperShowMap)").toggleClass('invisible', this.value != 'Dungeon')
$("#PokeClickerHelperToggleDungeonGYM").attr('disabled', this.value == 'Route')
$("#PokeClickerHelperDungeonGYMClearType").toggleClass('opacity-25', this.value != 'Dungeon')
$("#PokeClickerHelperDungeonGYMIndex").toggleClass('opacity-25', this.value != 'GYM')
$("#PokeClickerHelperDungeonGYMTimes").toggleClass('opacity-25', this.value == 'Route')
if (this.value == 'GYM') {
$(`#PokeClickerHelperDungeonGYMIndex [value]:not(:first)`).addClass('d-none')
const GYMList = player.town().content.filter(i => i.constructor.name == 'Gym')
for (let i = 0; i < GYMList.length; i++) {
const GYM = GYMList[i]
const select = $(`#PokeClickerHelperDungeonGYMIndex [value="${i}"]`)
select.text(GYM.buttonText)
if (i > 0) select.toggleClass('d-none', !GYM.isUnlocked())
}
$("#PokeClickerHelperDungeonGYMIndex").val('0')
} else {
$('#PokeClickerHelperDungeonGYMIndex option:first').text('道馆列表')
$('#PokeClickerHelperDungeonGYMIndex option:not(:first)').addClass('d-none')
}
}
//自动道馆
DungeonGYMHelper.AutoGYM = () => {
if ($("#PokeClickerHelperDungeonGYMTimes").val() == 0) return DungeonGYMHelper.ToggleDungeonGYM('', '剩余挑战次数为0')
const GYM = player.town().content.filter(i => i.constructor.name == 'Gym')[$("#PokeClickerHelperDungeonGYMIndex").val()]
if (!GYM.isUnlocked()) return DungeonGYMHelper.ToggleDungeonGYM('', 'Error 道馆未解锁')
if (document.querySelector("#PokeClickerHelperDungeonGYMTimes").value > 0) document.querySelector("#PokeClickerHelperDungeonGYMTimes").value--
GymRunner.startGym(GYM)
};
//let dungeonStart
//let dungeonEnd
//地牢步进 Generator函数
function* step(route) {
let retryTimes = 0
for (let i = 0; i < route.length; i++) {
const { x, y, value } = route[i]
DungeonRunner.map.moveToCoordinates(x, y)
const { x: px, y: py } = DungeonRunner.map.playerPosition()
if (retryTimes < 10 && (px != x || py != y)) {
route.splice(i + 1, 0, { x, y, value }) //如果没有到预期地点,在下一tick继续尝试,可能会不稳定导致卡死,增加10次重试上限
console.log('自动地牢没有到达预期地点,在下一tick继续尝试', route)
retryTimes++
}
if (value == 5) {
DungeonRunner.nextFloor()
if (DungeonRunner.map.playerPosition().floor != 1) {
route.splice(i + 1, 0, { x, y, value }) //如果没有到预期地点,在下一tick继续尝试,可能会不稳定导致卡死,增加10次重试上限
console.log('自动地牢没有到达第二层,在下一tick继续尝试', route)
retryTimes++
}
}
if (value == 3) DungeonRunner.openChest()
if (value != 4) yield value
}
DungeonRunner.startBossFight()
return 'Boss'
}
const DungeonCaughtTypeObj = {};
DungeonCaughtTypeObj.none = () => false;
DungeonCaughtTypeObj.CaughtAllPokemon = (dungeon) => DungeonRunner.dungeonCompleted(dungeon, false);
DungeonCaughtTypeObj.CaughtAllShinyPokemon = (dungeon) => DungeonRunner.dungeonCompleted(dungeon, true);
DungeonCaughtTypeObj.CaughtAllResistantPokemon = (dungeon) => RouteHelper.minPokerus(dungeon.allAvailablePokemon()) == 3;
//自动地牢 委托到Worker中执行,this实际指向PokeClickerHelper.Worker,因此不用function+this 改用箭头函数
DungeonGYMHelper.AutoDungeon = () => {
if ($("#PokeClickerHelperDungeonGYMTimes").val() == 0) return DungeonGYMHelper.ToggleDungeonGYM('', '剩余挑战次数为0')
if (init && App.game.gameState !== GameConstants.GameState.town) return DungeonGYMHelper.ToggleDungeonGYM('', '首次挑战需要在地牢外开始')
init = false
//初始化进入地牢
if (!DungeonGYMHelper.isDungeon) {
if ($('#dungeonMap').length > 0) return
let dungeon = player.town().dungeon
if (App.game.gameState === GameConstants.GameState.town && dungeon?.isUnlocked()) {
if (dungeon.tokenCost > App.game.wallet.currencies[GameConstants.Currency.dungeonToken]()) return DungeonGYMHelper.ToggleDungeonGYM('', '地牢币不足')
const DungeonCaughtType = document.querySelector('#PokeClickerHelperDungeonCaughtType').value
if (DungeonCaughtTypeObj[DungeonCaughtType](dungeon)) return DungeonGYMHelper.ToggleDungeonGYM('', '捕捉到全部符合条件宝可梦')
DungeonRunner.dungeonCompleted(player.town().dungeon, true)
DungeonGYMHelper.isDungeon = true
//dungeonStart = new Date().getTime()
DungeonRunner.initializeDungeon(dungeon)
PokeClickerHelper.Notify({ message: "自动地牢开始", timeout: 1000 });
} else {
DungeonGYMHelper.ToggleDungeonGYM('', '当前区域无可进入地牢')
}
return
}
//进入地牢
if ($('#dungeonMap').length == 0) return
if (DungeonRunner.fighting() || DungeonBattle.catching() || DungeonGYMHelper.generator == 'done') return
//DungeonGYMHelper[$("#PokeClickerHelperDungeonGYMClearType").val()]()
if (DungeonGYMHelper.generator == void 0) {
if ($("#PokeClickerHelperShowMap").prop('checked')) DungeonRunner.map.showAllTiles()
let map = DungeonRunner.map.board().map(i => i.map(i => i.map(i => i.type())))
let calc
let type = $("#PokeClickerHelperDungeonGYMClearType").val()
//读取内存地图格式化自动寻最短路线并生成Generator步进函数
switch (type) {
case 'OnlyBoss': calc = map.flatMap(i => JSON.parse(JSON.stringify(DungeonGYMHelper.calcBoss(i, false)))); break//深拷贝 便于闭包垃圾回收?
case 'BossOpen': calc = map.flatMap(i => JSON.parse(JSON.stringify(DungeonGYMHelper.calcBoss(i, true)))); break
case 'ClearOpen': calc = map.flatMap(i => JSON.parse(JSON.stringify(DungeonGYMHelper.calcClear(i, true)))); break
case 'OnlyClear': calc = map.flatMap(i => JSON.parse(JSON.stringify(DungeonGYMHelper.calcClear(i, false)))); break
}
DungeonGYMHelper.generator = step(calc)//直奔BOSS 只找尽量避免BOSS的最短路线 暂不考虑顺路开箱
}
//步进
let stepResult = DungeonGYMHelper.generator.next()
//console.log(stepResult)
if (stepResult.done) DungeonGYMHelper.generator = 'done'
}
//计算全清路线
DungeonGYMHelper.calcClear = function (m, openChest) {
const record = m.map((i, y) => i.map((m, x) => ({ x: x, y: y, value: m })));
//console.log(record)
let flat = record.flatMap((i, index) => {
const r = Math.floor(index / 2) == index / 2
const n = i.map(i => i)
return r ? n : n.reverse()
})
const startIndex = flat.findIndex(i => i.value == 1) //兼容傻逼360 不支持findLastIndex
const end = flat.find(i => i.value > 3)
let route = flat.slice(startIndex + 1).concat(flat.slice(0, startIndex).reverse()).map(({ ...i }) => {
if (i.value > 2) i.value = 0//路过BOSS、宝箱、楼梯视为普通道路
return i
})
if (openChest) route = route.concat(flat.filter(i => i.value == 3))
route.push(end)
return route
}
//计算直奔Boss最短路线
//《关于A*算法不能穿墙、dijkstra算法看不懂,只能被迫自己编算法,结果被迫编了3天才编出来这档事》 我还是太菜了……
DungeonGYMHelper.calcBoss = function (m, openChest) {
//获取可通过的周围四个点
function getChild({ x, y }) {
return [[x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]].filter(([x, y]) => { return (x < size && y < size && x >= 0 && y >= 0 && !(x == end.x && y == end.y)) }).map(([X, Y]) => flat.find(({ x, y }) => x == X && y == Y))
}
//尝试通路
function applyChild(parent) {
const child = getChild(parent)
const newChild = child.filter(c => {
const newCost = c.cost + parent.totalCost //子点总消耗=子点消耗+父点总消耗,只取总消耗最少的点作为最终路径点
const newLength = 1 + parent.totalLength //子点总步长=1+父点总步长,总消耗相同时只取总步长最少的点作为最终路径点
if (c.parent.size == 0) {
c.parent.add(parent)
c.totalCost = newCost
c.totalLength = newLength
} else if (c.parent.has(parent) && newCost >= c.totalCost) {
return false //终止走回路
} else {
if (c.totalCost == newCost) {
if (c.totalLength > newLength) {
c.totalLegnth = newLength
c.parent = new Set([parent])
} else if (c.totalLength == newLength) {
c.parent.add(parent)
} else {
return false
}
} else if (c.totalCost > newCost) {
c.totalCost = newCost
c.totalLength = newLength
c.parent = new Set([parent])
} else {
return false //终止消耗过高
}
}
return true
})
return newChild
}
//回溯路径 结果逆推顺序
function getRoute({ x, y, value, parent }, result) {
parent = [...parent].filter(({ x, y }) => !result.find(({ x: X, y: Y }) => x == X && y == Y))//不走回路
if (!(start.x == x && start.y == y)) {
result.push({ x, y, value })
parent.forEach(_parent => getRoute(_parent, [...result]))
} else {
routes.push(result)
}
}
function getPoint({ x, y, value }) {
return { x, y, value }
}
const startTime = Date.now()
const size = m.length;
const record = m.map((i, y) => i.map((m, x) => ({ x: x, y: y, value: m, cost: (m == 2) * 1, totalCost: 0, totalLength: 0, parent: new Set() })));
const flat = record.flat();
const start = flat.find(i => i.value == 1);
const end = flat.find(i => i.value > 3); //4为BOSS 5为楼梯
let result = applyChild(start);
let times = 1
//循环遍历通路
while (result.length > 0) {
result = result.map(c => applyChild(c)).flat()
times++
}
//console.log(`直奔BOSS 遍历路径计算完毕 花费${Date.now() - startTime}ms 长度${size} 循环计算${times}次`)
//遍历结果
result = getChild(end).sort((a, b) => a.totalCost - b.totalCost)
result = result.filter(i => i.totalCost == result[0].totalCost)
let routes = []
result.forEach(i => { getRoute(i, [getPoint(end)]) })
//最终结果,[步数,战斗次数,宝箱数,正序路径]
routes = routes.map(i => ({ length: i.length, battle: i.filter(i => i.value == 2).length, chest: i.filter(i => i.value == 3).length, route: i.reverse() }))
//排序战斗次数优先、其次步数
routes.sort((a, b) => a.length - b.length).sort((a, b) => a.battle - b.battle)
//console.log(`直奔BOSS 回溯路径计算完毕 花费${Date.now() - startTime}ms 长度${size} 循环计算${times}次`)
//console.log('直奔BOSS 路径长度:' + routes[0].length + ' 战斗次数:' + routes[0].battle)
//返回最优结果
if (openChest) {
//排序战斗次数优先、其次宝箱、最后步数
return routes.sort((a, b) => b.chest - a.chest).sort((a, b) => a.battle - b.battle)[0].route
} else {
//将宝箱视为通路
return routes[0].route.map(i => {
if (i.value == 3) i.value = 0
return i
})
}
}