// ============================================================ // 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) 才是战斗中的坦克 function IsTankAlive(tank) { return GetTankState(tank) === 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) ===== // 适用于固定定位的小窗口(雷达、后视镜等) // 实现要点:Pointer Events + setPointerCapture,保证 pointerup/pointercancel 必定回到元素, // 并在 window 捕获阶段兜底,避免游戏画布抢事件后"松手仍粘着鼠标"。 function MakeDraggable(elem) { if (!elem) return null; elem.style.cursor = 'move'; var Active = false; var PointerId = null; var GrabX = 0; var GrabY = 0; function MoveTo(ClientX, ClientY) { var NextX = Math.max(0, Math.min(window.innerWidth - 50, ClientX - GrabX)); var NextY = Math.max(0, Math.min(window.innerHeight - 30, ClientY - GrabY)); elem.style.left = NextX + 'px'; elem.style.top = NextY + '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'; } 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雷达启动 ====='); if (!window.__gameMode__) { console.error('请先运行 hook_gamemode.js'); return; } // ===== 清理旧雷达 ===== if (window.__hornetRadar__) { window.__hornetRadar__.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; // 组件查找:使用公共库(已修复 falsy 值漏判 bug) var FindComponentByField = lib.FindComponentByField; // 坦克位置:使用公共库 var GetTankPosition = lib.GetTankPosition; // ===== 获取坦克方向(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); } // ===== 获取全局碰撞检测器(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); 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++; try { var tankList = gm.q100_1.r11l(); var count = tankList.e1(); radar.tankCount = count; // 获取玩家坦克(带备用方案) var playerTank = GetPlayerTank(); 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 = getCameraHeading(); 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; 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; // 判断友方/敌方 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); // 信息文本 ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; ctx.font = '9px Arial'; ctx.textAlign = 'left'; ctx.fillText('Tanks: ' + count + ' (B:' + friendlyCount + ' R:' + enemyCount + ')', 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透视启动 ====='); if (!window.__gameMode__) { console.error('请先运行 hook_gamemode.js'); return; } // ===== 清理旧透视 ===== if (window.__hornet3DWireframe__) { window.__hornet3DWireframe__.stop(); console.log('已停止旧透视'); } if (window.__hornet3DOutline__) { window.__hornet3DOutline__.stop(); console.log('已停止旧描边透视'); } var gm = window.__gameMode__; // ===== 当前模式 ===== 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; var GetTankHealth = lib.GetTankHealth; 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 TransformVertex = lib.TransformVertex; // ===== 获取 KMt / TDt 渲染组件 ===== function GetKMtComponent(entity) { var 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; } 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 = 600; 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; } // ===== 幽灵节点检测(三重检测:z34_1 + 世界矩阵 + 位置) ===== var NodeMatrixHistory = new WeakMap(); var TankPrevPos = new Map(); var GhostThreshold = 30; var GhostCooldown = 120; function IsNodeGhost(bpNode, tankIsMoving) { if (!bpNode || !bpNode.n35_1) return false; // 方案1:z34_1 字段检测(主方案) var z34Val = bpNode.g35_1; if (z34Val !== undefined && z34Val < 0.9) return true; // 方案2:世界矩阵更新检测(后备方案) var curMatrix = bpNode.n35_1; var hist = NodeMatrixHistory.get(bpNode); if (!hist) { hist = { m: new Array(16), unchanged: 0, cooldown: 0 }; for (var i = 0; i < 16; i++) hist.m[i] = curMatrix[i]; NodeMatrixHistory.set(bpNode, hist); return false; } var same = true; for (var i = 0; i < 16; i++) { if (Math.abs(hist.m[i] - curMatrix[i]) > 0.01) { same = false; break; } } if (same) hist.unchanged++; else { hist.unchanged = 0; for (var i = 0; i < 16; i++) hist.m[i] = curMatrix[i]; } if (hist.cooldown > 0) { hist.cooldown--; return true; } if (tankIsMoving && hist.unchanged > GhostThreshold) { hist.cooldown = GhostCooldown; return true; } return false; } // ===== 递归收集mesh节点(每帧重新获取 Bp 节点引用) ===== function CollectMeshNodes(bpNode, nodes, depth, group) { if (!bpNode || depth > 5) return; var name = bpNode.e35_1 || ''; if (bpNode.t3m_1) { var gpuData = GetGpuData(bpNode.t3m_1); if (gpuData) { nodes.push({ name: name, node: bpNode, tp: bpNode.t3m_1, positions: gpuData.positions, indices: gpuData.indices, stride: gpuData.stride, group: group }); } } 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) { 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); } catch(e) {} } } } // ===== 提取坦克的mesh数据 ===== function GetTankMeshData(tank) { var nodes = []; var kmt = GetKMtComponent(tank); if (kmt && kmt.t14l_1 && kmt.t14l_1.s17b_1) { var skin = kmt.t14l_1; 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) continue; CollectMeshNodes(bpNode, nodes, 0, 'hull'); } } } catch(e) {} } var tdt = GetTDtComponent(tank); if (tdt && tdt.n11c_1 && tdt.n11c_1.m17a_1) { try { CollectMeshNodes(tdt.n11c_1.m17a_1, nodes, 0, 'turret'); } catch(e) {} } return nodes; } // ===== 检查节点是否有效(幽灵检测 + 位置检查) ===== function IsNodeValid(bpNode, pos, tankIsMoving, diagStats) { var worldMatrix = bpNode.n35_1; if (!worldMatrix) return null; if (diagStats) diagStats.totalNodes++; // 幽灵检测1:z34_1 字段 var z34Val = bpNode.g35_1; if (z34Val !== undefined && z34Val < 0.9) { if (diagStats) diagStats.ghostNodes++; return null; } // 幽灵检测2:世界矩阵更新 if (IsNodeGhost(bpNode, tankIsMoving)) { if (diagStats) diagStats.ghostNodes++; return null; } // 位置检查 var nodeDx = worldMatrix[12] - pos.x; var nodeDy = worldMatrix[13] - pos.y; var nodeDz = worldMatrix[14] - pos.z; if (nodeDx*nodeDx + nodeDy*nodeDy + nodeDz*nodeDz > 4000000) { if (diagStats) diagStats.filteredNodes++; return null; } return worldMatrix; } // ===== 线框模式:绘制单个节点的所有边 ===== function DrawWireframeNode(ctx, meshData, worldMatrix, pos, camInfo, screenW, screenH) { var positions = meshData.positions; var indicesArr = meshData.indices; var stride = meshData.stride || 3; var vertexCount = Math.floor(positions.length / stride); var projected = new Array(vertexCount); for (var v = 0; v < vertexCount; v++) { var wp = TransformVertex(worldMatrix, positions[v*stride], positions[v*stride+1], positions[v*stride+2]); // 顶点级合理性检查 var ddx = wp.x - pos.x, ddy = wp.y - pos.y, ddz = wp.z - pos.z; if (ddx*ddx + ddy*ddy + ddz*ddz > 6250000) { projected[v] = null; continue; } projected[v] = Project3D(wp, camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH); } var drawnEdges = new Set(); 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 (!projected[i0] || !projected[i1] || !projected[i2]) continue; var edges = [ [Math.min(i0, i1), Math.max(i0, i1)], [Math.min(i1, i2), Math.max(i1, i2)], [Math.min(i2, i0), Math.max(i2, i0)] ]; for (var e = 0; e < 3; e++) { var key = edges[e][0] + '_' + edges[e][1]; if (drawnEdges.has(key)) continue; drawnEdges.add(key); ctx.moveTo(projected[edges[e][0]].x, projected[edges[e][0]].y); ctx.lineTo(projected[edges[e][1]].x, projected[edges[e][1]].y); } } } ctx.stroke(); } // ===== 描边模式:屏幕空间光栅化轮廓提取 ===== // 算法(真正的"屏幕几何"方法): // 1. 把所有面向相机的三角形光栅化(扫描线填充)到像素掩码 // 2. 车身和炮塔在屏幕上重叠时,填充区域自动合并 // 3. 找出填充区域的边界像素(填充像素的4邻居中有未填充像素) // 4. 只画边界像素 = 屏幕空间外轮廓 var PixelMask = null; var PixelMaskW = 0; var PixelMaskH = 0; function GetPixelMask(w, h) { if (!PixelMask || PixelMaskW !== w || PixelMaskH !== h) { PixelMask = new Uint8Array(w * h); PixelMaskW = w; PixelMaskH = h; } else { PixelMask.fill(0); } return PixelMask; } // 扫描线光栅化三角形 function RasterizeTriangle(pixels, w, h, 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] = 1; } } } function DrawTankOutline(ctx, meshNodes, pos, tankIsMoving, camInfo, screenW, screenH, diagStats, color) { var pixels = GetPixelMask(screenW, screenH); // 记录坦克的屏幕边界框(用于后续边界检测优化) 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 positions = meshData.positions; var indicesArr = meshData.indices; var stride = meshData.stride || 3; // 变换顶点到世界空间并投影到屏幕 var vertexCount = Math.floor(positions.length / stride); var worldVerts = new Array(vertexCount); var screenVerts = new Array(vertexCount); for (var v = 0; v < vertexCount; v++) { worldVerts[v] = TransformVertex(worldMatrix, positions[v*stride], positions[v*stride+1], positions[v*stride+2]); screenVerts[v] = Project3D(worldVerts[v], camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH); if (screenVerts[v]) { if (screenVerts[v].x < minX) minX = screenVerts[v].x; if (screenVerts[v].y < minY) minY = screenVerts[v].y; if (screenVerts[v].x > maxX) maxX = screenVerts[v].x; if (screenVerts[v].y > maxY) maxY = screenVerts[v].y; } } // 遍历三角形,光栅化面向相机的 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]; var va = worldVerts[a], vb = worldVerts[b], vc = worldVerts[c]; var sa = screenVerts[a], sb = screenVerts[b], sc = screenVerts[c]; if (!sa || !sb || !sc) continue; // 计算法线判断是否面向相机 var e1x = vb.x - va.x, e1y = vb.y - va.y, e1z = vb.z - va.z; var e2x = vc.x - va.x, e2y = vc.y - va.y, e2z = vc.z - va.z; var nx = e1y * e2z - e1z * e2y; var ny = e1z * e2x - e1x * e2z; var nz = e1x * e2y - e1y * e2x; var cx = (va.x + vb.x + vc.x) / 3; var cy = (va.y + vb.y + vc.y) / 3; var cz = (va.z + vb.z + vc.z) / 3; var viewX = camInfo.pos.x - cx; var viewY = camInfo.pos.y - cy; var viewZ = camInfo.pos.z - cz; var dot = nx * viewX + ny * viewY + nz * viewZ; if (dot <= 0) continue; // 背向相机,跳过 // 光栅化三角形到像素掩码 RasterizeTriangle(pixels, screenW, screenH, sa.x, sa.y, sb.x, sb.y, sc.x, sc.y); } } } // 找出边界像素并绘制(只在坦克边界框内遍历) 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; var outlineCount = 0; for (var y = by0; y <= by1; y++) { var rowOffset = y * screenW; for (var x = bx0; x <= bx1; x++) { if (pixels[rowOffset + x] === 0) continue; // 检查4个邻居是否有未填充像素 var isBoundary = false; if (x === 0 || pixels[rowOffset + x - 1] === 0) isBoundary = true; else if (x === screenW - 1 || pixels[rowOffset + x + 1] === 0) isBoundary = true; else if (y === 0 || pixels[rowOffset - screenW + x] === 0) isBoundary = true; else if (y === screenH - 1 || pixels[rowOffset + screenW + x] === 0) isBoundary = true; if (isBoundary) { ctx.fillRect(x, y, 3, 3); outlineCount++; } } } // 清除像素掩码(供下一个坦克使用) for (var y2 = by0; y2 <= by1; y2++) { var rowOff = y2 * screenW; for (var x2 = bx0; x2 <= bx1; x2++) { pixels[rowOff + x2] = 0; } } 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, ghostNodes: 0, outlineEdges: 0 }; // ===== 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; // ===== 渲染循环 ===== function Render() { if (!esp.running) return; esp.frameCount++; // 定期清空 GPU 数据缓存 GpuCacheFrameCount++; if (GpuCacheFrameCount >= GpuCacheClearInterval) { GpuDataCache.clear(); GpuCacheFrameCount = 0; } DiagStats = { totalNodes: 0, filteredNodes: 0, renderedNodes: 0, ghostNodes: 0, outlineEdges: 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 }; } 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; var physicsComp = FindComponentByField(tank, 'gzh_1'); if (!physicsComp || !physicsComp.gzh_1) continue; var tankState = GetTankState(tank); if (tankState !== 3) continue; var pos = { x: physicsComp.gzh_1.f20_1, y: physicsComp.gzh_1.g20_1, z: physicsComp.gzh_1.h20_1 }; // 坦克移动检测 var tankId = tank.hyt_1; var prevPos = TankPrevPos.get(tankId); var tankIsMoving = false; if (prevPos) { var pdx = pos.x - prevPos.x, pdy = pos.y - prevPos.y, pdz = pos.z - prevPos.z; tankIsMoving = (pdx*pdx + pdy*pdy + pdz*pdz) > 100; } TankPrevPos.set(tankId, { x: pos.x, y: pos.y, z: pos.z }); // 距离检查(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)'; var tankColor = friendly ? 'rgba(68, 136, 255, 0.9)' : 'rgba(255, 68, 68, 0.9)'; // 先画中心点、距离和血量(独立于3D模型渲染) var centerProj = Project3D(pos, camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH); if (centerProj) { // 血量信息 var health = GetTankHealth(tank); var barW = 60, barH = 6; var barX = centerProj.x - barW / 2; var barY = centerProj.y - barH - 4; if (health) { // 背景 ctx.fillStyle = 'rgba(0, 0, 0, 0.6)'; ctx.fillRect(barX - 1, barY - 1, barW + 2, barH + 2); // 血量百分比颜色:绿>60%, 黄>30%, 红 var hpColor = health.percent > 0.6 ? '#44ff44' : health.percent > 0.3 ? '#ffaa00' : '#ff4444'; ctx.fillStyle = hpColor; ctx.fillRect(barX, barY, barW * health.percent, barH); // 边框 ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)'; ctx.lineWidth = 1; ctx.strokeRect(barX, barY, barW, barH); // 名称(血量条上方) ctx.fillStyle = tankColor; ctx.font = 'bold 11px Arial'; ctx.textAlign = 'center'; ctx.fillText(tankName, centerProj.x, barY - 14); // 距离(血量条上方第二行) ctx.font = '10px Arial'; ctx.fillText(Math.round(distance / 100) + 'm', centerProj.x, barY - 3); // 血量数值(血量条下方) ctx.fillStyle = hpColor; ctx.font = '10px Arial'; ctx.fillText(Math.round(health.current) + '/' + Math.round(health.max), centerProj.x, barY + barH + 10); } else { ctx.fillStyle = tankColor; ctx.font = 'bold 11px Arial'; ctx.textAlign = 'center'; ctx.fillText(tankName, centerProj.x, centerProj.y - 18); ctx.font = '10px Arial'; ctx.fillText(Math.round(distance / 100) + 'm', centerProj.x, centerProj.y - 6); } // 中心点 ctx.fillStyle = tankColor; ctx.beginPath(); ctx.arc(centerProj.x, centerProj.y, 2, 0, 2 * Math.PI); ctx.fill(); } // 3D模型渲染(仅在mesh数据可用时) var meshNodes = GetTankMeshData(tank); if (!meshNodes || meshNodes.length === 0) { drawnCount++; continue; } // 检查是否有炮塔 var hasTurret = false; for (var ti = 0; ti < meshNodes.length; ti++) { if (meshNodes[ti].group === 'turret') { hasTurret = true; break; } } if (!hasTurret) { drawnCount++; continue; } ctx.strokeStyle = color; ctx.lineWidth = LineWidth; 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, camInfo, screenW, screenH); DiagStats.renderedNodes++; } } else { DrawTankOutline(ctx, meshNodes, pos, tankIsMoving, camInfo, screenW, screenH, DiagStats, color); } drawnCount++; } // 玩家血量显示(左上角) var playerHealth = GetTankHealth(playerTank); if (playerHealth) { var pBarW = 200, pBarH = 12; var pBarX = 10, pBarY = 10; // 背景 ctx.fillStyle = 'rgba(0, 0, 0, 0.6)'; ctx.fillRect(pBarX - 1, pBarY - 1, pBarW + 2, pBarH + 2); // 血量颜色 var pHpColor = playerHealth.percent > 0.6 ? '#44ff44' : playerHealth.percent > 0.3 ? '#ffaa00' : '#ff4444'; ctx.fillStyle = pHpColor; ctx.fillRect(pBarX, pBarY, pBarW * playerHealth.percent, pBarH); // 边框 ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; ctx.lineWidth = 1; ctx.strokeRect(pBarX, pBarY, pBarW, pBarH); // 文字 ctx.fillStyle = '#ffffff'; ctx.font = 'bold 11px Arial'; ctx.textAlign = 'center'; ctx.fillText('HP: ' + Math.round(playerHealth.current) + ' / ' + Math.round(playerHealth.max) + ' (' + Math.round(playerHealth.percent * 100) + '%)', pBarX + pBarW / 2, pBarY + pBarH - 1); } // 屏幕信息 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 += ' | Ghost: ' + DiagStats.ghostNodes + ' | Frame: ' + esp.frameCount; ctx.fillText(infoText, 5, screenH - 5); } 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); 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; requestAnimationFrame(Loop); 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瞄准警告启动 ====='); if (!window.__gameMode__) { console.error('请先运行 hook_gamemode.js'); return; } // ===== 清理旧实例 ===== if (window.__hornetAimWarning__) { window.__hornetAimWarning__.Stop(); console.log('已停止旧瞄准警告'); } var gm = window.__gameMode__; // ===== 设置参数(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 节点世界矩阵(g35_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); 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; 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 是局部空间向量且可能未设置,不可靠 var hullComp = FindComponentByField(tank, 't14l_1') || FindComponentByField(tank, 'u14l_1') || FindComponentByField(tank, 'p14l_1'); if (!hullComp || !hullComp.t14l_1) return null; var skin = hullComp.t14l_1; if (!skin || !skin.s17b_1) 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; // 跳过幽灵节点(z34_1 < 0.9) if (bpNode.g35_1 !== undefined && bpNode.g35_1 < 0.9) continue; 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); 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(); // ===== 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() {}); // ===== 地图平移 ===== 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; frameCount++; 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_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) // 实现要点:Pointer Events + setPointerCapture 把指针"锁"在 handle 上, // pointerup/pointercancel 必定回到 handle;并在 window 捕获阶段加兜底, // 避免面板内 stopPropagation 或游戏画布抢事件导致"松手后仍粘着鼠标"。 // 返回:{ End, IsActive },供隐藏面板时强制结束拖动 function MakeDraggable(elem, handle, ignoreTags) { handle = handle || elem; ignoreTags = ignoreTags || ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA']; handle.style.cursor = 'move'; var Active = false; var PointerId = null; var GrabX = 0; var GrabY = 0; function MoveTo(ClientX, ClientY) { var MaxX = window.innerWidth - 40; var MaxY = window.innerHeight - 24; var NextX = Math.max(0, Math.min(MaxX, ClientX - GrabX)); var NextY = Math.max(0, Math.min(MaxY, ClientY - GrabY)); elem.style.left = NextX + 'px'; elem.style.top = NextY + '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'; } 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 } ]; } // ===== 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); // ===== 面板(可拖动,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); }); // ===== 后视镜设置 ===== 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); }); // 从工具读取当前设置(打开面板时同步) 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); // ===== 阻止面板内事件冒泡到 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(); }; 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); 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(); // 防止隐藏时拖动状态残留(粘手) } function TogglePanel() { if (panel.style.display === 'block') { HidePanel(); } else { panel.style.display = 'block'; UpdateButtons(); SyncWfSettings(); } } 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(); } } }); } window.addEventListener('keydown', OnHotkey); // 热键系统定义完成,更新显示 UpdateHotkeyDisplay(); // ===== 暴露接口 ===== window.__hornetPanel__ = { Toggle: TogglePanel, Show: function() { panel.style.display = 'block'; UpdateButtons(); SyncWfSettings(); }, 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/面板/窗口可拖动,热键已加载'); })();