自动地牢/道馆模块
// ==UserScript==
// @name 自动地牢/道馆模块
// @namespace PokeClickerHelper
// @version 0.1.2
// @description 船新版本的策略算法,极大提高自动地牢/道馆效率
// @author 超超
// @match https://pokemon.ccox.cc/
// @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://scriptcat.org/script-show-page/1228')
window.open("https://scriptcat.org/script-show-page/1228")
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
})
}
}