// ==UserScript== // @name PokéSurvivor 小地图 Minimapa // @name:en PokéSurvivor Minimap // @namespace https://github.com/yiyuan521/yiyuan-userscripts // @version 1.0.0 // @description 左上角半透明小地图:地形、敌人、Boss、闪光、宝箱/祭坛/泉眼、裂缝、陷阱、队友。M 键开关,Alt+拖动移动,Alt+拖右下角缩放。 // @author yiyuan521 // @match https://pokesurvivor.com/* // @match https://www.pokesurvivor.com/* // @match https://pokesurvivor.games/* // @icon https://pokesurvivor.com/assets/icons/icon-96.png // @run-at document-start // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @grant unsafeWindow // @downloadURL https://raw.githubusercontent.com/yiyuan521/yiyuan-userscripts/master/pokesurvivor/pokesurvivor-minimap.user.js // @updateURL https://raw.githubusercontent.com/yiyuan521/yiyuan-userscripts/master/pokesurvivor/pokesurvivor-minimap.user.js // ==/UserScript== /* * ───────────────────────────────────────────────────────────────────────────── * PokéSurvivor 小地图(油猴脚本) * * 做什么 * 在画面左上角(默认在 HUD 下面一点)画一个半透明的小地图,以你为中心: * · 地形:亮色=能走的地面,蓝色=水/岩浆,**暗到发黑=过不去**(石墙、树、石头) * 走过的区域会记住,越走越全;没探索过的地方是偏蓝的深灰,不会和黑墙看混 * · 白色五角星:你 彩色圆点:联机队友 * · 小红点:普通敌人 大金圆点(会呼吸):Boss 青点带圈:闪光 * · 金色小方块:小宝箱 金色大方块+锁孔+光晕:大宝箱 * · 彩色菱形:祭坛(加成柱子) 薄荷绿空心圈:泉眼 * · 紫色圆点:裂缝(格里塔) 金色/紫色小点:掉落物、票 * · X:陷阱,(毒:紫;粘:绿;爆炸:橙色) * · Boss / 裂缝 出了范围会贴在边框上指方向 * * 怎么用 * M 键 显示 / 隐藏。Alt + 左键拖动 可以挪到任意位置; * Alt + 拖右下角 可以把小地图拉大 / 缩小(96–640px)。 * 油猴菜单里能改:大小(滑块或直接输入数字)、缩放范围、透明度、位置、地形开关。 * * 为什么需要「捕获 G」 * 发布版(index.html + js/app.min.js)把整个游戏关在一个闭包里并做了混淆, * window.G 不存在,控制台也摸不到游戏状态,为的是防止改内存作弊。 * 小地图只是「看」,不需要改任何东西,所以这里用一个只读的办法拿到那个对象: * 游戏里 G 是 `var G = {}` 之后一个个挂属性:G.U = ...、G.World = ...、 * G.Game = ...(压缩后是 _0xABC['U'] = ... 这种形式,一共 77 个)。 * 我们在 Object.prototype 上给这些名字装上 setter —— 谁给对象赋 .U, * setter 里的 this 就是那个对象。于是 G 就到手了。 * (注意:不能用 G.LIFT = 18 那个,terser 会把它内联进对象字面量 * `var G = {LIFT: 18}`,那样根本不产生赋值。) * 每个 setter 触发后立刻自毁,并把值原样写回成对象自己的属性, * 游戏行为完全不变;抓到 G 之后(几十毫秒内)其余陷阱全部撤掉。 * 只读、不改游戏数据、不影响排名判定(js/game.js 的 clean() 只看玩家对象)。 * 如果哪天压缩方式变了、抓不到 G,脚本什么也不做,也不会报错刷屏。 * ───────────────────────────────────────────────────────────────────────────── */ (function () { 'use strict'; const CFG_KEY = 'psmm.cfg.v1'; const GK = '__PSMM_G__'; const LOG = 'color:#7fd0ff;font-weight:700'; // ============================ 配置 ============================ const DEF = { on: true, // 是否显示 size: 176, // 边长(CSS px) range: 1800, // 小地图覆盖的世界范围(世界 px,正方形边长) alpha: 0.82, // 整体不透明度 pos: 'tl-hud', // 位置预设 xy: null, // 自定义位置 { x, y } terrain: true, // 画地形 dots: true // 画敌人 / 物件 }; const POS = { 'tl-hud': { left: 12, top: 76 }, // 左上,让开血条/等级那一块(默认) 'tl': { left: 12, top: 12 }, // 左上角(会压住血条) 'tr': { right: 12, top: 76 }, 'bl': { left: 12, bottom: 14 }, 'br': { right: 12, bottom: 14 } }; const POS_ORDER = ['tl-hud', 'tl', 'tr', 'bl', 'br']; const POS_NAME = { 'tl-hud': '左上(HUD 下)', 'tl': '左上角', 'tr': '右上', 'bl': '左下', 'br': '右下', custom: '自定义' }; const RANGES = [1200, 1800, 2600]; const ALPHAS = [0.6, 0.82, 1]; // 小地图尺寸范围(CSS px) const SIZE_MIN = 96, SIZE_MAX = 640, SIZE_DEF = 176; function clampSize(v) { if (v === '' || v == null) return SIZE_DEF; v = Math.round(+v); if (!isFinite(v)) return SIZE_DEF; return Math.max(SIZE_MIN, Math.min(SIZE_MAX, v)); } const hasGM = (typeof GM_getValue === 'function' && typeof GM_setValue === 'function'); const cfg = (() => { try { const raw = hasGM ? GM_getValue(CFG_KEY, '') : localStorage.getItem(CFG_KEY); if (raw) { const o = Object.assign({}, DEF, JSON.parse(raw)); o.size = clampSize(o.size); return o; } } catch (e) { /* 配置坏了就用默认值 */ } return Object.assign({}, DEF); })(); function saveCfg() { try { const raw = JSON.stringify(cfg); if (hasGM) GM_setValue(CFG_KEY, raw); else localStorage.setItem(CFG_KEY, raw); } catch (e) { /* 存不了也没关系 */ } } // ============================ 拿到游戏内部的 G ============================ // 这段函数会被序列化成字符串,作为