// ==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(`
`) 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 }) } }