Files
TankIC/tools/hornet_3d_wireframe.js
T

1406 lines
60 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ===== 3DTank3D透视 - 线框/描边双模式 =====
// 在浏览器控制台运行(需先运行 hook_gamemode.js)
// 功能:在全屏叠加层上绘制敌方坦克的3D模型
// 模式:
// wireframe - 完整线框(绘制所有边)
// outline - 屏幕空间轮廓描边(只画外轮廓,合并所有mesh节点)
// 切换:window.__hornet3DWireframe__.setMode('outline')
(function() {
function _start() {
console.log('===== 3DTank3D透视启动 =====');
// 允许在未进入战局 / ght 尚未捕获时启动:先出叠加层与状态行,模型等进入战局再出
if (window.__hornet3DWireframe__) {
window.__hornet3DWireframe__.stop();
console.log('已停止旧透视');
}
if (window.__hornet3DOutline__) {
window.__hornet3DOutline__.stop();
console.log('已停止旧描边透视');
}
var gm = window.__gameMode__ || null;
// ===== 当前模式 =====
var Mode = 'outline'; // 'wireframe' | 'outline'
try {
var __savedMode = localStorage.getItem('hornet_wireframe_mode');
if (__savedMode === 'wireframe' || __savedMode === 'outline') Mode = __savedMode;
} catch(e) {}
// ===== 线宽设置 =====
var LineWidth = 2;
try {
var __savedLW = localStorage.getItem('hornet_wireframe_linewidth');
if (__savedLW) {
var __n = parseFloat(__savedLW);
if (!isNaN(__n) && __n >= 0.5 && __n <= 8) LineWidth = __n;
}
} catch(e) {}
// ===== 渲染模式 =====
// 'team' - 队伍模式:所有坦克按队伍渲染(蓝=友方,红=敌方)
// 'whitelist' - 白名单:只有名单中的坦克渲染(按队伍颜色),其他不渲染
// 'blacklist' - 黑名单:名单中的坦克不渲染,其他按队伍颜色渲染
var HighlightMode = 'team';
try {
var __savedHM = localStorage.getItem('hornet_wireframe_highlight_mode');
if (__savedHM && ['team','whitelist','blacklist'].indexOf(__savedHM) >= 0) {
HighlightMode = __savedHM;
}
} catch(e) {}
// ===== 名单 =====
var HighlightList = [];
try {
var __savedHL = localStorage.getItem('hornet_highlight_list');
if (__savedHL) HighlightList = JSON.parse(__savedHL);
} catch(e) {}
function SaveHighlightList() {
try { localStorage.setItem('hornet_highlight_list', JSON.stringify(HighlightList)); } catch(e) {}
}
function IsInList(name) {
if (!name) return false;
for (var i = 0; i < HighlightList.length; i++) {
if (HighlightList[i] === name) return true;
}
return false;
}
// 判断坦克是否应渲染(根据 HighlightMode)
function ShouldRender(name) {
switch (HighlightMode) {
case 'team': return true; // 全部渲染
case 'whitelist': return IsInList(name); // 名单内渲染
case 'blacklist': return !IsInList(name); // 名单外渲染
default: return true;
}
}
// ===== 引用公共库(hornet_lib.js) =====
var lib = window.__hornetLib__;
if (!lib) {
console.error('[透视] 请先运行 hornet_lib.js');
return;
}
var teamFns = lib.LoadTeamFunctions();
var ZCt = teamFns.ZCt, m2Fn = teamFns.m2Fn;
// 公共函数别名(调用处不变)
var FindComponentByField = lib.FindComponentByField;
var GetTankPosition = lib.GetTankPosition;
var GetTankState = lib.GetTankState;
var IsTankAlive = lib.IsTankAlive;
var GetTankName = lib.GetTankName;
function GetPlayerTank() { return lib.GetPlayerTank(gm); }
function IsFriendly(tank, playerTank) { return lib.IsFriendly(tank, playerTank, gm); }
function GetCameraInfo() {
// 自由视角激活时,用虚拟相机参数(位置/方向跟随鼠标)
var fl = window.__hornetFreeLook__;
if (fl && typeof fl.IsActive === 'function' && fl.IsActive()) {
var pos = fl.GetPosition();
var dir = fl.GetDirection();
if (pos && dir) {
return { pos: pos, yaw: dir.yaw, pitch: dir.pitch, fov: 1.0471976 };
}
}
return lib.GetCameraInfo(gm);
}
var Project3D = lib.Project3D;
var Project3DMatrix = lib.Project3DMatrix;
// 优先用相机矩阵投影(新版唯一可靠路径);自由视角虚拟相机没有矩阵时退回欧拉角投影
function ProjectPoint(pos, camInfo, screenW, screenH) {
if (camInfo && camInfo.matrix) {
var r = Project3DMatrix(camInfo, pos, screenW, screenH);
if (r) return r;
}
return Project3D(pos, camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH);
}
// 人机标记:同队内装备分数相同且 < 8000(lib.ScanBots 定期扫描 → IsBot 查询)
var ScanBots = lib.ScanBots, IsBot = lib.IsBot;
var LastBotScanAt = 0; // 人机扫描节流(约每 0.3 秒一次)
// ===== 热点优化:相机基每帧只算一次(避免逐顶点重复三角函数 + 对象分配)=====
var Cam = {
fx: 0, fy: 0, fz: 0, rx: 0, ry: 0, rz: 0, ux: 0, uy: 0, uz: 0,
focal: 1, halfW: 0, halfH: 0, px: 0, py: 0, pz: 0
};
function UpdateCam(camInfo, screenW, screenH) {
// 新 build:直接用相机自带的 world→clip 矩阵(g37_1),不依赖欧拉角约定
var M = camInfo.matrix;
if (M && typeof M.length === 'number' && M.length >= 16) {
Cam.M = M; Cam.hasM = true;
} else {
Cam.M = null; Cam.hasM = false;
}
var cp = Math.cos(camInfo.pitch), sp = Math.sin(camInfo.pitch);
var cy = Math.cos(camInfo.yaw), sy = Math.sin(camInfo.yaw);
Cam.fx = cp * (-sy); Cam.fy = cp * cy; Cam.fz = sp; // forward
Cam.rx = cy; Cam.ry = sy; Cam.rz = 0; // right
Cam.ux = Cam.ry * Cam.fz - Cam.rz * Cam.fy; // up = right × forward
Cam.uy = Cam.rz * Cam.fx - Cam.rx * Cam.fz;
Cam.uz = Cam.rx * Cam.fy - Cam.ry * Cam.fx;
Cam.focal = screenH / (2 * Math.tan(camInfo.fov / 2));
Cam.halfW = screenW / 2; Cam.halfH = screenH / 2;
Cam.screenW = screenW; Cam.screenH = screenH;
Cam.px = camInfo.pos.x; Cam.py = camInfo.pos.y; Cam.pz = camInfo.pos.z;
}
// 顶点缓冲(全局复用,杜绝每帧每顶点对象分配)
var VB_Wx = null, VB_Wy = null, VB_Wz = null; // 世界坐标(法线用)
var VB_Sx = null, VB_Sy = null, VB_Ok = null; // 屏幕坐标 + 可投影标志
function EnsureVerts(n) {
if (!VB_Wx || VB_Wx.length < n) {
var cap = Math.max(n, 2048);
VB_Wx = new Float32Array(cap); VB_Wy = new Float32Array(cap); VB_Wz = new Float32Array(cap);
VB_Sx = new Float32Array(cap); VB_Sy = new Float32Array(cap);
VB_Ok = new Uint8Array(cap);
}
}
// 变换 + 投影一个 mesh 节点的全部顶点到复用缓冲,返回顶点数
function PrepareNodeVerts(meshData, worldMatrix, pos) {
var positions = meshData.positions;
var stride = meshData.stride || 3;
var vc = Math.floor(positions.length / stride);
EnsureVerts(vc);
var m0 = worldMatrix[0], m1 = worldMatrix[1], m2 = worldMatrix[2],
m4 = worldMatrix[4], m5 = worldMatrix[5], m6 = worldMatrix[6],
m8 = worldMatrix[8], m9 = worldMatrix[9], m10 = worldMatrix[10],
m12 = worldMatrix[12], m13 = worldMatrix[13], m14 = worldMatrix[14];
var px = pos.x, py = pos.y, pz = pos.z;
// 新 build:矩阵投影(world→clip),完全跟随引擎自己的相机矩阵
if (Cam.hasM) {
var M = Cam.M;
var mm0 = M[0], mm1 = M[1], mm3 = M[3],
mm4 = M[4], mm5 = M[5], mm7 = M[7],
mm8 = M[8], mm9 = M[9], mm11 = M[11],
mm12 = M[12], mm13 = M[13], mm15 = M[15];
var sw = Cam.screenW, sh = Cam.screenH;
for (var v2 = 0; v2 < vc; v2++) {
var b2x = positions[v2 * stride], b2y = positions[v2 * stride + 1], b2z = positions[v2 * stride + 2];
var w2x = m0 * b2x + m4 * b2y + m8 * b2z + m12;
var w2y = m1 * b2x + m5 * b2y + m9 * b2z + m13;
var w2z = m2 * b2x + m6 * b2y + m10 * b2z + m14;
VB_Wx[v2] = w2x; VB_Wy[v2] = w2y; VB_Wz[v2] = w2z;
var dd2x = w2x - px, dd2y = w2y - py, dd2z = w2z - pz;
if (dd2x * dd2x + dd2y * dd2y + dd2z * dd2z > 6250000) { VB_Ok[v2] = 0; continue; }
var ccx = mm0 * w2x + mm4 * w2y + mm8 * w2z + mm12;
var ccy = mm1 * w2x + mm5 * w2y + mm9 * w2z + mm13;
var ccw = mm3 * w2x + mm7 * w2y + mm11 * w2z + mm15;
if (!(ccw > 0.0001)) { VB_Ok[v2] = 0; continue; }
VB_Sx[v2] = (ccx / ccw * 0.5 + 0.5) * sw;
VB_Sy[v2] = (1 - (ccy / ccw * 0.5 + 0.5)) * sh;
VB_Ok[v2] = 1;
}
return vc;
}
var cfx = Cam.fx, cfy = Cam.fy, cfz = Cam.fz;
var crx = Cam.rx, cry = Cam.ry, crz = Cam.rz;
var cux = Cam.ux, cuy = Cam.uy, cuz = Cam.uz;
var focal = Cam.focal, halfW = Cam.halfW, halfH = Cam.halfH;
var cpx = Cam.px, cpy = Cam.py, cpz = Cam.pz;
for (var v = 0; v < vc; v++) {
var bx = positions[v * stride], by = positions[v * stride + 1], bz = positions[v * stride + 2];
var wx = m0 * bx + m4 * by + m8 * bz + m12;
var wy = m1 * bx + m5 * by + m9 * bz + m13;
var wz = m2 * bx + m6 * by + m10 * bz + m14;
VB_Wx[v] = wx; VB_Wy[v] = wy; VB_Wz[v] = wz;
var ddx = wx - px, ddy = wy - py, ddz = wz - pz;
if (ddx * ddx + ddy * ddy + ddz * ddz > 6250000) { VB_Ok[v] = 0; continue; }
var relX = wx - cpx, relY = wy - cpy, relZ = wz - cpz;
var camZ = relX * cfx + relY * cfy + relZ * cfz;
if (camZ <= 0.1) { VB_Ok[v] = 0; continue; }
var camX = relX * crx + relY * cry + relZ * crz;
var camY = relX * cux + relY * cuy + relZ * cuz;
VB_Sx[v] = halfW + (camX / camZ) * focal;
VB_Sy[v] = halfH - (camY / camZ) * focal;
VB_Ok[v] = 1;
}
return vc;
}
// ===== 皮肤组件(新旧差异修正) =====
// 新版(main.7cfa4058):
// skin 组件(类 eUt)构造后由 j16r 把皮肤对象存到 **组件自己的 g159_1**,
// 皮肤对象.g17r_1 = 车身部件表;每个部件 part.j17r_1 是**单个 BP 节点**(旧版是节点列表)。
// 炮塔根节点 = skinComp.a17q_1(weaponSkin 组件 LUt.u11p() = e13a().a17q_1,e13a 取的就是 skin 组件)。
// 旧写法(w14w_1 / n11c_1 / m17a_1)在新版已被无关类占用(w14w_1=SetReverseBackTurn、m17a_1=数值参数),不再可用。
var SkinCompCache = new WeakMap();
function GetSkinComponent(entity) {
if (!entity) return null;
var Cached = SkinCompCache.get(entity);
if (Cached) return Cached;
var comp = FindComponentByField(entity, 'g159_1');
if (!comp) comp = FindComponentByField(entity, 'a17q_1');
// 兜底:按结构找 —— 组件字段值是"皮肤对象"(含 g17r_1 部件表)
if (!comp) {
try {
entity.qz1_1.s2a_1.forEach(function (VL) {
if (comp) return;
var C = (VL && typeof VL.g1 === 'function') ? VL.g1(0) : VL;
if (!C) return;
for (var K in C) {
var V = C[K];
if (V && typeof V === 'object' && V.g17r_1 && typeof V.g17r_1.e1 === 'function') { comp = C; return; }
}
});
} catch (e) {}
}
if (comp) SkinCompCache.set(entity, comp);
return comp;
}
// 取车身皮肤对象(含 g17r_1 部件表);stats 用于诊断来源
function GetHullSkin(tank, stats) {
var comp = GetSkinComponent(tank);
if (!comp) { if (stats) stats.hullSource = '无皮肤组件'; return null; }
if (comp.g159_1 && comp.g159_1.g17r_1) { if (stats) stats.hullSource = 'g159_1'; return comp.g159_1; }
if (stats) stats.hullSource = '有组件但无皮肤';
return null;
}
// 炮塔 BP 根节点
// 新版:weaponSkin 组件(类 LUt,构造首字段 a11p_1)里 e11p_1 = CUt 包装,其 a17q_1 = 炮塔节点
function GetTurretNode(entity) {
var wp = FindComponentByField(entity, 'a11p_1');
if (wp) {
if (wp.e11p_1 && wp.e11p_1.a17q_1) return wp.e11p_1.a17q_1;
if (typeof wp.u11p === 'function') { try { return wp.u11p(); } catch (e) {} }
}
var comp = GetSkinComponent(entity);
if (comp && comp.a17q_1) return comp.a17q_1;
return null;
}
// 兼容两种集合:引擎列表类(e1/g1)与迭代器(t/u/v)
function ForEachInList(list, fn) {
if (!list) return;
try {
if (typeof list.e1 === 'function' && typeof list.g1 === 'function') {
var n = list.e1();
for (var i = 0; i < n; i++) fn(list.g1(i));
return;
}
if (typeof list.t === 'function') {
for (var it = list.t(); it.u();) fn(it.v());
}
} catch (e) {}
}
// ===== 从GPU读取顶点位置数据(新版结构) =====
// 新旧差异(main.7cfa4058):
// Tp(类 sm): x4w_1 = 顶点组列表,每组 { h3x_1: 顶点缓冲资源 J$, i3x_1: 属性布局 }
// J$: v3x_1 = GPU 缓冲包装(kC,d75_1 = 原生 WebGLBuffer),w3x_1 = 顶点数
// 设备:V$.s3h_1 = GL 包装器 $C,$C.f75_1 = 原生 WebGL2RenderingContext
// ⚠ 旧字段 d3x_1 / s3h_1.b74_1 / v3x_1.z73_1 在新版分别是"渲染参数 / 雾色 uniform / uniform 槽位",全错
function GetGlFromResource(Res) {
if (!Res) return null;
var Dev = Res.s3h_1;
return (Dev && Dev.f75_1) ? Dev.f75_1 : null;
}
// 把引擎的 CPU 端缓冲区包装转成 TypedArray
// J$.x3x_1 / Y$.p3x_1 = 未上传时的原始数据(上传后置 null)
// 包装接口:v2u()=底层视图,g2t()=元素偏移,t2t()=元素个数
function ToTypedArray(Src, Kind) {
if (!Src) return null;
try {
if (typeof Src.v2u === 'function') {
var V = Src.v2u();
var Start = typeof Src.g2t === 'function' ? Src.g2t() : 0;
var Count = typeof Src.t2t === 'function' ? Src.t2t() : (V && V.length);
if (ArrayBuffer.isView(V) && Count > 0) {
var ByteOff = V.byteOffset + Start * V.BYTES_PER_ELEMENT;
if (Kind === 'u32') return new Uint32Array(V.buffer, ByteOff, Count);
if (Kind === 'u16') return new Uint16Array(V.buffer, ByteOff, Count);
return new Float32Array(V.buffer, ByteOff, Count);
}
}
if (ArrayBuffer.isView(Src)) {
if (Kind === 'u16') return (Src instanceof Uint16Array) ? Src : new Uint16Array(Src.buffer, Src.byteOffset, Src.byteLength >> 1);
if (Kind === 'u32') return (Src instanceof Uint32Array) ? Src : new Uint32Array(Src.buffer, Src.byteOffset, Src.byteLength >> 2);
return (Src instanceof Float32Array) ? Src : new Float32Array(Src.buffer, Src.byteOffset, Src.byteLength >> 2);
}
if (typeof Src.length === 'number' && Src.length > 0) {
return (Kind === 'u16') ? Uint16Array.from(Src) : (Kind === 'u32' ? Uint32Array.from(Src) : Float32Array.from(Src));
}
} catch (e) {}
return null;
}
function ReadVertexPositionsFromTp(tp) {
if (!tp || !tp.x4w_1 || typeof tp.x4w_1.e1 !== 'function' || tp.x4w_1.e1() === 0) return null;
var group = tp.x4w_1.g1(0);
if (!group) return null;
var Buf = group.h3x_1; // J$
if (!Buf) return null;
// ① 优先 CPU 端原始数据:未上传 GPU 的 mesh 也能读到(v3x_1.d75_1 此时为 null)
var Cpu = ToTypedArray(Buf.x3x_1, 'f32');
if (Cpu && Cpu.length) return Cpu;
// ② GPU 回读(引擎已上传并释放 CPU 数据时)
var gl = GetGlFromResource(Buf);
var webglBuf = Buf.v3x_1 && Buf.v3x_1.d75_1;
if (!gl || !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读取索引数据 =====
// y4w_1 = 索引列表,每项 { q4w_1: 索引缓冲资源 Y$ }(旧版是 il.u3p_1)
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) continue;
var IdxBuf = il.q4w_1 || il.u3p_1;
if (!IdxBuf) continue;
// ① CPU 端索引数据(Y$.p3x_1),未上传时也能用
var Cpu = ToTypedArray(IdxBuf.p3x_1, 'u16');
if (Cpu && Cpu.length) { allIndices.push(Cpu); continue; }
// ② GPU 回读
var gl = GetGlFromResource(IdxBuf);
var webglBuf = IdxBuf.n3x_1 && IdxBuf.n3x_1.d75_1;
if (!gl || !webglBuf) continue;
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, webglBuf);
var bufSize = gl.getBufferParameter(gl.ELEMENT_ARRAY_BUFFER, gl.BUFFER_SIZE);
if (bufSize <= 0) continue;
var indices = new Uint16Array(bufSize / 2);
gl.getBufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, indices);
allIndices.push(indices);
}
return allIndices;
}
// ===== GPU 数据缓存(按 Tp 资源引用,避免重复 GPU 读取) =====
var GpuDataCache = new Map();
var GpuCacheFrameCount = 0;
var GpuCacheClearInterval = 1800; // 定期清 GPU 数据缓存(getBufferSubData 会停顿 GPU,太频繁会周期性卡顿)
var StrideDiagPrinted = new WeakSet();
var GpuFailLogged = 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 = 0;
// 优先用引擎记录的顶点数反推 stride(J$.w3x_1 = 顶点数)
try {
var G0 = tp.x4w_1.g1(0);
var Vc = G0 && G0.h3x_1 && G0.h3x_1.w3x_1;
if (Vc > 0 && positions.length % Vc === 0) stride = Math.round(positions.length / Vc);
} catch (e) {}
if (!stride || stride < 3 || stride > 20) {
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;
}
// 采集失败诊断(每个资源只打一次):直接指出卡在顶点、索引还是 GL 上下文
if (!GpuFailLogged.has(tp)) {
GpuFailLogged.add(tp);
var D = {};
try {
var G = tp.x4w_1 && typeof tp.x4w_1.g1 === 'function' ? tp.x4w_1.g1(0) : null;
var B = G && G.h3x_1;
var Dev = B && B.s3h_1;
var I = tp.y4w_1 && typeof tp.y4w_1.e1 === 'function' && tp.y4w_1.e1() > 0 ? tp.y4w_1.g1(0) : null;
var IB = I && (I.q4w_1 || I.u3p_1);
D = {
顶点组: tp.x4w_1 && tp.x4w_1.e1 ? tp.x4w_1.e1() : -1,
顶点缓冲资源: !!B,
缓冲包装: !!(B && B.v3x_1),
原生Buffer: !!(B && B.v3x_1 && B.v3x_1.d75_1),
设备: !!Dev,
原生GL: !!(Dev && Dev.f75_1),
顶点数: B ? B.w3x_1 : -1,
位置长度: positions ? positions.length : -1,
索引组: tp.y4w_1 && tp.y4w_1.e1 ? tp.y4w_1.e1() : -1,
索引资源: !!IB,
索引Buffer: !!(IB && IB.n3x_1 && IB.n3x_1.d75_1),
索引个数: indices ? indices.length : -1
};
} catch (e) {}
console.warn('[透视][GPU] 采集失败', D);
}
return null;
}
// ===== 递归收集mesh节点(每帧重新获取 Bp 节点引用) =====
// Seen:跨「车身根 / 部件表 / 炮塔 / 扫描」多路径去重,避免同一节点被画多次
function CollectMeshNodes(bpNode, nodes, depth, group, stats, Seen) {
if (!bpNode || depth > 12) return;
if (Seen) {
if (Seen.has(bpNode)) return;
Seen.add(bpNode);
}
if (stats) stats.seen++;
var name = bpNode.g35_1 || '';
if (bpNode.v3m_1) {
if (stats) stats.hasMesh++;
var gpuData = GetGpuData(bpNode.v3m_1);
if (gpuData) {
if (stats) stats.gpuOk++;
nodes.push({
name: name, node: bpNode, tp: bpNode.v3m_1,
positions: gpuData.positions, indices: gpuData.indices, stride: gpuData.stride, group: group
});
} else if (stats) stats.gpuFail++;
}
if (bpNode.u4x_1 && typeof bpNode.u4x_1.e1 === 'function') {
try {
var lodCount = bpNode.u4x_1.e1();
for (var lod = 0; lod < lodCount && lod < 1; lod++) {
var lodLevel = bpNode.u4x_1.g1(lod);
if (!lodLevel) continue;
var lodTp = lodLevel.v3m_1 || lodLevel.v3m_1;
if (!lodTp) continue;
var lodGpuData = GetGpuData(lodTp);
if (lodGpuData) {
if (stats) stats.gpuOk++;
nodes.push({
name: name + '_lod' + lod, node: bpNode, tp: lodTp,
positions: lodGpuData.positions, indices: lodGpuData.indices, stride: lodGpuData.stride, group: group
});
}
}
} catch(e) {}
}
// 子节点:新版是 r35_1(zl 基类:this.r35_1 = new lo(),r3k 里递归子节点)
// 旧版是 p35_1;新版 p35_1 已变成"世界矩阵",绝不能当子节点列表
var Children = (bpNode.r35_1 && typeof bpNode.r35_1.e1 === 'function') ? bpNode.r35_1 :
((bpNode.p35_1 && typeof bpNode.p35_1.e1 === 'function') ? bpNode.p35_1 : null);
if (Children) {
ForEachInList(Children, function (child) {
if (child) CollectMeshNodes(child, nodes, depth + 1, group, stats, Seen);
});
}
}
// 兜底采集:完全不依赖皮肤组件结构,直接扫实体组件的字段值,找 3D 节点
// 新版节点(zl 基类)特征:g35_1 = 名字字符串,p35_1 = 世界矩阵(长度≥16),r35_1 = 子节点列表
function IsSceneNode(V) {
return !!(V && typeof V === 'object' &&
typeof V.g35_1 === 'string' &&
V.p35_1 && typeof V.p35_1.length === 'number' && V.p35_1.length >= 16 &&
V.r35_1 && typeof V.r35_1 === 'object');
}
// 只遍历普通对象:TypedArray / 数组 / Map / Set 的 for-in 会产出成千上万个键,必须跳过
function IsPlainObj(V) {
if (!V || typeof V !== 'object') return false;
if (Array.isArray(V) || ArrayBuffer.isView(V)) return false;
if (typeof Map !== 'undefined' && V instanceof Map) return false;
if (typeof Set !== 'undefined' && V instanceof Set) return false;
return true;
}
function ScanMeshNodes(tank, nodes, stats, Seen) {
Seen = Seen || new WeakSet();
function Try(V) {
if (!IsSceneNode(V) || Seen.has(V)) return;
CollectMeshNodes(V, nodes, 0, 'scan', stats, Seen);
}
try {
tank.qz1_1.s2a_1.forEach(function (VL) {
var comp = (VL && typeof VL.g1 === 'function') ? VL.g1(0) : VL;
if (!IsPlainObj(comp)) return;
for (var K in comp) {
var V = comp[K];
if (!IsPlainObj(V)) continue;
if (IsSceneNode(V)) { Try(V); continue; }
// 深度 2/3:节点常包在皮肤对象/包装对象里(如 skin.h159_1、weaponSkin.e11p_1.a17q_1)
try {
for (var K2 in V) {
var V2 = V[K2];
if (!IsPlainObj(V2)) continue;
if (IsSceneNode(V2)) { Try(V2); continue; }
for (var K3 in V2) {
var V3 = V2[K3];
if (IsPlainObj(V3)) Try(V3);
}
}
} catch (e) {}
}
});
} catch (e) {}
}
// 结果级去重:同一 mesh 资源 + 同一世界平移,只保留一份
// (引擎里同一逻辑节点可能存在「皮肤树 / 场景树」两套实例,AABB/矩阵更新时机不同,
// 两套都画会出现「复制出一个不动的身体,过一会才跟上」)
var TpIds = new WeakMap();
var TpNextId = 1;
function DedupNodes(nodes) {
if (nodes.length < 2) return nodes;
var Keys = new Set();
var Out = [];
for (var i = 0; i < nodes.length; i++) {
var N = nodes[i];
var Tp = N.tp;
var Id = TpIds.get(Tp);
if (!Id) { Id = TpNextId++; TpIds.set(Tp, Id); }
var M = N.node && N.node.p35_1;
var Pos = M ? (Math.round(M[12]) + ',' + Math.round(M[13]) + ',' + Math.round(M[14])) : '?';
var Key = Id + ':' + N.name + ':' + Pos;
if (Keys.has(Key)) continue;
Keys.add(Key);
Out.push(N);
}
return Out;
}
// 扫描节流:仅在主路径完全采不到时使用,且同一辆坦克至少间隔 10 帧
var ScanTick = 0;
var LastScanAt = new WeakMap();
function ShouldScanNow(tank) {
ScanTick++;
var Last = LastScanAt.get(tank) || 0;
if (ScanTick - Last < 10) return false;
LastScanAt.set(tank, ScanTick);
return true;
}
// ===== 提取坦克的mesh数据 =====
// 采集策略:**单一路径优先**,绝不把两条路径的结果混在一起(混在一起就是"复制身体")
// ① 皮肤组件 h159_1(实际渲染树根)→ ② 部件表 → ③ 组件字段扫描(节流兜底)
function GetTankMeshData(tank, stats) {
var nodes = [];
var Seen = new WeakSet();
var skinComp = GetSkinComponent(tank);
var skin = (skinComp && skinComp.g159_1 && skinComp.g159_1.g17r_1) ? skinComp.g159_1 : null;
if (stats) {
stats.hullSource = skin ? 'g159_1' : (skinComp ? '有组件但无皮肤' : '无皮肤组件');
stats.hullParts = (skin && typeof skin.g17r_1.e1 === 'function') ? skin.g17r_1.e1() : -1;
}
// ① 主路径:车身根节点(包含全部部件节点)
if (skinComp && skinComp.h159_1) {
try { CollectMeshNodes(skinComp.h159_1, nodes, 0, 'hull', stats ? stats.hull : null, Seen); } catch (e) {}
}
// ② 根节点采不到才退回部件表
if (nodes.length === 0 && skin) {
try {
ForEachInList(skin.g17r_1, function (part) {
if (!part) return;
var j = part.j17r_1; // 新版:单个 BP 节点;旧版:节点列表
if (!j) return;
if (typeof j === 'object' && typeof j.e1 === 'function' && typeof j.g1 === 'function') {
ForEachInList(j, function (bp) {
if (bp) CollectMeshNodes(bp, nodes, 0, 'hull', stats ? stats.hull : null, Seen);
});
} else {
CollectMeshNodes(j, nodes, 0, 'hull', stats ? stats.hull : null, Seen);
}
});
} catch(e) {}
}
// ③ 仍然为空才做组件字段扫描(节流,避免每帧全量遍历造成卡顿)
if (nodes.length === 0 && ShouldScanNow(tank)) {
try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {}
}
// ④ 炮塔(独立于车身根;重复实例由 DedupNodes 收掉)
var turretNode = GetTurretNode(tank);
if (stats) stats.turretComp = !!turretNode;
if (turretNode) {
if (stats) stats.turretRoot = true;
try { CollectMeshNodes(turretNode, nodes, 0, 'turret', stats ? stats.turret : null, Seen); } catch(e) {}
}
return DedupNodes(nodes);
}
// ===== 残留节点检测(世界矩阵"冻结")→ 拖拽跟随 =====
// 现象:实体已移动,但原处仍留着线框。
// 原因:引擎只为"本帧实际参与渲染"的节点更新世界矩阵,被裁剪/脱离的节点矩阵停在最后位置。
// 处理:检测到「节点矩阵平移长时间不动、而坦克在动」时**不再丢弃**(丢弃会让整辆模型只剩一个点),
// 而是用「冻结时刻的相对偏移 + 坦克当前位置」平移这份冻结矩阵,让它跟着坦克走:
// 既不在旧位置留残留,也不会因为引擎不更新矩阵而丢掉模型。
// 权衡:拖拽期间朝向沿用最后一次更新(位置正确、朝向可能滞后),对透视足够用。
var NodeMatrixHistory = new WeakMap(); // WeakMap:不持有节点强引用(引擎会重建节点树)
var TankPrevPos = new Map(); // tankId → 上一帧位置(含位移滑动平均)
var FrozenFrames = 20; // 连续不动帧数阈值(仅在坦克本身移动时才累计)
var NodeMatrixScratch = new Float32Array(16);
function IsNodeValid(bpNode, pos, tankIsMoving, diagStats) {
// 世界矩阵:新版是 p35_1(zl 基类构造里 = hl(),s3k/t3k/u3k 读 p35_1[12..14])
// 旧版是 n35_1;新版 n35_1 已变成布尔标志
var M = bpNode.p35_1;
if (!M || typeof M.length !== 'number' || M.length < 16) {
var OldM = bpNode.n35_1;
M = (OldM && typeof OldM.length === 'number' && OldM.length >= 16) ? OldM : null;
}
if (!M) return null;
if (diagStats) diagStats.totalNodes++;
var Hist = NodeMatrixHistory.get(bpNode);
if (!Hist) {
NodeMatrixHistory.set(bpNode, { x: M[12], y: M[13], z: M[14], still: 0, off: null });
} else {
var dx = M[12] - Hist.x, dy = M[13] - Hist.y, dz = M[14] - Hist.z;
if (dx * dx + dy * dy + dz * dz > 0.01) {
Hist.x = M[12]; Hist.y = M[13]; Hist.z = M[14];
Hist.still = 0;
Hist.off = null; // 矩阵恢复更新 → 恢复正常绘制
} else if (tankIsMoving) {
Hist.still++;
if (Hist.still >= FrozenFrames) {
if (!Hist.off) Hist.off = { x: M[12] - pos.x, y: M[13] - pos.y, z: M[14] - pos.z };
for (var k = 0; k < 16; k++) NodeMatrixScratch[k] = M[k];
NodeMatrixScratch[12] = pos.x + Hist.off.x;
NodeMatrixScratch[13] = pos.y + Hist.off.y;
NodeMatrixScratch[14] = pos.z + Hist.off.z;
if (diagStats) diagStats.draggedNodes++;
return NodeMatrixScratch; // 冻结但跟随坦克
}
} else {
Hist.still = 0;
}
}
// 位置检查(节点世界矩阵平移与坦克位置相差过大则丢弃)—— 只对"未冻结"的节点做
var nodeDx = M[12] - pos.x, nodeDy = M[13] - pos.y, nodeDz = M[14] - pos.z;
if (nodeDx * nodeDx + nodeDy * nodeDy + nodeDz * nodeDz > 4000000) {
if (diagStats) diagStats.filteredNodes++;
return null;
}
return M;
}
// ===== 线框模式:绘制单个节点的所有边 =====
// 顶点数据来自 PrepareNodeVerts 的复用缓冲;边去重用数值键 Set(避免字符串分配)
var _WfEdgeSet = new Set();
function WfAddEdge(ctx, a, b) {
var lo = a < b ? a : b, hi = a < b ? b : a;
var key = lo * 65536 + hi;
if (_WfEdgeSet.has(key)) return;
_WfEdgeSet.add(key);
ctx.moveTo(VB_Sx[lo], VB_Sy[lo]);
ctx.lineTo(VB_Sx[hi], VB_Sy[hi]);
}
function DrawWireframeNode(ctx, meshData, worldMatrix, pos) {
var indicesArr = meshData.indices;
var vertexCount = PrepareNodeVerts(meshData, worldMatrix, pos);
_WfEdgeSet.clear();
ctx.beginPath();
for (var mi = 0; mi < indicesArr.length; mi++) {
var indices = indicesArr[mi];
for (var idx = 0; idx < indices.length; idx += 3) {
var i0 = indices[idx], i1 = indices[idx+1], i2 = indices[idx+2];
if (!VB_Ok[i0] || !VB_Ok[i1] || !VB_Ok[i2]) continue;
WfAddEdge(ctx, i0, i1);
WfAddEdge(ctx, i1, i2);
WfAddEdge(ctx, i2, i0);
}
}
ctx.stroke();
}
// ===== 兜底标记点:模型采集不到/被过滤时,至少画一个点,避免坦克完全不可见 =====
function DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color) {
var p = ProjectPoint(pos, camInfo, screenW, screenH);
if (!p) return;
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(p.x, p.y, 3, 0, 2 * Math.PI);
ctx.fill();
}
// ===== 描边模式:屏幕空间光栅化轮廓提取 =====
// 算法(真正的"屏幕几何"方法):
// 1. 把所有面向相机的三角形光栅化(扫描线填充)到像素掩码
// 2. 车身和炮塔在屏幕上重叠时,填充区域自动合并
// 3. 找出填充区域的边界像素(填充像素的4邻居中有未填充像素)
// 4. 只画边界像素 = 屏幕空间外轮廓
var PixelMask = null;
var PixelMaskW = 0;
var PixelMaskH = 0;
var MaskGen = 0; // 掩码用"代数"标记:本辆坦克填充的像素 = gen,无需在坦克之间清空整块缓冲
function GetPixelMask(w, h) {
if (!PixelMask || PixelMaskW !== w || PixelMaskH !== h) {
PixelMask = new Uint32Array(w * h);
PixelMaskW = w;
PixelMaskH = h;
MaskGen = 0;
}
return PixelMask;
}
// 扫描线光栅化三角形(pixels 中写入 gen 表示"本辆坦克已填充")
function RasterizeTriangle(pixels, w, h, gen, x0, y0, x1, y1, x2, y2) {
// 按 y 排序三个顶点
if (y0 > y1) { var t = y0; y0 = y1; y1 = t; t = x0; x0 = x1; x1 = t; }
if (y1 > y2) { var t = y1; y1 = y2; y2 = t; t = x1; x1 = x2; x2 = t; }
if (y0 > y1) { var t = y0; y0 = y1; y1 = t; t = x0; x0 = x1; x1 = t; }
var minY = Math.max(0, Math.ceil(y0));
var maxY = Math.min(h - 1, Math.floor(y2));
if (minY > maxY) return;
for (var y = minY; y <= maxY; y++) {
var xa, xb;
if (y < y1) {
// 上半部分:y0 -> y1 和 y0 -> y2
xa = (y1 === y0) ? x0 : x0 + (x1 - x0) * (y - y0) / (y1 - y0);
xb = (y2 === y0) ? x0 : x0 + (x2 - x0) * (y - y0) / (y2 - y0);
} else {
// 下半部分:y1 -> y2 和 y0 -> y2
xa = (y2 === y1) ? x1 : x1 + (x2 - x1) * (y - y1) / (y2 - y1);
xb = (y2 === y0) ? x0 : x0 + (x2 - x0) * (y - y0) / (y2 - y0);
}
if (xa > xb) { var t = xa; xa = xb; xb = t; }
var minXA = Math.max(0, Math.ceil(xa - 0.5));
var maxXA = Math.min(w - 1, Math.floor(xb + 0.5));
var rowOffset = y * w;
for (var x = minXA; x <= maxXA; x++) {
pixels[rowOffset + x] = gen;
}
}
}
function DrawTankOutline(ctx, meshNodes, pos, tankIsMoving, screenW, screenH, diagStats, color) {
var pixels = GetPixelMask(screenW, screenH);
var gen = ++MaskGen;
if (gen === 0) { pixels.fill(0); gen = ++MaskGen; } // 代数回绕(理论值):整体清一次
// 记录坦克的屏幕边界框(用于后续边界检测优化)
var minX = screenW, minY = screenH, maxX = 0, maxY = 0;
for (var ni = 0; ni < meshNodes.length; ni++) {
var meshData = meshNodes[ni];
var bpNode = meshData.node;
var worldMatrix = IsNodeValid(bpNode, pos, tankIsMoving, diagStats);
if (!worldMatrix) continue;
var indicesArr = meshData.indices;
// 顶点变换+投影到复用缓冲(无对象分配)
var vertexCount = PrepareNodeVerts(meshData, worldMatrix, pos);
for (var v = 0; v < vertexCount; v++) {
if (!VB_Ok[v]) continue;
var sx = VB_Sx[v], sy = VB_Sy[v];
if (sx < minX) minX = sx;
if (sy < minY) minY = sy;
if (sx > maxX) maxX = sx;
if (sy > maxY) maxY = sy;
}
// 遍历三角形,光栅化面向相机的
for (var mi = 0; mi < indicesArr.length; mi++) {
var indices = indicesArr[mi];
for (var i = 0; i < indices.length; i += 3) {
var a = indices[i], b = indices[i+1], c = indices[i+2];
if (!VB_Ok[a] || !VB_Ok[b] || !VB_Ok[c]) continue;
// 计算法线判断是否面向相机(世界坐标,来自缓冲)
var ax = VB_Wx[a], ay = VB_Wy[a], az = VB_Wz[a];
var bx = VB_Wx[b], by = VB_Wy[b], bz = VB_Wz[b];
var gx = VB_Wx[c], gy = VB_Wy[c], gz = VB_Wz[c];
var e1x = bx - ax, e1y = by - ay, e1z = bz - az;
var e2x = gx - ax, e2y = gy - ay, e2z = gz - az;
var nx = e1y * e2z - e1z * e2y;
var ny = e1z * e2x - e1x * e2z;
var nz = e1x * e2y - e1y * e2x;
var cx = (ax + bx + gx) / 3;
var cy = (ay + by + gy) / 3;
var cz = (az + bz + gz) / 3;
var dot = nx * (Cam.px - cx) + ny * (Cam.py - cy) + nz * (Cam.pz - cz);
if (dot <= 0) continue; // 背向相机,跳过
// 光栅化三角形到像素掩码
RasterizeTriangle(pixels, screenW, screenH, gen, VB_Sx[a], VB_Sy[a], VB_Sx[b], VB_Sy[b], VB_Sx[c], VB_Sy[c]);
}
}
}
// 找出边界像素并绘制(只在坦克边界框内遍历;用单条路径 + 横向连续合并,替代逐像素 fillRect)
var bx0 = Math.max(0, Math.floor(minX) - 1);
var by0 = Math.max(0, Math.floor(minY) - 1);
var bx1 = Math.min(screenW - 1, Math.ceil(maxX) + 1);
var by1 = Math.min(screenH - 1, Math.ceil(maxY) + 1);
ctx.fillStyle = color;
ctx.beginPath();
var outlineCount = 0;
for (var y = by0; y <= by1; y++) {
var rowOffset = y * screenW;
var runStart = -1;
for (var x = bx0; x <= bx1 + 1; x++) {
var isBoundary = false;
if (x <= bx1 && pixels[rowOffset + x] === gen) {
// 检查4个邻居是否有未填充像素(== gen 即本辆坦克填充)
if (x === 0 || pixels[rowOffset + x - 1] !== gen) isBoundary = true;
else if (x === screenW - 1 || pixels[rowOffset + x + 1] !== gen) isBoundary = true;
else if (y === 0 || pixels[rowOffset - screenW + x] !== gen) isBoundary = true;
else if (y === screenH - 1 || pixels[rowOffset + screenW + x] !== gen) isBoundary = true;
}
if (isBoundary) {
if (runStart < 0) runStart = x;
outlineCount++;
} else if (runStart >= 0) {
ctx.rect(runStart, y, x - runStart + 2, 3); // 横向连续的边界像素合并为一条
runStart = -1;
}
}
}
ctx.fill();
if (diagStats) diagStats.outlineEdges += outlineCount;
return outlineCount;
}
// ===== 创建全屏Canvas =====
var canvas = document.createElement('canvas');
canvas.id = 'hornet3DWireframeCanvas';
canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:99998;pointer-events:none;';
document.body.appendChild(canvas);
var ctx = canvas.getContext('2d');
function ResizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
ResizeCanvas();
window.addEventListener('resize', ResizeCanvas);
// ===== 诊断统计 =====
var DiagStats = { totalNodes: 0, filteredNodes: 0, renderedNodes: 0, outlineEdges: 0 };
var LastDegradeLogFrame = -9999; // 退化诊断日志节流
// 退化诊断(节流输出):定位"存活坦克模型没画出来"的根因(皮肤/部件/GPU/炮塔逐级)
function LogTankDiag(tankName, tankState, S, hullCount, turretCount, rendered) {
if (esp.frameCount - LastDegradeLogFrame <= 120) return;
LastDegradeLogFrame = esp.frameCount;
console.warn('[透视][退化诊断] ' + tankName + ' state=' + tankState +
' | 皮肤=' + S.hullSource + ' 部件=' + S.hullParts +
' 车身[见/网格/GPU成功/失败]=' + S.hull.seen + '/' + S.hull.hasMesh + '/' + S.hull.gpuOk + '/' + S.hull.gpuFail +
' | 炮塔 组件=' + S.turretComp + ' 根节点=' + S.turretRoot +
' 炮塔[见/网格/GPU成功/失败]=' + S.turret.seen + '/' + S.turret.hasMesh + '/' + S.turret.gpuOk + '/' + S.turret.gpuFail +
' | 收集 车身=' + hullCount + ' 炮塔=' + turretCount + ' 绘制=' + rendered +
' | 本帧 总=' + DiagStats.totalNodes + ' 过滤=' + DiagStats.filteredNodes);
}
// ===== ESP状态 =====
var esp = {
canvas: canvas,
ctx: ctx,
running: true,
frameCount: 0,
mode: Mode,
setMode: function(m) {
if (m === 'wireframe' || m === 'outline') {
Mode = m;
this.mode = m;
try { localStorage.setItem('hornet_wireframe_mode', m); } catch(e) {}
console.log('[透视] 模式切换为: ' + m);
} else {
console.warn('[透视] 未知模式: ' + m + ',支持: wireframe | outline');
}
},
getMode: function() { return Mode; },
setLineWidth: function(w) {
var n = parseFloat(w);
if (!isNaN(n) && n >= 0.5 && n <= 8) {
LineWidth = n;
try { localStorage.setItem('hornet_wireframe_linewidth', String(n)); } catch(e) {}
console.log('[透视] 线宽设置为: ' + n);
} else {
console.warn('[透视] 线宽应在 0.5 ~ 8 之间,当前: ' + w);
}
},
getLineWidth: function() { return LineWidth; },
setHighlightMode: function(mode) {
var valid = ['team','whitelist','blacklist'];
if (valid.indexOf(mode) >= 0) {
HighlightMode = mode;
try { localStorage.setItem('hornet_wireframe_highlight_mode', mode); } catch(e) {}
console.log('[透视] 渲染模式切换为: ' + mode);
} else {
console.warn('[透视] 未知渲染模式: ' + mode + ',支持: ' + valid.join(', '));
}
},
getHighlightMode: function() { return HighlightMode; },
setHighlightList: function(names) {
HighlightList = Array.isArray(names) ? names.slice() : [];
SaveHighlightList();
console.log('[透视] 高亮名单已更新:', HighlightList.length + '人');
},
addHighlight: function(name) {
if (name && HighlightList.indexOf(name) < 0) {
HighlightList.push(name);
SaveHighlightList();
console.log('[透视] 已添加高亮:', name);
}
},
removeHighlight: function(name) {
var idx = HighlightList.indexOf(name);
if (idx >= 0) {
HighlightList.splice(idx, 1);
SaveHighlightList();
console.log('[透视] 已移除高亮:', name);
}
},
getHighlightList: function() {
return HighlightList.slice();
},
clearHighlight: function() {
HighlightList = [];
SaveHighlightList();
console.log('[透视] 高亮名单已清空');
},
stop: function() {
this.running = false;
window.removeEventListener('resize', ResizeCanvas);
if (canvas.parentNode) canvas.parentNode.removeChild(canvas);
console.log('透视已停止');
}
};
window.__hornet3DWireframe__ = esp;
// ===== 金箱子标记 =====
// 金箱子是独立实体(不是坦克)。引擎侧标志:
// · 补给实体 = entity.j10q_1(bonusData 组件,见 module_87072 的 ctt())
// · 类型枚举 mE(module_10032):GOLD_BOX = 6
// 做法:实体基类的组件创建方法 nyz()(module_61378「BattleEntity.createComponent」)上挂观察钩子,
// 发现「带 j10q_1 且类型=6」的实体就登记,之后每帧按它自己的位置画金色标记。
// 钩子目标沿 tank 的原型链找 nyz 的宿主原型 —— 不需要任何导出键。
var GoldBoxes = [];
var GoldPending = [];
var EntityProtoHooked = false;
var GOLD_BOX_TYPE = 6;
function FindEnumOf(Obj) {
for (var K in Obj) {
try {
var V = Obj[K];
if (V && typeof V === 'object' && typeof V.k3_1 === 'number' && typeof V.equals === 'function') return V;
} catch (e) {}
}
return null;
}
// 引擎口径:有 bonusData 且类型是 GOLD_BOX
function IsGoldBoxEntity(Entity) {
if (!Entity || !Entity.j10q_1) return false;
var Type = FindEnumOf(Entity.j10q_1);
return !!(Type && Type.k3_1 === GOLD_BOX_TYPE);
}
function RegisterGoldBox(Entity) {
for (var i = 0; i < GoldBoxes.length; i++) if (GoldBoxes[i] === Entity) return;
GoldBoxes.push(Entity);
console.log('[透视] 发现金箱子,已加入标记(当前 ' + GoldBoxes.length + ' 个)');
}
function InstallEntityHook(Tank) {
if (EntityProtoHooked || !Tank) return;
var Proto = Tank;
while (Proto && !Object.prototype.hasOwnProperty.call(Proto, 'nyz')) Proto = Object.getPrototypeOf(Proto);
if (!Proto || typeof Proto.nyz !== 'function') return;
var Orig = Proto.nyz;
Proto.nyz = function () {
var R = Orig.apply(this, arguments);
try {
if (IsGoldBoxEntity(this)) RegisterGoldBox(this);
else if (this && this.j10q_1 === undefined && GoldPending.length < 300) {
GoldPending.push({ e: this, f: esp.frameCount }); // 字段可能稍后才赋值,延后判定
}
} catch (E) {}
return R;
};
EntityProtoHooked = true;
console.log('[透视] 已挂补给实体观察钩子(金箱子)');
}
// 金箱位置:优先物理组件(与坦克同款 rzf_1),否则退到 bonusData 里的节点世界矩阵/向量
function GetGoldBoxPosition(Entity) {
var PhysPos = GetTankPosition(Entity); // 多级探测物理组件位置
if (PhysPos) return PhysPos;
var Bonus = Entity.j10q_1;
for (var K in Bonus) {
try {
var V = Bonus[K];
if (!V || typeof V !== 'object') continue;
if (V.n35_1 && V.n35_1.length >= 15) {
var M = V.n35_1;
return { x: M[12], y: M[13], z: M[14] };
}
if (typeof V.f20_1 === 'number' && typeof V.g20_1 === 'number' && typeof V.h20_1 === 'number') {
return { x: V.f20_1, y: V.g20_1, z: V.h20_1 };
}
} catch (e) {}
}
return null;
}
// 金色菱形 + 十字 + 「金箱」标签(穿透显示,不遮挡判断)
function DrawGoldBoxMarker(Pos, ScreenW, ScreenH) {
var X = Pos.x - Cam.px, Y = Pos.y - Cam.py, Z = Pos.z - Cam.pz;
var CamZ = X * Cam.fx + Y * Cam.fy + Z * Cam.fz;
if (CamZ <= 0.1) return false;
var CamX = X * Cam.rx + Y * Cam.ry + Z * Cam.rz;
var CamY = X * Cam.ux + Y * Cam.uy + Z * Cam.uz;
var Sx = Cam.halfW + (CamX / CamZ) * Cam.focal;
var Sy = Cam.halfH - (CamY / CamZ) * Cam.focal;
if (Sx < -60 || Sy < -60 || Sx > ScreenW + 60 || Sy > ScreenH + 60) return false;
var R = 12;
ctx.save();
ctx.strokeStyle = '#ffd700';
ctx.fillStyle = 'rgba(255, 215, 0, 0.22)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(Sx, Sy - R);
ctx.lineTo(Sx + R, Sy);
ctx.lineTo(Sx, Sy + R);
ctx.lineTo(Sx - R, Sy);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(Sx - R - 7, Sy); ctx.lineTo(Sx + R + 7, Sy);
ctx.moveTo(Sx, Sy - R - 7); ctx.lineTo(Sx, Sy + R + 7);
ctx.stroke();
ctx.fillStyle = '#ffd700';
ctx.font = 'bold 12px Consolas,monospace';
ctx.textAlign = 'center';
ctx.fillText('金箱', Sx, Sy - R - 9);
ctx.restore();
return true;
}
// ===== 冻结节点"拖拽跟随"诊断(节流 2s)=====
var LastFrozenLogAt = 0;
function LogDragDiag(Stats) {
if (!Stats.draggedNodes) return;
var Now = Date.now();
if (Now - LastFrozenLogAt < 2000) return;
LastFrozenLogAt = Now;
console.log('[透视][冻结] 本帧拖拽跟随节点 ' + Stats.draggedNodes + '/' + Stats.totalNodes +
'(引擎未更新这些节点矩阵,已按坦克位移平移,避免旧位置残留/模型丢失)');
}
// ===== 渲染循环 =====
function Render() {
if (!esp.running) return;
esp.frameCount++;
// 换图自刷新:gameMode 实例变化时重绑并清空该地图的缓存
var NewGm = window.__gameMode__;
if (NewGm && NewGm !== gm) {
gm = NewGm;
GpuDataCache.clear(); // 旧地图的 GPU 几何数据失效
TankPrevPos.clear(); // 位置历史(判断"在动")失效
GoldBoxes.length = 0; // 上一局的箱子实体全部作废
GoldPending.length = 0;
GpuCacheFrameCount = 0;
console.log('[透视] 检测到新战斗,已重置缓存');
}
// 旧实例已失效、新实例尚未捕获:清屏并显示等待提示,避免继续用悬空引用
if (!window.__gameMode__) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'rgba(255, 220, 120, 0.9)';
ctx.font = 'bold 12px Consolas,monospace';
ctx.textAlign = 'left';
ctx.fillText('[' + Mode.toUpperCase() + '] 等待战局…(ght 未捕获)', 5, canvas.height - 5);
requestAnimationFrame(Render);
return;
}
// 定期清空 GPU 数据缓存
GpuCacheFrameCount++;
if (GpuCacheFrameCount >= GpuCacheClearInterval) {
GpuDataCache.clear();
GpuCacheFrameCount = 0;
}
DiagStats = { totalNodes: 0, filteredNodes: 0, draggedNodes: 0, renderedNodes: 0, outlineEdges: 0, goldBoxes: 0 };
try {
var screenW = canvas.width;
var screenH = canvas.height;
ctx.clearRect(0, 0, screenW, screenH);
var camInfo = GetCameraInfo();
if (!camInfo) {
requestAnimationFrame(Render);
return;
}
var playerTank = GetPlayerTank();
if (!playerTank) {
requestAnimationFrame(Render);
return;
}
// 相机位置为0时使用玩家位置
if (Math.abs(camInfo.pos.x) < 0.1 && Math.abs(camInfo.pos.y) < 0.1 && Math.abs(camInfo.pos.z) < 0.1) {
var pPos = GetTankPosition(playerTank);
if (pPos) camInfo.pos = { x: pPos.x, y: pPos.y, z: pPos.z + 100 };
}
UpdateCam(camInfo, screenW, screenH); // 每帧只算一次相机基(需在相机位置兜底之后),供顶点变换/投影复用
var tankList = gm.m10b_1.j11y();
var count = tankList.e1();
var drawnCount = 0, botDrawn = 0;
// 人机扫描(装备分数不会每帧变,节流到约每 0.3 秒一次)
if (ScanBots && (Date.now() - LastBotScanAt > 300)) {
LastBotScanAt = Date.now();
ScanBots();
}
for (var i = 0; i < count; i++) {
var tank = tankList.g1(i);
if (!tank || tank === playerTank) continue;
if (!EntityProtoHooked) InstallEntityHook(tank); // 借坦克实例找到实体基类原型,挂补给观察钩子
var pos = GetTankPosition(tank); // 多级探测物理组件位置(字段名每个构建会平移)
if (!pos) continue;
var tankState = GetTankState(tank);
// 可见坦克 = ACTIVE(3) 或 SEMI_ACTIVE(2)(引擎 s14j() 判定),0/1/4 才跳过
if (tankState !== 2 && tankState !== 3) continue;
// 本帧该坦克是否在移动(供「残留节点」检测:坦克在动而节点矩阵不动 = 旧位置遗留)
// 用位移的指数滑动平均,而不是「单帧位移 > 10 单位」——后者对慢速移动几乎永远为假,
// 会让残留检测形同虚设(残留漏掉的根因就在这里)
var tankId = tank.iz1_1;
var prevPos = TankPrevPos.get(tankId);
var tankMove = 0;
if (prevPos) {
var pdx = pos.x - prevPos.x, pdy = pos.y - prevPos.y, pdz = pos.z - prevPos.z;
tankMove = (prevPos.move || 0) * 0.85 + Math.sqrt(pdx * pdx + pdy * pdy + pdz * pdz);
}
var tankIsMoving = tankMove > 0.5; // 0.5 单位/帧 ≈ 0.3 m/s;静置坦克的抖动低于此值
TankPrevPos.set(tankId, { x: pos.x, y: pos.y, z: pos.z, move: tankMove });
// 距离检查(50000 = 500米)
var dx = pos.x - camInfo.pos.x;
var dy = pos.y - camInfo.pos.y;
var dz = pos.z - camInfo.pos.z;
var distance = Math.sqrt(dx*dx + dy*dy + dz*dz);
if (distance > 50000) continue;
var friendly = IsFriendly(tank, playerTank);
var tankName = GetTankName(tank) || ('ID:' + tank.iz1_1);
// 根据 HighlightMode 判断是否渲染该坦克
if (!ShouldRender(tankName)) continue;
// 颜色按队伍:友方=蓝,敌方=红
var color = friendly ? 'rgba(68, 136, 255, 0.6)' : 'rgba(255, 68, 68, 0.6)';
// 3D模型渲染(仅在mesh数据可用时)
var MeshStats = {
hullSource: '', hullParts: 0, turretComp: false, turretRoot: false,
hull: { seen: 0, hasMesh: 0, gpuOk: 0, gpuFail: 0 },
turret: { seen: 0, hasMesh: 0, gpuOk: 0, gpuFail: 0 }
};
var meshNodes = GetTankMeshData(tank, MeshStats);
if (!meshNodes || meshNodes.length === 0) {
LogTankDiag(tankName, tankState, MeshStats, 0, 0, 0);
DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color); // 兜底:至少可见
drawnCount++;
continue;
}
// 统计各分组节点数(诊断用)
var hullCount = 0, turretCount = 0;
for (var gi = 0; gi < meshNodes.length; gi++) {
if (meshNodes[gi].group === 'turret') turretCount++; else hullCount++;
}
// 车身/炮塔任一有 mesh 就画模型;两者都为空才退回标记点
// (旧逻辑要求必须先拿到炮塔节点,炮塔字段漂移时会连带把车身模型也吞掉)
if (hullCount === 0 && turretCount === 0) {
LogTankDiag(tankName, tankState, MeshStats, hullCount, turretCount, 0);
DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color);
drawnCount++;
continue;
}
ctx.strokeStyle = color;
ctx.lineWidth = LineWidth;
var renderedThisTank = 0;
if (Mode === 'wireframe') {
for (var ni = 0; ni < meshNodes.length; ni++) {
var meshData = meshNodes[ni];
var bpNode = meshData.node;
var worldMatrix = IsNodeValid(bpNode, pos, tankIsMoving, DiagStats);
if (!worldMatrix) continue;
DrawWireframeNode(ctx, meshData, worldMatrix, pos);
DiagStats.renderedNodes++;
renderedThisTank++;
}
} else {
renderedThisTank = DrawTankOutline(ctx, meshNodes, pos, tankIsMoving, screenW, screenH, DiagStats, color);
}
// 诊断(节流):存活坦克模型退化时输出根因数据,便于一次性定位
if ((renderedThisTank === 0 && meshNodes.length > 0) || hullCount === 0) {
LogTankDiag(tankName, tankState, MeshStats, hullCount, turretCount, renderedThisTank);
}
// 一个节点都没画出来:画兜底标记,避免坦克完全不可见
if (renderedThisTank === 0) {
DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color);
}
// 人机标记:坦克上方写「人机」(同队装备分数相同且 < 8000)
if (typeof IsBot === 'function' && IsBot(tank)) {
var botPt = ProjectPoint({ x: pos.x, y: pos.y, z: pos.z + 1300 },
camInfo, screenW, screenH);
if (botPt) {
ctx.save();
ctx.font = 'bold 13px Consolas,monospace';
ctx.textAlign = 'center';
ctx.lineWidth = 3;
ctx.strokeStyle = 'rgba(0,0,0,0.85)';
ctx.strokeText('人机', botPt.x, botPt.y); // 先描黑边,保证任何背景下都看得清
ctx.fillStyle = '#ffd24a';
ctx.fillText('人机', botPt.x, botPt.y);
ctx.restore();
botDrawn++;
}
}
drawnCount++;
}
// ===== 金箱子:清理失效项 → 评估候选 → 画金色标记 =====
var goldDrawn = 0;
for (var gci = GoldBoxes.length - 1; gci >= 0; gci--) {
if (!IsGoldBoxEntity(GoldBoxes[gci])) GoldBoxes.splice(gci, 1); // 被拾取/销毁
}
for (var gpi = GoldPending.length - 1; gpi >= 0; gpi--) {
var PendingIt = GoldPending[gpi];
if (esp.frameCount - PendingIt.f > 90) { GoldPending.splice(gpi, 1); continue; }
if (IsGoldBoxEntity(PendingIt.e)) { RegisterGoldBox(PendingIt.e); GoldPending.splice(gpi, 1); }
}
for (var gbi = 0; gbi < GoldBoxes.length; gbi++) {
var GoldPos = GetGoldBoxPosition(GoldBoxes[gbi]);
if (GoldPos && DrawGoldBoxMarker(GoldPos, screenW, screenH)) goldDrawn++;
}
DiagStats.goldBoxes = goldDrawn;
// 屏幕信息
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.font = '10px Arial';
ctx.textAlign = 'left';
var infoText = '[' + Mode.toUpperCase() + '] | ' + drawnCount + ' tanks';
if (Mode === 'outline') {
infoText += ' | Edges: ' + DiagStats.outlineEdges;
} else {
infoText += ' | Nodes: ' + DiagStats.renderedNodes + '/' + DiagStats.totalNodes;
}
infoText += ' | Frame: ' + esp.frameCount;
if (DiagStats.draggedNodes > 0) infoText += ' | 拖拽:' + DiagStats.draggedNodes;
if (DiagStats.goldBoxes > 0) infoText += ' | 金箱:' + DiagStats.goldBoxes;
ctx.fillText(infoText, 5, screenH - 5);
LogDragDiag(DiagStats);
} catch(e) {
console.error('[透视] 错误:', e.message, e.stack ? e.stack.split('\n')[1] : '');
}
requestAnimationFrame(Render);
}
// 启动渲染
requestAnimationFrame(Render);
console.log('3D透视已启动(双模式: wireframe | outline)');
console.log('当前模式: ' + Mode);
console.log('提示: window.__hornet3DWireframe__.setMode("outline") 切换到描边模式');
console.log('提示: window.__hornet3DWireframe__.setMode("wireframe") 切换到线框模式');
console.log('提示: window.__hornet3DWireframe__.addHighlight("玩家名") 添加高亮(金色标记)');
console.log('提示: window.__hornet3DWireframe__.stop() 停止透视');
}
var Tool = {
Start: function() {
if (window.__hornet3DWireframe__ && window.__hornet3DWireframe__.running) return;
_start();
},
Stop: function() {
if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.stop();
},
Status: function() {
return (window.__hornet3DWireframe__ && window.__hornet3DWireframe__.running) ? ('运行中(' + window.__hornet3DWireframe__.mode + ')') : '已停止';
},
GetRunning: function() {
return !!(window.__hornet3DWireframe__ && window.__hornet3DWireframe__.running);
},
SetMode: function(m) {
if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.setMode(m);
},
GetMode: function() {
return window.__hornet3DWireframe__ ? window.__hornet3DWireframe__.getMode() : null;
},
SetLineWidth: function(w) {
if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.setLineWidth(w);
},
GetLineWidth: function() {
return window.__hornet3DWireframe__ ? window.__hornet3DWireframe__.getLineWidth() : 2;
},
SetHighlightMode: function(mode) {
if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.setHighlightMode(mode);
},
GetHighlightMode: function() {
return window.__hornet3DWireframe__ ? window.__hornet3DWireframe__.getHighlightMode() : 'team';
},
SetHighlightList: function(names) {
if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.setHighlightList(names);
},
AddHighlight: function(name) {
if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.addHighlight(name);
},
RemoveHighlight: function(name) {
if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.removeHighlight(name);
},
GetHighlightList: function() {
return window.__hornet3DWireframe__ ? window.__hornet3DWireframe__.getHighlightList() : [];
},
ClearHighlight: function() {
if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.clearHighlight();
}
};
window.__hornetTools__ = window.__hornetTools__ || {};
window.__hornetTools__.Wireframe = Tool;
console.log('[3DTank] 3D透视模块已加载');
})();