// ==UserScript== // @name Saltline Pro // @namespace local.saltline.pro // @version 2.1.0 // @description Saltline 辅助:无敌 / 无限子弹 / 一击必杀 / 自瞄 / ESP / 飞行 / 子弹追踪 / 子弹穿墙 / 移速 / 跳跃 // @author local // @match file:///*.html // @match file:///*/*.html // @match file:///*/*/*.html // @include file:///*.html // @include file:///*/*.html // @include file:///*/*/*.html // @include http://localhost/* // @include http://127.0.0.1/* // @grant none // @run-at document-idle // @noframes // @license All Rights Reserved // ==/UserScript== (function () { var CORE = "(function () {\n 'use strict';\n if (window.__SLP__) return;\n window.__SLP__ = true;\n\n const DRONE_COLOR = '#ff3b30';\n const HOUND_COLOR = '#ff3b30';\n const BARREL_COLOR = '#ffd166';\n const TYPE_NAME = { gull: '无人机', hound: '猎犬' };\n const BULLET_SPEED = 380;\n const TRACK_FOV = 75;\n const TRACK_RANGE = 200;\n\n const S = {\n god: false,\n ammo: false,\n oneShot: false,\n wallbang: false,\n barrels: false,\n barrelWallbang: false,\n aim: false,\n esp: false,\n fly: false,\n track: false,\n aimFov: 35,\n aimStrength: 100,\n aimVisible: true,\n aimLock: true,\n aimCircle: true,\n aimKey: 'Mouse3',\n espRange: 300,\n espLines: true,\n speed: false,\n speedMul: 1,\n jump: false,\n jumpMul: 1,\n flySpeed: 14,\n panel: true\n };\n\n const HELD = Object.create(null);\n let app = null;\n\n const waiter = setInterval(() => {\n const G = window.__G;\n if (G && G.player && G.weapon && G.enemies && G.combat && G.physics && G.game && G.camera) {\n clearInterval(waiter);\n try { init(G); } catch (e) { }\n }\n }, 250);\n setTimeout(() => clearInterval(waiter), 90000);\n\n function init(G) {\n app = G;\n const T = G.THREE;\n const clamp = (v, a, b) => (v < a ? a : v > b ? b : v);\n\n let panelEl = null;\n let ringEl = null;\n let bindBtn = null;\n let binding = false;\n\n const espRoot = document.createElement('div');\n const pool = [];\n let lineSvg = null;\n let miniEl = null;\n const flyPos = new T.Vector3();\n const vFwd = new T.Vector3(), vRight = new T.Vector3(), vDir = new T.Vector3();\n const projA = new T.Vector3(), projB = new T.Vector3();\n const tmpV = new T.Vector3(), tmpW = new T.Vector3();\n const velMap = new Map();\n let flyInit = false;\n\n const tuneBase = {};\n try {\n const tn = G.player && G.player.tuning;\n if (tn) ['walk', 'sprint', 'crouch', 'ads', 'accelGround', 'accelAir'].forEach(k => {\n if (typeof tn[k] === 'number') tuneBase[k] = tn[k];\n });\n } catch (_) { }\n\n function applyMoveSpeed() {\n const tn = G.player && G.player.tuning;\n if (!tn) return;\n const m = (S.speed && S.speedMul !== 1) ? S.speedMul : 1;\n for (const k in tuneBase) tn[k] = tuneBase[k] * m;\n }\n\n try {\n G.events.on('player:jump', () => {\n if (!S.jump || S.jumpMul === 1) return;\n const tn = G.player.tuning || {};\n const g = (typeof tn.gravity === 'number' && tn.gravity > 0) ? tn.gravity : 21;\n const apex = (typeof tn.jumpApex === 'number' ? tn.jumpApex : 1) * S.jumpMul;\n G.player.vel.y = Math.sqrt(2 * g * Math.max(0.05, apex));\n });\n } catch (_) { }\n\n function keyLabel(k) {\n if (!k) return '无';\n if (k === 'None') return '常开';\n if (k.startsWith('Mouse')) {\n const n = k.slice(5);\n return ({ '1': '左键', '2': '中键', '3': '右键', '4': '侧键1', '5': '侧键2' })[n] || ('鼠标' + n + '键');\n }\n return k.replace('Key', '').replace('Digit', '').replace('Arrow', '方向').toUpperCase();\n }\n\n function isTouchDevice() {\n try {\n if (G.input && G.input.touch) return true;\n } catch (_) { }\n try { return matchMedia('(pointer: coarse)').matches; } catch (_) { return false; }\n }\n\n function aimHeld() {\n if (!S.aim) return false;\n if (isTouchDevice()) {\n try { if (G.input && G.input.ads) return true; } catch (_) { }\n\n const btn = document.querySelector('.touch .t-btn.t-ads');\n return !!(btn && btn.classList.contains('active'));\n }\n if (S.aimKey === 'None') return true;\n return !!HELD[S.aimKey];\n }\n\n function enemyAimPoint(e, out) {\n const isHound = (e.type || 'gull') === 'hound';\n const hasHead = e.headPos && (e.headPos.x || e.headPos.y || e.headPos.z);\n if (isHound) {\n if (hasHead) return out.copy(e.headPos);\n return out.copy(e.pos).setY(e.pos.y + 0.9);\n }\n return out.copy(e.pos).setY(e.pos.y + 0.1);\n }\n\n function baseFovDeg() {\n const p = G.player;\n const v = p && p.baseFov;\n return (typeof v === 'number' && v > 20) ? v : 74;\n }\n\n function effAimFov() {\n const base = baseFovDeg() * Math.PI / 180;\n const curr = ((G.camera && G.camera.fov) || baseFovDeg()) * Math.PI / 180;\n const t = Math.tan(clamp(S.aimFov, 1, 88) * Math.PI / 180) * Math.tan(curr / 2) / Math.tan(base / 2);\n return Math.atan(t) * 180 / Math.PI;\n }\n\n function pickTarget(origin, dir, fovDeg, los, predict) {\n const list = G.enemies.list || [];\n const cosFov = Math.cos(clamp(fovDeg, 1, 179) * Math.PI / 180);\n let best = null, bestScore = -Infinity;\n for (let i = 0; i < list.length; i++) {\n const e = list[i];\n if (!e || !e.alive || e.active === false || !e.pos) continue;\n const aimPt = enemyAimPoint(e, tmpV);\n const to = tmpW.copy(aimPt).sub(origin);\n const d = to.length();\n if (d < 0.6 || d > (predict ? TRACK_RANGE : 400)) continue;\n to.multiplyScalar(1 / d);\n const dot = to.dot(dir);\n if (dot < cosFov) continue;\n if (los) {\n try {\n if (G.physics.lineOfSight && !G.physics.lineOfSight(origin, aimPt)) continue;\n } catch (_) { }\n }\n let point = aimPt.clone();\n if (predict) {\n const rec = velMap.get(e);\n if (rec) {\n const tFlight = Math.min(0.4, d / BULLET_SPEED);\n point.addScaledVector(rec.v, tFlight);\n }\n }\n const score = dot * 1000 - d;\n if (score > bestScore) { bestScore = score; best = { point, enemy: e, dist: d }; }\n }\n if (S.barrels && G.explosives && G.explosives.items) {\n const losB = S.aimVisible && !(S.wallbang && S.barrelWallbang);\n const items = G.explosives.items;\n for (let i = 0; i < items.length; i++) {\n const b = items[i];\n if (!b || !b.alive || !b.pos) continue;\n const to = tmpW.copy(b.pos).sub(origin);\n const d = to.length();\n if (d < 0.6 || d > (predict ? TRACK_RANGE : 400)) continue;\n to.multiplyScalar(1 / d);\n const dot = to.dot(dir);\n if (dot < cosFov) continue;\n if (losB) {\n const h = G.physics.raycast(origin, to, d + 0.6);\n if (h && h.box !== b.box && h.dist < d - 0.2) continue;\n }\n const score = dot * 1000 - d;\n if (score > bestScore) { bestScore = score; best = { point: b.pos.clone(), barrel: b, dist: d }; }\n }\n }\n return best;\n }\n\n function updateVelocities(dt) {\n const list = G.enemies.list || [];\n const k = 1 - Math.exp(-12 * Math.max(dt, 1e-3));\n for (let i = 0; i < list.length; i++) {\n const e = list[i];\n if (!e || !e.pos) continue;\n if (!e.alive || e.active === false) { velMap.delete(e); continue; }\n let rec = velMap.get(e);\n if (!rec) { velMap.set(e, { p: e.pos.clone(), v: new T.Vector3() }); continue; }\n rec.v.x += ((e.pos.x - rec.p.x) / Math.max(dt, 1e-3) - rec.v.x) * k;\n rec.v.y += ((e.pos.y - rec.p.y) / Math.max(dt, 1e-3) - rec.v.y) * k;\n rec.v.z += ((e.pos.z - rec.p.z) / Math.max(dt, 1e-3) - rec.v.z) * k;\n rec.p.copy(e.pos);\n }\n }\n\n function blendDir(from, want, strength01) {\n if (strength01 >= 0.999) return want;\n return from.clone().lerp(want, strength01).normalize();\n }\n\n function rayAabb(o, d, mn, mx, maxT) {\n const lo = [mn.x, mn.y, mn.z], hi = [mx.x, mx.y, mx.z];\n const oo = [o.x, o.y, o.z], dd = [d.x, d.y, d.z];\n let tmin = 0, tmax = maxT, axis = -1, sgn = 1;\n for (let i = 0; i < 3; i++) {\n if (Math.abs(dd[i]) < 1e-8) {\n if (oo[i] < lo[i] || oo[i] > hi[i]) return null;\n continue;\n }\n const inv = 1 / dd[i];\n let t1 = (lo[i] - oo[i]) * inv, t2 = (hi[i] - oo[i]) * inv, s = -1;\n if (t1 > t2) { const t = t1; t1 = t2; t2 = t; s = 1; }\n if (t1 > tmin) { tmin = t1; axis = i; sgn = s; }\n if (t2 < tmax) tmax = t2;\n if (tmin > tmax) return null;\n }\n if (axis < 0) { axis = 1; sgn = 1; }\n const n = new T.Vector3(0, 0, 0);\n n.setComponent(axis, sgn);\n return {\n dist: tmin,\n point: new T.Vector3(o.x + d.x * tmin, o.y + d.y * tmin, o.z + d.z * tmin),\n normal: n\n };\n }\n\n function barrelRaycast(o, d, range) {\n const items = (G.explosives && G.explosives.items) || [];\n const maxT = (typeof range === 'number' && range > 0) ? range : 400;\n let best = null, bestT = maxT;\n for (let i = 0; i < items.length; i++) {\n const b = items[i];\n if (!b || !b.alive || !b.box || !b.box.min || !b.box.max) continue;\n const hit = rayAabb(o, d, b.box.min, b.box.max, bestT);\n if (hit && hit.dist < bestT) {\n bestT = hit.dist;\n best = { dist: hit.dist, point: hit.point, normal: hit.normal, surface: 'metal', box: b.box };\n }\n }\n return best;\n }\n\n const origPlayerShot = G.combat.playerShot;\n G.combat.playerShot = function (origin, dir, opts) {\n\n if (S.oneShot) {\n opts = Object.assign({}, opts, { damage: 999 });\n }\n if (origin && dir && dir.isVector3) {\n let target = null;\n if (aimHeld()) {\n\n const pull = effAimFov() * clamp(S.aimStrength, 1, 100) / 100;\n const los = S.aimVisible && !S.wallbang;\n target = pull > 0.05 ? pickTarget(origin, dir, pull, los, false) : null;\n }\n if (!target && S.track) {\n target = pickTarget(origin, dir, TRACK_FOV, S.aimVisible && !S.wallbang, true);\n }\n if (target) {\n const want = target.point.clone().sub(origin);\n if (want.lengthSq() > 1e-6) dir = want.normalize();\n }\n }\n if (S.wallbang) {\n const phys = G.physics;\n const origRay = phys.raycast;\n phys.raycast = S.barrelWallbang ? function () { return null; } : barrelRaycast;\n try {\n return origPlayerShot.call(this, origin, dir, opts);\n } finally {\n phys.raycast = origRay;\n }\n }\n return origPlayerShot.call(this, origin, dir, opts);\n };\n\n const camFwd = new T.Vector3();\n function aimLockStep(dt) {\n const p = G.player;\n if (!p.alive) return;\n camFwd.set(0, 0, -1).applyQuaternion(G.camera.quaternion);\n const pull = effAimFov() * clamp(S.aimStrength, 1, 100) / 100;\n if (pull <= 0.05) return;\n const los = S.aimVisible && !S.wallbang;\n const target = pickTarget(G.camera.position, camFwd, pull, los, false);\n if (!target) return;\n const d = target.point.clone().sub(G.camera.position).normalize();\n const targetYaw = Math.atan2(-d.x, -d.z);\n const targetPitch = Math.asin(clamp(d.y, -1, 1));\n let dy = targetYaw - p.yaw;\n while (dy > Math.PI) dy -= Math.PI * 2;\n while (dy < -Math.PI) dy += Math.PI * 2;\n const s01 = clamp(S.aimStrength, 1, 100) / 100;\n if (s01 >= 0.99) {\n\n p.yaw = targetYaw;\n p.pitch = clamp(targetPitch, -1.5, 1.5);\n } else {\n const k = 1 - Math.exp(-(10 + 34 * s01 * s01) * dt);\n p.yaw += dy * k;\n p.pitch += (targetPitch - p.pitch) * k;\n p.pitch = clamp(p.pitch, -1.5, 1.5);\n }\n }\n\n const CSS = `\n #slcheat-panel{position:fixed;left:14px;top:14px;width:284px;z-index:999999;touch-action:none;\n font:400 12px/1.5 \"Microsoft YaHei\",system-ui,sans-serif;color:#e8e4da;\n background:rgba(9,11,13,.9);border:1px solid rgba(78,168,255,.45);border-radius:4px;\n box-shadow:0 10px 30px rgba(0,0,0,.6);pointer-events:auto;user-select:none;\n backdrop-filter:blur(6px)}\n #slcheat-panel .slc-h{display:flex;align-items:center;justify-content:space-between;\n padding:7px 10px;cursor:move;background:rgba(78,168,255,.13);border-bottom:1px solid rgba(78,168,255,.28)}\n #slcheat-panel .slc-h b{font:900 13px/1 \"Big Shoulders Display\",Impact,sans-serif;\n letter-spacing:.16em;color:#4ea8ff}\n #slcheat-panel .slc-h span{cursor:pointer;color:#8d9aa3;padding:0 3px}\n #slcheat-panel .slc-b{padding:7px 10px 9px}\n #slcheat-panel .slc-row{display:flex;align-items:center;gap:8px;padding:3px 0;cursor:pointer}\n #slcheat-panel .slc-row input{accent-color:#4ea8ff;margin:0}\n #slcheat-panel .slc-row .n{flex:1}\n #slcheat-panel .slc-row kbd{font:500 10px/1 \"IBM Plex Mono\",monospace;color:#8d9aa3;\n border:1px solid rgba(232,228,218,.22);border-radius:2px;padding:2px 4px}\n #slcheat-panel .slc-sub{display:flex;align-items:center;gap:6px;padding:0 0 3px 24px;color:#8d9aa3;font-size:11px}\n #slcheat-panel .slc-sub input[type=range]{flex:1;accent-color:#4ea8ff;height:14px}\n #slcheat-panel .slc-sub .v{width:34px;text-align:right;color:#c9d2d8}\n #slcheat-panel .slc-bind{font:500 11px/1 \"Microsoft YaHei\",sans-serif;color:#0b0d0f;\n background:#4ea8ff;border:0;border-radius:2px;padding:4px 8px;cursor:pointer}\n #slcheat-panel .slc-bind.binding{background:#ff5a40;color:#fff}\n #slcheat-panel .slc-stat{margin-top:7px;padding-top:6px;border-top:1px solid rgba(232,228,218,.16);\n color:#8d9aa3;font-size:11px;line-height:1.5;white-space:pre-line}\n #slcheat-panel .slc-tip{margin-top:3px;color:#67707a;font-size:10px;line-height:1.55}\n #slcheat-toast{position:fixed;left:50%;top:78px;transform:translateX(-50%);z-index:999999;\n padding:6px 14px;border-radius:3px;background:rgba(9,11,13,.88);\n border:1px solid rgba(78,168,255,.45);color:#4ea8ff;font:600 12px/1.4 \"Microsoft YaHei\",sans-serif;\n pointer-events:none;transition:opacity .25s}\n #slcheat-esp{position:fixed;inset:0;pointer-events:none;z-index:999998;overflow:hidden}\n #slcheat-esp .slc-box{position:absolute;left:0;top:0;display:none;will-change:transform;\n border:1px solid ${HOUND_COLOR};font:600 10px/1.2 \"Microsoft YaHei\",monospace;color:#ffe9c4;\n text-shadow:0 1px 2px #000;white-space:nowrap}\n #slcheat-esp .slc-label{position:absolute;left:0;top:-15px;padding:1px 3px;\n background:rgba(0,0,0,.62);border-radius:2px}\n #slcheat-esp .slc-bar{position:absolute;left:0;bottom:-5px;height:3px;background:rgba(0,0,0,.6)}\n #slcheat-esp .slc-bar i{display:block;height:100%;width:100%;background:#ff5a40}\n #slcheat-mini{position:fixed;right:14px;top:14px;z-index:999998;display:none;pointer-events:none;\n font:600 12px/1.45 \"Microsoft YaHei\",sans-serif;color:#cfe6ff;background:rgba(9,13,20,.8);\n border:1px solid rgba(78,168,255,.55);border-radius:3px;padding:5px 10px;letter-spacing:.02em;\n box-shadow:0 4px 14px rgba(0,0,0,.45)}\n #slcheat-mini b{color:#4ea8ff;font-weight:700}\n #slcheat-ring{position:fixed;left:0;top:0;border-radius:50%;pointer-events:none;z-index:999997;\n border:1px solid rgba(255,59,48,.5);display:none}\n #slcheat-ring::after{content:'';position:absolute;left:50%;top:50%;width:3px;height:3px;\n margin:-1.5px 0 0 -1.5px;border-radius:50%;background:rgba(255,59,48,.75)}\n @media (max-width: 560px) {\n #slcheat-panel{width:min(86vw, 320px);font-size:13px}\n #slcheat-panel .slc-row{padding:6px 0}\n #slcheat-panel .slc-row input[type=checkbox]{width:19px;height:19px}\n #slcheat-panel .slc-bind{padding:8px 12px;font-size:13px}\n }\n `;\n const styleEl = document.createElement('style');\n styleEl.id = 'slcheat-style';\n styleEl.textContent = CSS;\n document.head.appendChild(styleEl);\n\n const uiRoot = document.createElement('div');\n uiRoot.id = 'slcheat-root';\n uiRoot.style.cssText = 'position:absolute;inset:0;pointer-events:none;z-index:2147483000';\n document.body.appendChild(uiRoot);\n\n function mountUI() {\n const host = document.fullscreenElement || document.webkitFullscreenElement || document.body;\n if (host && uiRoot.parentNode !== host) host.appendChild(uiRoot);\n }\n document.addEventListener('fullscreenchange', mountUI);\n document.addEventListener('webkitfullscreenchange', mountUI);\n\n buildPanel();\n buildESP();\n buildRing();\n buildMini();\n\n function buildPanel() {\n panelEl = document.createElement('div');\n panelEl.id = 'slcheat-panel';\n panelEl.innerHTML = `\n
\n SALTLINE PRO\n —\n
\n
\n \n \n \n\n \n
\n 按键\n \n
\n
\n 角度\n 35°\n
\n
\n 力度\n 100%\n
\n \n \n \n\n \n \n \n
\n 速度\n 14\n
\n \n
\n 倍率\n 1.0×\n
\n \n
\n 倍率\n 1.0×\n
\n \n \n \n \n\n
`;\n uiRoot.appendChild(panelEl);\n\n const root = panelEl;\n let dx = 0, dy = 0, dragging = false;\n const head = root.querySelector('[data-drag]');\n head.addEventListener('pointerdown', e => {\n if (e.target.hasAttribute('data-min')) return;\n const r = root.getBoundingClientRect();\n dx = e.clientX - r.left; dy = e.clientY - r.top; dragging = true;\n try { head.setPointerCapture(e.pointerId); } catch (_) { }\n e.preventDefault();\n });\n addEventListener('pointermove', e => {\n if (!dragging) return;\n root.style.left = clamp(e.clientX - dx, 0, innerWidth - 90) + 'px';\n root.style.top = clamp(e.clientY - dy, 0, innerHeight - 40) + 'px';\n });\n addEventListener('pointerup', () => { dragging = false; });\n addEventListener('pointercancel', () => { dragging = false; });\n\n root.querySelector('[data-min]').addEventListener('click', () => {\n const b = root.querySelector('[data-body]');\n b.style.display = b.style.display === 'none' ? '' : 'none';\n });\n\n root.addEventListener('input', e => {\n const el = e.target;\n queueSave();\n if (el.hasAttribute('data-fov')) {\n S.aimFov = +el.value;\n root.querySelector('[data-fovv]').textContent = el.value + '°';\n } else if (el.hasAttribute('data-str')) {\n S.aimStrength = +el.value;\n root.querySelector('[data-strv]').textContent = el.value + '%';\n } else if (el.hasAttribute('data-spd')) {\n S.flySpeed = +el.value;\n root.querySelector('[data-spdv]').textContent = el.value;\n } else if (el.hasAttribute('data-mspeed')) {\n S.speedMul = +el.value;\n root.querySelector('[data-mspeedv]').textContent = (+el.value).toFixed(1) + '×';\n } else if (el.hasAttribute('data-jump')) {\n S.jumpMul = +el.value;\n root.querySelector('[data-jumpv]').textContent = (+el.value).toFixed(1) + '×';\n } else if (el.dataset.k === 'aimVisible') {\n S.aimVisible = el.checked;\n } else if (el.dataset.k === 'aimCircle') {\n S.aimCircle = el.checked;\n } else if (el.dataset.k === 'espLines') {\n S.espLines = el.checked;\n }\n });\n root.addEventListener('change', e => {\n const el = e.target;\n const k = el.dataset.k;\n queueSave();\n if (!k || k === 'aimVisible' || k === 'aimCircle' || k === 'espLines') return;\n S[k] = el.checked;\n toggle(k, el.checked);\n });\n\n bindBtn = root.querySelector('[data-bind]');\n bindBtn.textContent = keyLabel(S.aimKey);\n bindBtn.addEventListener('click', e => {\n e.preventDefault();\n binding = true;\n bindBtn.classList.add('binding');\n bindBtn.textContent = '按下按键…';\n });\n\n refreshChecks();\n }\n\n function refreshChecks() {\n if (!panelEl) return;\n panelEl.querySelectorAll('input[data-k]').forEach(el => {\n const k = el.dataset.k;\n el.checked = k === 'aimVisible' ? S.aimVisible\n : k === 'aimCircle' ? S.aimCircle\n : k === 'espLines' ? S.espLines\n : !!S[k];\n });\n if (bindBtn) bindBtn.textContent = keyLabel(S.aimKey);\n }\n\n const labelOf = k => ({\n god: '无敌', ammo: '无限子弹', aim: '自瞄', esp: 'ESP', fly: '自由飞行', track: '子弹追踪',\n barrels: '油桶辅助', barrelWallbang: '油桶穿墙'\n }[k] || k);\n\n function toggle(k, on) {\n queueSave();\n if (k === 'fly') {\n flyInit = false;\n if (!on) G.player.vel.set(0, 0, 0);\n }\n toast((on ? '开启 ' : '关闭 ') + labelOf(k));\n }\n\n let settingsBtn = null;\n const CFG_KEY = 'saltline.cheat.cfg.v1';\n let saveTimer = 0;\n\n function loadCfg() {\n try {\n const raw = localStorage.getItem(CFG_KEY);\n if (!raw) return;\n const o = JSON.parse(raw);\n if (!o || typeof o !== 'object') return;\n for (const k in S) {\n if (k === 'panel' || !(k in o)) continue;\n const v = o[k], cur = S[k];\n if (typeof cur === 'boolean') { if (typeof v === 'boolean') S[k] = v; }\n else if (typeof cur === 'number') { if (typeof v === 'number' && isFinite(v)) S[k] = v; }\n else if (typeof cur === 'string') { if (typeof v === 'string' && v.length < 40) S[k] = v; }\n }\n S.aimFov = clamp(S.aimFov, 3, 90);\n S.aimStrength = clamp(S.aimStrength, 1, 100);\n S.speedMul = clamp(S.speedMul, 0.5, 5);\n S.jumpMul = clamp(S.jumpMul, 0.5, 10);\n S.flySpeed = clamp(S.flySpeed, 4, 40);\n S.espRange = clamp(S.espRange, 50, 600);\n } catch (_) { }\n }\n\n function saveCfg() {\n try {\n const o = {};\n for (const k in S) { if (k !== 'panel') o[k] = S[k]; }\n localStorage.setItem(CFG_KEY, JSON.stringify(o));\n } catch (_) { }\n }\n\n function queueSave() {\n clearTimeout(saveTimer);\n saveTimer = setTimeout(saveCfg, 150);\n }\n\n function togglePanel(force) {\n S.panel = (force === undefined) ? !S.panel : !!force;\n if (panelEl) panelEl.style.display = S.panel ? '' : 'none';\n if (miniEl) miniEl.style.display = S.panel ? 'none' : 'block';\n if (settingsBtn) settingsBtn.textContent = S.panel ? '关闭' : '打开';\n toast(S.panel ? '作弊菜单已打开' : '作弊菜单已关闭');\n }\n\n function injectSettingsRow() {\n const pane = document.querySelector('.menu .panel-b[data-pane=\"settings\"]');\n if (!pane) return false;\n if (pane.querySelector('[data-slc-row]')) return true;\n const row = document.createElement('div');\n row.className = 'row';\n row.setAttribute('data-slc-row', '');\n row.innerHTML = '' +\n '';\n pane.appendChild(row);\n settingsBtn = row.querySelector('[data-slc-btn]');\n settingsBtn.addEventListener('click', ev => {\n ev.preventDefault();\n ev.stopPropagation();\n togglePanel();\n });\n return true;\n }\n\n let toastTimer = 0;\n function toast(msg) {\n let el = document.getElementById('slcheat-toast');\n if (!el) {\n el = document.createElement('div');\n el.id = 'slcheat-toast';\n uiRoot.appendChild(el);\n }\n el.textContent = msg;\n el.style.opacity = '1';\n clearTimeout(toastTimer);\n toastTimer = setTimeout(() => { el.style.opacity = '0'; }, 1100);\n }\n\n function buildMini() {\n miniEl = document.createElement('div');\n miniEl.id = 'slcheat-mini';\n miniEl.style.display = 'none';\n uiRoot.appendChild(miniEl);\n }\n\n function buildRing() {\n ringEl = document.createElement('div');\n ringEl.id = 'slcheat-ring';\n uiRoot.appendChild(ringEl);\n }\n\n function updateRing(playing) {\n if (!ringEl) return;\n const show = S.aim && S.aimCircle && playing;\n if (!show) { ringEl.style.display = 'none'; return; }\n\n const baseV = baseFovDeg() * Math.PI / 180;\n const r = Math.tan(clamp(S.aimFov, 1, 88) * Math.PI / 180) / Math.tan(baseV / 2) * (innerHeight / 2);\n const size = Math.max(10, Math.min(r * 2, Math.max(innerWidth, innerHeight) * 2));\n ringEl.style.display = 'block';\n ringEl.style.width = size.toFixed(0) + 'px';\n ringEl.style.height = size.toFixed(0) + 'px';\n ringEl.style.left = ((innerWidth - size) / 2).toFixed(0) + 'px';\n ringEl.style.top = ((innerHeight - size) / 2).toFixed(0) + 'px';\n ringEl.style.borderColor = aimHeld() ? 'rgba(255,59,48,.92)' : 'rgba(255,59,48,.45)';\n ringEl.style.borderWidth = aimHeld() ? '2px' : '1px';\n }\n\n function buildESP() {\n espRoot.id = 'slcheat-esp';\n uiRoot.appendChild(espRoot);\n\n lineSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n lineSvg.id = 'slcheat-lines';\n lineSvg.setAttribute('width', '100%');\n lineSvg.setAttribute('height', '100%');\n lineSvg.style.cssText = 'position:absolute;inset:0;pointer-events:none;overflow:visible';\n espRoot.appendChild(lineSvg);\n for (let i = 0; i < 32; i++) {\n const box = document.createElement('div');\n box.className = 'slc-box';\n const label = document.createElement('div');\n label.className = 'slc-label';\n const bar = document.createElement('div');\n bar.className = 'slc-bar';\n const fill = document.createElement('i');\n bar.appendChild(fill);\n box.appendChild(label);\n box.appendChild(bar);\n espRoot.appendChild(box);\n const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');\n line.setAttribute('stroke-width', '1');\n line.setAttribute('stroke-linecap', 'round');\n lineSvg.appendChild(line);\n pool.push({ box, label, bar, fill, line });\n }\n }\n\n function drawESP() {\n const cam = G.camera, w = innerWidth, h = innerHeight, ppos = G.player.pos;\n const list = (G.enemies.list || []).filter(e => e && e.alive && e.active !== false && e.pos);\n list.sort((a, b) => a.pos.distanceToSquared(ppos) - b.pos.distanceToSquared(ppos));\n let n = 0;\n for (let i = 0; i < list.length && n < pool.length; i++) {\n const e = list[i];\n const d = e.pos.distanceTo(ppos);\n if (d > S.espRange) continue;\n const isHound = (e.type || 'gull') === 'hound';\n const hasHead = e.headPos && (e.headPos.x || e.headPos.y || e.headPos.z);\n const topPt = isHound\n ? (hasHead ? e.headPos : { x: e.pos.x, y: e.pos.y + 1.75, z: e.pos.z })\n : { x: e.pos.x, y: e.pos.y + 0.45, z: e.pos.z };\n const botPt = isHound\n ? { x: e.pos.x, y: e.pos.y, z: e.pos.z }\n : { x: e.pos.x, y: e.pos.y - 0.45, z: e.pos.z };\n projA.set(topPt.x, topPt.y, topPt.z).project(cam);\n projB.set(botPt.x, botPt.y, botPt.z).project(cam);\n if (projA.z > 1 || projB.z > 1) continue;\n if (Math.abs(projA.x) > 1.5 || Math.abs(projB.x) > 1.5) continue;\n\n const x1 = (projA.x * .5 + .5) * w, y1 = (-projA.y * .5 + .5) * h;\n const x2 = (projB.x * .5 + .5) * w, y2 = (-projB.y * .5 + .5) * h;\n const bh = Math.max(10, Math.abs(y2 - y1));\n const bw = clamp(bh * .42, 10, 240);\n const left = x1 - bw / 2, top = Math.min(y1, y2);\n\n const slot = pool[n++];\n const tune = (G.enemies.TUNE || {})[isHound ? 'hound' : 'gull'] || {};\n const maxHp = tune.hp || 140;\n const hp = clamp((e.hp || 0) / maxHp, 0, 1);\n const col = isHound ? HOUND_COLOR : DRONE_COLOR;\n slot.box.style.display = 'block';\n slot.box.style.transform = `translate(${left.toFixed(1)}px,${top.toFixed(1)}px)`;\n slot.box.style.width = bw.toFixed(1) + 'px';\n slot.box.style.height = bh.toFixed(1) + 'px';\n slot.box.style.borderColor = col;\n slot.label.style.color = col;\n slot.label.textContent = `${TYPE_NAME[isHound ? 'hound' : 'gull']} ${d.toFixed(0)}m`;\n slot.fill.style.width = (hp * 100).toFixed(0) + '%';\n slot.fill.style.background = hp > .55 ? '#7ddc7d' : hp > .25 ? HOUND_COLOR : DRONE_COLOR;\n\n if (S.espLines) {\n const cx = w / 2, cy = h / 2;\n\n slot.line.setAttribute('x1', cx.toFixed(1));\n slot.line.setAttribute('y1', cy.toFixed(1));\n slot.line.setAttribute('x2', (left + bw / 2).toFixed(1));\n slot.line.setAttribute('y2', (top + bh / 2).toFixed(1));\n slot.line.setAttribute('stroke', col);\n slot.line.setAttribute('opacity', '0.45');\n slot.line.style.display = '';\n } else {\n slot.line.style.display = 'none';\n }\n }\n if (S.barrels && G.explosives && G.explosives.items) {\n const items = G.explosives.items;\n for (let i = 0; i < items.length && n < pool.length; i++) {\n const b = items[i];\n if (!b || !b.alive || !b.pos) continue;\n const d = b.pos.distanceTo(ppos);\n if (d > S.espRange) continue;\n const yTop = (b.max && typeof b.max.y === 'number') ? b.max.y : b.pos.y + 0.6;\n const yBot = (b.min && typeof b.min.y === 'number') ? b.min.y : b.pos.y - 0.6;\n projA.set(b.pos.x, yTop, b.pos.z).project(cam);\n projB.set(b.pos.x, yBot, b.pos.z).project(cam);\n if (projA.z > 1 || projB.z > 1) continue;\n if (Math.abs(projA.x) > 1.5 || Math.abs(projB.x) > 1.5) continue;\n const x1 = (projA.x * .5 + .5) * w, y1 = (-projA.y * .5 + .5) * h;\n const x2 = (projB.x * .5 + .5) * w, y2 = (-projB.y * .5 + .5) * h;\n const bh2 = Math.max(10, Math.abs(y2 - y1));\n const bw2 = clamp(bh2 * .5, 10, 240);\n const left2 = x1 - bw2 / 2, top2 = Math.min(y1, y2);\n const slot2 = pool[n++];\n const maxHp2 = (b.spec && b.spec.hp) || 50;\n const hp2 = clamp((b.hp || 0) / maxHp2, 0, 1);\n slot2.box.style.display = 'block';\n slot2.box.style.transform = `translate(${left2.toFixed(1)}px,${top2.toFixed(1)}px)`;\n slot2.box.style.width = bw2.toFixed(1) + 'px';\n slot2.box.style.height = bh2.toFixed(1) + 'px';\n slot2.box.style.borderColor = BARREL_COLOR;\n slot2.label.style.color = BARREL_COLOR;\n slot2.label.textContent = `${b.kind === 'gas_cylinders' ? '气瓶' : '油桶'} ${d.toFixed(0)}m`;\n slot2.fill.style.width = (hp2 * 100).toFixed(0) + '%';\n slot2.fill.style.background = BARREL_COLOR;\n if (S.espLines) {\n const cx2 = w / 2, cy2 = h / 2;\n slot2.line.setAttribute('x1', cx2.toFixed(1));\n slot2.line.setAttribute('y1', cy2.toFixed(1));\n slot2.line.setAttribute('x2', (left2 + bw2 / 2).toFixed(1));\n slot2.line.setAttribute('y2', (top2 + bh2 / 2).toFixed(1));\n slot2.line.setAttribute('stroke', BARREL_COLOR);\n slot2.line.setAttribute('opacity', '0.45');\n slot2.line.style.display = '';\n } else {\n slot2.line.style.display = 'none';\n }\n }\n }\n for (; n < pool.length; n++) { pool[n].box.style.display = 'none'; pool[n].line.style.display = 'none'; }\n }\n\n function hideESP() {\n for (let i = 0; i < pool.length; i++) {\n pool[i].box.style.display = 'none';\n if (pool[i].line) pool[i].line.style.display = 'none';\n }\n }\n\n function flyStep(dt) {\n const p = G.player;\n if (!flyInit) { flyPos.copy(p.pos); flyInit = true; }\n const q = G.camera.quaternion;\n vFwd.set(0, 0, -1).applyQuaternion(q);\n vRight.set(1, 0, 0).applyQuaternion(q);\n vDir.set(0, 0, 0);\n if (HELD['KeyW']) vDir.add(vFwd);\n if (HELD['KeyS']) vDir.sub(vFwd);\n if (HELD['KeyD']) vDir.add(vRight);\n if (HELD['KeyA']) vDir.sub(vRight);\n if (HELD['Space']) vDir.y += 1;\n if (HELD['ControlLeft'] || HELD['ControlRight'] || HELD['KeyC']) vDir.y -= 1;\n const boost = (HELD['ShiftLeft'] || HELD['ShiftRight']) ? 3.2 : 1;\n if (vDir.lengthSq() > 1e-6) {\n vDir.normalize().multiplyScalar(S.flySpeed * boost * dt);\n flyPos.add(vDir);\n }\n p.pos.copy(flyPos);\n p.vel.set(0, 0, 0);\n p.onGround = true;\n p.eye.set(flyPos.x, flyPos.y + 1.68, flyPos.z);\n G.camera.position.copy(p.eye);\n G.camera.updateMatrixWorld();\n }\n\n const origGameUpdate = G.game.update;\n G.game.update = function (dt) {\n const r = origGameUpdate.call(this, dt);\n try { cheatFrame(dt || 0.016); } catch (e) { }\n return r;\n };\n\n const origPlayerUpdate = G.player.update;\n G.player.update = function (dt) {\n const r = origPlayerUpdate.call(this, dt);\n try {\n const playing = G.game && (G.game.state === 'playing' || G.game.state === 'intro');\n if (S.fly && playing && G.player.enabled !== false) flyStep(dt || 0.016);\n else if (S.fly) flyPos.copy(G.player.pos);\n } catch (e) { }\n return r;\n };\n\n function cheatFrame(dt) {\n const p = G.player;\n const playing = G.game && (G.game.state === 'playing' || G.game.state === 'intro');\n\n updateVelocities(dt);\n applyMoveSpeed();\n tickFps();\n\n if (S.god) {\n p.invulnerable = true;\n if (p.alive && p.health < p.maxHealth) p.health = p.maxHealth;\n }\n\n if (S.ammo) {\n const slots = G.weapon.slots || [];\n for (let i = 0; i < slots.length; i++) {\n const s = slots[i];\n if (!s) continue;\n s.reserve = Infinity;\n if (s.ammo < s.magSize) s.ammo = s.magSize;\n }\n }\n\n if (S.aim && S.aimLock && aimHeld() && playing) aimLockStep(dt);\n if (S.esp && playing) drawESP(); else hideESP();\n updateRing(playing);\n updateMini();\n }\n\n let fpsFrames = 0, fpsT = performance.now(), fpsVal = 0;\n let miniText = '';\n\n function tickFps() {\n fpsFrames++;\n const now = performance.now();\n const dtms = now - fpsT;\n if (dtms >= 500) {\n fpsVal = fpsFrames * 1000 / dtms;\n fpsFrames = 0;\n fpsT = now;\n }\n }\n\n function updateMini() {\n if (!miniEl) return;\n if (S.panel) {\n if (miniEl.style.display !== 'none') miniEl.style.display = 'none';\n return;\n }\n miniEl.style.display = 'block';\n let alive = 0;\n const list = G.enemies.list || [];\n for (let i = 0; i < list.length; i++) if (list[i] && list[i].alive && list[i].active !== false) alive++;\n const txt = '敌人 ' + alive + ' · FPS ' + Math.round(fpsVal) + '';\n if (txt !== miniText) {\n miniText = txt;\n miniEl.innerHTML = txt;\n }\n }\n\n addEventListener('keydown', e => {\n if (binding) {\n e.preventDefault(); e.stopPropagation();\n if (e.code === 'Escape') {\n binding = false;\n if (bindBtn) { bindBtn.classList.remove('binding'); }\n refreshChecks();\n return;\n }\n if (e.code === 'AltLeft' || e.code === 'AltRight') return;\n S.aimKey = e.code;\n binding = false;\n queueSave();\n try { localStorage.setItem('saltline.cheat.key', S.aimKey); } catch (_) { }\n if (bindBtn) bindBtn.classList.remove('binding');\n refreshChecks();\n toast('自瞄按键:' + keyLabel(S.aimKey));\n return;\n }\n HELD[e.code] = true;\n\n if (e.code === 'End') {\n togglePanel();\n e.preventDefault(); e.stopPropagation();\n }\n }, true);\n\n addEventListener('keyup', e => {\n HELD[e.code] = false;\n if (binding && (e.code === 'Escape')) { binding = false; refreshChecks(); }\n }, true);\n\n addEventListener('mousedown', e => {\n HELD['Mouse' + (e.button + 1)] = true;\n if (!binding) return;\n e.preventDefault(); e.stopPropagation();\n S.aimKey = 'Mouse' + (e.button + 1);\n binding = false;\n queueSave();\n try { localStorage.setItem('saltline.cheat.key', S.aimKey); } catch (_) { }\n if (bindBtn) bindBtn.classList.remove('binding');\n refreshChecks();\n toast('自瞄按键:' + keyLabel(S.aimKey));\n }, true);\n\n addEventListener('mouseup', e => { HELD['Mouse' + (e.button + 1)] = false; }, true);\n addEventListener('blur', () => { for (const k in HELD) HELD[k] = false; });\n\n function parseParams() {\n const q = new URLSearchParams(location.search);\n const c = q.get('cheat');\n if (c) {\n c.split(/[,|+ ]+/).forEach(k => {\n if (k && k in S && typeof S[k] === 'boolean') S[k] = true;\n });\n }\n if (q.get('cheatfov')) S.aimFov = clamp(+q.get('cheatfov'), 1, 90);\n if (q.get('cheatstr')) S.aimStrength = clamp(+q.get('cheatstr'), 5, 100);\n if (q.get('cheatfly')) S.flySpeed = clamp(+q.get('cheatfly'), 1, 80);\n if (q.get('cheatkey')) S.aimKey = q.get('cheatkey');\n if (q.get('cheatmspeed')) S.speedMul = clamp(+q.get('cheatmspeed'), 0.5, 5);\n if (q.get('cheatjump')) S.jumpMul = clamp(+q.get('cheatjump'), 0.5, 10);\n if (q.get('cheatpanel') === '0' && panelEl) { S.panel = false; panelEl.style.display = 'none'; }\n if (panelEl) {\n const fov = panelEl.querySelector('[data-fov]');\n const str = panelEl.querySelector('[data-str]');\n const spd = panelEl.querySelector('[data-spd]');\n if (fov) { fov.value = S.aimFov; panelEl.querySelector('[data-fovv]').textContent = S.aimFov + '°'; }\n if (str) { str.value = S.aimStrength; panelEl.querySelector('[data-strv]').textContent = S.aimStrength + '%'; }\n if (spd) { spd.value = S.flySpeed; panelEl.querySelector('[data-spdv]').textContent = S.flySpeed; }\n const ms = panelEl.querySelector('[data-mspeed]');\n const jp = panelEl.querySelector('[data-jump]');\n if (ms) { ms.value = S.speedMul; panelEl.querySelector('[data-mspeedv]').textContent = S.speedMul.toFixed(1) + '×'; }\n if (jp) { jp.value = S.jumpMul; panelEl.querySelector('[data-jumpv]').textContent = S.jumpMul.toFixed(1) + '×'; }\n }\n refreshChecks();\n }\n\n loadCfg();\n try {\n const savedKey = localStorage.getItem('saltline.cheat.key');\n if (savedKey && S.aimKey === 'Mouse3') S.aimKey = savedKey;\n } catch (_) { }\n\n parseParams();\n\n let touchNow = isTouchDevice();\n if (touchNow && !new URLSearchParams(location.search).has('cheatpanel')) S.panel = false;\n if (panelEl) panelEl.style.display = S.panel ? '' : 'none';\n\n mountUI();\n injectSettingsRow();\n const rowTimer = setInterval(() => {\n if (injectSettingsRow()) clearInterval(rowTimer);\n }, 400);\n setTimeout(() => clearInterval(rowTimer), 120000);\n addEventListener('resize', mountUI);\n\n }\n})();\n"; if (window.__SLP_LOADER__) return; window.__SLP_LOADER__ = true; function injectByTag() { if (document.getElementById('slcheat-core')) return true; try { var s = document.createElement('script'); s.id = 'slcheat-core'; s.textContent = CORE; (document.head || document.documentElement || document.body).appendChild(s); s.remove(); return true; } catch (e) { return false; } } function injectByEval() { try { (0, eval)(CORE); return true; } catch (e) { return false; } } function boot() { if (!injectByTag()) injectByEval(); } if (document.documentElement) boot(); else addEventListener('DOMContentLoaded', boot, { once: true }); setTimeout(function () { if (!document.getElementById('slcheat-panel') && !window.__SLP__) boot(); }, 2500); })();