// ============================================================ // 3DTank 工具链 Bundle (自动生成,请勿手动编辑) // 构建: python build_bundle.py // 用法: 全选粘贴到 3DTank 游戏页面控制台 // 操作: Alt+T 打开控制面板 | 左上角 HUD 显示状态 // ============================================================ // ===== hook_gamemode.js ===== // ===== GameMode Hook 脚本 v2(快速获取版)===== // 改进: // 1. 每次 yht 调用都更新引用(解决新战斗中旧引用失效问题) // 2. ght.prototype 方法每次调用都更新 // 3. 定时器主动验证引用有效性,失效则重置 // 4. trySetGameMode 验证坦克列表,避免捕获未初始化实例 // // 原理: // module_87072.$_$.ac = yht 函数(接收场景,返回 ght 实例) // module_87072.$_$.zb = ght 类(GameMode 组件类) // module_87072.$_$.bc = kht 函数(接收场景,返回 GameMode entity) // yht(scene).xzq_1 = Tht 实例(坦克列表管理器) // Tht.z11b() = a12c_1(坦克列表,Lx 实例) (function() { console.log('===== GameMode Hook v2 开始 ====='); // 1. 检查 webpack 内部对象 if (!window.__wp_require__) { console.error('[错误] window.__wp_require__ 不存在'); return; } // 2. 获取 module_87072 的导出 var mod = window.__wp_require__(87072); if (!mod || !mod.$_$) { console.error('[错误] module_87072 未加载或无 $_$ 导出'); return; } var ghtClass = mod.$_$.zb; var yhtFunc = mod.$_$.ac; console.log('[1] ght 类:', ghtClass ? ghtClass.name : '未找到'); console.log('[1] yht 函数:', typeof yhtFunc === 'function' ? '已找到' : '未找到'); if (!ghtClass) { console.error('[错误] ght 类未导出'); return; } // ===== 辅助:设置 gameMode(无条件设置,坦克列表单独处理)===== function trySetGameMode(gm, source) { if (!gm) return false; var updated = window.__gameMode__ !== gm; window.__gameMode__ = gm; // 尝试获取坦克列表(失败也不影响 gameMode 设置) var tankCount = -1; try { var list = gm.q100_1.r11l(); tankCount = list.e1(); window.__tankList__ = list; } catch(e) { // xzq_1 或 z11b 暂时不可用,不影响 gameMode 设置 } if (updated) { console.log('%c[Hook] ght 已更新 (via ' + source + ') 坦克数:' + tankCount, 'color:#0f0;font-weight:bold'); } return true; } // ===== 3. Hook yht 函数 — 每次调用都更新 ===== if (typeof yhtFunc === 'function' && !window.__yht_hooked__) { window.__yht_hooked__ = true; var origYht = yhtFunc; mod.$_$.ac = function(t) { var result = origYht.call(this, t); if (result) trySetGameMode(result, 'yht'); return result; }; console.log('[2] yht Hook 已安装(每次调用都更新)'); } else if (window.__yht_hooked__) { console.log('[2] yht Hook 已存在'); } // ===== 4. Hook ght.prototype 方法 — 每次调用都更新 ===== if (!window.__ght_proto_hooked__) { window.__ght_proto_hooked__ = true; var proto = ghtClass.prototype; var methodCount = 0; Object.getOwnPropertyNames(proto).forEach(function(name) { if (name === 'constructor') return; var orig = Object.getOwnPropertyDescriptor(proto, name); if (!orig || typeof orig.value !== 'function') return; var origFn = orig.value; methodCount++; proto[name] = function() { trySetGameMode(this, 'ght.' + name); return origFn.apply(this, arguments); }; }); console.log('[3] ght.prototype 方法 Hook 已安装(' + methodCount + ' 个方法)'); } else { console.log('[3] ght.prototype Hook 已存在'); } // ===== 5. 定时器主动验证引用有效性 ===== if (!window.__ght_validator__) { window.__ght_validator__ = true; var validateCount = 0; setInterval(function() { validateCount++; if (window.__gameMode__) { // 验证当前引用是否有效 try { var list = window.__gameMode__.q100_1.r11l(); window.__tankList__ = list; } catch(e) { // 引用失效(新战斗/退出战斗),重置等待重新捕获 console.log('%c[验证] ght 引用失效,重置等待重新捕获', 'color:#f80;font-weight:bold'); window.__gameMode__ = null; window.__tankList__ = null; } } else { // 未捕获,前5次每秒提示 if (validateCount <= 5) { console.log('[验证] 等待 ght 捕获... (' + validateCount + ')'); } } }, 500); console.log('[4] 定时验证器已启动(每500ms)'); } else { console.log('[4] 定时验证器已存在'); } // ===== 6. 状态显示 ===== if (window.__gameMode__) { console.log('[5] 已有 ght 实例:', window.__gameMode__); if (window.__tankList__) { try { console.log(' 当前坦克数量:', window.__tankList__.e1()); } catch(e) {} } } else { console.log('[5] 等待 ght 实例捕获...'); console.log(' 提示: hook 已安装,战斗中触发事件会自动捕获'); } // ===== 7. 辅助函数 ===== window.__listTanks__ = function() { if (!window.__gameMode__) { console.log('ght 实例未捕获,请等待'); return; } var list = window.__gameMode__.q100_1.r11l(); var count = list.e1(); console.log('=== 坦克列表 (' + count + " 个) ==="); for (var i = 0; i < count; i++) { var tank = list.g1(i); console.log('[' + i + ']', tank); } return list; }; window.__probeTank__ = function(idx) { if (!window.__gameMode__) { console.log('ght 实例未捕获'); return; } var list = window.__gameMode__.q100_1.r11l(); var tank = list.g1(idx || 0); console.log('坦克[' + (idx || 0) + ']:', tank); console.log('字段:', Object.keys(tank)); Object.keys(tank).forEach(function(k) { var v = tank[k]; var type = typeof v; if (type === 'number' || type === 'string' || type === 'boolean') { console.log(' ' + k + ' = ' + v + ' (' + type + ')'); } else if (v === null) { console.log(' ' + k + ' = null'); } else if (type === 'object') { console.log(' ' + k + ' = [object] (' + (v.constructor ? v.constructor.name : typeof v) + ')'); } else if (type === 'function') { console.log(' ' + k + ' = [function]'); } }); return tank; }; // 主动刷新/检查 ght 实例 window.__refreshGameMode__ = function() { if (window.__gameMode__) { try { var list = window.__gameMode__.q100_1.r11l(); console.log('%c[刷新] ght 引用有效,坦克数:' + list.e1(), 'color:#0f0;font-weight:bold'); return window.__gameMode__; } catch(e) { console.log('%c[刷新] 旧引用失效,已重置', 'color:#f80;font-weight:bold'); window.__gameMode__ = null; window.__tankList__ = null; } } console.log('[刷新] 等待 hook 捕获...(战斗中触发事件即可)'); return null; }; console.log('\n===== Hook v2 安装完成 ====='); console.log('辅助函数:'); console.log(' __listTanks() - 列出所有坦克'); console.log(' __probeTank(idx) - 探查指定坦克的字段'); console.log(' __refreshGameMode__() - 刷新/检查 ght 实例'); console.log(' window.__gameMode__ - ght 实例(自动更新)'); console.log(' window.__tankList__ - 坦克列表(自动更新)'); })(); // ===== hornet_lib.js ===== // ===== 3DTank公共库 - 工具文件共享辅助函数 ===== // 在浏览器控制台运行(需先运行 hook_gamemode.js) // 功能:提供坦克查询、队伍判断、相机信息、3D投影等公共函数 // 用法:各工具文件通过 var lib = window.__hornetLib__ 访问 (function() { console.log('===== 3DTank公共库加载 ====='); if (!window.__gameMode__) { console.warn('[公共库] window.__gameMode__ 暂未捕获,工具启动时将失败。hook_gamemode.js 已安装 Hook,战斗中会自动捕获'); } // ===== 缓存队伍判断函数(ZCt/m2Fn) ===== var ZCt = null, m2Fn = null; // 加载队伍判断函数(从 module_87072 导出) // ZCt (mod.$_$.kd): 从坦克对象获取队伍ID // m2 (mod.$_$.rk): 判断两个队伍ID是否相同且非中立 = t.equals(n) && !t.equals(E$()) function LoadTeamFunctions() { if (ZCt && m2Fn) return { ZCt: ZCt, m2Fn: m2Fn }; try { var mod87072 = window.__wp_require__(87072); if (mod87072 && mod87072.$_$) { ZCt = mod87072.$_$.kd; m2Fn = mod87072.$_$.rk; console.log('[公共库] 队伍判断函数已加载: ZCt=' + (typeof ZCt) + ', m2=' + (typeof m2Fn)); } } catch(e) { console.warn('[公共库] 加载队伍判断函数失败:', e.message); } return { ZCt: ZCt, m2Fn: m2Fn }; } // ===== 从 entity 获取包含指定字段的组件 ===== // 遍历 entity.wyj_1.r2a_1(Map<组件标识, 组件列表>),value 是列表类(s_),g1(0) 获取实际组件 // 注意:用 !== undefined 检查字段存在性,避免 falsy 值(0/""/null/false)漏判 function FindComponentByField(entity, fieldName) { if (!entity || !entity.pyt_1 || !entity.pyt_1.s2a_1) return null; var result = null; entity.pyt_1.s2a_1.forEach(function(valueList, key) { if (result) return; try { var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList; if (comp && comp[fieldName] !== undefined) result = comp; } catch(e) {} }); return result; } // ===== 获取坦克位置(G$t 物理组件的 pz7_1 = O$向量) ===== // 返回 {x, y, z},x/y 水平面,z 高度 function GetTankPosition(tank) { var physicsComp = FindComponentByField(tank, 'gzh_1'); if (physicsComp && physicsComp.gzh_1) { return { x: physicsComp.gzh_1.f20_1, y: physicsComp.gzh_1.g20_1, z: physicsComp.gzh_1.h20_1 }; } return null; } // ===== 获取坦克状态值(Yjt 组件的 vzr_1.k3_1) ===== // 状态枚举:0=DEAD, 1=DEAD_PHANTOM, 2=SEMI_ACTIVE, 3=ACTIVE, 4=DISABLED // Yjt 特征:有 azs_1(状态机)和 vzr_1(状态枚举)字段 function GetTankState(tank) { if (!tank || !tank.pyt_1 || !tank.pyt_1.s2a_1) return -1; var tankComp = null; tank.pyt_1.s2a_1.forEach(function(valueList, key) { if (tankComp) return; try { var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList; if (comp && comp.t101_1 && comp.o101_1 !== undefined) tankComp = comp; } catch(e) {} }); if (tankComp && tankComp.o101_1) return tankComp.o101_1.k3_1; return -1; } // 可见/战斗中的坦克 = ACTIVE(3) 或 SEMI_ACTIVE(2) // 依据:引擎用 tankComponent.y147()(= SEMI_ACTIVE || ACTIVE)决定该坦克节点是否渲染, // 只认 ACTIVE 会漏掉半活跃状态(出生/过渡)的坦克 function IsTankAlive(tank) { var s = GetTankState(tank); return s === 2 || s === 3; } // ===== 获取坦克名称(yot 用户信息组件) ===== // s120_1 = userName, u120_1 = clanTag(可为null), t120_1 = userRank function GetTankName(tank) { var userComp = FindComponentByField(tank, 'k12a_1'); if (!userComp) return null; var name = userComp.k12a_1 || ''; var clanTag = userComp.m12a_1 || ''; return clanTag ? (clanTag + ' ' + name) : name; } // ===== 获取坦克血量(Vkt 血量组件) ===== // f122_1 = 最大血量(max), g122_1 = 当前血量(current, 死亡时=0) function GetTankHealth(tank) { var healthComp = FindComponentByField(tank, 'y12b_1'); if (!healthComp) return null; var max = healthComp.x12b_1 || 0; var cur = healthComp.y12b_1 || 0; if (max <= 0) return null; return { current: cur, max: max, percent: cur / max }; } // ===== 获取玩家坦克 ===== // 优先 ezr_1(当前观察坦克),备用 fzr_1(本地控制坦克),再备用坦克列表第一个 // checkPosition: 是否检查位置有效性(默认true;测试脚本传false以在死亡状态控制) function GetPlayerTank(gm, checkPosition) { if (checkPosition === undefined) checkPosition = true; var t = gm.x100_1; if (t && (!checkPosition || GetTankPosition(t))) return t; t = gm.y100_1; if (t && (!checkPosition || GetTankPosition(t))) return t; try { var tl = gm.q100_1.r11l(); if (tl.e1() > 0) { t = tl.g1(0); if (t && (!checkPosition || GetTankPosition(t))) return t; } } catch(e) {} return null; } // ===== 判断是否友方 ===== // 策略1(优先):ZCt 获取双方队伍ID,m2 比较(同队且非中立) // 策略2(备用):比较 gm.gzr_1(本地队伍ID)与坦克队伍ID function IsFriendly(tank, playerTank, gm) { if (!tank || !playerTank) return false; if (ZCt && m2Fn) { try { var tankTeam = ZCt(tank); var playerTeam = ZCt(playerTank); if (tankTeam && playerTeam) return m2Fn(tankTeam, playerTeam); } catch(e) {} } try { if (gm && gm.z100_1 && ZCt) { var team = ZCt(tank); if (team && gm.z100_1) { return team.equals(gm.z100_1) && m2Fn ? m2Fn(team, gm.z100_1) : team.equals(gm.z100_1); } } } catch(e) {} return false; } // ===== 获取相机信息($r/gq 相机组件) ===== // 遍历 GameMode entity 和所有坦克 entity 查找活跃相机组件 // 策略1:查找 $r/gq 相机组件(有 kym_1 位置 和 lym_1 方向) // 策略2:查找 k7 相机控制器(有 qz9_1 和 gz9_1),从 gz9_1 获取相机组件 // 返回 {yaw, pitch, fov, pos:{x,y,z}} // 注意:lym_1.h20_1 是 roll(绕Z),lym_1.f20_1 是 pitch(绕X) function GetCameraInfo(gm) { var entities = []; try { var gameScene = gm.jyw().iyt_1; var gmEntity = gameScene.xyu("GameMode"); if (gmEntity) entities.push(gmEntity); } catch(e) {} try { var tankList = gm.q100_1.r11l(); var tCount = tankList.e1(); for (var i = 0; i < tCount; i++) { var t = tankList.g1(i); if (t) entities.push(t); } } catch(e) {} for (var i = 0; i < entities.length; i++) { var entity = entities[i]; if (!entity || !entity.pyt_1 || !entity.pyt_1.s2a_1) continue; var found = null; entity.pyt_1.s2a_1.forEach(function(valueList, key) { if (found) return; try { var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList; if (!comp) return; // 策略1:$r/gq 相机组件 if (comp.cyw_1 && comp.dyw_1 && comp.cyw_1.f20_1 !== undefined && comp.dyw_1.f20_1 !== undefined) { if (Math.abs(comp.dyw_1.h20_1) > 0.001 || Math.abs(comp.dyw_1.f20_1) > 0.001) { found = comp; return; } } // 策略2:k7 相机控制器 if (comp.jzj_1 && comp.zzi_1) { var camComp = comp.zzi_1; if (camComp && camComp.cyw_1 && camComp.dyw_1 && (Math.abs(camComp.dyw_1.h20_1) > 0.001 || Math.abs(camComp.dyw_1.f20_1) > 0.001)) { found = camComp; return; } } } catch(e) {} }); if (found) { return { yaw: found.dyw_1.h20_1, pitch: found.dyw_1.f20_1, fov: found.byw_1 || 1.0471976, pos: { x: found.cyw_1.f20_1, y: found.cyw_1.g20_1, z: found.cyw_1.h20_1 } }; } } return null; } // ===== 获取全局碰撞检测器(uMt 组件,挂在 GameMode entity 上) ===== // g16x(startVec, dirVec, maxDist, resultObj, filterObj) = 全局射线检测 function GetUMtComponent(gm) { try { var gmEntity = gm.jyw(); if (!gmEntity || !gmEntity.pyt_1 || !gmEntity.pyt_1.s2a_1) return null; var uMt = null; gmEntity.pyt_1.s2a_1.forEach(function(valueList, key) { if (uMt) return; try { var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList; if (comp && typeof comp.m179 === 'function') uMt = comp; } catch(e) {} }); return uMt; } catch(e) { return null; } } // ===== 构造 mock O$ 向量(用于射线检测接口) ===== function MkVec(x, y, z) { return { f20_1: x || 0, g20_1: y || 0, h20_1: z || 0, k26: function(t) { this.f20_1 = t.f20_1; this.g20_1 = t.g20_1; this.h20_1 = t.h20_1; }, n23: function(x, y, z) { this.f20_1 = x; this.g20_1 = y; this.h20_1 = z; return this; } }; } // ===== 射线检测:返回命中距离(-1=未命中/异常) ===== // 从 fromPos 沿 (dirX,dirY,dirZ) 方向,最大距离 maxDist function RaycastDistance(gm, fromPos, dirX, dirY, dirZ, maxDist) { var uMt = GetUMtComponent(gm); if (!uMt) return -1; try { var startVec = MkVec(fromPos.x, fromPos.y, fromPos.z); var dirVec = MkVec(dirX, dirY, dirZ); var result = { y178_1: MkVec(0, 0, 0), z178_1: MkVec(0, 0, 0), a179_1: maxDist, c179_1: null, b179_1: null, d179: function(t, n, i, r, e) { this.y178_1.k26(t); this.z178_1.k26(n); this.a179_1 = i; this.c179_1 = r; this.b179_1 = e; }, d3: function() { this.b179_1 = null; } }; var filter = { o1co_1: { h1: function() { return false; }, d3: function() {} }, w179: function() { return true; }, d3: function() { this.o1co_1.d3(); }, p1co: function() {} }; var hit = uMt.m179(startVec, dirVec, maxDist, result, filter); return (hit && result.a179_1 < maxDist) ? result.a179_1 : -1; } catch(e) { return -1; } } // ===== 3D 到 2D 投影(用于 UI 标记) ===== // 标准透视投影:forward/right/up 三轴正交基 function Project3D(worldPos, camPos, yaw, pitch, fov, screenW, screenH) { var cp = Math.cos(pitch); var sp = Math.sin(pitch); var cy = Math.cos(yaw); var sy = Math.sin(yaw); var forward = { x: cp * (-sy), y: cp * cy, z: sp }; var right = { x: cy, y: sy, z: 0 }; var up = { x: right.y * forward.z - right.z * forward.y, y: right.z * forward.x - right.x * forward.z, z: right.x * forward.y - right.y * forward.x }; var relX = worldPos.x - camPos.x; var relY = worldPos.y - camPos.y; var relZ = worldPos.z - camPos.z; var camX = relX * right.x + relY * right.y + relZ * right.z; var camY = relX * up.x + relY * up.y + relZ * up.z; var camZ = relX * forward.x + relY * forward.y + relZ * forward.z; if (camZ <= 0.1) return null; var focalLength = screenH / (2 * Math.tan(fov / 2)); var screenX = screenW / 2 + (camX / camZ) * focalLength; var screenY = screenH / 2 - (camY / camZ) * focalLength; return { x: screenX, y: screenY, depth: camZ }; } // ===== 用世界变换矩阵变换顶点(列主序4x4矩阵) ===== function TransformVertex(matrix, x, y, z) { return { x: matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12], y: matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13], z: matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14] }; } // ===== 让元素可拖动(左键拖动,改变 left/top) ===== // 适用于固定定位的小窗口(雷达、后视镜等) // key: 可选,传入后会把窗口位置存到 localStorage(hornet_layout_)并在创建时还原 // 实现要点:Pointer Events + setPointerCapture,保证 pointerup/pointercancel 必定回到元素, // 并在 window 捕获阶段兜底,避免游戏画布抢事件后"松手仍粘着鼠标"。 function MakeDraggable(elem, key) { if (!elem) return null; elem.style.cursor = 'move'; var StorageKey = key ? ('hornet_layout_' + key) : null; function ClampX(v) { return Math.max(0, Math.min(window.innerWidth - 50, v)); } function ClampY(v) { return Math.max(0, Math.min(window.innerHeight - 30, v)); } // 还原上次保存的位置 if (StorageKey) { try { var __saved = localStorage.getItem(StorageKey); if (__saved) { var __p = JSON.parse(__saved); if (__p && typeof __p.left === 'number' && typeof __p.top === 'number') { elem.style.right = 'auto'; elem.style.transform = 'none'; elem.style.left = ClampX(__p.left) + 'px'; elem.style.top = ClampY(__p.top) + 'px'; } } } catch(e) {} } function SavePos() { if (!StorageKey) return; try { var r = elem.getBoundingClientRect(); localStorage.setItem(StorageKey, JSON.stringify({ left: Math.round(r.left), top: Math.round(r.top) })); } catch(e) {} } var Active = false; var PointerId = null; var GrabX = 0; var GrabY = 0; function MoveTo(ClientX, ClientY) { elem.style.left = ClampX(ClientX - GrabX) + 'px'; elem.style.top = ClampY(ClientY - GrabY) + 'px'; } function Begin(ClientX, ClientY) { var Rect = elem.getBoundingClientRect(); // 移除 right 定位,改用 left elem.style.right = 'auto'; elem.style.transform = 'none'; elem.style.left = Rect.left + 'px'; elem.style.top = Rect.top + 'px'; GrabX = ClientX - Rect.left; GrabY = ClientY - Rect.top; Active = true; elem.style.cursor = 'grabbing'; } function End() { if (!Active) return; Active = false; if (PointerId !== null && elem.releasePointerCapture) { try { elem.releasePointerCapture(PointerId); } catch (Err) {} } PointerId = null; elem.style.cursor = 'move'; SavePos(); } if (window.PointerEvent) { elem.addEventListener('pointerdown', function (e) { if (e.button !== 0) return; Begin(e.clientX, e.clientY); PointerId = e.pointerId; try { elem.setPointerCapture(e.pointerId); } catch (Err) {} e.preventDefault(); }); elem.addEventListener('pointermove', function (e) { if (!Active || (PointerId !== null && e.pointerId !== PointerId)) return; if (e.buttons === 0) { End(); return; } MoveTo(e.clientX, e.clientY); }); elem.addEventListener('pointerup', function () { End(); }); elem.addEventListener('pointercancel', function () { End(); }); elem.addEventListener('lostpointercapture', function () { End(); }); window.addEventListener('pointerup', function () { End(); }, true); } else { elem.addEventListener('mousedown', function (e) { if (e.button !== 0) return; Begin(e.clientX, e.clientY); e.preventDefault(); }); document.addEventListener('mousemove', function (e) { if (!Active) return; if (e.buttons === 0) { End(); return; } MoveTo(e.clientX, e.clientY); }, true); document.addEventListener('mouseup', function () { End(); }, true); } window.addEventListener('blur', End); window.addEventListener('mouseleave', End); return { End: End, IsActive: function () { return Active; } }; } // ===== 导出公共函数到 window.__hornetLib__ ===== window.__hornetLib__ = { LoadTeamFunctions: LoadTeamFunctions, FindComponentByField: FindComponentByField, GetTankPosition: GetTankPosition, GetTankState: GetTankState, IsTankAlive: IsTankAlive, GetTankName: GetTankName, GetTankHealth: GetTankHealth, GetPlayerTank: GetPlayerTank, IsFriendly: IsFriendly, GetCameraInfo: GetCameraInfo, GetUMtComponent: GetUMtComponent, MkVec: MkVec, RaycastDistance: RaycastDistance, Project3D: Project3D, TransformVertex: TransformVertex, MakeDraggable: MakeDraggable }; console.log('[公共库] 已加载,可用函数:', Object.keys(window.__hornetLib__).join(', ')); })(); // ===== hornet_radar.js ===== // ===== 3DTank雷达 - 2D俯视图雷达 ===== // 在浏览器控制台运行(需先运行 hook_gamemode.js) // 功能:在屏幕右上角显示2D雷达,显示所有坦克的相对位置 (function() { function _start() { console.log('===== 3DTank雷达启动 ====='); // 允许在未进入战局 / ght 尚未捕获时启动:先出雷达框,内容等进入战局再出 if (window.__hornetRadar__) { window.__hornetRadar__.stop(); console.log('已停止旧雷达'); } var gm = window.__gameMode__ || null; // ===== 引用公共库(hornet_lib.js) ===== var lib = window.__hornetLib__; if (!lib) { console.error('[雷达] 请先运行 hornet_lib.js'); return; } var teamFns = lib.LoadTeamFunctions(); var ZCt = teamFns.ZCt, m2Fn = teamFns.m2Fn; // 组件查找:使用公共库(已修复 falsy 值漏判 bug) var FindComponentByField = lib.FindComponentByField; // 坦克位置:使用公共库 var GetTankPosition = lib.GetTankPosition; // 坦克状态:使用公共库(0=DEAD,1=DEAD_PHANTOM,2=SEMI_ACTIVE,3=ACTIVE,4=DISABLED) var GetTankState = lib.GetTankState; // ===== 获取坦克方向(Kkt 组件的 dzc_1) ===== function getTankDirection(tank) { var hullComp = FindComponentByField(tank, 'vzl_1'); if (hullComp && hullComp.wzl_1) { return { x: hullComp.wzl_1.f20_1, y: hullComp.wzl_1.g20_1, z: hullComp.wzl_1.h20_1 }; } return null; } // ===== 获取相机方向(gq/$r 组件的 lym_1.h20_1 = 偏航角) ===== // 相机组件可能在 GameMode entity、玩家坦克或任何坦克上 // 策略1:查找 $r/gq 相机组件(有 kym_1 和 lym_1 字段) // 策略2:查找 k7 相机控制器(有 qz9_1 和 gz9_1 字段),从 gz9_1 获取相机组件 function getCameraHeading() { var entities = []; // 1. GameMode entity try { var gameScene = gm.jyw().iyt_1; var gmEntity = gameScene.xyu("GameMode"); if (gmEntity) entities.push(gmEntity); } catch(e) {} // 2. 所有坦克列表中的 entity try { var tankList = gm.q100_1.r11l(); var tCount = tankList.e1(); for (var i = 0; i < tCount; i++) { var t = tankList.g1(i); if (t) entities.push(t); } } catch(e) {} // 在每个 entity 上查找相机组件 for (var i = 0; i < entities.length; i++) { var entity = entities[i]; if (!entity || !entity.pyt_1 || !entity.pyt_1.s2a_1) continue; var found = null; entity.pyt_1.s2a_1.forEach(function(valueList, key) { if (found) return; try { var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList; if (!comp) return; // 策略1:查找 $r/gq 相机组件(有 kym_1 和 lym_1 字段) if (comp.cyw_1 && comp.dyw_1 && comp.cyw_1.f20_1 !== undefined && comp.dyw_1.f20_1 !== undefined) { // 检查是否是活跃的相机(lym_1 不全为0) if (Math.abs(comp.dyw_1.h20_1) > 0.001 || Math.abs(comp.dyw_1.f20_1) > 0.001) { found = comp; return; } } // 策略2:查找 k7 相机控制器(有 qz9_1 和 gz9_1 字段) if (comp.jzj_1 && comp.zzi_1) { var camComp = comp.zzi_1; if (camComp && camComp.cyw_1 && camComp.dyw_1 && (Math.abs(camComp.dyw_1.h20_1) > 0.001 || Math.abs(camComp.dyw_1.f20_1) > 0.001)) { found = camComp; return; } } } catch(e) {} }); if (found) { // lym_1.h20_1 = -atan2(t, n) = 偏航角 // lym_1.f20_1 = atan2(i, e) = 俯仰角 return { yaw: -found.dyw_1.h20_1, // 偏航角(转为正向) pitch: found.dyw_1.f20_1, // 俯仰角 pos: { x: found.cyw_1.f20_1, y: found.cyw_1.g20_1, z: found.cyw_1.h20_1 } }; } } return null; } // 玩家坦克:使用公共库 function GetPlayerTank() { return lib.GetPlayerTank(gm); } // 友方判断:使用公共库 function IsFriendly(tank, playerTank) { return lib.IsFriendly(tank, playerTank, gm); } // 画死亡标记(物理体仍存在但已阵亡):红色 × function DrawDeadCross(x, y) { var s = 5; ctx.strokeStyle = '#ff4444'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(x - s, y - s); ctx.lineTo(x + s, y + s); ctx.moveTo(x + s, y - s); ctx.lineTo(x - s, y + s); ctx.stroke(); } // ===== 获取全局碰撞检测器(uMt 组件,挂在 GameMode entity 上) ===== var UMtComponent = null; function getUMtComponent() { if (UMtComponent) return UMtComponent; try { var gmEntity = gm.jyw(); if (!gmEntity || !gmEntity.pyt_1 || !gmEntity.pyt_1.s2a_1) return null; gmEntity.pyt_1.s2a_1.forEach(function(valueList, key) { if (UMtComponent) return; try { var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList; if (comp && typeof comp.m179 === 'function') UMtComponent = comp; } catch(e) {} }); if (UMtComponent) console.log('[雷达] uMt 碰撞检测器已获取'); else UMtComponent = false; } catch(e) { console.warn('[雷达] 获取 uMt 失败:', e.message); } return UMtComponent || null; } // mock O$ 向量:使用公共库 var MkVec = lib.MkVec; // ===== 射线检测:从 from 点沿 dir 方向,最大距离 maxDist,返回命中距离(-1=未命中) ===== function raycastDistance(fromPos, dirX, dirY, dirZ, maxDist) { var uMt = getUMtComponent(); if (!uMt) return -1; try { var startVec = MkVec(fromPos.x, fromPos.y, fromPos.z); var dirVec = MkVec(dirX, dirY, dirZ); var result = { y178_1: MkVec(0,0,0), z178_1: MkVec(0,0,0), a179_1: maxDist, c179_1: null, b179_1: null, d179: function(t, n, i, r, e) { this.y178_1.k26(t); this.z178_1.k26(n); this.a179_1 = i; this.c179_1 = r; this.b179_1 = e; }, d3: function() { this.b179_1 = null; } }; var filter = { o1co_1: { h1: function() { return false; }, d3: function() {} }, w179: function() { return true; }, d3: function() {}, p1co: function() {} }; var hit = uMt.m179(startVec, dirVec, maxDist, result, filter); return (hit && result.a179_1 < maxDist) ? result.a179_1 : -1; } catch(e) { return -1; } } // ===== 地形高度采样(分帧,双缓冲) ===== // 双缓冲:重采样时写 newHeights,旧 heights 继续显示,完成后原子切换 // 解决闪烁:重采样期间不清空 heights,ready 保持 true var Terrain = { gridSize: 72, // 72x72 网格 range: 30000, // 采样范围(与雷达范围一致) // 当前显示的缓冲区 heights: null, originX: 0, originY: 0, minHeight: 99999, maxHeight: -99999, // 重采样时的新缓冲区(null=未在重采样) newHeights: null, newOriginX: 0, newOriginY: 0, newMinHeight: 99999, newMaxHeight: -99999, sampling: false, sampleIndex: 0, lastSampleX: 99999, lastSampleY: 99999, samplesPerFrame: 60, rayStartZ: 5000, rayMaxDist: 10000, ready: false }; // 首次初始化(heights 为 null 时调用) function initTerrain(playerPos) { Terrain.heights = new Float32Array(Terrain.gridSize * Terrain.gridSize); for (var i = 0; i < Terrain.heights.length; i++) Terrain.heights[i] = NaN; Terrain.originX = playerPos.x; Terrain.originY = playerPos.y; Terrain.minHeight = 99999; Terrain.maxHeight = -99999; Terrain.sampleIndex = 0; Terrain.sampling = true; Terrain.ready = false; } // 开始重采样(双缓冲:不清空 heights,保持 ready 状态) function startResample(playerPos) { Terrain.newHeights = new Float32Array(Terrain.gridSize * Terrain.gridSize); for (var i = 0; i < Terrain.newHeights.length; i++) Terrain.newHeights[i] = NaN; Terrain.newOriginX = playerPos.x; Terrain.newOriginY = playerPos.y; Terrain.newMinHeight = 99999; Terrain.newMaxHeight = -99999; Terrain.sampleIndex = 0; Terrain.sampling = true; // 不动 heights/originX/originY/min/max/ready —— 旧数据继续显示,避免闪烁 } function updateTerrainSampling(playerPos) { // 首次初始化 if (!Terrain.heights) { initTerrain(playerPos); Terrain.lastSampleX = playerPos.x; Terrain.lastSampleY = playerPos.y; } else if (!Terrain.sampling) { // 玩家移动超过 range/3 重新采样(双缓冲,不阻塞显示) var dx = playerPos.x - Terrain.lastSampleX; var dy = playerPos.y - Terrain.lastSampleY; if (dx*dx + dy*dy > (Terrain.range/3)*(Terrain.range/3)) { Terrain.lastSampleX = playerPos.x; Terrain.lastSampleY = playerPos.y; startResample(playerPos); } } if (!Terrain.sampling) return; var uMt = getUMtComponent(); if (!uMt) { Terrain.sampling = false; return; } var halfRange = Terrain.range / 2; var cellSize = Terrain.range / (Terrain.gridSize - 1); var sampled = 0; // 目标缓冲区:重采样时写 newHeights,首次时写 heights var target = Terrain.newHeights || Terrain.heights; var originX = Terrain.newHeights ? Terrain.newOriginX : Terrain.originX; var originY = Terrain.newHeights ? Terrain.newOriginY : Terrain.originY; var minH = Terrain.newHeights ? Terrain.newMinHeight : Terrain.minHeight; var maxH = Terrain.newHeights ? Terrain.newMaxHeight : Terrain.maxHeight; while (Terrain.sampleIndex < Terrain.gridSize * Terrain.gridSize && sampled < Terrain.samplesPerFrame) { var gx = Terrain.sampleIndex % Terrain.gridSize; var gy = Math.floor(Terrain.sampleIndex / Terrain.gridSize); var wx = originX - halfRange + gx * cellSize; var wy = originY - halfRange + gy * cellSize; // 向下射线检测地面高度 var dist = raycastDistance( { x: wx, y: wy, z: Terrain.rayStartZ }, 0, 0, -1, Terrain.rayMaxDist ); var height = dist > 0 ? (Terrain.rayStartZ - dist) : NaN; target[Terrain.sampleIndex] = height; if (!isNaN(height)) { if (height < minH) minH = height; if (height > maxH) maxH = height; } Terrain.sampleIndex++; sampled++; } // 回写 min/max(基本类型,需手动赋值) if (Terrain.newHeights) { Terrain.newMinHeight = minH; Terrain.newMaxHeight = maxH; } else { Terrain.minHeight = minH; Terrain.maxHeight = maxH; } if (Terrain.sampleIndex >= Terrain.gridSize * Terrain.gridSize) { // 采样完成:原子切换 if (Terrain.newHeights) { Terrain.heights = Terrain.newHeights; Terrain.originX = Terrain.newOriginX; Terrain.originY = Terrain.newOriginY; Terrain.minHeight = Terrain.newMinHeight; Terrain.maxHeight = Terrain.newMaxHeight; Terrain.newHeights = null; console.log('[雷达] 地形重采样完成, 高度范围:', Terrain.minHeight.toFixed(0), '~', Terrain.maxHeight.toFixed(0)); } else { console.log('[雷达] 地形采样完成, 高度范围:', Terrain.minHeight.toFixed(0), '~', Terrain.maxHeight.toFixed(0)); } Terrain.sampling = false; Terrain.ready = true; } } var _terrainCanvas = null; function drawTerrain(ctx, playerPos, playerAngle) { if (!Terrain.ready || !Terrain.heights) return; if (!_terrainCanvas) { _terrainCanvas = document.createElement('canvas'); _terrainCanvas.width = radius * 2; _terrainCanvas.height = radius * 2; } var tCtx = _terrainCanvas.getContext('2d'); var w = radius * 2; var imgData = tCtx.createImageData(w, w); var data = imgData.data; var cosA = Math.cos(playerAngle); var sinA = Math.sin(playerAngle); var halfRange = Terrain.range / 2; var cellSize = Terrain.range / (Terrain.gridSize - 1); var heightRange = Terrain.maxHeight - Terrain.minHeight; if (heightRange < 1) heightRange = 1; var originX = Terrain.originX - halfRange; var originY = Terrain.originY - halfRange; var scale = Terrain.range / (radius * 2); // 雷达像素→世界距离 // 超出网格范围/未采样点的默认暗色(避免空洞) var DEF_R = 15, DEF_G = 20, DEF_B = 35, DEF_A = 180; // 遍历每个雷达像素,反算世界坐标,双线性插值查询高度图 // 海拔配色:蓝(低)→绿(中)→紫(高) for (var py = 0; py < w; py++) { for (var px = 0; px < w; px++) { var ox = px - radius; // 相对中心x var oy = radius - py; // 相对中心y(反转) // 逆旋转(雷达→世界) var rdx = ox * cosA + oy * sinA; var rdy = -ox * sinA + oy * cosA; var wx = playerPos.x + rdx * scale; var wy = playerPos.y + rdy * scale; var idx = (py * w + px) * 4; // 双线性插值查询高度图 var gxf = (wx - originX) / cellSize; var gyf = (wy - originY) / cellSize; var gx0 = Math.floor(gxf); var gy0 = Math.floor(gyf); // 超出网格范围:填充默认暗色(不跳过,解决高度图不全) if (gx0 < 0 || gx0 >= Terrain.gridSize || gy0 < 0 || gy0 >= Terrain.gridSize) { data[idx] = DEF_R; data[idx + 1] = DEF_G; data[idx + 2] = DEF_B; data[idx + 3] = DEF_A; continue; } var gx1 = Math.min(gx0 + 1, Terrain.gridSize - 1); var gy1 = Math.min(gy0 + 1, Terrain.gridSize - 1); var fx = gxf - gx0; var fy = gyf - gy0; // 四个角的高度 var h00 = Terrain.heights[gy0 * Terrain.gridSize + gx0]; var h10 = Terrain.heights[gy0 * Terrain.gridSize + gx1]; var h01 = Terrain.heights[gy1 * Terrain.gridSize + gx0]; var h11 = Terrain.heights[gy1 * Terrain.gridSize + gx1]; // 未采样到的点:填充默认暗色 if (isNaN(h00) || isNaN(h10) || isNaN(h01) || isNaN(h11)) { data[idx] = DEF_R; data[idx + 1] = DEF_G; data[idx + 2] = DEF_B; data[idx + 3] = DEF_A; continue; } // 双线性插值 var h0 = h00 * (1 - fx) + h10 * fx; // y=gy0 行 x 方向插值 var h1 = h01 * (1 - fx) + h11 * fx; // y=gy1 行 x 方向插值 var h = h0 * (1 - fy) + h1 * fy; // y 方向插值 var norm = (h - Terrain.minHeight) / heightRange; // 蓝(50,100,230) → 绿(80,210,90) → 紫(190,80,220) var r, g, b; if (norm < 0.5) { var t = norm * 2; // 0~1 r = Math.floor(50 + (80 - 50) * t); g = Math.floor(100 + (210 - 100) * t); b = Math.floor(230 + (90 - 230) * t); } else { var t = (norm - 0.5) * 2; // 0~1 r = Math.floor(80 + (190 - 80) * t); g = Math.floor(210 + (80 - 210) * t); b = Math.floor(90 + (220 - 90) * t); } data[idx] = r; data[idx + 1] = g; data[idx + 2] = b; data[idx + 3] = 200; } } tCtx.putImageData(imgData, 0, 0); ctx.save(); ctx.globalAlpha = 0.6; ctx.drawImage(_terrainCanvas, cx - radius, cy - radius); ctx.restore(); } // ===== 创建Canvas ===== var canvas = document.createElement('canvas'); canvas.id = 'hornetRadarCanvas'; canvas.width = 320; canvas.height = 320; canvas.style.cssText = 'position:fixed;top:10px;right:10px;z-index:99999;pointer-events:auto;cursor:move;border:2px solid rgba(255,255,255,0.3);border-radius:5px;background:rgba(0,0,0,0.3);box-shadow:0 0 10px rgba(0,0,0,0.3);'; document.body.appendChild(canvas); if (lib.MakeDraggable) lib.MakeDraggable(canvas, 'radar'); var ctx = canvas.getContext('2d'); var cx = canvas.width / 2; var cy = canvas.height / 2; var radius = 150; var radarRange = 30000; // 雷达范围(世界单位),可通过 Alt+/Alt- 调整 var RADAR_MIN = 3000, RADAR_MAX = 60000, RADAR_STEP = 1.5; // 从 localStorage 恢复范围 try { var __savedRR = localStorage.getItem('hornet_radar_range'); if (__savedRR) { var __rr = parseFloat(__savedRR); if (!isNaN(__rr) && __rr >= RADAR_MIN && __rr <= RADAR_MAX) radarRange = __rr; } } catch(e) {} // 透明度 var RadarOpacity = 1.0; try { var __savedRO = localStorage.getItem('hornet_radar_opacity'); if (__savedRO) { var __ro = parseFloat(__savedRO); if (!isNaN(__ro) && __ro >= 0.2 && __ro <= 1) { RadarOpacity = __ro; canvas.style.opacity = String(__ro); } } } catch(e) {} // ===== Alt+/Alt- 调整雷达范围(Alt+ 放大看局部,Alt- 缩小看全场) ===== function adjustRange(zoomIn) { var newRange = zoomIn ? Math.max(RADAR_MIN, radarRange / RADAR_STEP) : Math.min(RADAR_MAX, radarRange * RADAR_STEP); if (newRange === radarRange) return; radarRange = newRange; try { localStorage.setItem('hornet_radar_range', String(radarRange)); } catch(e) {} Terrain.range = newRange; // 强制下一帧 updateTerrainSampling 触发重采样(双缓冲:保持旧数据可见) Terrain.lastSampleX = 99999; Terrain.lastSampleY = 99999; // 不设 ready=false —— 旧 heights 继续显示,避免闪烁 // 若正在采样则中止,用新 range 重新开始 Terrain.newHeights = null; Terrain.sampling = false; console.log('[雷达] 范围' + (zoomIn ? '缩小(放大局部)' : '扩大(看全场)') + ': ' + (radarRange / 100).toFixed(0) + 'm'); } var _keyHandler = function(e) { if (!e.altKey) return; var key = e.key.toLowerCase(); if (key === '+' || key === '=' || e.code === 'NumpadAdd' || e.code === 'Equal') { e.preventDefault(); adjustRange(true); } else if (key === '-' || key === '_' || e.code === 'NumpadSubtract' || e.code === 'Minus') { e.preventDefault(); adjustRange(false); } }; window.addEventListener('keydown', _keyHandler); // ===== 雷达状态 ===== var radar = { canvas: canvas, ctx: ctx, running: true, frameCount: 0, tankCount: 0, playerFound: false, lastPlayerPos: null, // 上一帧玩家位置(用于计算朝向) playerHeading: 0, // 玩家朝向角度(弧度) stop: function() { this.running = false; if (canvas.parentNode) canvas.parentNode.removeChild(canvas); window.removeEventListener('keydown', _keyHandler); console.log('雷达已停止'); }, setRange: function(r) { var n = parseFloat(r); if (!isNaN(n) && n >= RADAR_MIN && n <= RADAR_MAX) { radarRange = n; try { localStorage.setItem('hornet_radar_range', String(n)); } catch(e) {} Terrain.range = n; Terrain.lastSampleX = 99999; Terrain.lastSampleY = 99999; Terrain.newHeights = null; Terrain.sampling = false; console.log('[雷达] 范围设为: ' + (n/100).toFixed(0) + 'm'); } else { console.warn('[雷达] 范围应在 ' + (RADAR_MIN/100) + '~' + (RADAR_MAX/100) + 'm'); } }, getRange: function() { return radarRange; }, setOpacity: function(o) { var n = parseFloat(o); if (!isNaN(n) && n >= 0.2 && n <= 1) { RadarOpacity = n; canvas.style.opacity = String(n); try { localStorage.setItem('hornet_radar_opacity', String(n)); } catch(e) {} } else { console.warn('[雷达] 透明度应在 0.2~1'); } }, getOpacity: function() { return RadarOpacity; } }; window.__hornetRadar__ = radar; // ===== 渲染循环 ===== function render() { if (!radar.running) return; radar.frameCount++; // ===== 新战斗/新地图自刷新:gameMode 实例变化时重置缓存并重新采样地形 ===== var NewGm = window.__gameMode__; if (NewGm && NewGm !== gm) { gm = NewGm; UMtComponent = null; // 碰撞检测器需从新场景重新获取 radar.lastPlayerPos = null; radar.playerHeading = 0; Terrain.heights = null; // 丢弃旧地图高度图,触发重新采样 Terrain.newHeights = null; Terrain.sampling = false; Terrain.ready = false; Terrain.lastSampleX = 99999; Terrain.lastSampleY = 99999; radar.tankCount = 0; console.log('[雷达] 检测到新战斗/新地图,已重置缓存并重新采样地形'); } // ght 尚未捕获 / 不在战局:仍画静态雷达框,内容留空(等进入战局自动出) var Ready = !!gm; try { var tankList = null, count = 0; if (Ready) { tankList = gm.q100_1.r11l(); count = tankList.e1(); } radar.tankCount = count; // 获取玩家坦克(带备用方案) var playerTank = Ready ? GetPlayerTank() : null; radar.playerFound = (playerTank !== null && playerTank !== undefined); // 自由视角激活时,用虚拟相机位置 var fl = window.__hornetFreeLook__; var flActive = fl && typeof fl.IsActive === 'function' && fl.IsActive(); // 获取玩家位置和方向 var playerPos = flActive ? fl.GetPosition() : (playerTank ? GetTankPosition(playerTank) : null); var playerDir = flActive ? fl.GetDirection() : (playerTank ? getTankDirection(playerTank) : null); // 地形高度采样(分帧) if (playerPos) updateTerrainSampling(playerPos); // 清空Canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // 画雷达背景(方形) ctx.fillStyle = 'rgba(0, 20, 0, 0.4)'; ctx.fillRect(cx - radius, cy - radius, radius * 2, radius * 2); ctx.strokeStyle = 'rgba(100, 255, 100, 0.5)'; ctx.lineWidth = 2; ctx.strokeRect(cx - radius, cy - radius, radius * 2, radius * 2); // 画十字线 ctx.beginPath(); ctx.moveTo(cx - radius, cy); ctx.lineTo(cx + radius, cy); ctx.moveTo(cx, cy - radius); ctx.lineTo(cx, cy + radius); ctx.strokeStyle = 'rgba(100, 255, 100, 0.2)'; ctx.lineWidth = 1; ctx.stroke(); // 画距离网格线(方形) for (var r = radius / 3; r < radius; r += radius / 3) { ctx.beginPath(); ctx.moveTo(cx - radius, cy - r); ctx.lineTo(cx + radius, cy - r); ctx.moveTo(cx - radius, cy + r); ctx.lineTo(cx + radius, cy + r); ctx.moveTo(cx - r, cy - radius); ctx.lineTo(cx - r, cy + radius); ctx.moveTo(cx + r, cy - radius); ctx.lineTo(cx + r, cy + radius); ctx.strokeStyle = 'rgba(100, 255, 100, 0.15)'; ctx.stroke(); } // 获取相机朝向(偏航角)——仅在战局内 var camHeading = Ready ? getCameraHeading() : null; var playerAngle = radar.playerHeading; // 自由视角激活时,直接用虚拟相机 yaw(取反:雷达旋转方向与相机yaw相反) if (flActive && playerDir) { playerAngle = -playerDir.yaw; radar.playerHeading = playerAngle; } else if (camHeading && camHeading.yaw !== undefined) { // 使用相机偏航角作为雷达朝向 playerAngle = camHeading.yaw; radar.playerHeading = playerAngle; } else if (playerPos && radar.lastPlayerPos) { // 备用方案:通过玩家位置变化计算朝向 var ddx = playerPos.x - radar.lastPlayerPos.x; var ddy = playerPos.y - radar.lastPlayerPos.y; var moveDist = Math.sqrt(ddx * ddx + ddy * ddy); if (moveDist > 0.5) { playerAngle = Math.atan2(ddx, ddy); radar.playerHeading = playerAngle; } } if (playerPos) radar.lastPlayerPos = playerPos; radar._camHeading = camHeading; // 保存用于调试 // 绘制地形高度图 if (playerPos) drawTerrain(ctx, playerPos, playerAngle); // 绘制所有坦克 var friendlyCount = 0, enemyCount = 0, deadCount = 0; for (var i = 0; i < count; i++) { var tank = tankList.g1(i); if (!tank) continue; // 跳过玩家自己 if (playerTank && tank === playerTank) continue; var pos = GetTankPosition(tank); if (!pos || !playerPos) continue; // 计算相对位置(x和y是水平面) var dx = pos.x - playerPos.x; var dy = pos.y - playerPos.y; // 旋转到相机朝向(相机朝上=北) var cosA = Math.cos(playerAngle); var sinA = Math.sin(playerAngle); var rx = dx * cosA - dy * sinA; var ry = dx * sinA + dy * cosA; // 缩放到雷达范围 var radarX = cx + (rx / radarRange) * radius; var radarY = cy - (ry / radarRange) * radius; // 注意y轴反转(屏幕y向下) // 限制在雷达方形内 if (radarX < cx - radius) radarX = cx - radius; if (radarX > cx + radius) radarX = cx + radius; if (radarY < cy - radius) radarY = cy - radius; if (radarY > cy + radius) radarY = cy + radius; // 死亡判断(同 3D透视):可见坦克 = ACTIVE(3) 或 SEMI_ACTIVE(2);其余画红 × var tankState = GetTankState(tank); if (tankState !== 2 && tankState !== 3) { DrawDeadCross(radarX, radarY); deadCount++; continue; } // 判断友方/敌方 var friendly = IsFriendly(tank, playerTank); var color = friendly ? '#4488ff' : '#ff4444'; if (friendly) friendlyCount++; else enemyCount++; // 画坦克点 ctx.beginPath(); ctx.arc(radarX, radarY, 5, 0, 2 * Math.PI); ctx.fillStyle = color; ctx.fill(); ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 1; ctx.stroke(); } // 画玩家(中心,三角形朝上) ctx.beginPath(); ctx.moveTo(cx, cy - 7); ctx.lineTo(cx - 5, cy + 5); ctx.lineTo(cx + 5, cy + 5); ctx.closePath(); ctx.fillStyle = '#00ff00'; ctx.fill(); ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 1; ctx.stroke(); // 画北向指示 ctx.fillStyle = 'rgba(100, 255, 100, 0.8)'; ctx.font = '10px Arial'; ctx.textAlign = 'center'; ctx.fillText('N', cx, cy - radius + 12); // 等待战局提示(ght 尚未捕获 / 不在战局) if (!Ready) { ctx.fillStyle = 'rgba(255, 220, 120, 0.95)'; ctx.font = 'bold 11px Arial'; ctx.textAlign = 'center'; ctx.fillText('等待战局…', cx, cy + 4); } // 信息文本 ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; ctx.font = '9px Arial'; ctx.textAlign = 'left'; ctx.fillText('Tanks: ' + count + ' (B:' + friendlyCount + ' R:' + enemyCount + ' X:' + deadCount + ')', 5, 12); if (radar.playerFound) { ctx.fillText('Player: ' + (playerTank ? playerTank.hyt_1 : '?'), 5, 24); } else { ctx.fillText('Player: NOT FOUND', 5, 24); } ctx.fillText('Cam: ' + (radar._camHeading ? 'OK' : 'NULL'), 5, 36); ctx.fillText('Angle: ' + (radar.playerHeading * 180 / Math.PI).toFixed(0), 5, 48); ctx.fillText('Range: ' + (radarRange / 100).toFixed(0) + 'm', 5, 60); ctx.fillText('Frame: ' + radar.frameCount, 5, canvas.height - 16); ctx.fillText('Alt+/-: zoom', 5, canvas.height - 5); } catch(e) { ctx.fillStyle = 'rgba(255, 0, 0, 0.8)'; ctx.font = '10px Arial'; ctx.textAlign = 'left'; ctx.fillText('Error: ' + e.message, 5, 15); } requestAnimationFrame(render); } // 启动渲染 requestAnimationFrame(render); console.log('雷达已启动'); console.log('提示: window.__hornetRadar__.stop() 停止雷达'); console.log('提示: window.__hornetRadar__.canvas 获取Canvas元素'); } var Tool = { Start: function() { if (window.__hornetRadar__ && window.__hornetRadar__.running) return; _start(); }, Stop: function() { if (window.__hornetRadar__) window.__hornetRadar__.stop(); }, Status: function() { return (window.__hornetRadar__ && window.__hornetRadar__.running) ? '运行中' : '已停止'; }, GetRunning: function() { return !!(window.__hornetRadar__ && window.__hornetRadar__.running); }, SetRange: function(r) { if (window.__hornetRadar__) window.__hornetRadar__.setRange(r); }, GetRange: function() { return window.__hornetRadar__ ? window.__hornetRadar__.getRange() : 30000; }, SetOpacity: function(o) { if (window.__hornetRadar__) window.__hornetRadar__.setOpacity(o); }, GetOpacity: function() { return window.__hornetRadar__ ? window.__hornetRadar__.getOpacity() : 1.0; } }; window.__hornetTools__ = window.__hornetTools__ || {}; window.__hornetTools__.Radar = Tool; console.log('[3DTank] 雷达模块已加载'); })(); // ===== hornet_3d_wireframe.js ===== // ===== 3DTank3D透视 - 线框/描边双模式 ===== // 在浏览器控制台运行(需先运行 hook_gamemode.js) // 功能:在全屏叠加层上绘制敌方坦克的3D模型 // 模式: // wireframe - 完整线框(绘制所有边) // outline - 屏幕空间轮廓描边(只画外轮廓,合并所有mesh节点) // 切换:window.__hornet3DWireframe__.setMode('outline') (function() { function _start() { console.log('===== 3DTank3D透视启动 ====='); // 允许在未进入战局 / ght 尚未捕获时启动:先出叠加层与状态行,模型等进入战局再出 if (window.__hornet3DWireframe__) { window.__hornet3DWireframe__.stop(); console.log('已停止旧透视'); } if (window.__hornet3DOutline__) { window.__hornet3DOutline__.stop(); console.log('已停止旧描边透视'); } var gm = window.__gameMode__ || null; // ===== 当前模式 ===== var Mode = 'outline'; // 'wireframe' | 'outline' try { var __savedMode = localStorage.getItem('hornet_wireframe_mode'); if (__savedMode === 'wireframe' || __savedMode === 'outline') Mode = __savedMode; } catch(e) {} // ===== 线宽设置 ===== var LineWidth = 2; try { var __savedLW = localStorage.getItem('hornet_wireframe_linewidth'); if (__savedLW) { var __n = parseFloat(__savedLW); if (!isNaN(__n) && __n >= 0.5 && __n <= 8) LineWidth = __n; } } catch(e) {} // ===== 渲染模式 ===== // 'team' - 队伍模式:所有坦克按队伍渲染(蓝=友方,红=敌方) // 'whitelist' - 白名单:只有名单中的坦克渲染(按队伍颜色),其他不渲染 // 'blacklist' - 黑名单:名单中的坦克不渲染,其他按队伍颜色渲染 var HighlightMode = 'team'; try { var __savedHM = localStorage.getItem('hornet_wireframe_highlight_mode'); if (__savedHM && ['team','whitelist','blacklist'].indexOf(__savedHM) >= 0) { HighlightMode = __savedHM; } } catch(e) {} // ===== 名单 ===== var HighlightList = []; try { var __savedHL = localStorage.getItem('hornet_highlight_list'); if (__savedHL) HighlightList = JSON.parse(__savedHL); } catch(e) {} function SaveHighlightList() { try { localStorage.setItem('hornet_highlight_list', JSON.stringify(HighlightList)); } catch(e) {} } function IsInList(name) { if (!name) return false; for (var i = 0; i < HighlightList.length; i++) { if (HighlightList[i] === name) return true; } return false; } // 判断坦克是否应渲染(根据 HighlightMode) function ShouldRender(name) { switch (HighlightMode) { case 'team': return true; // 全部渲染 case 'whitelist': return IsInList(name); // 名单内渲染 case 'blacklist': return !IsInList(name); // 名单外渲染 default: return true; } } // ===== 引用公共库(hornet_lib.js) ===== var lib = window.__hornetLib__; if (!lib) { console.error('[透视] 请先运行 hornet_lib.js'); return; } var teamFns = lib.LoadTeamFunctions(); var ZCt = teamFns.ZCt, m2Fn = teamFns.m2Fn; // 公共函数别名(调用处不变) var FindComponentByField = lib.FindComponentByField; var GetTankPosition = lib.GetTankPosition; var GetTankState = lib.GetTankState; var IsTankAlive = lib.IsTankAlive; var GetTankName = lib.GetTankName; function GetPlayerTank() { return lib.GetPlayerTank(gm); } function IsFriendly(tank, playerTank) { return lib.IsFriendly(tank, playerTank, gm); } function GetCameraInfo() { // 自由视角激活时,用虚拟相机参数(位置/方向跟随鼠标) var fl = window.__hornetFreeLook__; if (fl && typeof fl.IsActive === 'function' && fl.IsActive()) { var pos = fl.GetPosition(); var dir = fl.GetDirection(); if (pos && dir) { return { pos: pos, yaw: dir.yaw, pitch: dir.pitch, fov: 1.0471976 }; } } return lib.GetCameraInfo(gm); } var Project3D = lib.Project3D; // ===== 热点优化:相机基每帧只算一次(避免逐顶点重复三角函数 + 对象分配)===== var Cam = { fx: 0, fy: 0, fz: 0, rx: 0, ry: 0, rz: 0, ux: 0, uy: 0, uz: 0, focal: 1, halfW: 0, halfH: 0, px: 0, py: 0, pz: 0 }; function UpdateCam(camInfo, screenW, screenH) { var cp = Math.cos(camInfo.pitch), sp = Math.sin(camInfo.pitch); var cy = Math.cos(camInfo.yaw), sy = Math.sin(camInfo.yaw); Cam.fx = cp * (-sy); Cam.fy = cp * cy; Cam.fz = sp; // forward Cam.rx = cy; Cam.ry = sy; Cam.rz = 0; // right Cam.ux = Cam.ry * Cam.fz - Cam.rz * Cam.fy; // up = right × forward Cam.uy = Cam.rz * Cam.fx - Cam.rx * Cam.fz; Cam.uz = Cam.rx * Cam.fy - Cam.ry * Cam.fx; Cam.focal = screenH / (2 * Math.tan(camInfo.fov / 2)); Cam.halfW = screenW / 2; Cam.halfH = screenH / 2; Cam.px = camInfo.pos.x; Cam.py = camInfo.pos.y; Cam.pz = camInfo.pos.z; } // 顶点缓冲(全局复用,杜绝每帧每顶点对象分配) var VB_Wx = null, VB_Wy = null, VB_Wz = null; // 世界坐标(法线用) var VB_Sx = null, VB_Sy = null, VB_Ok = null; // 屏幕坐标 + 可投影标志 function EnsureVerts(n) { if (!VB_Wx || VB_Wx.length < n) { var cap = Math.max(n, 2048); VB_Wx = new Float32Array(cap); VB_Wy = new Float32Array(cap); VB_Wz = new Float32Array(cap); VB_Sx = new Float32Array(cap); VB_Sy = new Float32Array(cap); VB_Ok = new Uint8Array(cap); } } // 变换 + 投影一个 mesh 节点的全部顶点到复用缓冲,返回顶点数 function PrepareNodeVerts(meshData, worldMatrix, pos) { var positions = meshData.positions; var stride = meshData.stride || 3; var vc = Math.floor(positions.length / stride); EnsureVerts(vc); var m0 = worldMatrix[0], m1 = worldMatrix[1], m2 = worldMatrix[2], m4 = worldMatrix[4], m5 = worldMatrix[5], m6 = worldMatrix[6], m8 = worldMatrix[8], m9 = worldMatrix[9], m10 = worldMatrix[10], m12 = worldMatrix[12], m13 = worldMatrix[13], m14 = worldMatrix[14]; var px = pos.x, py = pos.y, pz = pos.z; var cfx = Cam.fx, cfy = Cam.fy, cfz = Cam.fz; var crx = Cam.rx, cry = Cam.ry, crz = Cam.rz; var cux = Cam.ux, cuy = Cam.uy, cuz = Cam.uz; var focal = Cam.focal, halfW = Cam.halfW, halfH = Cam.halfH; var cpx = Cam.px, cpy = Cam.py, cpz = Cam.pz; for (var v = 0; v < vc; v++) { var bx = positions[v * stride], by = positions[v * stride + 1], bz = positions[v * stride + 2]; var wx = m0 * bx + m4 * by + m8 * bz + m12; var wy = m1 * bx + m5 * by + m9 * bz + m13; var wz = m2 * bx + m6 * by + m10 * bz + m14; VB_Wx[v] = wx; VB_Wy[v] = wy; VB_Wz[v] = wz; var ddx = wx - px, ddy = wy - py, ddz = wz - pz; if (ddx * ddx + ddy * ddy + ddz * ddz > 6250000) { VB_Ok[v] = 0; continue; } var relX = wx - cpx, relY = wy - cpy, relZ = wz - cpz; var camZ = relX * cfx + relY * cfy + relZ * cfz; if (camZ <= 0.1) { VB_Ok[v] = 0; continue; } var camX = relX * crx + relY * cry + relZ * crz; var camY = relX * cux + relY * cuy + relZ * cuz; VB_Sx[v] = halfW + (camX / camZ) * focal; VB_Sy[v] = halfH - (camY / camZ) * focal; VB_Ok[v] = 1; } return vc; } // ===== 获取 KMt / TDt 渲染组件 ===== // 新 build:车身皮肤组件字段是 w14w_1(kDt(t)=t.w14w_1,缺失报 "skin";RDt.v16b 里赋值) // 旧 build:u14l_1 / t14l_1 / p14l_1(新版 t14l_1 已改成悬停车体状态 Qmt,务必别再当皮肤用) function GetKMtComponent(entity) { var comp = FindComponentByField(entity, 'w14w_1'); if (comp) return comp; comp = FindComponentByField(entity, 'u14l_1'); if (comp) return comp; comp = FindComponentByField(entity, 't14l_1'); if (comp) return comp; comp = FindComponentByField(entity, 'p14l_1'); return comp; } // 取车身皮肤对象(含 s17b_1 部件表),跨版本兼容;stats 用于诊断来源 function GetHullSkin(tank, stats) { var comp = GetKMtComponent(tank); if (!comp) { if (stats) stats.hullSource = '无组件'; return null; } if (comp.w14w_1 && comp.w14w_1.s17b_1) { if (stats) stats.hullSource = 'w14w_1'; return comp.w14w_1; } // 新 build if (comp.t14l_1 && comp.t14l_1.s17b_1) { if (stats) stats.hullSource = 't14l_1'; return comp.t14l_1; } // 旧 build if (comp.p14l_1 && comp.p14l_1.s17b_1) { if (stats) stats.hullSource = 'p14l_1'; return comp.p14l_1; } // 旧 build if (stats) stats.hullSource = '有组件但无皮肤'; return null; } function GetTDtComponent(entity) { var comp = FindComponentByField(entity, 'n11c_1'); if (comp) return comp; comp = FindComponentByField(entity, 'q112_1'); return comp; } // ===== 从GPU读取顶点位置数据(缓冲区[0]) ===== function ReadVertexPositionsFromTp(tp) { if (!tp || !tp.t4w_1) return null; var tl = tp.t4w_1.g1(0); if (!tl || !tl.d3x_1) return null; var vq = tl.d3x_1; var bs = vq.s3h_1; if (!bs || !bs.b74_1) return null; var gl = bs.b74_1; var webglBuf = vq.r3x_1 && vq.r3x_1.z73_1; if (!webglBuf) return null; gl.bindBuffer(gl.ARRAY_BUFFER, webglBuf); var bufSize = gl.getBufferParameter(gl.ARRAY_BUFFER, gl.BUFFER_SIZE); if (bufSize <= 0) return null; var f32 = new Float32Array(bufSize / 4); gl.getBufferSubData(gl.ARRAY_BUFFER, 0, f32); return f32; } // ===== 从GPU读取索引数据 ===== function ReadIndicesFromTp(tp) { if (!tp.y4w_1 || typeof tp.y4w_1.e1 !== 'function') return null; var meshCount = tp.y4w_1.e1(); if (meshCount === 0) return null; var allIndices = []; for (var m = 0; m < meshCount; m++) { var il = tp.y4w_1.g1(m); if (!il || !il.q3p_1) continue; var fq = il.q3p_1; var bs = fq.s3h_1; if (!bs || !bs.b74_1) continue; var gl = bs.b74_1; var webglBuf = fq.j3x_1 && fq.j3x_1.z73_1; if (!webglBuf) continue; gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, webglBuf); var bufSize = gl.getBufferParameter(gl.ELEMENT_ARRAY_BUFFER, gl.BUFFER_SIZE); if (bufSize <= 0) continue; var indices = new Uint16Array(bufSize / 2); gl.getBufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, indices); allIndices.push(indices); } return allIndices; } // ===== GPU 数据缓存(按 Tp 资源引用,避免重复 GPU 读取) ===== var GpuDataCache = new Map(); var GpuCacheFrameCount = 0; var GpuCacheClearInterval = 1800; // 定期清 GPU 数据缓存(getBufferSubData 会停顿 GPU,太频繁会周期性卡顿) var StrideDiagPrinted = new WeakSet(); function GetGpuData(tp) { if (!tp) return null; if (GpuDataCache.has(tp)) return GpuDataCache.get(tp); var positions = ReadVertexPositionsFromTp(tp); var indices = ReadIndicesFromTp(tp); if (positions && indices && indices.length > 0) { // 计算最大索引值,推断顶点 stride var maxIdx = 0; for (var i = 0; i < indices.length; i++) { var idxArr = indices[i]; for (var j = 0; j < idxArr.length; j++) { if (idxArr[j] > maxIdx) maxIdx = idxArr[j]; } } var stride = 3; var vc3 = Math.floor(positions.length / 3); if (maxIdx >= vc3) { // stride=3 顶点数不够,尝试常见 interleaved 布局 var candidates = [8, 11, 5, 7, 14]; for (var c = 0; c < candidates.length; c++) { var vc = Math.floor(positions.length / candidates[c]); if (maxIdx < vc) { stride = candidates[c]; break; } } } var data = { positions: positions, indices: indices, stride: stride }; GpuDataCache.set(tp, data); if (!StrideDiagPrinted.has(tp)) { StrideDiagPrinted.add(tp); console.log('[透视][GPU数据] stride=' + stride + ', 缓冲区=' + positions.length + ' float, 最大索引=' + maxIdx + ', 顶点数=' + Math.floor(positions.length / stride)); } return data; } return null; } // ===== 递归收集mesh节点(每帧重新获取 Bp 节点引用) ===== function CollectMeshNodes(bpNode, nodes, depth, group, stats) { if (!bpNode || depth > 5) return; if (stats) stats.seen++; var name = bpNode.e35_1 || ''; if (bpNode.t3m_1) { if (stats) stats.hasMesh++; var gpuData = GetGpuData(bpNode.t3m_1); if (gpuData) { if (stats) stats.gpuOk++; nodes.push({ name: name, node: bpNode, tp: bpNode.t3m_1, positions: gpuData.positions, indices: gpuData.indices, stride: gpuData.stride, group: group }); } else if (stats) stats.gpuFail++; } if (bpNode.q4x_1 && typeof bpNode.q4x_1.e1 === 'function') { try { var lodCount = bpNode.q4x_1.e1(); for (var lod = 0; lod < lodCount && lod < 1; lod++) { var lodLevel = bpNode.q4x_1.g1(lod); if (!lodLevel) continue; var lodTp = lodLevel.t3m_1 || lodLevel.v3m_1; if (!lodTp) continue; var lodGpuData = GetGpuData(lodTp); if (lodGpuData) { if (stats) stats.gpuOk++; nodes.push({ name: name + '_lod' + lod, node: bpNode, tp: lodTp, positions: lodGpuData.positions, indices: lodGpuData.indices, stride: lodGpuData.stride, group: group }); } } } catch(e) {} } if (bpNode.p35_1 && typeof bpNode.p35_1.e1 === 'function') { var childCount = bpNode.p35_1.e1(); for (var c = 0; c < childCount; c++) { try { var child = bpNode.p35_1.g1(c); if (child) CollectMeshNodes(child, nodes, depth + 1, group, stats); } catch(e) {} } } } // ===== 提取坦克的mesh数据 ===== function GetTankMeshData(tank, stats) { var nodes = []; var skin = GetHullSkin(tank, stats); if (skin) { try { var partCount = skin.s17b_1.e1(); if (stats) stats.hullParts = partCount; for (var p = 0; p < partCount; p++) { var part = skin.s17b_1.g1(p); if (!part || !part.v17b_1) continue; var nodeCount = part.v17b_1.e1(); for (var n = 0; n < nodeCount; n++) { var bpNode = part.v17b_1.g1(n); if (!bpNode) continue; CollectMeshNodes(bpNode, nodes, 0, 'hull', stats ? stats.hull : null); } } } catch(e) {} } var tdt = GetTDtComponent(tank); if (stats) stats.turretComp = !!tdt; if (tdt && tdt.n11c_1 && tdt.n11c_1.m17a_1) { if (stats) stats.turretRoot = true; try { CollectMeshNodes(tdt.n11c_1.m17a_1, nodes, 0, 'turret', stats ? stats.turret : null); } catch(e) {} } return nodes; } // ===== 残留节点检测(世界矩阵"冻结")→ 拖拽跟随 ===== // 现象:实体已移动,但原处仍留着线框。 // 原因:引擎只为"本帧实际参与渲染"的节点更新世界矩阵,被裁剪/脱离的节点矩阵停在最后位置。 // 处理:检测到「节点矩阵平移长时间不动、而坦克在动」时**不再丢弃**(丢弃会让整辆模型只剩一个点), // 而是用「冻结时刻的相对偏移 + 坦克当前位置」平移这份冻结矩阵,让它跟着坦克走: // 既不在旧位置留残留,也不会因为引擎不更新矩阵而丢掉模型。 // 权衡:拖拽期间朝向沿用最后一次更新(位置正确、朝向可能滞后),对透视足够用。 var NodeMatrixHistory = new WeakMap(); // WeakMap:不持有节点强引用(引擎会重建节点树) var TankPrevPos = new Map(); // tankId → 上一帧位置(含位移滑动平均) var FrozenFrames = 20; // 连续不动帧数阈值(仅在坦克本身移动时才累计) var NodeMatrixScratch = new Float32Array(16); function IsNodeValid(bpNode, pos, tankIsMoving, diagStats) { var M = bpNode.n35_1; if (!M) return null; if (diagStats) diagStats.totalNodes++; var Hist = NodeMatrixHistory.get(bpNode); if (!Hist) { NodeMatrixHistory.set(bpNode, { x: M[12], y: M[13], z: M[14], still: 0, off: null }); } else { var dx = M[12] - Hist.x, dy = M[13] - Hist.y, dz = M[14] - Hist.z; if (dx * dx + dy * dy + dz * dz > 0.01) { Hist.x = M[12]; Hist.y = M[13]; Hist.z = M[14]; Hist.still = 0; Hist.off = null; // 矩阵恢复更新 → 恢复正常绘制 } else if (tankIsMoving) { Hist.still++; if (Hist.still >= FrozenFrames) { if (!Hist.off) Hist.off = { x: M[12] - pos.x, y: M[13] - pos.y, z: M[14] - pos.z }; for (var k = 0; k < 16; k++) NodeMatrixScratch[k] = M[k]; NodeMatrixScratch[12] = pos.x + Hist.off.x; NodeMatrixScratch[13] = pos.y + Hist.off.y; NodeMatrixScratch[14] = pos.z + Hist.off.z; if (diagStats) diagStats.draggedNodes++; return NodeMatrixScratch; // 冻结但跟随坦克 } } else { Hist.still = 0; } } // 位置检查(节点世界矩阵平移与坦克位置相差过大则丢弃)—— 只对"未冻结"的节点做 var nodeDx = M[12] - pos.x, nodeDy = M[13] - pos.y, nodeDz = M[14] - pos.z; if (nodeDx * nodeDx + nodeDy * nodeDy + nodeDz * nodeDz > 4000000) { if (diagStats) diagStats.filteredNodes++; return null; } return M; } // ===== 线框模式:绘制单个节点的所有边 ===== // 顶点数据来自 PrepareNodeVerts 的复用缓冲;边去重用数值键 Set(避免字符串分配) var _WfEdgeSet = new Set(); function WfAddEdge(ctx, a, b) { var lo = a < b ? a : b, hi = a < b ? b : a; var key = lo * 65536 + hi; if (_WfEdgeSet.has(key)) return; _WfEdgeSet.add(key); ctx.moveTo(VB_Sx[lo], VB_Sy[lo]); ctx.lineTo(VB_Sx[hi], VB_Sy[hi]); } function DrawWireframeNode(ctx, meshData, worldMatrix, pos) { var indicesArr = meshData.indices; var vertexCount = PrepareNodeVerts(meshData, worldMatrix, pos); _WfEdgeSet.clear(); ctx.beginPath(); for (var mi = 0; mi < indicesArr.length; mi++) { var indices = indicesArr[mi]; for (var idx = 0; idx < indices.length; idx += 3) { var i0 = indices[idx], i1 = indices[idx+1], i2 = indices[idx+2]; if (!VB_Ok[i0] || !VB_Ok[i1] || !VB_Ok[i2]) continue; WfAddEdge(ctx, i0, i1); WfAddEdge(ctx, i1, i2); WfAddEdge(ctx, i2, i0); } } ctx.stroke(); } // ===== 兜底标记点:模型采集不到/被过滤时,至少画一个点,避免坦克完全不可见 ===== function DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color) { var p = Project3D(pos, camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH); if (!p) return; ctx.fillStyle = color; ctx.beginPath(); ctx.arc(p.x, p.y, 3, 0, 2 * Math.PI); ctx.fill(); } // ===== 描边模式:屏幕空间光栅化轮廓提取 ===== // 算法(真正的"屏幕几何"方法): // 1. 把所有面向相机的三角形光栅化(扫描线填充)到像素掩码 // 2. 车身和炮塔在屏幕上重叠时,填充区域自动合并 // 3. 找出填充区域的边界像素(填充像素的4邻居中有未填充像素) // 4. 只画边界像素 = 屏幕空间外轮廓 var PixelMask = null; var PixelMaskW = 0; var PixelMaskH = 0; var MaskGen = 0; // 掩码用"代数"标记:本辆坦克填充的像素 = gen,无需在坦克之间清空整块缓冲 function GetPixelMask(w, h) { if (!PixelMask || PixelMaskW !== w || PixelMaskH !== h) { PixelMask = new Uint32Array(w * h); PixelMaskW = w; PixelMaskH = h; MaskGen = 0; } return PixelMask; } // 扫描线光栅化三角形(pixels 中写入 gen 表示"本辆坦克已填充") function RasterizeTriangle(pixels, w, h, gen, x0, y0, x1, y1, x2, y2) { // 按 y 排序三个顶点 if (y0 > y1) { var t = y0; y0 = y1; y1 = t; t = x0; x0 = x1; x1 = t; } if (y1 > y2) { var t = y1; y1 = y2; y2 = t; t = x1; x1 = x2; x2 = t; } if (y0 > y1) { var t = y0; y0 = y1; y1 = t; t = x0; x0 = x1; x1 = t; } var minY = Math.max(0, Math.ceil(y0)); var maxY = Math.min(h - 1, Math.floor(y2)); if (minY > maxY) return; for (var y = minY; y <= maxY; y++) { var xa, xb; if (y < y1) { // 上半部分:y0 -> y1 和 y0 -> y2 xa = (y1 === y0) ? x0 : x0 + (x1 - x0) * (y - y0) / (y1 - y0); xb = (y2 === y0) ? x0 : x0 + (x2 - x0) * (y - y0) / (y2 - y0); } else { // 下半部分:y1 -> y2 和 y0 -> y2 xa = (y2 === y1) ? x1 : x1 + (x2 - x1) * (y - y1) / (y2 - y1); xb = (y2 === y0) ? x0 : x0 + (x2 - x0) * (y - y0) / (y2 - y0); } if (xa > xb) { var t = xa; xa = xb; xb = t; } var minXA = Math.max(0, Math.ceil(xa - 0.5)); var maxXA = Math.min(w - 1, Math.floor(xb + 0.5)); var rowOffset = y * w; for (var x = minXA; x <= maxXA; x++) { pixels[rowOffset + x] = gen; } } } function DrawTankOutline(ctx, meshNodes, pos, tankIsMoving, screenW, screenH, diagStats, color) { var pixels = GetPixelMask(screenW, screenH); var gen = ++MaskGen; if (gen === 0) { pixels.fill(0); gen = ++MaskGen; } // 代数回绕(理论值):整体清一次 // 记录坦克的屏幕边界框(用于后续边界检测优化) var minX = screenW, minY = screenH, maxX = 0, maxY = 0; for (var ni = 0; ni < meshNodes.length; ni++) { var meshData = meshNodes[ni]; var bpNode = meshData.node; var worldMatrix = IsNodeValid(bpNode, pos, tankIsMoving, diagStats); if (!worldMatrix) continue; var indicesArr = meshData.indices; // 顶点变换+投影到复用缓冲(无对象分配) var vertexCount = PrepareNodeVerts(meshData, worldMatrix, pos); for (var v = 0; v < vertexCount; v++) { if (!VB_Ok[v]) continue; var sx = VB_Sx[v], sy = VB_Sy[v]; if (sx < minX) minX = sx; if (sy < minY) minY = sy; if (sx > maxX) maxX = sx; if (sy > maxY) maxY = sy; } // 遍历三角形,光栅化面向相机的 for (var mi = 0; mi < indicesArr.length; mi++) { var indices = indicesArr[mi]; for (var i = 0; i < indices.length; i += 3) { var a = indices[i], b = indices[i+1], c = indices[i+2]; if (!VB_Ok[a] || !VB_Ok[b] || !VB_Ok[c]) continue; // 计算法线判断是否面向相机(世界坐标,来自缓冲) var ax = VB_Wx[a], ay = VB_Wy[a], az = VB_Wz[a]; var bx = VB_Wx[b], by = VB_Wy[b], bz = VB_Wz[b]; var gx = VB_Wx[c], gy = VB_Wy[c], gz = VB_Wz[c]; var e1x = bx - ax, e1y = by - ay, e1z = bz - az; var e2x = gx - ax, e2y = gy - ay, e2z = gz - az; var nx = e1y * e2z - e1z * e2y; var ny = e1z * e2x - e1x * e2z; var nz = e1x * e2y - e1y * e2x; var cx = (ax + bx + gx) / 3; var cy = (ay + by + gy) / 3; var cz = (az + bz + gz) / 3; var dot = nx * (Cam.px - cx) + ny * (Cam.py - cy) + nz * (Cam.pz - cz); if (dot <= 0) continue; // 背向相机,跳过 // 光栅化三角形到像素掩码 RasterizeTriangle(pixels, screenW, screenH, gen, VB_Sx[a], VB_Sy[a], VB_Sx[b], VB_Sy[b], VB_Sx[c], VB_Sy[c]); } } } // 找出边界像素并绘制(只在坦克边界框内遍历;用单条路径 + 横向连续合并,替代逐像素 fillRect) var bx0 = Math.max(0, Math.floor(minX) - 1); var by0 = Math.max(0, Math.floor(minY) - 1); var bx1 = Math.min(screenW - 1, Math.ceil(maxX) + 1); var by1 = Math.min(screenH - 1, Math.ceil(maxY) + 1); ctx.fillStyle = color; ctx.beginPath(); var outlineCount = 0; for (var y = by0; y <= by1; y++) { var rowOffset = y * screenW; var runStart = -1; for (var x = bx0; x <= bx1 + 1; x++) { var isBoundary = false; if (x <= bx1 && pixels[rowOffset + x] === gen) { // 检查4个邻居是否有未填充像素(== gen 即本辆坦克填充) if (x === 0 || pixels[rowOffset + x - 1] !== gen) isBoundary = true; else if (x === screenW - 1 || pixels[rowOffset + x + 1] !== gen) isBoundary = true; else if (y === 0 || pixels[rowOffset - screenW + x] !== gen) isBoundary = true; else if (y === screenH - 1 || pixels[rowOffset + screenW + x] !== gen) isBoundary = true; } if (isBoundary) { if (runStart < 0) runStart = x; outlineCount++; } else if (runStart >= 0) { ctx.rect(runStart, y, x - runStart + 2, 3); // 横向连续的边界像素合并为一条 runStart = -1; } } } ctx.fill(); if (diagStats) diagStats.outlineEdges += outlineCount; return outlineCount; } // ===== 创建全屏Canvas ===== var canvas = document.createElement('canvas'); canvas.id = 'hornet3DWireframeCanvas'; canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:99998;pointer-events:none;'; document.body.appendChild(canvas); var ctx = canvas.getContext('2d'); function ResizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } ResizeCanvas(); window.addEventListener('resize', ResizeCanvas); // ===== 诊断统计 ===== var DiagStats = { totalNodes: 0, filteredNodes: 0, renderedNodes: 0, outlineEdges: 0 }; var LastDegradeLogFrame = -9999; // 退化诊断日志节流 // 退化诊断(节流输出):定位"存活坦克模型没画出来"的根因(皮肤/部件/GPU/炮塔逐级) function LogTankDiag(tankName, tankState, S, hullCount, turretCount, rendered) { if (esp.frameCount - LastDegradeLogFrame <= 120) return; LastDegradeLogFrame = esp.frameCount; console.warn('[透视][退化诊断] ' + tankName + ' state=' + tankState + ' | 皮肤=' + S.hullSource + ' 部件=' + S.hullParts + ' 车身[见/网格/GPU成功/失败]=' + S.hull.seen + '/' + S.hull.hasMesh + '/' + S.hull.gpuOk + '/' + S.hull.gpuFail + ' | 炮塔 组件=' + S.turretComp + ' 根节点=' + S.turretRoot + ' 炮塔[见/网格/GPU成功/失败]=' + S.turret.seen + '/' + S.turret.hasMesh + '/' + S.turret.gpuOk + '/' + S.turret.gpuFail + ' | 收集 车身=' + hullCount + ' 炮塔=' + turretCount + ' 绘制=' + rendered + ' | 本帧 总=' + DiagStats.totalNodes + ' 过滤=' + DiagStats.filteredNodes); } // ===== ESP状态 ===== var esp = { canvas: canvas, ctx: ctx, running: true, frameCount: 0, mode: Mode, setMode: function(m) { if (m === 'wireframe' || m === 'outline') { Mode = m; this.mode = m; try { localStorage.setItem('hornet_wireframe_mode', m); } catch(e) {} console.log('[透视] 模式切换为: ' + m); } else { console.warn('[透视] 未知模式: ' + m + ',支持: wireframe | outline'); } }, getMode: function() { return Mode; }, setLineWidth: function(w) { var n = parseFloat(w); if (!isNaN(n) && n >= 0.5 && n <= 8) { LineWidth = n; try { localStorage.setItem('hornet_wireframe_linewidth', String(n)); } catch(e) {} console.log('[透视] 线宽设置为: ' + n); } else { console.warn('[透视] 线宽应在 0.5 ~ 8 之间,当前: ' + w); } }, getLineWidth: function() { return LineWidth; }, setHighlightMode: function(mode) { var valid = ['team','whitelist','blacklist']; if (valid.indexOf(mode) >= 0) { HighlightMode = mode; try { localStorage.setItem('hornet_wireframe_highlight_mode', mode); } catch(e) {} console.log('[透视] 渲染模式切换为: ' + mode); } else { console.warn('[透视] 未知渲染模式: ' + mode + ',支持: ' + valid.join(', ')); } }, getHighlightMode: function() { return HighlightMode; }, setHighlightList: function(names) { HighlightList = Array.isArray(names) ? names.slice() : []; SaveHighlightList(); console.log('[透视] 高亮名单已更新:', HighlightList.length + '人'); }, addHighlight: function(name) { if (name && HighlightList.indexOf(name) < 0) { HighlightList.push(name); SaveHighlightList(); console.log('[透视] 已添加高亮:', name); } }, removeHighlight: function(name) { var idx = HighlightList.indexOf(name); if (idx >= 0) { HighlightList.splice(idx, 1); SaveHighlightList(); console.log('[透视] 已移除高亮:', name); } }, getHighlightList: function() { return HighlightList.slice(); }, clearHighlight: function() { HighlightList = []; SaveHighlightList(); console.log('[透视] 高亮名单已清空'); }, stop: function() { this.running = false; window.removeEventListener('resize', ResizeCanvas); if (canvas.parentNode) canvas.parentNode.removeChild(canvas); console.log('透视已停止'); } }; window.__hornet3DWireframe__ = esp; // ===== 金箱子标记 ===== // 金箱子是独立实体(不是坦克)。引擎侧标志: // · 补给实体 = entity.j10q_1(bonusData 组件,见 module_87072 的 ctt()) // · 类型枚举 mE(module_10032):GOLD_BOX = 6 // 做法:实体基类的组件创建方法 nyz()(module_61378「BattleEntity.createComponent」)上挂观察钩子, // 发现「带 j10q_1 且类型=6」的实体就登记,之后每帧按它自己的位置画金色标记。 // 钩子目标沿 tank 的原型链找 nyz 的宿主原型 —— 不需要任何导出键。 var GoldBoxes = []; var GoldPending = []; var EntityProtoHooked = false; var GOLD_BOX_TYPE = 6; function FindEnumOf(Obj) { for (var K in Obj) { try { var V = Obj[K]; if (V && typeof V === 'object' && typeof V.k3_1 === 'number' && typeof V.equals === 'function') return V; } catch (e) {} } return null; } // 引擎口径:有 bonusData 且类型是 GOLD_BOX function IsGoldBoxEntity(Entity) { if (!Entity || !Entity.j10q_1) return false; var Type = FindEnumOf(Entity.j10q_1); return !!(Type && Type.k3_1 === GOLD_BOX_TYPE); } function RegisterGoldBox(Entity) { for (var i = 0; i < GoldBoxes.length; i++) if (GoldBoxes[i] === Entity) return; GoldBoxes.push(Entity); console.log('[透视] 发现金箱子,已加入标记(当前 ' + GoldBoxes.length + ' 个)'); } function InstallEntityHook(Tank) { if (EntityProtoHooked || !Tank) return; var Proto = Tank; while (Proto && !Object.prototype.hasOwnProperty.call(Proto, 'nyz')) Proto = Object.getPrototypeOf(Proto); if (!Proto || typeof Proto.nyz !== 'function') return; var Orig = Proto.nyz; Proto.nyz = function () { var R = Orig.apply(this, arguments); try { if (IsGoldBoxEntity(this)) RegisterGoldBox(this); else if (this && this.j10q_1 === undefined && GoldPending.length < 300) { GoldPending.push({ e: this, f: esp.frameCount }); // 字段可能稍后才赋值,延后判定 } } catch (E) {} return R; }; EntityProtoHooked = true; console.log('[透视] 已挂补给实体观察钩子(金箱子)'); } // 金箱位置:优先物理组件(与坦克同款 gzh_1),否则退到 bonusData 里的节点世界矩阵/向量 function GetGoldBoxPosition(Entity) { var Phys = FindComponentByField(Entity, 'gzh_1'); if (Phys && Phys.gzh_1) { return { x: Phys.gzh_1.f20_1, y: Phys.gzh_1.g20_1, z: Phys.gzh_1.h20_1 }; } var Bonus = Entity.j10q_1; for (var K in Bonus) { try { var V = Bonus[K]; if (!V || typeof V !== 'object') continue; if (V.n35_1 && V.n35_1.length >= 15) { var M = V.n35_1; return { x: M[12], y: M[13], z: M[14] }; } if (typeof V.f20_1 === 'number' && typeof V.g20_1 === 'number' && typeof V.h20_1 === 'number') { return { x: V.f20_1, y: V.g20_1, z: V.h20_1 }; } } catch (e) {} } return null; } // 金色菱形 + 十字 + 「金箱」标签(穿透显示,不遮挡判断) function DrawGoldBoxMarker(Pos, ScreenW, ScreenH) { var X = Pos.x - Cam.px, Y = Pos.y - Cam.py, Z = Pos.z - Cam.pz; var CamZ = X * Cam.fx + Y * Cam.fy + Z * Cam.fz; if (CamZ <= 0.1) return false; var CamX = X * Cam.rx + Y * Cam.ry + Z * Cam.rz; var CamY = X * Cam.ux + Y * Cam.uy + Z * Cam.uz; var Sx = Cam.halfW + (CamX / CamZ) * Cam.focal; var Sy = Cam.halfH - (CamY / CamZ) * Cam.focal; if (Sx < -60 || Sy < -60 || Sx > ScreenW + 60 || Sy > ScreenH + 60) return false; var R = 12; ctx.save(); ctx.strokeStyle = '#ffd700'; ctx.fillStyle = 'rgba(255, 215, 0, 0.22)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(Sx, Sy - R); ctx.lineTo(Sx + R, Sy); ctx.lineTo(Sx, Sy + R); ctx.lineTo(Sx - R, Sy); ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.moveTo(Sx - R - 7, Sy); ctx.lineTo(Sx + R + 7, Sy); ctx.moveTo(Sx, Sy - R - 7); ctx.lineTo(Sx, Sy + R + 7); ctx.stroke(); ctx.fillStyle = '#ffd700'; ctx.font = 'bold 12px Consolas,monospace'; ctx.textAlign = 'center'; ctx.fillText('金箱', Sx, Sy - R - 9); ctx.restore(); return true; } // ===== 冻结节点"拖拽跟随"诊断(节流 2s)===== var LastFrozenLogAt = 0; function LogDragDiag(Stats) { if (!Stats.draggedNodes) return; var Now = Date.now(); if (Now - LastFrozenLogAt < 2000) return; LastFrozenLogAt = Now; console.log('[透视][冻结] 本帧拖拽跟随节点 ' + Stats.draggedNodes + '/' + Stats.totalNodes + '(引擎未更新这些节点矩阵,已按坦克位移平移,避免旧位置残留/模型丢失)'); } // ===== 渲染循环 ===== function Render() { if (!esp.running) return; esp.frameCount++; // 换图自刷新:gameMode 实例变化时重绑并清空该地图的缓存 var NewGm = window.__gameMode__; if (NewGm && NewGm !== gm) { gm = NewGm; GpuDataCache.clear(); // 旧地图的 GPU 几何数据失效 TankPrevPos.clear(); // 位置历史(判断"在动")失效 GoldBoxes.length = 0; // 上一局的箱子实体全部作废 GoldPending.length = 0; GpuCacheFrameCount = 0; console.log('[透视] 检测到新战斗,已重置缓存'); } // 旧实例已失效、新实例尚未捕获:清屏并显示等待提示,避免继续用悬空引用 if (!window.__gameMode__) { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = 'rgba(255, 220, 120, 0.9)'; ctx.font = 'bold 12px Consolas,monospace'; ctx.textAlign = 'left'; ctx.fillText('[' + Mode.toUpperCase() + '] 等待战局…(ght 未捕获)', 5, canvas.height - 5); requestAnimationFrame(Render); return; } // 定期清空 GPU 数据缓存 GpuCacheFrameCount++; if (GpuCacheFrameCount >= GpuCacheClearInterval) { GpuDataCache.clear(); GpuCacheFrameCount = 0; } DiagStats = { totalNodes: 0, filteredNodes: 0, draggedNodes: 0, renderedNodes: 0, outlineEdges: 0, goldBoxes: 0 }; try { var screenW = canvas.width; var screenH = canvas.height; ctx.clearRect(0, 0, screenW, screenH); var camInfo = GetCameraInfo(); if (!camInfo) { requestAnimationFrame(Render); return; } var playerTank = GetPlayerTank(); if (!playerTank) { requestAnimationFrame(Render); return; } // 相机位置为0时使用玩家位置 if (Math.abs(camInfo.pos.x) < 0.1 && Math.abs(camInfo.pos.y) < 0.1 && Math.abs(camInfo.pos.z) < 0.1) { var pPos = GetTankPosition(playerTank); if (pPos) camInfo.pos = { x: pPos.x, y: pPos.y, z: pPos.z + 100 }; } UpdateCam(camInfo, screenW, screenH); // 每帧只算一次相机基(需在相机位置兜底之后),供顶点变换/投影复用 var tankList = gm.q100_1.r11l(); var count = tankList.e1(); var drawnCount = 0; for (var i = 0; i < count; i++) { var tank = tankList.g1(i); if (!tank || tank === playerTank) continue; if (!EntityProtoHooked) InstallEntityHook(tank); // 借坦克实例找到实体基类原型,挂补给观察钩子 var physicsComp = FindComponentByField(tank, 'gzh_1'); if (!physicsComp || !physicsComp.gzh_1) continue; var tankState = GetTankState(tank); // 可见坦克 = ACTIVE(3) 或 SEMI_ACTIVE(2)(引擎 y147() 判定),0/1/4 才跳过 if (tankState !== 2 && tankState !== 3) continue; var pos = { x: physicsComp.gzh_1.f20_1, y: physicsComp.gzh_1.g20_1, z: physicsComp.gzh_1.h20_1 }; // 本帧该坦克是否在移动(供「残留节点」检测:坦克在动而节点矩阵不动 = 旧位置遗留) // 用位移的指数滑动平均,而不是「单帧位移 > 10 单位」——后者对慢速移动几乎永远为假, // 会让残留检测形同虚设(残留漏掉的根因就在这里) var tankId = tank.hyt_1; var prevPos = TankPrevPos.get(tankId); var tankMove = 0; if (prevPos) { var pdx = pos.x - prevPos.x, pdy = pos.y - prevPos.y, pdz = pos.z - prevPos.z; tankMove = (prevPos.move || 0) * 0.85 + Math.sqrt(pdx * pdx + pdy * pdy + pdz * pdz); } var tankIsMoving = tankMove > 0.5; // 0.5 单位/帧 ≈ 0.3 m/s;静置坦克的抖动低于此值 TankPrevPos.set(tankId, { x: pos.x, y: pos.y, z: pos.z, move: tankMove }); // 距离检查(50000 = 500米) var dx = pos.x - camInfo.pos.x; var dy = pos.y - camInfo.pos.y; var dz = pos.z - camInfo.pos.z; var distance = Math.sqrt(dx*dx + dy*dy + dz*dz); if (distance > 50000) continue; var friendly = IsFriendly(tank, playerTank); var tankName = GetTankName(tank) || ('ID:' + tank.hyt_1); // 根据 HighlightMode 判断是否渲染该坦克 if (!ShouldRender(tankName)) continue; // 颜色按队伍:友方=蓝,敌方=红 var color = friendly ? 'rgba(68, 136, 255, 0.6)' : 'rgba(255, 68, 68, 0.6)'; // 3D模型渲染(仅在mesh数据可用时) var MeshStats = { hullSource: '', hullParts: 0, turretComp: false, turretRoot: false, hull: { seen: 0, hasMesh: 0, gpuOk: 0, gpuFail: 0 }, turret: { seen: 0, hasMesh: 0, gpuOk: 0, gpuFail: 0 } }; var meshNodes = GetTankMeshData(tank, MeshStats); if (!meshNodes || meshNodes.length === 0) { LogTankDiag(tankName, tankState, MeshStats, 0, 0, 0); DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color); // 兜底:至少可见 drawnCount++; continue; } // 统计各分组节点数(诊断用) var hullCount = 0, turretCount = 0; for (var gi = 0; gi < meshNodes.length; gi++) { if (meshNodes[gi].group === 'turret') turretCount++; else hullCount++; } if (turretCount === 0) { // 没有炮塔节点则不画模型,仅画兜底标记 LogTankDiag(tankName, tankState, MeshStats, hullCount, turretCount, 0); DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color); drawnCount++; continue; } ctx.strokeStyle = color; ctx.lineWidth = LineWidth; var renderedThisTank = 0; if (Mode === 'wireframe') { for (var ni = 0; ni < meshNodes.length; ni++) { var meshData = meshNodes[ni]; var bpNode = meshData.node; var worldMatrix = IsNodeValid(bpNode, pos, tankIsMoving, DiagStats); if (!worldMatrix) continue; DrawWireframeNode(ctx, meshData, worldMatrix, pos); DiagStats.renderedNodes++; renderedThisTank++; } } else { renderedThisTank = DrawTankOutline(ctx, meshNodes, pos, tankIsMoving, screenW, screenH, DiagStats, color); } // 诊断(节流):存活坦克模型退化时输出根因数据,便于一次性定位 if ((renderedThisTank === 0 && meshNodes.length > 0) || hullCount === 0) { LogTankDiag(tankName, tankState, MeshStats, hullCount, turretCount, renderedThisTank); } // 一个节点都没画出来:画兜底标记,避免坦克完全不可见 if (renderedThisTank === 0) { DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color); } drawnCount++; } // ===== 金箱子:清理失效项 → 评估候选 → 画金色标记 ===== var goldDrawn = 0; for (var gci = GoldBoxes.length - 1; gci >= 0; gci--) { if (!IsGoldBoxEntity(GoldBoxes[gci])) GoldBoxes.splice(gci, 1); // 被拾取/销毁 } for (var gpi = GoldPending.length - 1; gpi >= 0; gpi--) { var PendingIt = GoldPending[gpi]; if (esp.frameCount - PendingIt.f > 90) { GoldPending.splice(gpi, 1); continue; } if (IsGoldBoxEntity(PendingIt.e)) { RegisterGoldBox(PendingIt.e); GoldPending.splice(gpi, 1); } } for (var gbi = 0; gbi < GoldBoxes.length; gbi++) { var GoldPos = GetGoldBoxPosition(GoldBoxes[gbi]); if (GoldPos && DrawGoldBoxMarker(GoldPos, screenW, screenH)) goldDrawn++; } DiagStats.goldBoxes = goldDrawn; // 屏幕信息 ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; ctx.font = '10px Arial'; ctx.textAlign = 'left'; var infoText = '[' + Mode.toUpperCase() + '] | ' + drawnCount + ' tanks'; if (Mode === 'outline') { infoText += ' | Edges: ' + DiagStats.outlineEdges; } else { infoText += ' | Nodes: ' + DiagStats.renderedNodes + '/' + DiagStats.totalNodes; } infoText += ' | Frame: ' + esp.frameCount; if (DiagStats.draggedNodes > 0) infoText += ' | 拖拽:' + DiagStats.draggedNodes; if (DiagStats.goldBoxes > 0) infoText += ' | 金箱:' + DiagStats.goldBoxes; ctx.fillText(infoText, 5, screenH - 5); LogDragDiag(DiagStats); } catch(e) { console.error('[透视] 错误:', e.message, e.stack ? e.stack.split('\n')[1] : ''); } requestAnimationFrame(Render); } // 启动渲染 requestAnimationFrame(Render); console.log('3D透视已启动(双模式: wireframe | outline)'); console.log('当前模式: ' + Mode); console.log('提示: window.__hornet3DWireframe__.setMode("outline") 切换到描边模式'); console.log('提示: window.__hornet3DWireframe__.setMode("wireframe") 切换到线框模式'); console.log('提示: window.__hornet3DWireframe__.addHighlight("玩家名") 添加高亮(金色标记)'); console.log('提示: window.__hornet3DWireframe__.stop() 停止透视'); } var Tool = { Start: function() { if (window.__hornet3DWireframe__ && window.__hornet3DWireframe__.running) return; _start(); }, Stop: function() { if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.stop(); }, Status: function() { return (window.__hornet3DWireframe__ && window.__hornet3DWireframe__.running) ? ('运行中(' + window.__hornet3DWireframe__.mode + ')') : '已停止'; }, GetRunning: function() { return !!(window.__hornet3DWireframe__ && window.__hornet3DWireframe__.running); }, SetMode: function(m) { if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.setMode(m); }, GetMode: function() { return window.__hornet3DWireframe__ ? window.__hornet3DWireframe__.getMode() : null; }, SetLineWidth: function(w) { if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.setLineWidth(w); }, GetLineWidth: function() { return window.__hornet3DWireframe__ ? window.__hornet3DWireframe__.getLineWidth() : 2; }, SetHighlightMode: function(mode) { if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.setHighlightMode(mode); }, GetHighlightMode: function() { return window.__hornet3DWireframe__ ? window.__hornet3DWireframe__.getHighlightMode() : 'team'; }, SetHighlightList: function(names) { if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.setHighlightList(names); }, AddHighlight: function(name) { if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.addHighlight(name); }, RemoveHighlight: function(name) { if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.removeHighlight(name); }, GetHighlightList: function() { return window.__hornet3DWireframe__ ? window.__hornet3DWireframe__.getHighlightList() : []; }, ClearHighlight: function() { if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.clearHighlight(); } }; window.__hornetTools__ = window.__hornetTools__ || {}; window.__hornetTools__.Wireframe = Tool; console.log('[3DTank] 3D透视模块已加载'); })(); // ===== hornet_rearview.js ===== // ===== 3DTank后视镜 - 原生渲染方案 ===== // 在浏览器控制台运行(需先运行 hook_gamemode.js) // 功能:复用游戏原生渲染器(Ff.y3s)+ FBO 重定向,在小 canvas 中渲染玩家背后的 3D 场景 // 原理:Hook gl.bindFramebuffer 重定向到自定义 FBO,用后视相机参数调用游戏渲染器,readback 显示 (function() { function _start() { console.log('===== 3DTank后视镜(原生渲染版)启动 ====='); if (!window.__gameMode__) { console.error('请先运行 hook_gamemode.js'); return; } if (window.__hornetRearview__) { window.__hornetRearview__.Stop(); console.log('已停止旧后视镜'); } var gm = window.__gameMode__; // ===== 引用公共库(hornet_lib.js) ===== var lib = window.__hornetLib__; if (!lib) { console.error('[后视] 请先运行 hornet_lib.js'); return; } var teamFns = lib.LoadTeamFunctions(); var ZCt = teamFns.ZCt, m2Fn = teamFns.m2Fn; // 公共函数别名(调用处不变) var FindComponentByField = lib.FindComponentByField; var GetTankPosition = lib.GetTankPosition; var GetTankState = lib.GetTankState; var IsTankAlive = lib.IsTankAlive; var GetTankName = lib.GetTankName; var GetTankHealth = lib.GetTankHealth; function GetPlayerTank() { return lib.GetPlayerTank(gm); } function IsFriendly(tank, playerTank) { return lib.IsFriendly(tank, playerTank, gm); } function GetCameraInfo() { return lib.GetCameraInfo(gm); } // ===== 配置 ===== var Config = { width: 400, height: 240, fov: 1.2, maxRange: 15000, pitchOffset: -0.05, cameraHeight: 250, near: 50, far: 30000, refreshInterval: 0 // 每帧渲染(0=每帧,用 PBO 异步回读避免 GPU 停顿) }; // 从 localStorage 恢复 FOV 和透明度 try { var __savedFov = localStorage.getItem('hornet_rearview_fov'); if (__savedFov) { var __fv = parseFloat(__savedFov); if (!isNaN(__fv) && __fv >= 0.5 && __fv <= 2.5) Config.fov = __fv; } } catch(e) {} var RearviewOpacity = 1.0; try { var __savedROp = localStorage.getItem('hornet_rearview_opacity'); if (__savedROp) { var __rop = parseFloat(__savedROp); if (!isNaN(__rop) && __rop >= 0.2 && __rop <= 1) RearviewOpacity = __rop; } } catch(e) {} // ===== UI 层禁用(防止技能条等 UI 混入后视 FBO)===== // ff.x38_1 是渲染层队列数组,索引对应层: // 20=Hud, 21=GuiBack, 22=GuiMain, 23=GuiFront, 24=DebugGui // 替换为空假对象,使 z3k 填充和渲染都跳过这些层 var uiLayerIndices = [20, 21, 22, 23, 24]; var fakeQueue = { e1: function() { return 0; }, g1: function(i) { return null; }, w2a: function(x) {}, d3: function() {}, ii: function(x) {} }; // ===== 获取游戏渲染对象 ===== var scene = null, ff = null, rootNode = null, mainCamera = null, gl = null; var initError = null; try { scene = gm.jyw().iyt_1; console.log('[后视] 场景对象:', scene); // hC 渲染器实例(jr.lye_1) var renderer = scene.fyo_1; console.log('[后视] 渲染器:', renderer); // Ff 绘制器实例(hC.r74_1) ff = renderer.d75_1; console.log('[后视] Ff绘制器:', ff); // 场景根节点(jr.kyf_1) rootNode = scene.eyp_1; console.log('[后视] 根节点:', rootNode); // 主相机 Ha 实例(jr.nyf_1) mainCamera = scene.hyp_1; console.log('[后视] 主相机:', mainCamera); // 游戏 gl context(Ff.p39_1.p73_1 = BS.p73_1) gl = ff.w39_1.b74_1; console.log('[后视] GL上下文:', gl); if (!ff || !rootNode || !mainCamera || !gl) { throw new Error('关键字段缺失'); } // 探测相机字段 console.log('[后视] 相机s35_1:', mainCamera.s35_1); console.log('[后视] 相机r35_1:', mainCamera.r35_1); console.log('[后视] 相机b37_1:', mainCamera.b37_1); if (!mainCamera.s35_1 || !mainCamera.r35_1) { throw new Error('相机字段缺失'); } } catch(e) { initError = e; console.error('[后视] 初始化失败:', e.message, e.stack); } // ===== 创建 UI canvas(显示后视画面 + UI 标记)===== var uiCanvas = document.createElement('canvas'); uiCanvas.width = Config.width; uiCanvas.height = Config.height; uiCanvas.style.cssText = 'position:fixed;top:10px;right:10px;width:' + Config.width + 'px;height:' + Config.height + 'px;z-index:99999;border:2px solid rgba(120,200,255,0.6);border-radius:6px;pointer-events:auto;cursor:move;box-shadow:0 0 12px rgba(0,0,0,0.6);opacity:' + RearviewOpacity + ';'; document.body.appendChild(uiCanvas); if (lib.MakeDraggable) lib.MakeDraggable(uiCanvas, 'rearview'); var uiCtx = uiCanvas.getContext('2d'); // ===== UI 绘制函数 ===== function DrawText(text, x, y, color, font, align, baseline) { uiCtx.font = font || '12px monospace'; uiCtx.textAlign = align || 'left'; uiCtx.textBaseline = baseline || 'middle'; uiCtx.fillStyle = color || 'rgba(255,255,255,1)'; uiCtx.fillText(text, x, y); } function DrawTextOutlined(text, x, y, color, font, align, baseline) { uiCtx.font = font || '12px monospace'; uiCtx.textAlign = align || 'left'; uiCtx.textBaseline = baseline || 'middle'; uiCtx.strokeStyle = 'rgba(0,0,0,0.85)'; uiCtx.lineWidth = 3; uiCtx.strokeText(text, x, y); uiCtx.fillStyle = color || 'rgba(255,255,255,1)'; uiCtx.fillText(text, x, y); } // ===== FBO 创建(后视镜渲染目标)===== var rw = Config.width, rh = Config.height; var rearFbo = null, rearTex = null, rearDepthRb = null; var pixels = new Uint8Array(rw * rh * 4); var imageData = uiCtx.createImageData(rw, rh); // PBO 双缓冲异步回读(避免 readPixels 同步 GPU 停顿) var pbos = [null, null]; var pboIndex = 0; var pboReady = false; // 第一帧没有前帧数据,跳过回读 var pboSupported = false; // Uint32 视图用于批量设置 alpha=255(修复树叶等半透明边缘) var pixels32 = new Uint32Array(pixels.buffer); if (gl && !initError) { try { rearFbo = gl.createFramebuffer(); rearTex = gl.createTexture(); rearDepthRb = gl.createRenderbuffer(); // 颜色纹理 gl.bindTexture(gl.TEXTURE_2D, rearTex); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, rw, rh, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); // 深度缓冲 gl.bindRenderbuffer(gl.RENDERBUFFER, rearDepthRb); gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, rw, rh); // 组装 FBO gl.bindFramebuffer(gl.FRAMEBUFFER, rearFbo); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, rearTex, 0); gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, rearDepthRb); var fboStatus = gl.checkFramebufferStatus(gl.FRAMEBUFFER); console.log('[后视] FBO状态:', fboStatus, '(完整=' + gl.FRAMEBUFFER_COMPLETE + ')'); gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.bindTexture(gl.TEXTURE_2D, null); gl.bindRenderbuffer(gl.RENDERBUFFER, null); if (fboStatus !== gl.FRAMEBUFFER_COMPLETE) { throw new Error('FBO不完整: ' + fboStatus); } // 创建 PBO 双缓冲(WebGL2 PIXEL_PACK_BUFFER) try { var byteSize = rw * rh * 4; for (var pi = 0; pi < 2; pi++) { pbos[pi] = gl.createBuffer(); gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbos[pi]); gl.bufferData(gl.PIXEL_PACK_BUFFER, byteSize, gl.STREAM_READ); } gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null); pboSupported = true; console.log('[后视] PBO 双缓冲已创建(异步回读)'); } catch(pboErr) { console.warn('[后视] PBO 不支持,回退到同步 readPixels:', pboErr.message); pboSupported = false; } } catch(e) { initError = e; console.error('[后视] FBO创建失败:', e.message); } } // ===== Hook gl.bindFramebuffer(后视渲染时重定向到我们的 FBO)===== var rearviewRendering = false; var mainFbo = null; var origBindFramebuffer = null; var origH3c = null, origG3c = null; if (gl && !initError) { origBindFramebuffer = gl.bindFramebuffer.bind(gl); gl.bindFramebuffer = function(target, fbo) { // 记录主 FBO(非后视渲染时的绑定) if (!rearviewRendering && target === gl.FRAMEBUFFER) { mainFbo = fbo; } // 后视渲染时重定向到我们的 FBO if (rearviewRendering && target === gl.FRAMEBUFFER && fbo !== rearFbo) { return origBindFramebuffer(target, rearFbo); } return origBindFramebuffer(target, fbo); }; console.log('[后视] gl.bindFramebuffer 已 Hook'); // Hook BS.h3c/g3c:后视渲染时返回 FBO 尺寸(修复 screenSize/aspect ratio) // y3s 会调用 BS.h3c()/g3c() 获取 drawingBufferWidth/Height 设置相机 l36_1/m36_1 // 不 Hook 的话 aspect ratio 会用游戏 canvas 尺寸(如1920x1080),导致画面比例错误 origH3c = ff.w39_1.o3c.bind(ff.w39_1); origG3c = ff.w39_1.n3c.bind(ff.w39_1); ff.w39_1.o3c = function() { return rearviewRendering ? rw : origH3c(); }; ff.w39_1.n3c = function() { return rearviewRendering ? rh : origG3c(); }; console.log('[后视] BS.o3c/n3c 已 Hook'); } // ===== 渲染后视镜 ===== var frameCount = 0; var running = true; var debugInfo = { frame: 0, tankCount: 0, renderedTanks: 0, lastError: null }; function RenderRearview() { if (!running || initError || !ff || !mainCamera || !rootNode || !gl || !rearFbo) return; frameCount++; debugInfo.frame = frameCount; // 频率控制 if (frameCount % (Config.refreshInterval + 1) !== 0) return; var playerTank = GetPlayerTank(); if (!playerTank) { uiCtx.clearRect(0, 0, rw, rh); DrawText('未找到玩家坦克', rw/2, rh/2, 'rgba(255,200,80,1)', '13px monospace', 'center'); return; } var playerPos = GetTankPosition(playerTank); if (!playerPos) { uiCtx.clearRect(0, 0, rw, rh); DrawText('无位置数据', rw/2, rh/2, 'rgba(255,200,80,1)', '13px monospace', 'center'); return; } var camInfo = GetCameraInfo(); if (!camInfo) { uiCtx.clearRect(0, 0, rw, rh); DrawText('无相机数据', rw/2, rh/2, 'rgba(255,200,80,1)', '13px monospace', 'center'); return; } // 后视方向修复:camInfo.yaw = $r.lym_1.h20_1 是 roll(不是 yaw!) // o37: forward.y = d[5] = cos(pitch)*cos(roll) + sin(pitch)*sin(yaw)*sin(roll) // yaw=π 时 sin(yaw)=0,故后视 forward.y = cos(rearPitch)*cos(roll) // roll = camInfo.yaw+π → cos(camInfo.yaw+π) = -cos(camInfo.yaw) // 故后视 forward.y = -cos(rearPitch)*cos(camInfo.yaw),主视为 +cos(pitch)*cos(camInfo.yaw) // → 后视水平方向 = -主视水平方向(朝后看 ✓,与 rearPitch 符号无关) // o37: forward.z = d[9] = sin(pitch)*cos(yaw) // 后视 yaw=π: forward.z = -sin(rearPitch) // 要后视与主视同垂直方向(低头时都朝下看地形):rearPitch = -camInfo.pitch // → 后视 forward.z = -sin(-camInfo.pitch) = sin(camInfo.pitch) = 主视 forward.z ✓ // up 向量:rearPitch=-camInfo.pitch 时 up.z<0(朝下),FBO 画面颠倒 // GL readPixels(自底向上) + putImageData(自顶向下) = Y 翻转,使显示画面正立 ✓ var rearPitch = -camInfo.pitch; // 后视相机位置:玩家位置 + 高度偏移 var camPosX = playerPos.x; var camPosY = playerPos.y; var camPosZ = playerPos.z + Config.cameraHeight; // ===== 保存主相机参数 ===== var savedPitch = mainCamera.s35_1.f20_1; var savedYaw = mainCamera.s35_1.g20_1; var savedRoll = mainCamera.s35_1.h20_1; var savedX = mainCamera.r35_1.f20_1; var savedY = mainCamera.r35_1.g20_1; var savedZ = mainCamera.r35_1.h20_1; var savedL36 = mainCamera.s36_1; var savedM36 = mainCamera.t36_1; var savedS36 = mainCamera.z36_1; var savedQ36 = mainCamera.x36_1; var savedR36 = mainCamera.y36_1; // 临时暂停自由视角的 h37 hook,避免覆盖后视镜相机参数 var fl = window.__hornetFreeLook__; if (fl && typeof fl.SetSuspended === 'function') fl.SetSuspended(true); try { // ===== 设置后视相机参数 ===== // g=π 使水平方向反向(朝后看),h=yaw+π 保持水平视角正确 // f=-pitch 使垂直方向与主视一致(低头看后方地形) // up 朝下 → FBO 画面颠倒 → GL readback Y 翻转修正 mainCamera.s35_1.f20_1 = rearPitch; mainCamera.s35_1.g20_1 = Math.PI; mainCamera.s35_1.h20_1 = camInfo.yaw + Math.PI; mainCamera.r35_1.f20_1 = camPosX; mainCamera.r35_1.g20_1 = camPosY; mainCamera.r35_1.h20_1 = camPosZ; mainCamera.s36_1 = rw; mainCamera.t36_1 = rh; mainCamera.z36_1 = Config.fov; mainCamera.x36_1 = Config.near; mainCamera.y36_1 = Config.far; // 更新相机矩阵(viewMatrix, projectionMatrix, viewProjectionMatrix) mainCamera.o37(); // 保存后视视图矩阵副本(用于 UI 标记投影,finally 会恢复相机所以需提前保存) var rearViewMatrix = new Float32Array(mainCamera.b37_1); // ===== 保存并禁用阴影/后处理(避免它们切换 FBO 覆盖 rearFbo)===== // p38_1.x3n_1 控制 z39_1(阴影),y3n_1 控制 c3a_1/d3a_1(后处理),z3n_1 控制 f3a_1 // 禁用后 y3s 只执行主场景渲染 z3k,跳过所有 FBO 切换的 pass var savedX3n = ff.w38_1.e3o_1; var savedY3n = ff.w38_1.f3o_1; var savedZ3n = ff.w38_1.g3o_1; ff.w38_1.e3o_1 = false; ff.w38_1.f3o_1 = false; ff.w38_1.g3o_1 = false; // ===== 禁用 UI 渲染层(防止技能条等 UI 混入后视 FBO)===== // 保存 UI 层队列,替换为空假对象,y3s 后恢复 var savedUiQueues = {}; for (var ui = 0; ui < uiLayerIndices.length; ui++) { var li = uiLayerIndices[ui]; savedUiQueues[li] = ff.e39_1[li]; ff.e39_1[li] = fakeQueue; } // ===== 渲染到 FBO ===== rearviewRendering = true; // 清除 FBO origBindFramebuffer(gl.FRAMEBUFFER, rearFbo); gl.viewport(0, 0, rw, rh); gl.clearColor(0.15, 0.18, 0.22, 1.0); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // 调用游戏原生渲染器(已禁用阴影/后处理/UI层,只渲染 3D 场景) ff.f3t(rootNode, mainCamera); rearviewRendering = false; // ===== Readback FBO 到 canvas(PBO 异步回读,避免 GPU 管线停顿)===== origBindFramebuffer(gl.FRAMEBUFFER, rearFbo); if (pboSupported) { // PBO 异步回读:当前帧发起异步 readPixels 到 PBO[current],读取上一帧的 PBO[previous] 数据 var curPbo = pboIndex; var prevPbo = 1 - pboIndex; // 发起当前帧的异步 readPixels(写入 PBO,不阻塞 GPU) gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbos[curPbo]); gl.readPixels(0, 0, rw, rh, gl.RGBA, gl.UNSIGNED_BYTE, 0); // 读取上一帧的 PBO 数据(此时 GPU 应已完成) if (pboReady) { gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbos[prevPbo]); gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, pixels); } gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null); pboIndex = prevPbo; pboReady = true; } else { // 回退:同步 readPixels gl.readPixels(0, 0, rw, rh, gl.RGBA, gl.UNSIGNED_BYTE, pixels); } // 显示画面(g=π 渲染已自带 Y 翻转,直接复制) // 修复半透明边缘:强制 alpha=255(树叶等 alpha 混合会导致 canvas 透明) for (var ai = 0; ai < pixels32.length; ai++) { pixels32[ai] = pixels32[ai] | 0xFF000000; // little-endian: 高字节=alpha } imageData.data.set(pixels); uiCtx.putImageData(imageData, 0, 0); // 恢复主 FBO origBindFramebuffer(gl.FRAMEBUFFER, mainFbo); } catch(e) { debugInfo.lastError = e.message; console.error('[后视] 渲染失败:', e.message, e.stack); uiCtx.clearRect(0, 0, rw, rh); DrawText('渲染错误: ' + e.message, rw/2, rh/2, 'rgba(255,100,100,1)', '11px monospace', 'center'); } finally { // ===== 确保 rearviewRendering 已重置 ===== rearviewRendering = false; // ===== 恢复阴影/后处理(异常时也要恢复)===== try { if (typeof savedX3n !== 'undefined') { ff.w38_1.e3o_1 = savedX3n; ff.w38_1.f3o_1 = savedY3n; ff.w38_1.g3o_1 = savedZ3n; } } catch(e) { console.error('[后视] 恢复阴影/后处理失败:', e.message); } // ===== 恢复 UI 渲染层(异常时也要恢复,否则游戏 UI 永久失效)===== try { if (savedUiQueues) { for (var ui2 = 0; ui2 < uiLayerIndices.length; ui2++) { var li2 = uiLayerIndices[ui2]; ff.e39_1[li2] = savedUiQueues[li2]; } } } catch(e) { console.error('[后视] 恢复 UI 层失败:', e.message); } // ===== 恢复主相机参数 ===== try { mainCamera.s35_1.f20_1 = savedPitch; mainCamera.s35_1.g20_1 = savedYaw; mainCamera.s35_1.h20_1 = savedRoll; mainCamera.r35_1.f20_1 = savedX; mainCamera.r35_1.g20_1 = savedY; mainCamera.r35_1.h20_1 = savedZ; mainCamera.s36_1 = savedL36; mainCamera.t36_1 = savedM36; mainCamera.z36_1 = savedS36; mainCamera.x36_1 = savedQ36; mainCamera.y36_1 = savedR36; mainCamera.o37(); } catch(e) { console.error('[后视] 恢复相机失败:', e.message); } // 恢复自由视角的 h37 hook if (fl && typeof fl.SetSuspended === 'function') fl.SetSuspended(false); } // ===== 绘制 UI 标记(坦克位置标记)===== if (rearViewMatrix) { DrawUIMarkers(playerTank, playerPos, camInfo, rearViewMatrix); } } // ===== UI 标记绘制 ===== function DrawUIMarkers(playerTank, playerPos, camInfo, viewMatrix) { var tankList = gm.q100_1.r11l(); var count = tankList.e1(); debugInfo.tankCount = count; var visibleTanks = 0; for (var i = 0; i < count; i++) { var tank = tankList.g1(i); if (!tank || tank === playerTank) continue; if (!IsTankAlive(tank)) continue; var pos = GetTankPosition(tank); if (!pos) continue; // 使用视图矩阵投影(与 3D 渲染一致) // o37 变换前矩阵行:right(d[0,4,8]), forward(d[1,5,9]), up(d[2,6,10]) // Ua 变换后行:right, up, -forward // 故变换后相机空间:camX=右,camY=上(up·rel),camZ=深度(-forward·rel,负=前方) // 后视相机 up 朝下:世界"上"rel.z>0 → camY<0;渲染+FBO readback 使世界"上"显示在 canvas 顶部 // marker 用 + 号匹配:camY<0(上方) → +负 → screen.y= -0.1) continue; // 剔除相机后方(深度非负) var depth = -camZ; // 正值=距离 var focalLength = rh / (2 * Math.tan(Config.fov / 2)); var screen = { x: rw / 2 + (camX / depth) * focalLength, y: rh / 2 + (camY / depth) * focalLength, // +号匹配 GL readback Y 翻转后的画面 depth: depth }; if (screen.x < 0 || screen.x > rw || screen.y < 0 || screen.y > rh) continue; if (screen.depth > Config.maxRange) continue; visibleTanks++; var friendly = IsFriendly(tank, playerTank); var color = friendly ? 'rgba(80,180,255,1)' : 'rgba(255,80,60,1)'; var bgColor = friendly ? 'rgba(40,90,140,0.7)' : 'rgba(140,40,30,0.7)'; // 方框标记 uiCtx.strokeStyle = color; uiCtx.lineWidth = 1.5; uiCtx.strokeRect(screen.x - 6, screen.y - 6, 12, 12); // 名称 var name = GetTankName(tank); if (name) { DrawTextOutlined(name, screen.x, screen.y - 12, color, '10px monospace', 'center'); } // 血量条 var health = GetTankHealth(tank); if (health) { var barW = 24, barH = 3; var barX = screen.x - barW / 2, barY = screen.y + 8; uiCtx.fillStyle = 'rgba(0,0,0,0.6)'; uiCtx.fillRect(barX, barY, barW, barH); var hpColor = health.percent > 0.6 ? 'rgba(80,255,80,1)' : health.percent > 0.3 ? 'rgba(255,200,60,1)' : 'rgba(255,80,60,1)'; uiCtx.fillStyle = hpColor; uiCtx.fillRect(barX, barY, barW * health.percent, barH); } } debugInfo.renderedTanks = visibleTanks; // 边框 + 标题 uiCtx.strokeStyle = 'rgba(120,200,255,0.4)'; uiCtx.lineWidth = 1; uiCtx.strokeRect(0, 0, rw, rh); DrawTextOutlined('后视镜', 8, 12, 'rgba(120,200,255,0.9)', '11px monospace', 'left'); DrawTextOutlined(visibleTanks + '/' + count, rw - 8, 12, 'rgba(180,180,180,0.9)', '11px monospace', 'right'); if (debugInfo.lastError) { DrawTextOutlined('ERR: ' + debugInfo.lastError.substring(0, 30), 8, rh - 8, 'rgba(255,100,100,0.9)', '10px monospace', 'left'); } } // ===== 主循环 ===== function Loop() { if (!running) return; // 换图自刷新:gameMode 实例变化时整体重启,重新获取场景/相机/GL hook var NewGm = window.__gameMode__; if (NewGm && NewGm !== gm) { console.log('[后视] 检测到新战斗,正在重启后视镜...'); Stop(); _start(); return; } requestAnimationFrame(Loop); // 旧实例已失效、新实例尚未捕获:跳过本帧 if (!window.__gameMode__) return; RenderRearview(); } // ===== 停止函数 ===== function Stop() { running = false; console.log('[后视] 停止后视镜'); // 恢复 gl.bindFramebuffer if (origBindFramebuffer && gl) { gl.bindFramebuffer = origBindFramebuffer; console.log('[后视] gl.bindFramebuffer 已恢复'); } // 恢复 BS.h3c/g3c if (origH3c && ff && ff.w39_1) { ff.w39_1.o3c = origH3c; ff.w39_1.n3c = origG3c; console.log('[后视] BS.o3c/n3c 已恢复'); } // 清理 FBO if (gl && rearFbo) gl.deleteFramebuffer(rearFbo); if (gl && rearTex) gl.deleteTexture(rearTex); if (gl && rearDepthRb) gl.deleteRenderbuffer(rearDepthRb); // 清理 PBO if (gl && pbos[0]) gl.deleteBuffer(pbos[0]); if (gl && pbos[1]) gl.deleteBuffer(pbos[1]); // 移除 canvas if (uiCanvas && uiCanvas.parentNode) { uiCanvas.parentNode.removeChild(uiCanvas); } window.__hornetRearview__ = null; } // ===== 启动 ===== window.__hornetRearview__ = { Stop: Stop, Config: Config, setFov: function(f) { var n = parseFloat(f); if (!isNaN(n) && n >= 0.5 && n <= 2.5) { Config.fov = n; try { localStorage.setItem('hornet_rearview_fov', String(n)); } catch(e) {} console.log('[后视] FOV 设为: ' + n); } else { console.warn('[后视] FOV 应在 0.5~2.5'); } }, getFov: function() { return Config.fov; }, setOpacity: function(o) { var n = parseFloat(o); if (!isNaN(n) && n >= 0.2 && n <= 1) { RearviewOpacity = n; uiCanvas.style.opacity = String(n); try { localStorage.setItem('hornet_rearview_opacity', String(n)); } catch(e) {} } else { console.warn('[后视] 透明度应在 0.2~1'); } }, getOpacity: function() { return RearviewOpacity; } }; if (initError) { uiCtx.clearRect(0, 0, rw, rh); DrawText('初始化失败:', rw/2, rh/2 - 10, 'rgba(255,100,100,1)', '13px monospace', 'center'); DrawText(initError.message, rw/2, rh/2 + 10, 'rgba(255,200,80,1)', '11px monospace', 'center'); console.error('[后视] 初始化失败,请检查字段访问'); return; } console.log('[后视] 原生渲染后视镜已启动'); console.log('[后视] 配置: ' + rw + 'x' + rh + ', FOV=' + Config.fov + ', 间隔=' + Config.refreshInterval); Loop(); } var Tool = { Start: function() { if (window.__hornetRearview__) return; _start(); }, Stop: function() { if (window.__hornetRearview__) window.__hornetRearview__.Stop(); }, Status: function() { return window.__hornetRearview__ ? '运行中' : '已停止'; }, GetRunning: function() { return !!window.__hornetRearview__; }, SetFov: function(f) { if (window.__hornetRearview__) window.__hornetRearview__.setFov(f); }, GetFov: function() { return window.__hornetRearview__ ? window.__hornetRearview__.getFov() : 1.2; }, SetOpacity: function(o) { if (window.__hornetRearview__) window.__hornetRearview__.setOpacity(o); }, GetOpacity: function() { return window.__hornetRearview__ ? window.__hornetRearview__.getOpacity() : 1.0; } }; window.__hornetTools__ = window.__hornetTools__ || {}; window.__hornetTools__.Rearview = Tool; console.log('[3DTank] 后视镜模块已加载'); })(); // ===== hornet_aim_warning.js ===== // ===== 3DTank瞄准警告 - 敌方坦克瞄准线预判 ===== // 在浏览器控制台运行(需先运行 hook_gamemode.js) // 功能:当敌方坦克炮塔朝向玩家时,立刻显示HUD警告并标记方向 // 独立轻量级脚本,不依赖3D透视渲染 (function() { function _start() { console.log('===== 3DTank瞄准警告启动 ====='); // 允许在未进入战局 / ght 尚未捕获时启动:先出状态行,威胁判定等进入战局再算 if (window.__hornetAimWarning__) { window.__hornetAimWarning__.Stop(); console.log('已停止旧瞄准警告'); } var gm = window.__gameMode__ || null; // ===== 设置参数(localStorage 持久化)===== // Sensitivity: 灵敏度系数,>1 更灵敏(阈值更大,更易触发),<1 更迟钝 var Sensitivity = 1.0; try { var __savedSens = localStorage.getItem('hornet_aim_sensitivity'); if (__savedSens) { var __s = parseFloat(__savedSens); if (!isNaN(__s) && __s >= 0.3 && __s <= 3.0) Sensitivity = __s; } } catch(e) {} // SoundEnabled: 警告声音开关 var SoundEnabled = false; try { var __savedSE = localStorage.getItem('hornet_aim_sound'); if (__savedSE === 'true') SoundEnabled = true; } catch(e) {} var _audioCtx = null; var _lastBeepTime = 0; function PlayBeep() { if (!SoundEnabled) return; var now = Date.now(); if (now - _lastBeepTime < 300) return; // 最多每300ms响一次 _lastBeepTime = now; try { if (!_audioCtx) _audioCtx = new (window.AudioContext || window.webkitAudioContext)(); var osc = _audioCtx.createOscillator(); var gain = _audioCtx.createGain(); osc.frequency.value = 880; osc.type = 'square'; gain.gain.value = 0.08; osc.connect(gain); gain.connect(_audioCtx.destination); osc.start(); gain.gain.exponentialRampToValueAtTime(0.001, _audioCtx.currentTime + 0.15); osc.stop(_audioCtx.currentTime + 0.15); } catch(e) {} } // ===== 引用公共库(hornet_lib.js) ===== var lib = window.__hornetLib__; if (!lib) { console.error('[瞄准] 请先运行 hornet_lib.js'); return; } var teamFns = lib.LoadTeamFunctions(); var ZCt = teamFns.ZCt, m2Fn = teamFns.m2Fn; // 公共函数别名(调用处不变) var FindComponentByField = lib.FindComponentByField; var GetTankPosition = lib.GetTankPosition; var GetTankState = lib.GetTankState; var IsTankAlive = lib.IsTankAlive; // ===== 获取 TDt 武器皮肤组件(炮塔) ===== function GetTDtComponent(entity) { var comp = FindComponentByField(entity, 'n11c_1'); if (comp) return comp; comp = FindComponentByField(entity, 'q112_1'); return comp; } // 坦克名称/血量:使用公共库 var GetTankName = lib.GetTankName; var GetTankHealth = lib.GetTankHealth; // ===== 获取炮塔瞄准角度 ===== // 从炮塔 Bp 节点世界矩阵(n35_1)提取水平面前向方向 // 返回 { angle: 炮塔朝向与"敌方→玩家"方向的夹角, distance: 距离, turretAngle: 炮塔朝向角度, turretDir: {x,y} } function GetTurretAimAngle(tank, enemyPos, playerPos) { var tdt = GetTDtComponent(tank); if (!tdt || !tdt.n11c_1 || !tdt.n11c_1.m17a_1) return null; var bpNode = tdt.n11c_1.m17a_1; var m = bpNode.n35_1; if (!m) return null; // 敌方→玩家方向(x,y 水平面) var dx = playerPos.x - enemyPos.x; var dy = playerPos.y - enemyPos.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < 100) return null; // 太近不判断 var tx = dx / dist, ty = dy / dist; // 尝试 X 轴(m0,m1) 和 Y 轴(m4,m5),取最小夹角 var minAngle = Math.PI; var turretAngle = 0; var turretDir = { x: 1, y: 0 }; var axes = [ { x: m[0], y: m[1] }, { x: m[4], y: m[5] } ]; for (var ai = 0; ai < axes.length; ai++) { var ax = axes[ai].x, ay = axes[ai].y; var len = Math.sqrt(ax * ax + ay * ay); if (len < 0.001) continue; var nx = ax / len, ny = ay / len; var dot = nx * tx + ny * ty; var ang = Math.acos(Math.max(-1, Math.min(1, dot))); if (ang < minAngle) { minAngle = ang; turretAngle = Math.atan2(ny, nx); turretDir = { x: nx, y: ny }; } } return { angle: minAngle, distance: dist, turretAngle: turretAngle, turretDir: turretDir }; } // ===== 全局碰撞检测器(uMt 组件,挂在 GameMode entity 上) ===== var UMtComponent = null; function GetUMtComponent() { if (UMtComponent) return UMtComponent; try { var gmEntity = gm.jyw(); if (!gmEntity || !gmEntity.pyt_1 || !gmEntity.pyt_1.s2a_1) return null; gmEntity.pyt_1.s2a_1.forEach(function(valueList, key) { if (UMtComponent) return; try { var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList; if (comp && typeof comp.m179 === 'function') UMtComponent = comp; } catch(e) {} }); if (UMtComponent) console.log('[瞄准] uMt 碰撞检测器已获取'); else UMtComponent = false; } catch(e) { console.warn('[瞄准] 获取 uMt 组件失败:', e.message); } return UMtComponent || null; } // mock O$ 向量:使用公共库 var MkVec = lib.MkVec; // ===== 射线检测视线遮挡(缓存:每10帧更新) ===== var RaycastCache = new Map(); var RaycastFailCount = 0; function IsLineOfSightBlocked(enemyPos, playerPos, tankId, frame) { var cached = RaycastCache.get(tankId); if (cached && (frame - cached.frame) < 10) return cached.blocked; var uMt = GetUMtComponent(); if (!uMt) return false; var blocked = false; try { var dx = playerPos.x - enemyPos.x; var dy = playerPos.y - enemyPos.y; var dz = playerPos.z - enemyPos.z; var dist = Math.sqrt(dx*dx + dy*dy + dz*dz); if (dist < 200) { RaycastCache.set(tankId, { frame: frame, blocked: false }); return false; } var ndx = dx / dist, ndy = dy / dist, ndz = dz / dist; var startVec = MkVec(enemyPos.x, enemyPos.y, enemyPos.z + 200); var dirVec = MkVec(ndx, ndy, ndz); var maxDist = dist - 300; var result = { y178_1: MkVec(0,0,0), z178_1: MkVec(0,0,0), a179_1: maxDist, c179_1: null, b179_1: null, d179: function(t, n, i, r, e) { this.y178_1.k26(t); this.z178_1.k26(n); this.a179_1 = i; this.c179_1 = r; this.b179_1 = e; }, d3: function() { this.b179_1 = null; } }; var filter = { o1co_1: { h1: function() { return false; }, d3: function() {} }, w179: function() { return true; }, d3: function() { this.o1co_1.d3(); }, p1co: function() {} }; var hit = uMt.m179(startVec, dirVec, maxDist, result, filter); blocked = !!hit && result.a179_1 < (maxDist - 50); RaycastFailCount = 0; } catch(e) { RaycastFailCount++; if (RaycastFailCount <= 3) console.warn('[瞄准] 射线检测异常:', e.message); blocked = false; } RaycastCache.set(tankId, { frame: frame, blocked: blocked }); return blocked; } // 玩家坦克/友方/相机/投影:使用公共库 function GetPlayerTank() { return lib.GetPlayerTank(gm); } function IsFriendly(tank, playerTank) { return lib.IsFriendly(tank, playerTank, gm); } function GetCameraInfo() { return lib.GetCameraInfo(gm); } var Project3D = lib.Project3D; // ===== 创建 Canvas ===== var canvas = document.createElement('canvas'); canvas.id = 'hornetAimWarningCanvas'; canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:99998;pointer-events:none;'; document.body.appendChild(canvas); var ctx = canvas.getContext('2d'); function ResizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } ResizeCanvas(); window.addEventListener('resize', ResizeCanvas); // ===== 警告闪烁状态 ===== var WarningPulse = 0; // 闪烁动画相位 // ===== 死亡检测状态 ===== var PlayerPrevHealth = -1; // 上一帧血量(-1=未初始化) var DeathInfo = null; // 死亡信息 { killerName, distance, expected, time, threats } var DeathDisplayTime = 360; // 死亡信息显示帧数(约6秒) var DeathStats = { total: 0, expected: 0, unexpected: 0 }; // 死亡统计 // ===== 主循环 ===== var running = true; var frameCount = 0; function Loop() { if (!running) return; frameCount++; requestAnimationFrame(Loop); // 换图自刷新:gameMode 实例变化时重绑并清空该地图的缓存 var NewGm = window.__gameMode__; if (NewGm && NewGm !== gm) { gm = NewGm; UMtComponent = null; // 碰撞检测器需从新场景重新获取 RaycastCache.clear(); // 旧地图的视线遮挡缓存失效 DeathInfo = null; // 清掉旧地图的死亡提示 console.log('[瞄准] 检测到新战斗,已重置缓存'); } // ght 尚未捕获 / 不在战局:清屏并显示等待提示 if (!window.__gameMode__) { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = 'rgba(255, 220, 120, 0.85)'; ctx.font = '11px Arial'; ctx.textAlign = 'left'; ctx.fillText('[瞄准警告] 等待战局…(ght 未捕获)', 5, 18); return; } var screenW = canvas.width; var screenH = canvas.height; ctx.clearRect(0, 0, screenW, screenH); var playerTank = GetPlayerTank(); if (!playerTank) return; var playerPos = GetTankPosition(playerTank); if (!playerPos) return; var camInfo = GetCameraInfo(); if (!camInfo) return; // 相机位置为0时使用玩家位置 if (Math.abs(camInfo.pos.x) < 0.1 && Math.abs(camInfo.pos.y) < 0.1 && Math.abs(camInfo.pos.z) < 0.1) { camInfo.pos = { x: playerPos.x, y: playerPos.y, z: playerPos.z + 100 }; } var tankList = gm.q100_1.r11l(); var count = tankList.e1(); var threats = []; // 遍历所有敌方坦克,检测炮塔朝向 for (var i = 0; i < count; i++) { var tank = tankList.g1(i); if (!tank || tank === playerTank) continue; if (!IsTankAlive(tank)) continue; var pos = GetTankPosition(tank); if (!pos) continue; // 只检测敌方 if (IsFriendly(tank, playerTank)) continue; var aimInfo = GetTurretAimAngle(tank, pos, playerPos); if (!aimInfo) continue; // 瞄准阈值随距离变化:近距离~11度(0.20rad),远距离~3度 // 公式:0.20 / (1 + distance/8000),乘以灵敏度系数 var aimThreshold = 0.20 / (1 + aimInfo.distance / 8000) * Sensitivity; if (aimInfo.angle >= aimThreshold) continue; // 墙体阻隔判断 var blocked = IsLineOfSightBlocked(pos, playerPos, tank.hyt_1, frameCount); if (blocked) continue; // 记录威胁 var proj = Project3D(pos, camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH); var tankName = GetTankName(tank) || ('ID:' + tank.hyt_1); threats.push({ pos: pos, distance: aimInfo.distance, angle: aimInfo.angle, turretDir: aimInfo.turretDir, screenProj: proj, tankId: tank.hyt_1, tankName: tankName }); } // ===== 死亡检测 ===== var playerHealth = GetTankHealth(playerTank); var playerCurHealth = playerHealth ? playerHealth.current : 0; // 检测血量从 >0 变为 0 = 死亡 if (PlayerPrevHealth > 0 && playerCurHealth <= 0) { // 死亡时,从 threats 列表找最近的瞄准者作为可能的击杀者 var killer = null; var minDist = Infinity; for (var ki = 0; ki < threats.length; ki++) { if (threats[ki].distance < minDist) { minDist = threats[ki].distance; killer = threats[ki]; } } DeathInfo = { killerName: killer ? killer.tankName : null, killerDistance: killer ? killer.distance : 0, expected: threats.length > 0, // 有瞄准者 = 意料之内 threatCount: threats.length, time: frameCount }; DeathStats.total++; if (DeathInfo.expected) DeathStats.expected++; else DeathStats.unexpected++; console.log('[瞄准] 玩家死亡! ' + (killer ? '被 ' + killer.tankName + ' 击杀' : '被偷击杀') + ' | 意料之' + (threats.length > 0 ? '内' : '外') + ' | 瞄准者: ' + threats.length + ' | 统计: ' + DeathStats.total + '死 (内' + DeathStats.expected + '/外' + DeathStats.unexpected + ')'); } // 复活检测:血量恢复 >0 if (PlayerPrevHealth <= 0 && playerCurHealth > 0) { DeathInfo = null; console.log('[瞄准] 玩家复活,重置死亡信息'); } PlayerPrevHealth = playerCurHealth; // ===== 绘制瞄准线(每个威胁的弹道轨迹) ===== if (threats.length > 0) { PlayBeep(); ctx.strokeStyle = 'rgba(255, 40, 40, 0.5)'; ctx.lineWidth = 2; for (var ti = 0; ti < threats.length; ti++) { var tr = threats[ti]; var startPos = { x: tr.pos.x, y: tr.pos.y, z: tr.pos.z + 200 }; // 炮塔高度 var endPos = { x: playerPos.x, y: playerPos.y, z: playerPos.z }; var segs = 20; var prevScreen = null; ctx.beginPath(); for (var si = 0; si <= segs; si++) { var st = si / segs; var swp = { x: startPos.x + (endPos.x - startPos.x) * st, y: startPos.y + (endPos.y - startPos.y) * st, z: startPos.z + (endPos.z - startPos.z) * st }; var sp = Project3D(swp, camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH); if (sp) { if (prevScreen) ctx.lineTo(sp.x, sp.y); else ctx.moveTo(sp.x, sp.y); prevScreen = sp; } else { prevScreen = null; } } ctx.stroke(); } } // ===== 每个威胁的方向标记 ===== for (var thi = 0; thi < threats.length; thi++) { var threat = threats[thi]; var proj = threat.screenProj; if (proj && proj.x >= 0 && proj.x <= screenW && proj.y >= 0 && proj.y <= screenH) { // ===== 威胁在屏幕内:在威胁位置画红色警告三角形 ===== var mx = proj.x, my = proj.y - 35; ctx.strokeStyle = 'rgba(255, 0, 0, 1)'; ctx.lineWidth = 2.5; ctx.fillStyle = 'rgba(255, 0, 0, 0.4)'; ctx.beginPath(); ctx.moveTo(mx, my - 10); ctx.lineTo(mx - 9, my + 6); ctx.lineTo(mx + 9, my + 6); ctx.closePath(); ctx.fill(); ctx.stroke(); // 感叹号 ctx.fillStyle = 'rgba(255, 255, 255, 1)'; ctx.font = 'bold 13px Arial'; ctx.textAlign = 'center'; ctx.fillText('!', mx, my + 4); // 距离标签 ctx.fillStyle = 'rgba(255, 200, 200, 0.95)'; ctx.font = 'bold 11px Arial'; ctx.fillText(Math.round(threat.distance / 100) + 'm', mx, my + 22); // 坦克名称 ctx.fillStyle = 'rgba(255, 240, 240, 0.95)'; ctx.font = 'bold 11px Arial'; ctx.fillText(threat.tankName, mx, my + 36); } else { // ===== 威胁不在屏幕内:屏幕边缘红色箭头指示方向 ===== var tdx = threat.pos.x - playerPos.x; var tdy = threat.pos.y - playerPos.y; var tlen = Math.sqrt(tdx * tdx + tdy * tdy); if (tlen > 0.001) { var ndx = tdx / tlen, ndy = tdy / tlen; // 计算相对相机偏航角的方向 var fwdX = -Math.sin(camInfo.yaw), fwdY = Math.cos(camInfo.yaw); var rgtX = Math.cos(camInfo.yaw), rgtY = Math.sin(camInfo.yaw); var screenRight = ndx * rgtX + ndy * rgtY; var screenFwd = ndx * fwdX + ndy * fwdY; var sAng = Math.atan2(screenRight, screenFwd); var edgeR = Math.min(screenW, screenH) * 0.42; var ex = screenW / 2 + Math.sin(sAng) * edgeR; var ey = screenH / 2 - Math.cos(sAng) * edgeR; // 限制在屏幕边缘内 ex = Math.max(30, Math.min(screenW - 30, ex)); ey = Math.max(30, Math.min(screenH - 30, ey)); // 画箭头(指向威胁方向) ctx.fillStyle = 'rgba(255, 0, 0, 0.95)'; ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(ex + Math.sin(sAng) * 14, ey - Math.cos(sAng) * 14); ctx.lineTo(ex + Math.sin(sAng + 2.5) * 9, ey - Math.cos(sAng + 2.5) * 9); ctx.lineTo(ex + Math.sin(sAng - 2.5) * 9, ey - Math.cos(sAng - 2.5) * 9); ctx.closePath(); ctx.fill(); ctx.stroke(); // 距离标签 ctx.fillStyle = 'rgba(255, 255, 255, 0.95)'; ctx.font = 'bold 11px Arial'; ctx.textAlign = 'center'; ctx.fillText(Math.round(threat.distance / 100) + 'm', ex, ey + 22); // 坦克名称 ctx.fillStyle = 'rgba(255, 240, 240, 0.95)'; ctx.font = 'bold 10px Arial'; ctx.fillText(threat.tankName, ex, ey + 36); } } } // ===== 左侧中央常显警告框(有威胁时) ===== if (threats.length > 0) { WarningPulse += 0.15; var pulseAlpha = 0.55 + Math.sin(WarningPulse) * 0.2; // 脉动透明度 // 动态高度:标题区 + 威胁列表 var maxListed = 4; // 最多列出4个威胁名称 var listedCount = Math.min(threats.length, maxListed); var boxW = 240; var boxH = 70 + listedCount * 18 + (threats.length > maxListed ? 16 : 0); var boxX = 20; var boxY = screenH / 2 - boxH / 2; // 警告框背景(脉动) ctx.fillStyle = 'rgba(80, 0, 0, ' + pulseAlpha + ')'; ctx.fillRect(boxX, boxY, boxW, boxH); ctx.strokeStyle = 'rgba(255, 0, 0, 1)'; ctx.lineWidth = 2.5; ctx.strokeRect(boxX, boxY, boxW, boxH); // 警告标题 ctx.fillStyle = 'rgba(255, 60, 60, 1)'; ctx.font = 'bold 26px Arial'; ctx.textAlign = 'center'; ctx.fillText('⚠ 被瞄准', boxX + boxW / 2, boxY + 32); ctx.font = 'bold 13px Arial'; ctx.fillStyle = 'rgba(255, 220, 220, 0.95)'; ctx.fillText(threats.length + ' 个威胁锁定你', boxX + boxW / 2, boxY + 54); // 威胁坦克名称列表 ctx.textAlign = 'left'; ctx.font = 'bold 12px Arial'; for (var li = 0; li < listedCount; li++) { var t = threats[li]; var lineY = boxY + 72 + li * 18; // 名称 + 距离 ctx.fillStyle = 'rgba(255, 240, 240, 0.95)'; ctx.fillText('• ' + t.tankName, boxX + 12, lineY); ctx.fillStyle = 'rgba(255, 180, 180, 0.85)'; ctx.textAlign = 'right'; ctx.fillText(Math.round(t.distance / 100) + 'm', boxX + boxW - 12, lineY); ctx.textAlign = 'left'; } // 超出部分 if (threats.length > maxListed) { ctx.fillStyle = 'rgba(255, 200, 200, 0.7)'; ctx.font = 'italic 11px Arial'; ctx.textAlign = 'center'; ctx.fillText('等 ' + (threats.length - maxListed) + ' 个...', boxX + boxW / 2, boxY + 72 + listedCount * 18 + 12); } } // ===== 死亡信息显示(屏幕中央,持续6秒或直到复活) ===== if (DeathInfo && (frameCount - DeathInfo.time) < DeathDisplayTime) { var dBoxW = 400, dBoxH = 120; var dBoxX = screenW / 2 - dBoxW / 2; var dBoxY = screenH / 2 - dBoxH / 2 - 60; // 偏上一点 // 半透明背景 ctx.fillStyle = 'rgba(0, 0, 0, 0.75)'; ctx.fillRect(dBoxX, dBoxY, dBoxW, dBoxH); // 边框(意料之内=红色,意料之外=紫色) var borderColor = DeathInfo.expected ? 'rgba(255, 60, 60, 1)' : 'rgba(200, 80, 255, 1)'; ctx.strokeStyle = borderColor; ctx.lineWidth = 3; ctx.strokeRect(dBoxX, dBoxY, dBoxW, dBoxH); // 标题 ctx.textAlign = 'center'; ctx.fillStyle = 'rgba(255, 80, 80, 1)'; ctx.font = 'bold 26px Arial'; ctx.fillText('你被击杀', dBoxX + dBoxW / 2, dBoxY + 36); // 击杀者信息 ctx.font = 'bold 18px Arial'; if (DeathInfo.killerName) { ctx.fillStyle = 'rgba(255, 220, 220, 1)'; ctx.fillText('击杀者: ' + DeathInfo.killerName, dBoxX + dBoxW / 2, dBoxY + 66); ctx.font = '12px Arial'; ctx.fillStyle = 'rgba(255, 180, 180, 0.8)'; ctx.fillText('距离 ' + Math.round(DeathInfo.killerDistance / 100) + 'm', dBoxX + dBoxW / 2, dBoxY + 86); } else { ctx.fillStyle = 'rgba(220, 180, 255, 1)'; ctx.fillText('未知来源(偷袭)', dBoxX + dBoxW / 2, dBoxY + 66); } // 意料之内/外标签 ctx.font = 'bold 16px Arial'; if (DeathInfo.expected) { ctx.fillStyle = 'rgba(255, 100, 100, 1)'; ctx.fillText('⚠ 意料之内(已在瞄准名单中)', dBoxX + dBoxW / 2, dBoxY + 108); } else { ctx.fillStyle = 'rgba(200, 100, 255, 1)'; ctx.fillText('✦ 意料之外(偷袭!)', dBoxX + dBoxW / 2, dBoxY + 108); } } // ===== 顶部状态栏(始终显示) ===== ctx.fillStyle = 'rgba(255, 255, 255, 0.4)'; ctx.font = '11px Arial'; ctx.textAlign = 'left'; var statusText = '[瞄准警告] 监控中 | 威胁: ' + threats.length + ' | 死亡: ' + DeathStats.total + '(内' + DeathStats.expected + '/外' + DeathStats.unexpected + ')' + ' | Frame: ' + frameCount; ctx.fillText(statusText, 5, 18); } // ===== 启动 ===== Loop(); console.log('[瞄准] 警告系统已启动,监控敌方坦克炮塔朝向'); // ===== 暴露控制接口 ===== window.__hornetAimWarning__ = { running: true, Stop: function() { this.running = false; running = false; window.removeEventListener('resize', ResizeCanvas); if (canvas && canvas.parentNode) canvas.parentNode.removeChild(canvas); console.log('[瞄准] 警告系统已停止'); }, GetDeathStats: function() { return DeathStats; }, ResetDeathStats: function() { DeathStats = { total: 0, expected: 0, unexpected: 0 }; console.log('[瞄准] 死亡统计已重置'); }, setSensitivity: function(s) { var n = parseFloat(s); if (!isNaN(n) && n >= 0.3 && n <= 3.0) { Sensitivity = n; try { localStorage.setItem('hornet_aim_sensitivity', String(n)); } catch(e) {} console.log('[瞄准] 灵敏度设为: ' + n); } else { console.warn('[瞄准] 灵敏度应在 0.3~3.0'); } }, getSensitivity: function() { return Sensitivity; }, setSound: function(on) { SoundEnabled = !!on; try { localStorage.setItem('hornet_aim_sound', SoundEnabled ? 'true' : 'false'); } catch(e) {} console.log('[瞄准] 警告声音: ' + (SoundEnabled ? '开' : '关')); }, getSound: function() { return SoundEnabled; } }; } var Tool = { Start: function() { if (window.__hornetAimWarning__ && window.__hornetAimWarning__.running) return; _start(); }, Stop: function() { if (window.__hornetAimWarning__) window.__hornetAimWarning__.Stop(); }, Status: function() { return (window.__hornetAimWarning__ && window.__hornetAimWarning__.running) ? '运行中' : '已停止'; }, GetRunning: function() { return !!(window.__hornetAimWarning__ && window.__hornetAimWarning__.running); }, SetSensitivity: function(s) { if (window.__hornetAimWarning__) window.__hornetAimWarning__.setSensitivity(s); }, GetSensitivity: function() { return window.__hornetAimWarning__ ? window.__hornetAimWarning__.getSensitivity() : 1.0; }, SetSound: function(on) { if (window.__hornetAimWarning__) window.__hornetAimWarning__.setSound(on); }, GetSound: function() { return window.__hornetAimWarning__ ? window.__hornetAimWarning__.getSound() : false; } }; window.__hornetTools__ = window.__hornetTools__ || {}; window.__hornetTools__.AimWarning = Tool; console.log('[3DTank] 瞄准警告模块已加载'); })(); // ===== hornet_freelook.js ===== // ===== 3DTank自由视角 ===== // 在浏览器控制台运行(需先运行 hook_gamemode.js 和 hornet_lib.js) // 功能:解锁主相机,WASD移动/鼠标右键旋转/QE升降/Shift加速/滚轮调速 // 原理:Hook mainCamera.h37,在相机矩阵更新前覆盖l35_1/k35_1参数 (function() { function _start() { console.log('===== 3DTank自由视角启动 ====='); if (!window.__gameMode__) { console.error('请先运行 hook_gamemode.js'); return; } if (window.__hornetFreeLook__) { window.__hornetFreeLook__.Stop(); console.log('已停止旧自由视角'); } var gm = window.__gameMode__; // ===== 引用公共库 ===== var lib = window.__hornetLib__; if (!lib) { console.error('[自由视角] 请先运行 hornet_lib.js'); return; } function GetPlayerTank() { return lib.GetPlayerTank(gm); } function GetTankPosition(tank) { return lib.GetTankPosition(tank); } // ===== 获取游戏渲染对象 ===== var scene = null, mainCamera = null, ff = null; var initError = null; try { scene = gm.jyw().iyt_1; mainCamera = scene.hyp_1; ff = scene.fyo_1.d75_1; if (!mainCamera || !mainCamera.s35_1 || !mainCamera.r35_1 || !mainCamera.o37) { throw new Error('相机字段缺失'); } console.log('[自由视角] 主相机:', mainCamera.constructor.name, 'l35=(' + mainCamera.s35_1.f20_1.toFixed(2) + ',' + mainCamera.s35_1.g20_1.toFixed(2) + ',' + mainCamera.s35_1.h20_1.toFixed(2) + ')', 'k35=(' + mainCamera.r35_1.f20_1.toFixed(0) + ',' + mainCamera.r35_1.g20_1.toFixed(0) + ',' + mainCamera.r35_1.h20_1.toFixed(0) + ')'); } catch(e) { initError = e; console.error('[自由视角] 初始化失败:', e.message); } if (initError) return; // ===== 自由视角状态 ===== var freeLookActive = false; var running = true; // 自由视角参数(从当前相机参数初始化) var freeYaw = 0, freePitch = 0, freeRoll = 0; var freePosX = 0, freePosY = 0, freePosZ = 0; var moveSpeed = 1200; // 基础移动速度(世界单位/秒) var speedMultiplier = 1; // 滚轮调速倍数(0.2~5.0) // 按键状态 var keys = {}; var mouseSensitivity = 0.005; // 鼠标灵敏度 // ===== Hook mainCamera.h37 ===== var hadOwnH37 = mainCamera.hasOwnProperty('o37'); var origH37 = mainCamera.o37; function applyFreeLookParams() { mainCamera.s35_1.f20_1 = freePitch; // pitch (f) mainCamera.s35_1.g20_1 = freeRoll; // roll (g) 保持初始值 mainCamera.s35_1.h20_1 = freeYaw; // yaw (h) 鼠标左右控制 mainCamera.r35_1.f20_1 = freePosX; mainCamera.r35_1.g20_1 = freePosY; mainCamera.r35_1.h20_1 = freePosZ; } // 缓存从 viewMatrix 提取的前方向/右方向(h37 更新后设置) // o37 行向量布局(列主序存储): // 行0 = (d[0], d[4], d[8]) = right 向量 // 行1 = (d[1], d[5], d[9]) = up 向量 // 行2 = (d[2], d[6], d[10]) = -forward 向量 var cachedFwdX = 0, cachedFwdY = 0, cachedRightX = 0, cachedRightY = 0; var cachedInitialized = false; var suspended = false; // 后视镜等模块渲染时临时暂停 hook mainCamera.o37 = function() { if (freeLookActive && !suspended) { applyFreeLookParams(); origH37.call(this); // 从更新后的 viewMatrix (u36_1) 提取前方向和右方向(水平分量 x,y) if (this.b37_1) { cachedFwdX = -this.b37_1[2]; // forward.x cachedFwdY = -this.b37_1[6]; // forward.y cachedRightX = this.b37_1[0]; // right.x cachedRightY = this.b37_1[4]; // right.y cachedInitialized = true; } } else { return origH37.call(this); } }; console.log('[自由视角] mainCamera.o37 已 Hook'); // ===== 开启/关闭自由视角 ===== function enableFreeLook() { // 从当前相机参数初始化(平滑过渡) // l35_1: f=pitch, g=roll, h=yaw freePitch = mainCamera.s35_1.f20_1; freeRoll = mainCamera.s35_1.g20_1; // roll 保持初始值 freeYaw = mainCamera.s35_1.h20_1; // yaw 鼠标控制 freePosX = mainCamera.r35_1.f20_1; freePosY = mainCamera.r35_1.g20_1; freePosZ = mainCamera.r35_1.h20_1; freeLookActive = true; console.log('[自由视角] 已开启 — WASD移动 | 鼠标旋转 | 空格上升/Shift下降 | 滚轮调速 | Alt+F关闭'); updateHud(); } function disableFreeLook() { freeLookActive = false; keys = {}; // 清空按键状态,防止卡死 console.log('[自由视角] 已关闭,恢复游戏相机控制'); updateHud(); } function toggleFreeLook() { if (freeLookActive) disableFreeLook(); else enableFreeLook(); } // ===== 键盘事件(capture阶段拦截,防止游戏接收WASD) ===== // 用 e.code(物理键位)匹配,比 e.key 更稳定,不受 Shift 修饰影响 var keyMap = { 'KeyW': 'w', 'KeyA': 'a', 'KeyS': 's', 'KeyD': 'd', 'KeyQ': 'q', 'KeyE': 'e', 'Space': ' ', 'ShiftLeft': 'shift', 'ShiftRight': 'shift' }; function onKeyDown(e) { // 焦点在输入框/选择框时不拦截(允许面板输入) var ae = document.activeElement; if (ae && (ae.tagName === 'INPUT' || ae.tagName === 'SELECT' || ae.tagName === 'TEXTAREA')) return; // Alt+F 切换(全局,无论是否开启) if (e.altKey && (e.key === 'f' || e.key === 'F')) { e.preventDefault(); e.stopPropagation(); toggleFreeLook(); return; } if (!freeLookActive) return; var key = keyMap[e.code] || e.key.toLowerCase(); if (keyMap[e.code]) { keys[key] = true; e.preventDefault(); e.stopPropagation(); } } function onKeyUp(e) { // 焦点在输入框/选择框时不拦截 var ae = document.activeElement; if (ae && (ae.tagName === 'INPUT' || ae.tagName === 'SELECT' || ae.tagName === 'TEXTAREA')) return; var key = keyMap[e.code] || e.key.toLowerCase(); if (keyMap[e.code]) { keys[key] = false; // 总是清理按键状态,防止卡死 // 只在自由视角激活时拦截事件,暂停时放行给游戏 if (freeLookActive) { e.preventDefault(); e.stopPropagation(); } } } // ===== 鼠标事件(自由视角激活时鼠标移动直接旋转,不按键) ===== function onMouseMove(e) { if (!freeLookActive) return; // 鼠标左移→视角左转(yaw减),鼠标上移→视角上抬(pitch增) freeYaw -= e.movementX * mouseSensitivity; freePitch -= e.movementY * mouseSensitivity; // 限制俯仰角到±80°,避免接近90°时的万向锁翻转 var limit = 80 * Math.PI / 180; if (freePitch > limit) freePitch = limit; if (freePitch < -limit) freePitch = -limit; } function onWheel(e) { if (!freeLookActive) return; e.preventDefault(); e.stopPropagation(); // 滚轮上=加速,下=减速 if (e.deltaY < 0) speedMultiplier = Math.min(5.0, speedMultiplier + 0.2); else speedMultiplier = Math.max(0.2, speedMultiplier - 0.2); updateHud(); } // capture 阶段拦截键盘 window.addEventListener('keydown', onKeyDown, true); window.addEventListener('keyup', onKeyUp, true); document.addEventListener('mousemove', onMouseMove); window.addEventListener('wheel', onWheel, true); // ===== HUD 显示 ===== var hud = document.createElement('div'); hud.id = 'hornetFreeLookHud'; hud.style.cssText = 'position:fixed;bottom:10px;left:10px;z-index:99999;' + 'background:rgba(0,0,0,0.7);color:#8f8;padding:6px 10px;border-radius:4px;' + 'font:12px monospace;pointer-events:none;border:1px solid rgba(120,255,120,0.5);' + 'display:none;user-select:none;'; document.body.appendChild(hud); function updateHud() { if (!freeLookActive) { hud.style.display = 'none'; return; } hud.style.display = 'block'; hud.innerHTML = '[自由视角 ON] 速度:' + (moveSpeed * speedMultiplier).toFixed(0) + '/秒
' + '位置:(' + freePosX.toFixed(0) + ',' + freePosY.toFixed(0) + ',' + freePosZ.toFixed(0) + ')
' + 'Yaw:' + freeYaw.toFixed(2) + ' Pitch:' + freePitch.toFixed(2) + '
' + 'WASD移动 鼠标旋转 空格上升/Shift下降 滚轮调速 Alt+F关闭'; } // ===== 主循环:处理按键 → 更新位置 ===== var rafId = null; var lastTime = performance.now(); function loop(now) { if (!running) return; // 换图自刷新:gameMode 实例变化时整体重启,重新 Hook 新相机的 o37 var NewGm = window.__gameMode__; if (NewGm && NewGm !== gm) { console.log('[自由视角] 检测到新战斗,正在重启以绑定新相机...'); self.Stop(); _start(); return; } // 旧实例已失效、新实例尚未捕获:跳过本帧 if (!window.__gameMode__) { rafId = requestAnimationFrame(loop); return; } var dt = (now - lastTime) / 1000; lastTime = now; if (dt > 0.1) dt = 0.1; // 防止卡顿后大跳 if (freeLookActive) { var speed = moveSpeed * speedMultiplier * dt; // 每秒 moveSpeed 单位 // 相机前方向/右方向:优先用 viewMatrix 提取的真实方向(跟随鼠标朝向) var fwdX, fwdY, rightX, rightY; if (cachedInitialized) { // 从 viewMatrix 提取的水平方向分量(列主序) fwdX = cachedFwdX; fwdY = cachedFwdY; rightX = cachedRightX; rightY = cachedRightY; // 归一化水平方向(pitch 不为0时向量长度<1,保持移动速度一致) var fwdLen = Math.sqrt(fwdX * fwdX + fwdY * fwdY); if (fwdLen > 1e-6) { fwdX /= fwdLen; fwdY /= fwdLen; } var rightLen = Math.sqrt(rightX * rightX + rightY * rightY); if (rightLen > 1e-6) { rightX /= rightLen; rightY /= rightLen; } } else { // fallback: sin/cos(viewMatrix 尚未初始化时) fwdX = -Math.sin(freeYaw); fwdY = -Math.cos(freeYaw); rightX = Math.cos(freeYaw); rightY = -Math.sin(freeYaw); } var moved = false; // W/S 前后(水平移动,可与其他键组合) if (keys['w']) { freePosX += fwdX * speed; freePosY += fwdY * speed; moved = true; } if (keys['s']) { freePosX -= fwdX * speed; freePosY -= fwdY * speed; moved = true; } // A/D 左右(水平移动,可与其他键组合) if (keys['a']) { freePosX -= rightX * speed; freePosY -= rightY * speed; moved = true; } if (keys['d']) { freePosX += rightX * speed; freePosY += rightY * speed; moved = true; } // 空格上升 / Shift 下降(绝对 z 轴,可与 WASD 组合) if (keys[' ']) { freePosZ += speed; moved = true; } if (keys['shift']) { freePosZ -= speed; moved = true; } // Q/E 升降(备用) if (keys['q']) { freePosZ -= speed; moved = true; } if (keys['e']) { freePosZ += speed; moved = true; } if (moved) updateHud(); } rafId = requestAnimationFrame(loop); } rafId = requestAnimationFrame(loop); // ===== 对外接口 ===== var self = { Stop: function() { running = false; if (rafId) cancelAnimationFrame(rafId); disableFreeLook(); // 恢复 h37 if (hadOwnH37) mainCamera.o37 = origH37; else delete mainCamera.o37; // 移除监听 window.removeEventListener('keydown', onKeyDown, true); window.removeEventListener('keyup', onKeyUp, true); document.removeEventListener('mousemove', onMouseMove); window.removeEventListener('wheel', onWheel, true); if (hud.parentNode) hud.parentNode.removeChild(hud); window.__hornetFreeLook__ = null; console.log('[自由视角] 已停止'); }, Status: function() { // 返回字符串供面板 HUD 显示(其他工具也是字符串) if (!running) return '未运行'; return freeLookActive ? '已开启' : '已暂停'; }, GetRunning: function() { return freeLookActive; }, IsActive: function() { return freeLookActive; }, Toggle: function() { toggleFreeLook(); }, Enable: function() { if (!freeLookActive) enableFreeLook(); }, Disable: function() { if (freeLookActive) disableFreeLook(); }, // 供其他模块(雷达/3D透视)获取虚拟相机参数 GetPosition: function() { return freeLookActive ? { x: freePosX, y: freePosY, z: freePosZ } : null; }, GetDirection: function() { return freeLookActive ? { yaw: freeYaw, pitch: freePitch } : null; }, // 供后视镜等模块渲染时临时暂停 h37 hook SetSuspended: function(s) { suspended = s; } }; window.__hornetFreeLook__ = self; window.__hornetTools__ = window.__hornetTools__ || {}; window.__hornetTools__.FreeLook = self; // 启动时自动激活自由视角 enableFreeLook(); console.log('[自由视角] 就绪 — 已激活,Alt+F 切换激活/暂停'); } function _stop() { if (window.__hornetFreeLook__) window.__hornetFreeLook__.Stop(); } function _status() { return window.__hornetFreeLook__ ? window.__hornetFreeLook__.Status() : '未运行'; } function _getRunning() { return !!(window.__hornetFreeLook__ && window.__hornetFreeLook__.GetRunning()); } window.__hornetTools__ = window.__hornetTools__ || {}; window.__hornetTools__.FreeLook = { Start: _start, Stop: _stop, Status: _status, GetRunning: _getRunning }; })(); // ===== hornet_warsandbox.js ===== // ===== 3DTank 战争沙盘 - 真实3D俯瞰渲染 ===== // 在浏览器控制台运行(需先运行 hook_gamemode.js 和 hornet_lib.js) // 功能:80%x90% 可拖拽缩放的全景俯瞰图,使用游戏原生渲染器渲染 3D 场景 // 原理:Hook gl.bindFramebuffer 重定向到自定义 FBO,用俯瞰相机参数调用游戏渲染器 // 操作:拖拽标题移动窗口 | 拖拽地图平移 | 滚轮缩放 | 双击居中 | Alt+M 开关 (function() { function _start() { console.log('===== 3DTank 战争沙盘(3D俯瞰版)启动 ====='); if (!window.__gameMode__) { console.error('请先运行 hook_gamemode.js'); return; } if (window.__hornetWarSandbox__) { window.__hornetWarSandbox__.Stop(); console.log('已停止旧战争沙盘'); } var gm = window.__gameMode__; var lib = window.__hornetLib__; if (!lib) { console.error('[沙盘] 请先运行 hornet_lib.js'); return; } var teamFns = lib.LoadTeamFunctions(); var FindComponentByField = lib.FindComponentByField; var GetTankPosition = lib.GetTankPosition; var GetTankState = lib.GetTankState; var IsTankAlive = lib.IsTankAlive; var GetTankName = lib.GetTankName; var GetTankHealth = lib.GetTankHealth; function GetPlayerTank() { return lib.GetPlayerTank(gm); } function IsFriendly(tank, playerTank) { return lib.IsFriendly(tank, playerTank, gm); } function getTankDirection(tank) { // 从车体皮肤组件(KMt)的Bp节点世界矩阵提取前向方向 // dzc_1 是局部空间向量且可能未设置,不可靠 // 皮肤字段:新 build 是 w14w_1;旧 build 是 t14l_1(新版 t14l_1 已改成悬停车体状态 Qmt) var hullComp = FindComponentByField(tank, 'w14w_1') || FindComponentByField(tank, 't14l_1') || FindComponentByField(tank, 'u14l_1') || FindComponentByField(tank, 'p14l_1'); if (!hullComp) return null; var skin = (hullComp.w14w_1 && hullComp.w14w_1.s17b_1) ? hullComp.w14w_1 : (hullComp.t14l_1 && hullComp.t14l_1.s17b_1) ? hullComp.t14l_1 : (hullComp.p14l_1 && hullComp.p14l_1.s17b_1) ? hullComp.p14l_1 : null; if (!skin) return null; try { var partCount = skin.s17b_1.e1(); for (var p = 0; p < partCount; p++) { var part = skin.s17b_1.g1(p); if (!part || !part.v17b_1) continue; var nodeCount = part.v17b_1.e1(); for (var n = 0; n < nodeCount; n++) { var bpNode = part.v17b_1.g1(n); if (!bpNode || !bpNode.n35_1) continue; // 不按 g35_1 过滤:它是引擎的细节/淡出权重(0~1),不是幽灵标志, // 过滤会漏掉低细节节点、导致取不到朝向 var m = bpNode.n35_1; // 列主序世界矩阵:Col0=(m[0],m[1],m[2]) Col1=(m[4],m[5],m[6]) Col2=(m[8],m[9],m[10]) // 游戏坐标:x,y 水平面; z(h20_1)=高度。找"上"列(|z|最大),其余为水平方向 var az0 = Math.abs(m[2]), az1 = Math.abs(m[6]), az2 = Math.abs(m[10]); // 尝试 Col1(Y前向约定,Z-up游戏常见)作为前向,若 Col1 是"上"则用 Col2 var fx, fy; if (az1 < 0.7) { // Col1 不是上方向,用作前向 fx = m[4]; fy = m[5]; } else if (az2 < 0.7) { // Col1 是上,用 Col2 作前向 fx = m[8]; fy = m[9]; } else { // Col0 不是上,用作前向 fx = m[0]; fy = m[1]; } var len = Math.sqrt(fx*fx + fy*fy); if (len > 0.01) { return { x: fx/len, y: fy/len, z: 0 }; } } } } catch(e) {} return null; } // ===== 获取游戏渲染对象 ===== var scene = null, ff = null, rootNode = null, mainCamera = null, gl = null; var initError = null; try { scene = gm.jyw().iyt_1; ff = scene.fyo_1.d75_1; rootNode = scene.eyp_1; mainCamera = scene.hyp_1; gl = ff.w39_1.b74_1; if (!ff || !rootNode || !mainCamera || !gl) throw new Error('关键字段缺失'); } catch(e) { initError = e; console.error('[沙盘] 初始化失败:', e.message); } if (initError) return; // ===== FBO 配置(固定内部分辨率,性能优先)===== var FBO_W = 960, FBO_H = 720; var Config = { fov: 1.0, // 俯瞰 FOV near: 100, far: 80000, minHeight: 3000, // 最小相机高度(放大) maxHeight: 60000, // 最大相机高度(缩小) defaultHeight: 20000 }; // 设置参数(localStorage 持久化) var ShowGrid = true; try { if (localStorage.getItem('hornet_ws_showgrid') === 'false') ShowGrid = false; } catch(e) {} var MarkerSize = 1.0; try { var __savedMS = localStorage.getItem('hornet_ws_marker_size'); if (__savedMS) { var __ms = parseFloat(__savedMS); if (!isNaN(__ms) && __ms >= 0.5 && __ms <= 3.0) MarkerSize = __ms; } } catch(e) {} // ===== 创建 UI ===== var container = document.createElement('div'); container.id = 'hornetWarSandbox'; var winW = Math.floor(window.innerWidth * 0.8); var winH = Math.floor(window.innerHeight * 0.9); container.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:100002;' + 'width:' + winW + 'px;height:' + winH + 'px;' + 'background:rgba(8,12,20,0.82);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);' + 'border:1px solid rgba(125,249,255,0.25);border-radius:12px;' + 'box-shadow:0 12px 48px rgba(0,0,0,0.7);display:flex;flex-direction:column;overflow:hidden;' + 'user-select:none;-webkit-user-select:none;'; document.body.appendChild(container); // ===== 窗口布局持久化(位置 + 尺寸)===== function SaveWinLayout() { try { var r = container.getBoundingClientRect(); localStorage.setItem('hornet_layout_warsandbox', JSON.stringify({ left: Math.round(r.left), top: Math.round(r.top), width: Math.round(r.width), height: Math.round(r.height) })); } catch(e) {} } try { var __wsSaved = localStorage.getItem('hornet_layout_warsandbox'); if (__wsSaved) { var __wsP = JSON.parse(__wsSaved); if (__wsP && typeof __wsP.left === 'number' && typeof __wsP.top === 'number') { container.style.transform = 'none'; container.style.left = Math.max(0, __wsP.left) + 'px'; container.style.top = Math.max(0, __wsP.top) + 'px'; if (typeof __wsP.width === 'number') container.style.width = Math.max(320, __wsP.width) + 'px'; if (typeof __wsP.height === 'number') container.style.height = Math.max(240, __wsP.height) + 'px'; } } } catch(e) {} var titleBar = document.createElement('div'); titleBar.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:10px 16px;' + 'background:rgba(125,249,255,0.06);border-bottom:1px solid rgba(125,249,255,0.15);cursor:move;' + 'font:14px Consolas,monospace;color:#7df9ff;flex-shrink:0;'; titleBar.innerHTML = '战争沙盘'; container.appendChild(titleBar); var ctrlBox = document.createElement('div'); ctrlBox.style.cssText = 'display:flex;gap:10px;align-items:center;'; titleBar.appendChild(ctrlBox); var infoLabel = document.createElement('span'); infoLabel.style.cssText = 'font-size:11px;color:#5a7a8a;'; ctrlBox.appendChild(infoLabel); var closeBtn = document.createElement('button'); closeBtn.textContent = 'X'; closeBtn.style.cssText = 'padding:2px 8px;border:1px solid rgba(255,100,100,0.4);border-radius:4px;' + 'background:rgba(200,50,50,0.3);color:#ff8080;cursor:pointer;font:12px Consolas;'; closeBtn.onclick = function() { _stop(); }; ctrlBox.appendChild(closeBtn); var canvas = document.createElement('canvas'); canvas.style.cssText = 'flex:1;cursor:grab;display:block;'; container.appendChild(canvas); var ctx = canvas.getContext('2d'); function resizeCanvas() { var rect = canvas.getBoundingClientRect(); canvas.width = Math.floor(rect.width); canvas.height = Math.floor(rect.height); } resizeCanvas(); // ===== 右下角缩放手柄:允许自由缩放窗口 ===== var resizeGrip = document.createElement('div'); resizeGrip.style.cssText = 'position:absolute;right:0;bottom:0;width:18px;height:18px;cursor:nwse-resize;z-index:2;' + 'background:linear-gradient(135deg,transparent 45%,rgba(125,249,255,0.35) 45%);' + 'border-bottom-right-radius:12px;'; container.appendChild(resizeGrip); var __rsx = 0, __rsy = 0, __rsw = 0, __rsh = 0; BindPointerDrag(resizeGrip, function(e) { var r = container.getBoundingClientRect(); __rsx = e.clientX; __rsy = e.clientY; __rsw = r.width; __rsh = r.height; return true; }, function(e) { container.style.transform = 'none'; container.style.width = Math.max(320, __rsw + (e.clientX - __rsx)) + 'px'; container.style.height = Math.max(240, __rsh + (e.clientY - __rsy)) + 'px'; resizeCanvas(); }, function() { resizeCanvas(); SaveWinLayout(); }); // ===== FBO 创建 ===== var sandboxFbo = null, sandboxTex = null, sandboxDepthRb = null; var pixels = new Uint8Array(FBO_W * FBO_H * 4); var pixels32 = new Uint32Array(pixels.buffer); var imageData = ctx.createImageData(FBO_W, FBO_H); var tempCanvas = document.createElement('canvas'); tempCanvas.width = FBO_W; tempCanvas.height = FBO_H; var tempCtx = tempCanvas.getContext('2d'); var pbos = [null, null], pboIndex = 0, pboReady = false, pboSupported = false; try { sandboxFbo = gl.createFramebuffer(); sandboxTex = gl.createTexture(); sandboxDepthRb = gl.createRenderbuffer(); gl.bindTexture(gl.TEXTURE_2D, sandboxTex); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, FBO_W, FBO_H, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.bindRenderbuffer(gl.RENDERBUFFER, sandboxDepthRb); gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, FBO_W, FBO_H); gl.bindFramebuffer(gl.FRAMEBUFFER, sandboxFbo); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, sandboxTex, 0); gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, sandboxDepthRb); var status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.bindTexture(gl.TEXTURE_2D, null); gl.bindRenderbuffer(gl.RENDERBUFFER, null); if (status !== gl.FRAMEBUFFER_COMPLETE) throw new Error('FBO不完整: ' + status); // PBO try { var byteSize = FBO_W * FBO_H * 4; for (var i = 0; i < 2; i++) { pbos[i] = gl.createBuffer(); gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbos[i]); gl.bufferData(gl.PIXEL_PACK_BUFFER, byteSize, gl.STREAM_READ); } gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null); pboSupported = true; } catch(e) { pboSupported = false; } console.log('[沙盘] FBO ' + FBO_W + 'x' + FBO_H + ' 已创建' + (pboSupported ? ' (PBO)' : ' (同步)')); } catch(e) { initError = e; console.error('[沙盘] FBO创建失败:', e.message); } if (initError) return; // ===== Hook gl.bindFramebuffer + BS.h3c/g3c ===== var sandboxRendering = false; var mainFbo = null; var origBindFramebuffer = gl.bindFramebuffer.bind(gl); gl.bindFramebuffer = function(target, fbo) { if (!sandboxRendering && target === gl.FRAMEBUFFER) mainFbo = fbo; if (sandboxRendering && target === gl.FRAMEBUFFER && fbo !== sandboxFbo) { return origBindFramebuffer(target, sandboxFbo); } return origBindFramebuffer(target, fbo); }; var origH3c = ff.w39_1.o3c.bind(ff.w39_1); var origG3c = ff.w39_1.n3c.bind(ff.w39_1); ff.w39_1.o3c = function() { return sandboxRendering ? FBO_W : origH3c(); }; ff.w39_1.n3c = function() { return sandboxRendering ? FBO_H : origG3c(); }; // UI 层禁用 var uiLayerIndices = [20, 21, 22, 23, 24]; var fakeQueue = { e1: function() { return 0; }, g1: function(i) { return null; }, w2a: function(x) {}, d3: function() {}, ii: function(x) {} }; // ===== 视图状态 ===== var viewCenterX = 0, viewCenterY = 0; var camHeight = Config.defaultHeight; var viewYaw = 0; // 地图旋转(0=北朝上) var initialized = false; // ===== 指针拖动绑定(Pointer Events + setPointerCapture,避免松手后粘手)===== // HandleEl: 触发拖动的元素;OnStart/OnMove/OnEnd 分别处理开始/移动/结束 function BindPointerDrag(HandleEl, OnStart, OnMove, OnEnd) { var Active = false; var PointerId = null; function Finish() { if (!Active) return; Active = false; if (PointerId !== null && HandleEl.releasePointerCapture) { try { HandleEl.releasePointerCapture(PointerId); } catch (Err) {} } PointerId = null; OnEnd(); } if (window.PointerEvent) { HandleEl.addEventListener('pointerdown', function(e) { if (e.button !== 0) return; if (OnStart(e) === false) return; Active = true; PointerId = e.pointerId; try { HandleEl.setPointerCapture(e.pointerId); } catch (Err) {} e.preventDefault(); e.stopPropagation(); }, true); HandleEl.addEventListener('pointermove', function(e) { if (!Active || (PointerId !== null && e.pointerId !== PointerId)) return; if (e.buttons === 0) { Finish(); return; } // 兜底:按键已松开 OnMove(e); e.preventDefault(); }, true); HandleEl.addEventListener('pointerup', Finish, true); HandleEl.addEventListener('pointercancel', Finish, true); HandleEl.addEventListener('lostpointercapture', Finish, true); window.addEventListener('pointerup', Finish, true); } else { HandleEl.addEventListener('mousedown', function(e) { if (e.button !== 0) return; if (OnStart(e) === false) return; Active = true; e.preventDefault(); }, true); document.addEventListener('mousemove', function(e) { if (!Active) return; if (e.buttons === 0) { Finish(); return; } OnMove(e); }, true); document.addEventListener('mouseup', Finish, true); } window.addEventListener('blur', Finish); window.addEventListener('mouseleave', Finish); return { End: Finish, IsActive: function() { return Active; } }; } // ===== 窗口拖动 ===== var dsx = 0, dsy = 0, dwx = 0, dwy = 0; BindPointerDrag(titleBar, function(e) { if (e.target.tagName === 'BUTTON') return false; dsx = e.clientX; dsy = e.clientY; var rect = container.getBoundingClientRect(); dwx = rect.left; dwy = rect.top; container.style.transform = 'none'; container.style.left = dwx + 'px'; container.style.top = dwy + 'px'; }, function(e) { var nx = Math.max(0, Math.min(window.innerWidth - 100, dwx + e.clientX - dsx)); var ny = Math.max(0, Math.min(window.innerHeight - 40, dwy + e.clientY - dsy)); container.style.left = nx + 'px'; container.style.top = ny + 'px'; }, function() { SaveWinLayout(); }); // ===== 地图平移 ===== var psx = 0, psy = 0, pcx = 0, pcy = 0; BindPointerDrag(canvas, function(e) { psx = e.clientX; psy = e.clientY; pcx = viewCenterX; pcy = viewCenterY; canvas.style.cursor = 'grabbing'; }, function(e) { // 俯瞰视图:屏幕像素→世界单位,X/Y 方向缩放不同(FBO 拉伸到 canvas) var ext = getViewExtents(); var dx = (e.clientX - psx) * ext.wppX; var dy = (e.clientY - psy) * ext.wppY; viewCenterX = pcx - dx; viewCenterY = pcy + dy; // y 反转 }, function() { canvas.style.cursor = 'grab'; }); // 滚轮缩放(改变相机高度) canvas.addEventListener('wheel', function(e) { e.preventDefault(); var factor = e.deltaY < 0 ? 0.85 : 1.15; var newHeight = camHeight * factor; if (newHeight < Config.minHeight || newHeight > Config.maxHeight) return; camHeight = newHeight; }, { passive: false }); // 双击居中 canvas.addEventListener('dblclick', function() { var player = GetPlayerTank(); if (player) { var pos = GetTankPosition(player); if (pos) { viewCenterX = pos.x; viewCenterY = pos.y; } } else { var fl = window.__hornetFreeLook__; if (fl && fl.IsActive && fl.IsActive()) { var p = fl.GetPosition(); if (p) { viewCenterX = p.x; viewCenterY = p.y; } } } }); // ===== 世界→屏幕投影(俯瞰视图,用于 2D 叠加层)===== // 注意:FBO(960x720) 被拉伸到 canvas(cw x ch),若两者比例不同则 X/Y 缩放不同 // 相机以 FBO_W/FBO_H 为 aspect 渲染,可见世界范围: // visH = 2*camHeight*tan(fov/2) (世界单位,纵向) // visW = visH * (FBO_W/FBO_H) (世界单位,横向) // FBO 拉伸到 canvas 后: // worldPerPixelX = visW / cw // worldPerPixelY = visH / ch function getViewExtents() { var ch = canvas.height, cw = canvas.width; var visH = 2 * camHeight * Math.tan(Config.fov / 2); var visW = visH * (FBO_W / FBO_H); return { visW: visW, visH: visH, wppX: visW / cw, // X方向 世界单位/像素 wppY: visH / ch // Y方向 世界单位/像素 }; } function worldToScreen(wx, wy) { var ext = getViewExtents(); var cw = canvas.width, ch = canvas.height; return { x: cw / 2 + (wx - viewCenterX) / ext.wppX, y: ch / 2 - (wy - viewCenterY) / ext.wppY }; } // ===== 2D 叠加层绘制 ===== function drawOverlays() { var cw = canvas.width, ch = canvas.height; var playerTank = GetPlayerTank(); // 网格(X/Y 方向缩放不同,用 ext 计算) if (ShowGrid) { ctx.strokeStyle = 'rgba(125,249,255,0.08)'; ctx.lineWidth = 1; var steps = [500, 1000, 2000, 5000, 10000, 20000, 50000]; var step = 5000; var ext = getViewExtents(); // 用较小的 wpp 选择网格间距(保证密度合适) var minWpp = Math.min(ext.wppX, ext.wppY); for (var i = 0; i < steps.length; i++) { if (steps[i] / minWpp > 60) { step = steps[i]; break; } } var left = viewCenterX - (cw/2) * ext.wppX; var right = viewCenterX + (cw/2) * ext.wppX; var top = viewCenterY + (ch/2) * ext.wppY; var bottom = viewCenterY - (ch/2) * ext.wppY; var startX = Math.floor(left/step)*step; var startY = Math.floor(bottom/step)*step; ctx.beginPath(); for (var wx = startX; wx <= right; wx += step) { var p = worldToScreen(wx, 0); ctx.moveTo(p.x, 0); ctx.lineTo(p.x, ch); } for (var wy = startY; wy <= top; wy += step) { var p = worldToScreen(0, wy); ctx.moveTo(0, p.y); ctx.lineTo(cw, p.y); } ctx.stroke(); // 距离标签 ctx.fillStyle = 'rgba(125,249,255,0.35)'; ctx.font = '10px Consolas'; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; for (var wx2 = startX; wx2 <= right; wx2 += step) { var p = worldToScreen(wx2, 0); ctx.fillText((wx2/100).toFixed(0)+'m', p.x+2, 2); } } // 坦克标记 var tanks = []; try { var list = gm.q100_1.r11l(); var n = list.e1(); for (var i = 0; i < n; i++) { var t = list.g1(i); if (t) tanks.push(t); } } catch(e) {} var margin = 50; tanks.forEach(function(tank) { var pos = GetTankPosition(tank); if (!pos) return; var p = worldToScreen(pos.x, pos.y); if (p.x < -margin || p.x > cw+margin || p.y < -margin || p.y > ch+margin) return; var isPlayer = (tank === playerTank); var friendly = IsFriendly(tank, playerTank); var alive = IsTankAlive(tank); var state = GetTankState(tank); if (state !== 3 && !isPlayer) return; var color, glow; if (isPlayer) { color = '#ffdd44'; glow = 'rgba(255,221,68,0.7)'; } else if (friendly) { color = '#44ff88'; glow = 'rgba(68,255,136,0.5)'; } else { color = '#ff5566'; glow = 'rgba(255,85,102,0.5)'; } var radius = (isPlayer ? 8 : 6) * MarkerSize; if (!alive) { radius = 4 * MarkerSize; color = '#666'; glow = 'none'; } // 方向指示(真实坦克朝向) var dir = getTankDirection(tank); if (dir && alive) { var dirLen = Math.sqrt(dir.x*dir.x + dir.y*dir.y); if (dirLen > 0.01) { var dx = dir.x / dirLen, dy = dir.y / dirLen; var arrowLen = 22; ctx.strokeStyle = color; ctx.lineWidth = 2.5; ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(p.x + dx * arrowLen, p.y - dy * arrowLen); ctx.stroke(); // 箭头头 var ax = p.x + dx * arrowLen, ay = p.y - dy * arrowLen; var ang = Math.atan2(-dy, dx); ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(ax - 7*Math.cos(ang-0.4), ay - 7*Math.sin(ang-0.4)); ctx.moveTo(ax, ay); ctx.lineTo(ax - 7*Math.cos(ang+0.4), ay - 7*Math.sin(ang+0.4)); ctx.stroke(); } } // 光晕标记 if (alive) { ctx.save(); ctx.shadowColor = glow; ctx.shadowBlur = 12; ctx.fillStyle = color; ctx.beginPath(); ctx.arc(p.x, p.y, radius, 0, Math.PI*2); ctx.fill(); ctx.restore(); } else { ctx.fillStyle = color; ctx.fillRect(p.x-3, p.y-3, 6, 6); } // 玩家外圈 if (isPlayer && alive) { ctx.strokeStyle = 'rgba(255,221,68,0.6)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(p.x, p.y, radius+6, 0, Math.PI*2); ctx.stroke(); } // 名称 var name = GetTankName(tank); if (name) { ctx.font = 'bold 11px Consolas'; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom'; ctx.strokeStyle = 'rgba(0,0,0,0.85)'; ctx.lineWidth = 3; ctx.strokeText(name, p.x, p.y - radius - 5); ctx.fillStyle = alive ? '#fff' : '#666'; ctx.fillText(name, p.x, p.y - radius - 5); } // 血量条 if (alive) { var health = GetTankHealth(tank); if (health) { var pct = health.cur / health.max; if (pct > 1) pct = 1; var barW = 34, barH = 5; var bx = p.x - barW/2, by = p.y + radius + 4; ctx.fillStyle = 'rgba(0,0,0,0.7)'; ctx.fillRect(bx, by, barW, barH); var hpColor = pct > 0.6 ? '#2ecc71' : (pct > 0.3 ? '#f5a623' : '#e05555'); ctx.fillStyle = hpColor; ctx.fillRect(bx, by, barW * pct, barH); ctx.strokeStyle = 'rgba(255,255,255,0.3)'; ctx.lineWidth = 0.5; ctx.strokeRect(bx, by, barW, barH); } } }); // 罗盘 var ccx = cw - 50, ccy = 50, cr = 28; ctx.save(); ctx.strokeStyle = 'rgba(125,249,255,0.4)'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(ccx, ccy, cr, 0, Math.PI*2); ctx.stroke(); ctx.fillStyle = 'rgba(8,12,20,0.6)'; ctx.beginPath(); ctx.arc(ccx, ccy, cr-1, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#ff5566'; ctx.beginPath(); ctx.moveTo(ccx, ccy - cr + 4); ctx.lineTo(ccx - 5, ccy); ctx.lineTo(ccx + 5, ccy); ctx.closePath(); ctx.fill(); ctx.fillStyle = 'rgba(200,210,220,0.6)'; ctx.beginPath(); ctx.moveTo(ccx, ccy + cr - 4); ctx.lineTo(ccx - 5, ccy); ctx.lineTo(ccx + 5, ccy); ctx.closePath(); ctx.fill(); ctx.fillStyle = '#7df9ff'; ctx.font = 'bold 12px Consolas'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('N', ccx, ccy - cr - 8); ctx.restore(); // 中心十字准星 ctx.strokeStyle = 'rgba(125,249,255,0.2)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(cw/2 - 15, ch/2); ctx.lineTo(cw/2 + 15, ch/2); ctx.moveTo(cw/2, ch/2 - 15); ctx.lineTo(cw/2, ch/2 + 15); ctx.stroke(); } // ===== 主渲染循环 ===== var running = true; var rafId = null; var frameCount = 0; function render() { if (!running) return; // 换图自刷新:gameMode 实例变化时整体重启,重新获取场景/相机/GL hook var NewGm = window.__gameMode__; if (NewGm && NewGm !== gm) { console.log('[沙盘] 检测到新战斗,正在重启战争沙盘...'); self.Stop(); _start(); return; } frameCount++; // 旧实例已失效、新实例尚未捕获:跳过本帧 if (!window.__gameMode__) { rafId = requestAnimationFrame(render); return; } var cw = canvas.width, ch = canvas.height; // 初始化视图中心 if (!initialized) { var player = GetPlayerTank(); if (player) { var pos = GetTankPosition(player); if (pos) { viewCenterX = pos.x; viewCenterY = pos.y; initialized = true; } } } // 暂停 FreeLook var fl = window.__hornetFreeLook__; if (fl && typeof fl.SetSuspended === 'function') fl.SetSuspended(true); try { // 保存主相机参数 var sPitch = mainCamera.s35_1.f20_1; var sRoll = mainCamera.s35_1.g20_1; var sYaw = mainCamera.s35_1.h20_1; var sX = mainCamera.r35_1.f20_1; var sY = mainCamera.r35_1.g20_1; var sZ = mainCamera.r35_1.h20_1; var sL36 = mainCamera.s36_1, sM36 = mainCamera.t36_1; var sS36 = mainCamera.z36_1, sQ36 = mainCamera.x36_1, sR36 = mainCamera.y36_1; // 设置俯瞰相机参数 // pitch=-π/2 朝下看,roll=0 不翻转,yaw=0 北朝上 mainCamera.s35_1.f20_1 = -Math.PI / 2; mainCamera.s35_1.g20_1 = 0; mainCamera.s35_1.h20_1 = 0; mainCamera.r35_1.f20_1 = viewCenterX; mainCamera.r35_1.g20_1 = viewCenterY; mainCamera.r35_1.h20_1 = camHeight; mainCamera.s36_1 = FBO_W; mainCamera.t36_1 = FBO_H; mainCamera.z36_1 = Config.fov; mainCamera.x36_1 = Config.near; mainCamera.y36_1 = Config.far; mainCamera.o37(); // 禁用阴影/后处理 var savedX3n = ff.w38_1.e3o_1; var savedY3n = ff.w38_1.f3o_1; var savedZ3n = ff.w38_1.g3o_1; ff.w38_1.e3o_1 = false; ff.w38_1.f3o_1 = false; ff.w38_1.g3o_1 = false; // 禁用 UI 层 var savedUiQueues = {}; for (var ui = 0; ui < uiLayerIndices.length; ui++) { var li = uiLayerIndices[ui]; savedUiQueues[li] = ff.e39_1[li]; ff.e39_1[li] = fakeQueue; } // 渲染到 FBO sandboxRendering = true; origBindFramebuffer(gl.FRAMEBUFFER, sandboxFbo); gl.viewport(0, 0, FBO_W, FBO_H); gl.clearColor(0.05, 0.08, 0.12, 1.0); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); ff.f3t(rootNode, mainCamera); sandboxRendering = false; // Readback origBindFramebuffer(gl.FRAMEBUFFER, sandboxFbo); if (pboSupported) { var curPbo = pboIndex, prevPbo = 1 - pboIndex; gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbos[curPbo]); gl.readPixels(0, 0, FBO_W, FBO_H, gl.RGBA, gl.UNSIGNED_BYTE, 0); if (pboReady) { gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbos[prevPbo]); gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, pixels); } gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null); pboIndex = prevPbo; pboReady = true; } else { gl.readPixels(0, 0, FBO_W, FBO_H, gl.RGBA, gl.UNSIGNED_BYTE, pixels); } // 强制 alpha=255 for (var ai = 0; ai < pixels32.length; ai++) pixels32[ai] = pixels32[ai] | 0xFF000000; imageData.data.set(pixels); // Y 翻转显示(roll=0 时 GL readback 产生上下颠倒图像,需翻转) tempCtx.putImageData(imageData, 0, 0); ctx.clearRect(0, 0, cw, ch); ctx.save(); ctx.imageSmoothingEnabled = true; ctx.scale(1, -1); ctx.drawImage(tempCanvas, 0, -ch, cw, ch); ctx.restore(); // 恢复主 FBO origBindFramebuffer(gl.FRAMEBUFFER, mainFbo); // 恢复渲染状态 ff.w38_1.e3o_1 = savedX3n; ff.w38_1.f3o_1 = savedY3n; ff.w38_1.g3o_1 = savedZ3n; for (var ui2 = 0; ui2 < uiLayerIndices.length; ui2++) { ff.e39_1[uiLayerIndices[ui2]] = savedUiQueues[uiLayerIndices[ui2]]; } // 恢复主相机 mainCamera.s35_1.f20_1 = sPitch; mainCamera.s35_1.g20_1 = sRoll; mainCamera.s35_1.h20_1 = sYaw; mainCamera.r35_1.f20_1 = sX; mainCamera.r35_1.g20_1 = sY; mainCamera.r35_1.h20_1 = sZ; mainCamera.s36_1 = sL36; mainCamera.t36_1 = sM36; mainCamera.z36_1 = sS36; mainCamera.x36_1 = sQ36; mainCamera.y36_1 = sR36; mainCamera.o37(); // 绘制 2D 叠加层(坦克标记/网格/罗盘) drawOverlays(); } catch(e) { console.error('[沙盘] 渲染失败:', e.message); sandboxRendering = false; } finally { // 确保 FreeLook 恢复 if (fl && typeof fl.SetSuspended === 'function') fl.SetSuspended(false); } // 信息更新 if (frameCount % 30 === 0) { var ext2 = getViewExtents(); var viewRange = cw * ext2.wppX; // 横向可视范围 infoLabel.textContent = '高度:' + (camHeight/100).toFixed(0) + 'm | 范围:' + (viewRange/100).toFixed(0) + 'm | 拖拽平移 滚轮缩放 双击居中'; } rafId = requestAnimationFrame(render); } window.addEventListener('resize', resizeCanvas); rafId = requestAnimationFrame(render); console.log('[沙盘] 战争沙盘已就绪 — 拖拽地图平移 | 滚轮缩放 | 双击居中玩家 | Alt+M 或 X 关闭'); // ===== 对外接口 ===== var self = { Stop: function() { running = false; if (rafId) cancelAnimationFrame(rafId); // 恢复 hook gl.bindFramebuffer = origBindFramebuffer; ff.w39_1.o3c = origH3c; ff.w39_1.n3c = origG3c; window.removeEventListener('resize', resizeCanvas); if (container.parentNode) container.parentNode.removeChild(container); window.__hornetWarSandbox__ = null; console.log('[沙盘] 战争沙盘已关闭'); }, Status: function() { return running ? '已开启' : '未运行'; }, GetRunning: function() { return running; }, SetShowGrid: function(on) { ShowGrid = !!on; try { localStorage.setItem('hornet_ws_showgrid', ShowGrid ? 'true' : 'false'); } catch(e) {} console.log('[沙盘] 网格: ' + (ShowGrid ? '显示' : '隐藏')); }, GetShowGrid: function() { return ShowGrid; }, SetMarkerSize: function(s) { var n = parseFloat(s); if (!isNaN(n) && n >= 0.5 && n <= 3.0) { MarkerSize = n; try { localStorage.setItem('hornet_ws_marker_size', String(n)); } catch(e) {} console.log('[沙盘] 标记大小: ' + n); } else { console.warn('[沙盘] 标记大小应在 0.5~3.0'); } }, GetMarkerSize: function() { return MarkerSize; } }; window.__hornetWarSandbox__ = self; window.__hornetTools__ = window.__hornetTools__ || {}; window.__hornetTools__.WarSandbox = self; } function _stop() { if (window.__hornetWarSandbox__) window.__hornetWarSandbox__.Stop(); } function _status() { return window.__hornetWarSandbox__ ? window.__hornetWarSandbox__.Status() : '未运行'; } function _getRunning() { return !!(window.__hornetWarSandbox__ && window.__hornetWarSandbox__.GetRunning()); } window.__hornetTools__ = window.__hornetTools__ || {}; window.__hornetTools__.WarSandbox = { Start: _start, Stop: _stop, Status: _status, GetRunning: _getRunning, SetShowGrid: function(on) { if (window.__hornetWarSandbox__) window.__hornetWarSandbox__.SetShowGrid(on); }, GetShowGrid: function() { return window.__hornetWarSandbox__ ? window.__hornetWarSandbox__.GetShowGrid() : true; }, SetMarkerSize: function(s) { if (window.__hornetWarSandbox__) window.__hornetWarSandbox__.SetMarkerSize(s); }, GetMarkerSize: function() { return window.__hornetWarSandbox__ ? window.__hornetWarSandbox__.GetMarkerSize() : 1.0; } }; })(); // ===== hornet_mouse.js ===== // hornet_mouse.js — 鼠标输入稳定器 // 目标:缓解原版「指针锁定下鼠标乱晃 / 难控制」的问题(游戏自身行为,非本工具链引入)。 // // 游戏侧机制(module_03569): // · 锁定:Lh() 先 requestPointerLock({unadjustedMovement:true})(原始输入),**失败则静默回退** // 成不带 unadjustedMovement 的普通锁定 —— 后者会带上 OS 鼠标加速,手感立刻变飘。 // · 视角:mousemove 里直接把 e.movementX / e.movementY 喂给输入管理器 // (fal_1.ia9(qh(), movementX, movementY)),没有任何尖峰/跳变过滤。 // // 本模块做三件事,全部可在面板「鼠标稳定」里开关: // ① 强制原始输入:接管 Element.prototype.requestPointerLock,始终带 unadjustedMovement, // 并把「回退/失败」打到控制台(手感异常的第一嫌疑人)。 // ② 锁后/恢复跳变抑制:指针锁定刚建立、或事件流中断(切窗口/失焦)后恢复的头 DropMs 毫秒内 // 丢弃 mousemove —— 这一两下常带巨大 movementX/Y,会直接把视角甩飞。 // ③ 尖峰钳制:单事件 |movementX| / |movementY| 超过 MaxDelta(0 = 关闭)时截断, // 抑制驱动/焦点抖动造成的瞬时尖峰。 // // 状态:HUD 里显示「鼠标: RAW 48/s 尖峰320 钳0 丢1」,或 // console.log(window.__hornetTools__.Mouse.GetStatus()) (function () { 'use strict'; var lib = window.__hornetLib__; if (!lib) { console.error('[鼠标] 请先加载 hornet_lib.js'); return; } // ===== 配置(localStorage 持久化)===== var CONFIG_KEY = 'hornet_mouse_cfg'; var Config = { ForceRaw: true, // 强制原始输入(unadjustedMovement),关掉则完全不干预指针锁定 DropMs: 120, // 锁定建立 / 事件流恢复后的忽略时长(ms,0 = 不丢事件) GapMs: 250, // 事件间隔超过它视为「恢复」,同样触发忽略窗口(ms) MaxDelta: 0 // 单事件位移钳制阈值(px,0 = 不钳制) }; try { var Saved = JSON.parse(localStorage.getItem(CONFIG_KEY) || '{}'); if (typeof Saved.ForceRaw === 'boolean') Config.ForceRaw = Saved.ForceRaw; if (typeof Saved.DropMs === 'number') Config.DropMs = Saved.DropMs; if (typeof Saved.GapMs === 'number') Config.GapMs = Saved.GapMs; if (typeof Saved.MaxDelta === 'number') Config.MaxDelta = Saved.MaxDelta; } catch (e) {} function SaveConfig() { try { localStorage.setItem(CONFIG_KEY, JSON.stringify(Config)); } catch (e) {} } function Now() { return (window.performance && window.performance.now) ? window.performance.now() : Date.now(); } // ===== 运行状态与统计 ===== var Stat = { running: false, rawMode: '未知', // 原始输入 / 带加速 / 未知 locked: false, dropUntil: 0, // 该时刻前的 mousemove 全部丢弃 dropped: 0, clamped: 0, events: 0, maxAbs: 0, // 本统计窗口内最大单事件位移 lastEventAt: 0, rate: 0, rateEvents: 0, rateWindow: 0, lastAnomalyAt: 0 }; var OrigRequestPointerLock = null; var Attached = false; // ===== ① 指针锁定:强制原始输入 ===== function InstallLockPatch() { if (OrigRequestPointerLock) return; OrigRequestPointerLock = Element.prototype.requestPointerLock; Element.prototype.requestPointerLock = function (Options) { var Opt = Options; if (Config.ForceRaw && !(Opt && Opt.unadjustedMovement)) { Opt = { unadjustedMovement: true }; // 显式请求原始输入(不改调用方对象) } // 只有在「本来没锁」时才算一次真正的锁定(避免已锁定时的重复请求反复刷新忽略窗口, // 那会让鼠标一直被丢弃而失去响应) var WasLocked = !!document.pointerLockElement; Stat.locked = true; if (!WasLocked) { Stat.lastEventAt = 0; // 重新计时,避免把旧的间隔当成「恢复」 Stat.dropUntil = Config.DropMs > 0 ? (Now() + Config.DropMs) : 0; } var P; try { P = OrigRequestPointerLock.call(this, Opt); } catch (Err) { console.error('[鼠标] requestPointerLock 调用异常:', Err && Err.message); return null; } var WantRaw = !!(Opt && Opt.unadjustedMovement); if (P && typeof P.then === 'function') { P.then( function () { Stat.rawMode = WantRaw ? '原始输入' : '带加速'; console.log('[鼠标] 指针锁定成功,模式=' + Stat.rawMode + (WantRaw ? '' : '(带 OS 加速,手感会飘;可在面板勾选「强制原始输入」)')); }, function (Err) { Stat.rawMode = '未知'; console.warn('[鼠标] 指针锁定失败: ' + (Err && Err.message)); }, ); } else { Stat.rawMode = WantRaw ? '原始输入' : '带加速'; } return P; }; } function UninstallLockPatch() { if (!OrigRequestPointerLock) return; Element.prototype.requestPointerLock = OrigRequestPointerLock; OrigRequestPointerLock = null; } // ===== ②③ mousemove 拦截:丢弃跳变 + 钳制尖峰 ===== // 注册在 window 的捕获阶段:一定早于游戏挂在画布/document 上的处理器,丢弃/改写才有效 function OnMouseMoveCapture(e) { Stat.events++; var T = Now(); // 事件流「恢复」判定(切窗口/失焦回来时的第一下常是巨幅位移) if (Config.GapMs > 0 && Stat.lastEventAt && (T - Stat.lastEventAt) > Config.GapMs && Config.DropMs > 0) { Stat.dropUntil = T + Config.DropMs; } Stat.lastEventAt = T; // ② 忽略窗口内:整条事件丢弃(stopImmediatePropagation 阻断游戏处理器) if (Stat.dropUntil && T < Stat.dropUntil) { Stat.dropped++; e.stopImmediatePropagation(); return; } var Dx = e.movementX, Dy = e.movementY; if (typeof Dx !== 'number' || typeof Dy !== 'number') return; var Ax = Math.abs(Dx), Ay = Math.abs(Dy); if (Ax > Stat.maxAbs) Stat.maxAbs = Ax; if (Ay > Stat.maxAbs) Stat.maxAbs = Ay; // ③ 尖峰钳制:改写事件上的 movementX/Y(own property 遮蔽原型 getter) if (Config.MaxDelta > 0 && (Ax > Config.MaxDelta || Ay > Config.MaxDelta)) { var Nx = Ax > Config.MaxDelta ? (Dx > 0 ? Config.MaxDelta : -Config.MaxDelta) : Dx; var Ny = Ay > Config.MaxDelta ? (Dy > 0 ? Config.MaxDelta : -Config.MaxDelta) : Dy; try { Object.defineProperty(e, 'movementX', { value: Nx, configurable: true }); Object.defineProperty(e, 'movementY', { value: Ny, configurable: true }); Stat.clamped++; } catch (Err) { // 属性不可写则退化为丢弃该事件(同样能压住甩飞) Stat.clamped++; e.stopImmediatePropagation(); return; } } // 事件率统计(2s 窗口),尖峰异常时节流提示,帮助定位是「抖动」还是「跳变」 Stat.rateEvents++; if (!Stat.rateWindow) Stat.rateWindow = T; if (T - Stat.rateWindow >= 2000) { Stat.rate = Math.round((Stat.rateEvents * 1000) / (T - Stat.rateWindow)); Stat.rateWindow = T; Stat.rateEvents = 0; var Bad = Stat.maxAbs > 400 && (T - Stat.lastAnomalyAt) > 3000; if (Bad) { Stat.lastAnomalyAt = T; console.warn('[鼠标] 出现大位移尖峰: ' + Stat.maxAbs.toFixed(0) + 'px(事件率 ' + Stat.rate + '/s,累计钳制 ' + Stat.clamped + ' 次)。若频繁甩飞,把「尖峰钳制」调成 250~400 试试'); } Stat.maxAbs = 0; // 只统计窗口内最大值 } } // ===== 启动 / 停止 ===== function _start() { if (Stat.running) return; if (Config.ForceRaw) InstallLockPatch(); if (!Attached) { window.addEventListener('mousemove', OnMouseMoveCapture, true); Attached = true; } Stat.running = true; console.log('[鼠标] 稳定器已启动 强制原始输入=' + Config.ForceRaw + ' 锁后忽略=' + Config.DropMs + 'ms 尖峰钳制=' + (Config.MaxDelta > 0 ? Config.MaxDelta + 'px' : '关')); } function _stop() { if (!Stat.running) return; UninstallLockPatch(); if (Attached) { window.removeEventListener('mousemove', OnMouseMoveCapture, true); Attached = false; } Stat.running = false; Stat.dropped = 0; Stat.clamped = 0; Stat.events = 0; Stat.maxAbs = 0; Stat.dropUntil = 0; Stat.lastEventAt = 0; Stat.rawMode = '未知'; console.log('[鼠标] 稳定器已停止'); } var Api = { config: Config, _start: _start, _stop: _stop, setForceRaw: function (On) { Config.ForceRaw = !!On; SaveConfig(); if (Stat.running) { if (On) InstallLockPatch(); else UninstallLockPatch(); } }, setDropMs: function (Ms) { Config.DropMs = Math.max(0, Number(Ms) || 0); SaveConfig(); }, setMaxDelta: function (Px) { Config.MaxDelta = Math.max(0, Number(Px) || 0); SaveConfig(); }, getStatus: function () { return { running: Stat.running, rawMode: Stat.rawMode, rate: Stat.rate, maxAbs: Math.round(Stat.maxAbs), clamped: Stat.clamped, dropped: Stat.dropped, events: Stat.events }; } }; window.__hornetMouse__ = Api; var Tool = { Start: function () { _start(); }, Stop: function () { _stop(); }, Status: function () { if (!Stat.running) return '已停止'; return Stat.rawMode + ' ' + Stat.rate + '/s 尖峰' + Math.round(Stat.maxAbs) + ' 钳' + Stat.clamped + ' 丢' + Stat.dropped; }, GetRunning: function () { return Stat.running; }, SetForceRaw: function (On) { Api.setForceRaw(On); }, GetForceRaw: function () { return Config.ForceRaw; }, SetDropMs: function (Ms) { Api.setDropMs(Ms); }, GetDropMs: function () { return Config.DropMs; }, SetMaxDelta: function (Px) { Api.setMaxDelta(Px); }, GetMaxDelta: function () { return Config.MaxDelta; }, GetStatus: function () { return Api.getStatus(); } }; window.__hornetTools__ = window.__hornetTools__ || {}; window.__hornetTools__.Mouse = Tool; console.log('[3DTank] 鼠标稳定模块已加载'); })(); // ===== hornet_panel.js ===== // ===== 3DTank 控制面板 ===== // 功能:Alt+T 切换面板显示,控制各工具开关 // 左上角 HUD 实时显示 GHT 捕获状态和各工具运行状态 // HUD 和面板支持鼠标拖动 // 依赖:hook_gamemode.js + hornet_lib.js + 各 hornet_*.js 工具模块 (function() { console.log('[3DTank] 控制面板加载'); // ===== 清理旧面板 ===== if (window.__hornetPanel__) { window.__hornetPanel__.Destroy(); console.log('已清理旧控制面板'); } // 通用拖动功能 // elem: 要拖动的元素,handle: 触发拖动的元素(默认=elem) // ignoreTags: 点击这些标签时不触发拖动(如 BUTTON) // key: 可选,传入后把窗口位置存到 localStorage(hornet_layout_)并在创建时还原 // 实现要点:Pointer Events + setPointerCapture 把指针"锁"在 handle 上, // pointerup/pointercancel 必定回到 handle;并在 window 捕获阶段加兜底, // 避免面板内 stopPropagation 或游戏画布抢事件导致"松手后仍粘着鼠标"。 // 返回:{ End, IsActive },供隐藏面板时强制结束拖动 function MakeDraggable(elem, handle, ignoreTags, key) { handle = handle || elem; ignoreTags = ignoreTags || ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA']; handle.style.cursor = 'move'; var StorageKey = key ? ('hornet_layout_' + key) : null; function ClampX(v) { return Math.max(0, Math.min(window.innerWidth - 40, v)); } function ClampY(v) { return Math.max(0, Math.min(window.innerHeight - 24, v)); } // 还原上次保存的位置 if (StorageKey) { try { var __saved = localStorage.getItem(StorageKey); if (__saved) { var __p = JSON.parse(__saved); if (__p && typeof __p.left === 'number' && typeof __p.top === 'number') { elem.style.transform = 'none'; elem.style.left = ClampX(__p.left) + 'px'; elem.style.top = ClampY(__p.top) + 'px'; } } } catch(e) {} } function SavePos() { if (!StorageKey) return; try { var r = elem.getBoundingClientRect(); localStorage.setItem(StorageKey, JSON.stringify({ left: Math.round(r.left), top: Math.round(r.top) })); } catch(e) {} } var Active = false; var PointerId = null; var GrabX = 0; var GrabY = 0; function MoveTo(ClientX, ClientY) { elem.style.left = ClampX(ClientX - GrabX) + 'px'; elem.style.top = ClampY(ClientY - GrabY) + 'px'; } function Begin(ClientX, ClientY) { var Rect = elem.getBoundingClientRect(); // 移除 transform 居中,改用绝对定位,避免拖动起点跳变 elem.style.transform = 'none'; elem.style.left = Rect.left + 'px'; elem.style.top = Rect.top + 'px'; GrabX = ClientX - Rect.left; GrabY = ClientY - Rect.top; Active = true; handle.style.cursor = 'grabbing'; } function End() { if (!Active) return; Active = false; if (PointerId !== null && handle.releasePointerCapture) { try { handle.releasePointerCapture(PointerId); } catch (Err) {} } PointerId = null; handle.style.cursor = 'move'; SavePos(); } function CanStart(Target) { return !Target || ignoreTags.indexOf(Target.tagName) < 0; } if (window.PointerEvent) { // 捕获阶段监听:先于元素自身的 stopPropagation 执行 handle.addEventListener('pointerdown', function (e) { if (e.button !== 0) return; if (!CanStart(e.target)) return; Begin(e.clientX, e.clientY); PointerId = e.pointerId; try { handle.setPointerCapture(e.pointerId); } catch (Err) {} e.preventDefault(); e.stopPropagation(); }, true); handle.addEventListener('pointermove', function (e) { if (!Active || (PointerId !== null && e.pointerId !== PointerId)) return; // 兜底:按键已经松开(例如在窗口外松手)→ 结束拖动 if (e.buttons === 0) { End(); return; } MoveTo(e.clientX, e.clientY); e.preventDefault(); }, true); handle.addEventListener('pointerup', function () { End(); }, true); handle.addEventListener('pointercancel', function () { End(); }, true); handle.addEventListener('lostpointercapture', function () { End(); }, true); window.addEventListener('pointerup', function () { End(); }, true); } else { // 旧浏览器回退:mouse 事件 + document 捕获阶段监听 handle.addEventListener('mousedown', function (e) { if (e.button !== 0) return; if (!CanStart(e.target)) return; Begin(e.clientX, e.clientY); e.preventDefault(); }, true); document.addEventListener('mousemove', function (e) { if (!Active) return; if (e.buttons === 0) { End(); return; } MoveTo(e.clientX, e.clientY); }, true); document.addEventListener('mouseup', function () { End(); }, true); } // 公共兜底:窗口失焦 / 页面切后台 / 鼠标离开窗口 window.addEventListener('blur', End); window.addEventListener('mouseleave', End); document.addEventListener('visibilitychange', function () { if (document.hidden) End(); }); return { End: End, IsActive: function () { return Active; } }; } // ===== 工具列表(动态从 __hornetTools__ 获取)===== function GetToolList() { var tools = window.__hornetTools__ || {}; return [ { key: 'Radar', label: '雷达', obj: tools.Radar }, { key: 'Wireframe', label: '3D透视', obj: tools.Wireframe, hasMode: true }, { key: 'Rearview', label: '后视镜', obj: tools.Rearview }, { key: 'AimWarning', label: '瞄准警告', obj: tools.AimWarning }, { key: 'FreeLook', label: '自由视角', obj: tools.FreeLook }, { key: 'WarSandbox', label: '战争沙盘', obj: tools.WarSandbox }, { key: 'Mouse', label: '鼠标稳定', obj: tools.Mouse } ]; } // ===== 工具启用状态(持久化 + 默认启用)===== // 默认除「自由视角」外全部开启:自由视角会直接接管相机,默认开启过于突兀 var ToolKeys = ['Radar', 'Wireframe', 'Rearview', 'AimWarning', 'FreeLook', 'WarSandbox', 'Mouse']; var ToolDefaults = { Radar: true, Wireframe: true, Rearview: true, AimWarning: true, FreeLook: false, WarSandbox: true, Mouse: true }; var ToolEnabled = {}; try { var __savedEnabled = JSON.parse(localStorage.getItem('hornet_tool_enabled') || '{}'); ToolKeys.forEach(function(k) { ToolEnabled[k] = (typeof __savedEnabled[k] === 'boolean') ? __savedEnabled[k] : ToolDefaults[k]; }); } catch(e) { ToolKeys.forEach(function(k) { ToolEnabled[k] = ToolDefaults[k]; }); } // 把各工具当前的运行状态写回持久化(开关时调用) function SaveToolEnabled() { try { var tools = window.__hornetTools__ || {}; ToolKeys.forEach(function(k) { var t = tools[k]; if (t && typeof t.GetRunning === 'function') ToolEnabled[k] = !!t.GetRunning(); }); localStorage.setItem('hornet_tool_enabled', JSON.stringify(ToolEnabled)); } catch(e) {} } // 需要 ght 才能初始化的工具(要 hook 相机 / GL)——等捕获后再启动,避免失败报错刷屏 var GameModeTools = { FreeLook: true, Rearview: true, WarSandbox: true }; // 自动按保存状态开启:覆盖层(雷达/透视/瞄准)无 ght 也能先开并出 UI; // 每 30 帧检查一次,全部进入运行状态即停 var AutoEnableDone = false; var AutoEnableFrame = 0; function TryAutoEnable() { if (AutoEnableDone) return; AutoEnableFrame++; if (AutoEnableFrame % 30 !== 0) return; var HasGm = !!window.__gameMode__; var tools = window.__hornetTools__ || {}; var pending = 0; ToolKeys.forEach(function(k) { if (!ToolEnabled[k]) return; if (GameModeTools[k] && !HasGm) { pending++; return; } // 等 ght 捕获 var t = tools[k]; if (!t || typeof t.Start !== 'function') return; if (typeof t.GetRunning === 'function' && t.GetRunning()) return; pending++; try { t.Start(); } catch(e) {} }); if (pending === 0) { AutoEnableDone = true; UpdateButtons(); console.log('[3DTank] 已按保存状态自动开启工具(覆盖层无 ght 也可先开;后视镜/沙盘/自由视角等 ght 捕获后开)'); } } // ===== HUD(左上角状态显示,可拖动)===== var hud = document.createElement('div'); hud.id = '3dtank-hud'; hud.style.cssText = 'position:fixed;top:10px;left:10px;z-index:100000;pointer-events:auto;cursor:move;' + 'font:11px Consolas,monospace;color:#7df9ff;background:rgba(5,10,18,0.55);' + 'backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);' + 'padding:8px 12px;border-radius:6px;border:1px solid rgba(125,249,255,0.25);' + 'white-space:pre;line-height:1.6;text-shadow:0 0 3px rgba(125,249,255,0.4);' + 'user-select:none;-webkit-user-select:none;box-shadow:0 2px 12px rgba(0,0,0,0.4);'; document.body.appendChild(hud); var HudDrag = MakeDraggable(hud, null, null, 'hud'); // ===== 面板(可拖动,Alt+T 切换)===== var panel = document.createElement('div'); panel.id = '3dtank-panel'; // 初始居中:用 left/top 50% + transform,拖动后移除 transform panel.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:100001;' + 'background:rgba(12,16,24,0.72);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);' + 'border:1px solid rgba(125,249,255,0.2);border-radius:12px;' + 'padding:18px 22px;color:#e8eef5;font:13px Consolas,monospace;min-width:340px;' + 'box-shadow:0 8px 32px rgba(0,0,0,0.6),0 0 1px rgba(125,249,255,0.3),inset 0 1px 0 rgba(255,255,255,0.05);' + 'display:none;user-select:none;-webkit-user-select:none;'; // 标题(可拖动手柄) var title = document.createElement('div'); title.textContent = '3DTank 控制面板'; title.style.cssText = 'font-size:16px;font-weight:bold;color:#7df9ff;margin-bottom:4px;' + 'text-shadow:0 0 6px rgba(125,249,255,0.4);' + 'cursor:move;padding-bottom:8px;border-bottom:1px solid rgba(125,249,255,0.15);' + 'letter-spacing:0.5px;'; panel.appendChild(title); var subtitle = document.createElement('div'); subtitle.textContent = 'Alt+T 切换显示/隐藏 | 拖动标题移动面板'; subtitle.style.cssText = 'font-size:11px;color:#5a7a8a;margin:6px 0 14px;'; panel.appendChild(subtitle); // 工具按钮容器 var btnContainer = document.createElement('div'); panel.appendChild(btnContainer); // ===== 3D透视设置子面板(可折叠)===== var wfSettings = document.createElement('div'); wfSettings.style.cssText = 'margin-top:12px;padding-top:10px;border-top:1px solid rgba(125,249,255,0.08);'; var wfHeader = document.createElement('div'); wfHeader.style.cssText = 'display:flex;justify-content:space-between;align-items:center;cursor:pointer;color:#7df9ff;font-weight:bold;font-size:13px;'; wfHeader.innerHTML = '3D透视设置▼'; wfSettings.appendChild(wfHeader); var wfBody = document.createElement('div'); wfBody.style.cssText = 'margin-top:8px;display:block;'; wfSettings.appendChild(wfBody); var wfCollapsed = false; wfHeader.onclick = function() { wfCollapsed = !wfCollapsed; wfBody.style.display = wfCollapsed ? 'none' : 'block'; document.getElementById('wfToggle').textContent = wfCollapsed ? '▶' : '▼'; }; // 行布局辅助 function MakeRow(labelText) { var row = document.createElement('div'); row.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:6px 0;'; var lab = document.createElement('span'); lab.style.cssText = 'color:#b8c8d8;font-size:12px;'; lab.textContent = labelText; row.appendChild(lab); var ctrl = document.createElement('div'); ctrl.style.cssText = 'display:flex;gap:6px;align-items:center;'; row.appendChild(ctrl); wfBody.appendChild(row); return { row: row, ctrl: ctrl }; } // 线宽调节 var lwRow = MakeRow('线宽'); var lwSlider = document.createElement('input'); lwSlider.type = 'range'; lwSlider.min = '0.5'; lwSlider.max = '8'; lwSlider.step = '0.5'; lwSlider.style.cssText = 'width:120px;accent-color:#7df9ff;'; var lwVal = document.createElement('span'); lwVal.style.cssText = 'color:#7df9ff;font-size:11px;min-width:28px;text-align:right;'; lwRow.ctrl.appendChild(lwSlider); lwRow.ctrl.appendChild(lwVal); lwSlider.oninput = function() { var v = parseFloat(lwSlider.value); lwVal.textContent = v.toFixed(1); var tools = window.__hornetTools__ || {}; if (tools.Wireframe) tools.Wireframe.SetLineWidth(v); }; // 渲染模式选择 var hmRow = MakeRow('渲染模式'); var hmSelect = document.createElement('select'); hmSelect.style.cssText = 'padding:3px 6px;border:1px solid rgba(125,249,255,0.25);border-radius:4px;' + 'background:rgba(20,30,40,0.6);color:#e8eef5;font:11px Consolas;cursor:pointer;'; var hmOptions = [ { v: 'team', t: '队伍模式(蓝友/红敌)' }, { v: 'whitelist', t: '白名单(仅名单内渲染)' }, { v: 'blacklist', t: '黑名单(名单内不渲染)' } ]; hmOptions.forEach(function(o) { var opt = document.createElement('option'); opt.value = o.v; opt.textContent = o.t; hmSelect.appendChild(opt); }); hmRow.ctrl.appendChild(hmSelect); hmSelect.onchange = function() { var tools = window.__hornetTools__ || {}; if (tools.Wireframe) tools.Wireframe.SetHighlightMode(hmSelect.value); }; // 名单管理 var hlRow = MakeRow('名单'); var hlInput = document.createElement('input'); hlInput.type = 'text'; hlInput.placeholder = '玩家名'; hlInput.style.cssText = 'width:120px;padding:3px 6px;border:1px solid rgba(125,249,255,0.25);border-radius:4px;' + 'background:rgba(20,30,40,0.6);color:#e8eef5;font:11px Consolas;'; var hlAddBtn = document.createElement('button'); hlAddBtn.textContent = '+'; hlAddBtn.style.cssText = 'padding:3px 8px;border:1px solid rgba(46,204,113,0.4);border-radius:4px;' + 'background:rgba(46,204,113,0.2);color:#2ecc71;cursor:pointer;font:bold 12px Consolas;'; hlRow.ctrl.appendChild(hlInput); hlRow.ctrl.appendChild(hlAddBtn); var hlList = document.createElement('div'); hlList.style.cssText = 'margin-top:6px;max-height:120px;overflow-y:auto;font-size:11px;'; wfBody.appendChild(hlList); function RefreshHlList() { var tools = window.__hornetTools__ || {}; var arr = tools.Wireframe ? tools.Wireframe.GetHighlightList() : []; hlList.innerHTML = ''; if (arr.length === 0) { var empty = document.createElement('div'); empty.style.cssText = 'color:#5a7a8a;padding:4px 0;font-style:italic;'; empty.textContent = '(空)'; hlList.appendChild(empty); return; } arr.forEach(function(name) { var item = document.createElement('div'); item.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:3px 4px;border-bottom:1px solid rgba(125,249,255,0.05);'; var ns = document.createElement('span'); ns.style.cssText = 'color:#b8c8d8;'; ns.textContent = name; item.appendChild(ns); var delBtn = document.createElement('button'); delBtn.textContent = '×'; delBtn.style.cssText = 'padding:1px 6px;border:1px solid rgba(224,85,85,0.4);border-radius:3px;' + 'background:rgba(224,85,85,0.2);color:#e05555;cursor:pointer;font:bold 11px Consolas;'; delBtn.onclick = function() { if (tools.Wireframe) tools.Wireframe.RemoveHighlight(name); RefreshHlList(); }; item.appendChild(delBtn); hlList.appendChild(item); }); } function AddFromInput() { var name = hlInput.value.trim(); if (!name) return; var tools = window.__hornetTools__ || {}; if (tools.Wireframe) tools.Wireframe.AddHighlight(name); hlInput.value = ''; RefreshHlList(); } hlAddBtn.onclick = AddFromInput; hlInput.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); AddFromInput(); } }); var hlClearRow = document.createElement('div'); hlClearRow.style.cssText = 'margin-top:4px;text-align:right;'; var hlClearBtn = document.createElement('button'); hlClearBtn.textContent = '清空名单'; hlClearBtn.style.cssText = 'padding:3px 8px;border:1px solid rgba(224,85,85,0.4);border-radius:4px;' + 'background:rgba(224,85,85,0.15);color:#e05555;cursor:pointer;font:11px Consolas;'; hlClearBtn.onclick = function() { var tools = window.__hornetTools__ || {}; if (tools.Wireframe) tools.Wireframe.ClearHighlight(); RefreshHlList(); }; hlClearRow.appendChild(hlClearBtn); wfBody.appendChild(hlClearRow); panel.appendChild(wfSettings); // ===== 通用可折叠设置子面板辅助函数 ===== function MakeSettingsSection(titleText) { var sec = document.createElement('div'); sec.style.cssText = 'margin-top:10px;padding-top:8px;border-top:1px solid rgba(125,249,255,0.08);'; var hdr = document.createElement('div'); hdr.style.cssText = 'display:flex;justify-content:space-between;align-items:center;cursor:pointer;color:#7df9ff;font-weight:bold;font-size:12px;'; var toggleChar = document.createElement('span'); toggleChar.style.cssText = 'font-size:10px;color:#5a7a8a;'; toggleChar.textContent = '▶'; hdr.innerHTML = '' + titleText + ''; hdr.appendChild(toggleChar); sec.appendChild(hdr); var body = document.createElement('div'); body.style.cssText = 'margin-top:6px;display:none;'; sec.appendChild(body); hdr.onclick = function() { var shown = body.style.display !== 'none'; body.style.display = shown ? 'none' : 'block'; toggleChar.textContent = shown ? '▶' : '▼'; }; panel.appendChild(sec); function row(labelText) { var r = document.createElement('div'); r.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:5px 0;'; var lab = document.createElement('span'); lab.style.cssText = 'color:#b8c8d8;font-size:11px;'; lab.textContent = labelText; r.appendChild(lab); var ctrl = document.createElement('div'); ctrl.style.cssText = 'display:flex;gap:6px;align-items:center;'; r.appendChild(ctrl); body.appendChild(r); return { row: r, ctrl: ctrl }; } function slider(labelText, min, max, step, fmt, onInput) { var r = row(labelText); var s = document.createElement('input'); s.type = 'range'; s.min = String(min); s.max = String(max); s.step = String(step); s.style.cssText = 'width:110px;accent-color:#7df9ff;'; var v = document.createElement('span'); v.style.cssText = 'color:#7df9ff;font-size:10px;min-width:32px;text-align:right;'; s.oninput = function() { var val = parseFloat(s.value); v.textContent = fmt(val); onInput(val); }; r.ctrl.appendChild(s); r.ctrl.appendChild(v); return { set: function(val) { s.value = val; v.textContent = fmt(parseFloat(val)); }, get: function() { return parseFloat(s.value); } }; } function checkbox(labelText, onChange) { var r = row(labelText); var c = document.createElement('input'); c.type = 'checkbox'; c.style.cssText = 'accent-color:#7df9ff;width:16px;height:16px;cursor:pointer;'; c.onchange = function() { onChange(c.checked); }; r.ctrl.appendChild(c); return { set: function(on) { c.checked = !!on; }, get: function() { return c.checked; } }; } return { sec: sec, body: body, row: row, slider: slider, checkbox: checkbox }; } // ===== 雷达设置 ===== var radarSec = MakeSettingsSection('雷达设置'); var radarRangeCtl = radarSec.slider('范围(m)', 30, 600, 10, function(v){ return v.toFixed(0); }, function(v) { var tools = window.__hornetTools__ || {}; if (tools.Radar) tools.Radar.SetRange(v * 100); }); var radarOpacityCtl = radarSec.slider('透明度', 0.2, 1.0, 0.05, function(v){ return v.toFixed(2); }, function(v) { var tools = window.__hornetTools__ || {}; if (tools.Radar) tools.Radar.SetOpacity(v); }); // ===== 鼠标稳定设置 ===== // 强制原始输入:游戏锁定失败会静默回退成带 OS 加速的普通锁定,手感会飘 // 锁后忽略:锁定刚建立 / 失焦恢复后的头几毫秒位移常是巨幅跳变,直接丢弃 // 尖峰钳制:单事件位移超过阈值就截断(0 = 关闭) var mouseSec = MakeSettingsSection('鼠标稳定'); var mouseRawCtl = mouseSec.checkbox('强制原始输入', function(on) { var tools = window.__hornetTools__ || {}; if (tools.Mouse) tools.Mouse.SetForceRaw(on); }); var mouseDropCtl = mouseSec.slider('锁后忽略(ms)', 0, 500, 20, function(v){ return v.toFixed(0); }, function(v) { var tools = window.__hornetTools__ || {}; if (tools.Mouse) tools.Mouse.SetDropMs(v); }); var mouseClampCtl = mouseSec.slider('尖峰钳制(px)', 0, 1000, 25, function(v){ return v.toFixed(0); }, function(v) { var tools = window.__hornetTools__ || {}; if (tools.Mouse) tools.Mouse.SetMaxDelta(v); }); (function () { // 回填鼠标模块里已保存的配置 var tools = window.__hornetTools__ || {}; if (!tools.Mouse) return; mouseRawCtl.set(tools.Mouse.GetForceRaw()); mouseDropCtl.set(tools.Mouse.GetDropMs()); mouseClampCtl.set(tools.Mouse.GetMaxDelta()); })(); // ===== 后视镜设置 ===== var rvSec = MakeSettingsSection('后视镜设置'); var rvFovCtl = rvSec.slider('FOV', 0.5, 2.5, 0.05, function(v){ return v.toFixed(2); }, function(v) { var tools = window.__hornetTools__ || {}; if (tools.Rearview) tools.Rearview.SetFov(v); }); var rvOpacityCtl = rvSec.slider('透明度', 0.2, 1.0, 0.05, function(v){ return v.toFixed(2); }, function(v) { var tools = window.__hornetTools__ || {}; if (tools.Rearview) tools.Rearview.SetOpacity(v); }); // ===== 瞄准警告设置 ===== var awSec = MakeSettingsSection('瞄准警告设置'); var awSensCtl = awSec.slider('灵敏度', 0.3, 3.0, 0.1, function(v){ return v.toFixed(1); }, function(v) { var tools = window.__hornetTools__ || {}; if (tools.AimWarning) tools.AimWarning.SetSensitivity(v); }); var awSoundCtl = awSec.checkbox('警告声音', function(on) { var tools = window.__hornetTools__ || {}; if (tools.AimWarning) tools.AimWarning.SetSound(on); }); // ===== 战争沙盘设置 ===== var wsSec = MakeSettingsSection('战争沙盘设置'); var wsGridCtl = wsSec.checkbox('显示网格', function(on) { var tools = window.__hornetTools__ || {}; if (tools.WarSandbox) tools.WarSandbox.SetShowGrid(on); }); var wsMarkerCtl = wsSec.slider('标记大小', 0.5, 3.0, 0.1, function(v){ return v.toFixed(1); }, function(v) { var tools = window.__hornetTools__ || {}; if (tools.WarSandbox) tools.WarSandbox.SetMarkerSize(v); }); // ===== 电磁炮(RAILGUN / z4t)设置 ===== // 锁定/蓄力时长实时覆盖 window.__hornetLockSec__(游戏内 z4t.w187() 优先读它), // 拖动滑块**立即生效**,不需要重打包或刷新页面。 // 实测门槛:1.5s / 2.0s / 原版3~5s = 满伤;1.0s / 0.1s = 无伤害 ⇒ 门槛在 1.0~1.5s。 var rgSec = MakeSettingsSection('电磁炮设置'); var rgSecVal = 1.5; function ApplyLock(v) { window.__hornetLockSec__ = (v > 0 ? v : undefined); } function SaveLock(on, v) { try { localStorage.setItem('hornet_lock_on', on ? '1' : '0'); localStorage.setItem('hornet_lock_sec', String(v)); } catch (e) {} } var rgOnCtl = rgSec.checkbox('启用时长覆盖', function(on) { ApplyLock(on ? rgSecVal : 0); SaveLock(on, rgSecVal); }); var rgLockCtl = rgSec.slider('锁定/蓄力(s)', 0.1, 5.0, 0.05, function(v) { return v.toFixed(2); }, function(v) { rgSecVal = v; if (rgOnCtl.get()) ApplyLock(v); SaveLock(rgOnCtl.get(), v); }); (function () { // 回填上次保存的配置 try { var on = localStorage.getItem('hornet_lock_on') === '1'; var v = parseFloat(localStorage.getItem('hornet_lock_sec')); if (isFinite(v) && v > 0) rgSecVal = v; rgLockCtl.set(rgSecVal); rgOnCtl.set(on); ApplyLock(on ? rgSecVal : 0); } catch (e) {} })(); // 从工具读取当前设置(打开面板时同步) function SyncWfSettings() { var tools = window.__hornetTools__ || {}; if (tools.Wireframe) { var lw = tools.Wireframe.GetLineWidth(); lwSlider.value = lw; lwVal.textContent = lw.toFixed(1); var hm = tools.Wireframe.GetHighlightMode(); hmSelect.value = hm; } RefreshHlList(); // 同步其他工具设置 if (tools.Radar) { radarRangeCtl.set(tools.Radar.GetRange() / 100); radarOpacityCtl.set(tools.Radar.GetOpacity()); } if (tools.Rearview) { rvFovCtl.set(tools.Rearview.GetFov()); rvOpacityCtl.set(tools.Rearview.GetOpacity()); } if (tools.AimWarning) { awSensCtl.set(tools.AimWarning.GetSensitivity()); awSoundCtl.set(tools.AimWarning.GetSound()); } if (tools.WarSandbox) { wsGridCtl.set(tools.WarSandbox.GetShowGrid()); wsMarkerCtl.set(tools.WarSandbox.GetMarkerSize()); } } // 底部状态 var footer = document.createElement('div'); footer.style.cssText = 'margin-top:14px;padding-top:10px;border-top:1px solid rgba(125,249,255,0.08);font-size:11px;color:#5a7a8a;text-align:center;'; footer.textContent = '点击按钮切换 开/关'; panel.appendChild(footer); // 热键显示区域(UpdateHotkeyDisplay 在热键系统定义后调用) var hotkeySection = document.createElement('div'); hotkeySection.style.cssText = 'margin-top:12px;padding-top:10px;border-top:1px solid rgba(125,249,255,0.08);font-size:11px;'; function UpdateHotkeyDisplay() { if (!HotkeyLabels || !Hotkeys) return; var html = '
热键(控制台可改: __hornetPanel__.SetHotkey)
'; Object.keys(HotkeyLabels).forEach(function(k) { html += '
' + '' + HotkeyLabels[k] + '' + '' + Hotkeys[k] + '
'; }); hotkeySection.innerHTML = html; } panel.appendChild(hotkeySection); document.body.appendChild(panel); // 面板通过标题栏拖动 var PanelDrag = MakeDraggable(panel, title, null, 'panel'); // ===== 阻止面板内事件冒泡到 window(防止游戏/工具拦截键盘鼠标)===== // 鼠标事件:面板上所有鼠标事件都阻止冒泡(防止游戏转视角/开炮等) ['mousedown','mouseup','mousemove','wheel','click','dblclick','contextmenu'].forEach(function(evtName) { panel.addEventListener(evtName, function(e) { e.stopPropagation(); }); }); // 键盘事件:只在输入元素上阻止冒泡(保留面板上其他位置的热键功能) ['keydown','keypress','keyup'].forEach(function(evtName) { panel.addEventListener(evtName, function(e) { var tag = e.target.tagName; if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA') { e.stopPropagation(); } }); }); // HUD 也阻止鼠标事件冒泡(防止拖动HUD时游戏响应) ['mousedown','mouseup','mousemove','wheel','click','contextmenu'].forEach(function(evtName) { hud.addEventListener(evtName, function(e) { e.stopPropagation(); }); }); // ===== 构建工具按钮(含模式切换)===== var btnMap = {}; // key -> { row, btn, label, tool, modeBtns } function BuildButtons() { btnContainer.innerHTML = ''; btnMap = {}; var list = GetToolList(); list.forEach(function(t) { var row = document.createElement('div'); row.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid rgba(125,249,255,0.06);'; var label = document.createElement('span'); label.textContent = t.label; label.style.cssText = 'color:#b8c8d8;font-size:13px;'; row.appendChild(label); // 右侧按钮容器 var btnBox = document.createElement('div'); btnBox.style.cssText = 'display:flex;gap:4px;align-items:center;'; var modeBtns = null; // Wireframe 模式切换按钮 if (t.hasMode && t.obj && t.obj.SetMode) { modeBtns = {}; var modes = [ { mode: 'wireframe', text: '线框' }, { mode: 'outline', text: '描边' } ]; modes.forEach(function(mb) { var b = document.createElement('button'); b.textContent = mb.text; b.style.cssText = 'padding:4px 8px;border:1px solid rgba(125,249,255,0.25);border-radius:4px;' + 'font:11px Consolas,monospace;cursor:pointer;color:#e8eef5;min-width:44px;' + 'background:rgba(20,30,40,0.6);transition:all 0.2s;'; b.onclick = function() { if (t.obj) { t.obj.SetMode(mb.mode); UpdateButtons(); } }; btnBox.appendChild(b); modeBtns[mb.mode] = b; }); } var btn = document.createElement('button'); btn.style.cssText = 'padding:6px 16px;border:none;border-radius:6px;font:bold 12px Consolas,monospace;' + 'cursor:pointer;min-width:72px;color:#fff;transition:all 0.2s;' + 'box-shadow:0 2px 8px rgba(0,0,0,0.3);'; btn.onclick = function() { if (!t.obj) { console.warn('[3DTank] 工具 ' + t.key + ' 未加载'); return; } if (t.obj.GetRunning()) { t.obj.Stop(); } else { t.obj.Start(); } UpdateButtons(); SaveToolEnabled(); }; btnBox.appendChild(btn); row.appendChild(btnBox); btnContainer.appendChild(row); btnMap[t.key] = { btn: btn, label: label, tool: t.obj, modeBtns: modeBtns }; }); } BuildButtons(); // ===== 更新按钮状态 ===== function UpdateButtons() { Object.keys(btnMap).forEach(function(key) { var info = btnMap[key]; if (!info.tool) { info.btn.textContent = '未加载'; info.btn.style.background = '#555'; info.btn.disabled = true; return; } var running = info.tool.GetRunning(); info.btn.textContent = running ? '关闭' : '开启'; info.btn.style.background = running ? 'linear-gradient(135deg,#e05555,#c0333d)' : 'linear-gradient(135deg,#2ecc71,#27ae60)'; info.btn.disabled = false; // 更新模式按钮高亮 if (info.modeBtns) { var curMode = null; try { curMode = window.__hornet3DWireframe__ && window.__hornet3DWireframe__.mode; } catch(e) {} Object.keys(info.modeBtns).forEach(function(mk) { var mb = info.modeBtns[mk]; if (mk === curMode && running) { mb.style.background = 'linear-gradient(135deg,#2ecc71,#27ae60)'; mb.style.opacity = '1'; mb.style.color = '#fff'; } else { mb.style.background = 'rgba(20,30,40,0.6)'; mb.style.opacity = '0.6'; mb.style.color = '#8899aa'; } }); } }); } // ===== 更新 HUD ===== function UpdateHud() { requestAnimationFrame(UpdateHud); TryAutoEnable(); // gameMode 捕获后自动按保存状态开启工具 var ght = window.__gameMode__; var ghtStatus = ght ? '已捕获' : '未捕获'; var ghtColor = ght ? '#7df9ff' : '#f5a623'; var lines = []; lines.push('3DTank 控制台 [拖动移动]'); lines.push('GHT: ' + ghtStatus); var list = GetToolList(); list.forEach(function(t) { var status = t.obj ? t.obj.Status() : '未加载'; var dot = t.obj && t.obj.GetRunning() ? '●' : '○'; lines.push(dot + ' ' + t.label + ': ' + status); }); hud.textContent = lines.join('\n'); hud.style.color = ghtColor; } UpdateHud(); // ===== 热键系统(localStorage 持久化)===== var DefaultHotkeys = { panel: 'Alt+T', radar: 'Alt+R', wireframe: 'Alt+W', rearview: 'Alt+V', aimWarning: 'Alt+A', freeLook: 'Alt+F', warSandbox: 'Alt+M' }; var HotkeyLabels = { panel: '面板', radar: '雷达', wireframe: '3D透视', rearview: '后视镜', aimWarning: '瞄准警告', freeLook: '自由视角', warSandbox: '战争沙盘' }; var Hotkeys = {}; try { var saved = localStorage.getItem('3dtank_hotkeys'); var parsed = saved ? JSON.parse(saved) : {}; Object.keys(DefaultHotkeys).forEach(function(k) { Hotkeys[k] = parsed[k] || DefaultHotkeys[k]; }); } catch(e) { Object.keys(DefaultHotkeys).forEach(function(k) { Hotkeys[k] = DefaultHotkeys[k]; }); } function SaveHotkeys() { try { localStorage.setItem('3dtank_hotkeys', JSON.stringify(Hotkeys)); } catch(e) {} } function EventToHotkey(e) { var parts = []; if (e.altKey) parts.push('Alt'); if (e.ctrlKey) parts.push('Ctrl'); if (e.shiftKey) parts.push('Shift'); var key = e.key; if (key.length === 1) key = key.toUpperCase(); if (key === ' ') key = 'Space'; parts.push(key); return parts.join('+'); } function HidePanel() { panel.style.display = 'none'; PanelDrag.End(); // 防止隐藏时拖动状态残留(粘手) } // 释放鼠标指针锁定:游戏中鼠标被 Pointer Lock 捕获时指针隐藏、锁定在画布上, // 面板点不到,需退出锁定让光标恢复可交互 function ReleasePointerLock() { try { if (document.exitPointerLock && document.pointerLockElement) { document.exitPointerLock(); } } catch(e) {} } function ShowPanel() { panel.style.display = 'block'; ReleasePointerLock(); UpdateButtons(); SyncWfSettings(); } function TogglePanel() { if (panel.style.display === 'block') { HidePanel(); } else { ShowPanel(); } } function OnHotkey(e) { if (e.key === 'Escape' && panel.style.display === 'block') { HidePanel(); return; } var hk = EventToHotkey(e); if (hk === Hotkeys.panel) { e.preventDefault(); TogglePanel(); return; } var toolMap = { radar: 'Radar', wireframe: 'Wireframe', rearview: 'Rearview', aimWarning: 'AimWarning', freeLook: 'FreeLook', warSandbox: 'WarSandbox' }; Object.keys(toolMap).forEach(function(k) { if (hk === Hotkeys[k]) { e.preventDefault(); var tools = window.__hornetTools__ || {}; var tool = tools[toolMap[k]]; if (tool) { if (tool.GetRunning()) tool.Stop(); else tool.Start(); UpdateButtons(); SaveToolEnabled(); } } }); } // F5 刷新页面(游戏会吞掉 F5)——捕获阶段兜底,先于游戏处理 window.addEventListener('keydown', function(e) { if (e.key === 'F5') { e.preventDefault(); e.stopImmediatePropagation(); location.reload(); } }, true); window.addEventListener('keydown', OnHotkey); // 热键系统定义完成,更新显示 UpdateHotkeyDisplay(); // ===== 暴露接口 ===== window.__hornetPanel__ = { Toggle: TogglePanel, Show: ShowPanel, Hide: HidePanel, UpdateButtons: UpdateButtons, RebuildButtons: BuildButtons, GetHotkeys: function() { var r = {}; Object.keys(Hotkeys).forEach(function(k) { r[k] = Hotkeys[k]; }); return r; }, SetHotkey: function(name, key) { if (DefaultHotkeys[name]) { Hotkeys[name] = key; SaveHotkeys(); UpdateHotkeyDisplay(); console.log('[3DTank] 热键 ' + name + ' 已设为 ' + key); } else { console.warn('[3DTank] 未知热键名: ' + name + ',可用: ' + Object.keys(DefaultHotkeys).join(', ')); } }, ResetHotkeys: function() { Object.keys(DefaultHotkeys).forEach(function(k) { Hotkeys[k] = DefaultHotkeys[k]; }); SaveHotkeys(); UpdateHotkeyDisplay(); console.log('[3DTank] 热键已重置为默认值'); }, Destroy: function() { HudDrag.End(); PanelDrag.End(); if (hud.parentNode) hud.parentNode.removeChild(hud); if (panel.parentNode) panel.parentNode.removeChild(panel); window.removeEventListener('keydown', OnHotkey); } }; console.log('[3DTank] 控制面板已就绪,按 Alt+T 打开面板,HUD/面板/窗口可拖动,热键已加载'); })();