1171 lines
49 KiB
JavaScript
1171 lines
49 KiB
JavaScript
// ===== 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;
|
||
}
|
||
|
||
// ===== 获取 KMt / TDt 渲染组件 =====
|
||
// 新 build:车身皮肤组件字段是 w14w_1(kDt(t)=t.w14w_1,缺失报 "skin";RDt.j16r 里赋值)
|
||
// 旧 build:w14x_1 / t14l_1 / p14l_1(新版 t14l_1 已改成悬停车体状态 Qmt,务必别再当皮肤用)
|
||
function GetKMtComponent(entity) {
|
||
var comp = FindComponentByField(entity, 'w14w_1');
|
||
if (comp) return comp;
|
||
comp = FindComponentByField(entity, 'w14x_1');
|
||
if (comp) return comp;
|
||
comp = FindComponentByField(entity, 't14l_1');
|
||
if (comp) return comp;
|
||
comp = FindComponentByField(entity, 'p14l_1');
|
||
return comp;
|
||
}
|
||
|
||
// 取车身皮肤对象(含 g17r_1 部件表),跨版本兼容;stats 用于诊断来源
|
||
function GetHullSkin(tank, stats) {
|
||
var comp = GetKMtComponent(tank);
|
||
if (!comp) { if (stats) stats.hullSource = '无组件'; return null; }
|
||
if (comp.w14w_1 && comp.w14w_1.g17r_1) { if (stats) stats.hullSource = 'w14w_1'; return comp.w14w_1; } // 新 build
|
||
if (comp.t14l_1 && comp.t14l_1.g17r_1) { if (stats) stats.hullSource = 't14l_1'; return comp.t14l_1; } // 旧 build
|
||
if (comp.p14l_1 && comp.p14l_1.g17r_1) { if (stats) stats.hullSource = 'p14l_1'; return comp.p14l_1; } // 旧 build
|
||
if (stats) stats.hullSource = '有组件但无皮肤';
|
||
return null;
|
||
}
|
||
|
||
function GetTDtComponent(entity) {
|
||
var comp = FindComponentByField(entity, 'n11c_1');
|
||
if (comp) return comp;
|
||
comp = FindComponentByField(entity, 'q112_1');
|
||
return comp;
|
||
}
|
||
|
||
// ===== 从GPU读取顶点位置数据(缓冲区[0]) =====
|
||
function ReadVertexPositionsFromTp(tp) {
|
||
if (!tp || !tp.x4w_1) return null;
|
||
var tl = tp.x4w_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.v3x_1 && vq.v3x_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.u3p_1) continue;
|
||
var fq = il.u3p_1;
|
||
var bs = fq.s3h_1;
|
||
if (!bs || !bs.b74_1) continue;
|
||
var gl = bs.b74_1;
|
||
var webglBuf = fq.n3x_1 && fq.n3x_1.z73_1;
|
||
if (!webglBuf) continue;
|
||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, webglBuf);
|
||
var bufSize = gl.getBufferParameter(gl.ELEMENT_ARRAY_BUFFER, gl.BUFFER_SIZE);
|
||
if (bufSize <= 0) continue;
|
||
var indices = new Uint16Array(bufSize / 2);
|
||
gl.getBufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, indices);
|
||
allIndices.push(indices);
|
||
}
|
||
return allIndices;
|
||
}
|
||
|
||
// ===== GPU 数据缓存(按 Tp 资源引用,避免重复 GPU 读取) =====
|
||
var GpuDataCache = new Map();
|
||
var GpuCacheFrameCount = 0;
|
||
var GpuCacheClearInterval = 1800; // 定期清 GPU 数据缓存(getBufferSubData 会停顿 GPU,太频繁会周期性卡顿)
|
||
|
||
var StrideDiagPrinted = new WeakSet();
|
||
function GetGpuData(tp) {
|
||
if (!tp) return null;
|
||
if (GpuDataCache.has(tp)) return GpuDataCache.get(tp);
|
||
var positions = ReadVertexPositionsFromTp(tp);
|
||
var indices = ReadIndicesFromTp(tp);
|
||
if (positions && indices && indices.length > 0) {
|
||
// 计算最大索引值,推断顶点 stride
|
||
var maxIdx = 0;
|
||
for (var i = 0; i < indices.length; i++) {
|
||
var idxArr = indices[i];
|
||
for (var j = 0; j < idxArr.length; j++) {
|
||
if (idxArr[j] > maxIdx) maxIdx = idxArr[j];
|
||
}
|
||
}
|
||
var stride = 3;
|
||
var vc3 = Math.floor(positions.length / 3);
|
||
if (maxIdx >= vc3) {
|
||
// stride=3 顶点数不够,尝试常见 interleaved 布局
|
||
var candidates = [8, 11, 5, 7, 14];
|
||
for (var c = 0; c < candidates.length; c++) {
|
||
var vc = Math.floor(positions.length / candidates[c]);
|
||
if (maxIdx < vc) { stride = candidates[c]; break; }
|
||
}
|
||
}
|
||
var data = { positions: positions, indices: indices, stride: stride };
|
||
GpuDataCache.set(tp, data);
|
||
if (!StrideDiagPrinted.has(tp)) {
|
||
StrideDiagPrinted.add(tp);
|
||
console.log('[透视][GPU数据] stride=' + stride + ', 缓冲区=' + positions.length + ' float, 最大索引=' + maxIdx + ', 顶点数=' + Math.floor(positions.length / stride));
|
||
}
|
||
return data;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// ===== 递归收集mesh节点(每帧重新获取 Bp 节点引用) =====
|
||
function CollectMeshNodes(bpNode, nodes, depth, group, stats) {
|
||
if (!bpNode || depth > 5) return;
|
||
if (stats) stats.seen++;
|
||
var name = bpNode.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) {}
|
||
}
|
||
if (bpNode.p35_1 && typeof bpNode.p35_1.e1 === 'function') {
|
||
var childCount = bpNode.p35_1.e1();
|
||
for (var c = 0; c < childCount; c++) {
|
||
try {
|
||
var child = bpNode.p35_1.g1(c);
|
||
if (child) CollectMeshNodes(child, nodes, depth + 1, group, stats);
|
||
} catch(e) {}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ===== 提取坦克的mesh数据 =====
|
||
function GetTankMeshData(tank, stats) {
|
||
var nodes = [];
|
||
var skin = GetHullSkin(tank, stats);
|
||
if (skin) {
|
||
try {
|
||
var partCount = skin.g17r_1.e1();
|
||
if (stats) stats.hullParts = partCount;
|
||
for (var p = 0; p < partCount; p++) {
|
||
var part = skin.g17r_1.g1(p);
|
||
if (!part || !part.j17r_1) continue;
|
||
var nodeCount = part.j17r_1.e1();
|
||
for (var n = 0; n < nodeCount; n++) {
|
||
var bpNode = part.j17r_1.g1(n);
|
||
if (!bpNode) continue;
|
||
CollectMeshNodes(bpNode, nodes, 0, 'hull', stats ? stats.hull : null);
|
||
}
|
||
}
|
||
} catch(e) {}
|
||
}
|
||
var tdt = GetTDtComponent(tank);
|
||
if (stats) stats.turretComp = !!tdt;
|
||
if (tdt && tdt.n11c_1 && tdt.n11c_1.m17a_1) {
|
||
if (stats) stats.turretRoot = true;
|
||
try { CollectMeshNodes(tdt.n11c_1.m17a_1, nodes, 0, 'turret', stats ? stats.turret : null); } catch(e) {}
|
||
}
|
||
return nodes;
|
||
}
|
||
|
||
// ===== 残留节点检测(世界矩阵"冻结")→ 拖拽跟随 =====
|
||
// 现象:实体已移动,但原处仍留着线框。
|
||
// 原因:引擎只为"本帧实际参与渲染"的节点更新世界矩阵,被裁剪/脱离的节点矩阵停在最后位置。
|
||
// 处理:检测到「节点矩阵平移长时间不动、而坦克在动」时**不再丢弃**(丢弃会让整辆模型只剩一个点),
|
||
// 而是用「冻结时刻的相对偏移 + 坦克当前位置」平移这份冻结矩阵,让它跟着坦克走:
|
||
// 既不在旧位置留残留,也不会因为引擎不更新矩阵而丢掉模型。
|
||
// 权衡:拖拽期间朝向沿用最后一次更新(位置正确、朝向可能滞后),对透视足够用。
|
||
var NodeMatrixHistory = new WeakMap(); // WeakMap:不持有节点强引用(引擎会重建节点树)
|
||
var TankPrevPos = new Map(); // tankId → 上一帧位置(含位移滑动平均)
|
||
var FrozenFrames = 20; // 连续不动帧数阈值(仅在坦克本身移动时才累计)
|
||
var NodeMatrixScratch = new Float32Array(16);
|
||
|
||
function IsNodeValid(bpNode, pos, tankIsMoving, diagStats) {
|
||
var M = bpNode.n35_1;
|
||
if (!M) return null;
|
||
if (diagStats) diagStats.totalNodes++;
|
||
|
||
var Hist = NodeMatrixHistory.get(bpNode);
|
||
if (!Hist) {
|
||
NodeMatrixHistory.set(bpNode, { x: M[12], y: M[13], z: M[14], still: 0, off: null });
|
||
} else {
|
||
var dx = M[12] - Hist.x, dy = M[13] - Hist.y, dz = M[14] - Hist.z;
|
||
if (dx * dx + dy * dy + dz * dz > 0.01) {
|
||
Hist.x = M[12]; Hist.y = M[13]; Hist.z = M[14];
|
||
Hist.still = 0;
|
||
Hist.off = null; // 矩阵恢复更新 → 恢复正常绘制
|
||
} else if (tankIsMoving) {
|
||
Hist.still++;
|
||
if (Hist.still >= FrozenFrames) {
|
||
if (!Hist.off) Hist.off = { x: M[12] - pos.x, y: M[13] - pos.y, z: M[14] - pos.z };
|
||
for (var k = 0; k < 16; k++) NodeMatrixScratch[k] = M[k];
|
||
NodeMatrixScratch[12] = pos.x + Hist.off.x;
|
||
NodeMatrixScratch[13] = pos.y + Hist.off.y;
|
||
NodeMatrixScratch[14] = pos.z + Hist.off.z;
|
||
if (diagStats) diagStats.draggedNodes++;
|
||
return NodeMatrixScratch; // 冻结但跟随坦克
|
||
}
|
||
} else {
|
||
Hist.still = 0;
|
||
}
|
||
}
|
||
|
||
// 位置检查(节点世界矩阵平移与坦克位置相差过大则丢弃)—— 只对"未冻结"的节点做
|
||
var nodeDx = M[12] - pos.x, nodeDy = M[13] - pos.y, nodeDz = M[14] - pos.z;
|
||
if (nodeDx * nodeDx + nodeDy * nodeDy + nodeDz * nodeDz > 4000000) {
|
||
if (diagStats) diagStats.filteredNodes++;
|
||
return null;
|
||
}
|
||
|
||
return M;
|
||
}
|
||
|
||
// ===== 线框模式:绘制单个节点的所有边 =====
|
||
// 顶点数据来自 PrepareNodeVerts 的复用缓冲;边去重用数值键 Set(避免字符串分配)
|
||
var _WfEdgeSet = new Set();
|
||
|
||
function WfAddEdge(ctx, a, b) {
|
||
var lo = a < b ? a : b, hi = a < b ? b : a;
|
||
var key = lo * 65536 + hi;
|
||
if (_WfEdgeSet.has(key)) return;
|
||
_WfEdgeSet.add(key);
|
||
ctx.moveTo(VB_Sx[lo], VB_Sy[lo]);
|
||
ctx.lineTo(VB_Sx[hi], VB_Sy[hi]);
|
||
}
|
||
|
||
function DrawWireframeNode(ctx, meshData, worldMatrix, pos) {
|
||
var indicesArr = meshData.indices;
|
||
var vertexCount = PrepareNodeVerts(meshData, worldMatrix, pos);
|
||
|
||
_WfEdgeSet.clear();
|
||
ctx.beginPath();
|
||
for (var mi = 0; mi < indicesArr.length; mi++) {
|
||
var indices = indicesArr[mi];
|
||
for (var idx = 0; idx < indices.length; idx += 3) {
|
||
var i0 = indices[idx], i1 = indices[idx+1], i2 = indices[idx+2];
|
||
if (!VB_Ok[i0] || !VB_Ok[i1] || !VB_Ok[i2]) continue;
|
||
WfAddEdge(ctx, i0, i1);
|
||
WfAddEdge(ctx, i1, i2);
|
||
WfAddEdge(ctx, i2, i0);
|
||
}
|
||
}
|
||
ctx.stroke();
|
||
}
|
||
|
||
// ===== 兜底标记点:模型采集不到/被过滤时,至少画一个点,避免坦克完全不可见 =====
|
||
function DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color) {
|
||
var p = 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++;
|
||
}
|
||
if (turretCount === 0) { // 没有炮塔节点则不画模型,仅画兜底标记
|
||
LogTankDiag(tankName, tankState, MeshStats, hullCount, turretCount, 0);
|
||
DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color);
|
||
drawnCount++;
|
||
continue;
|
||
}
|
||
|
||
ctx.strokeStyle = color;
|
||
ctx.lineWidth = LineWidth;
|
||
|
||
var renderedThisTank = 0;
|
||
if (Mode === 'wireframe') {
|
||
for (var ni = 0; ni < meshNodes.length; ni++) {
|
||
var meshData = meshNodes[ni];
|
||
var bpNode = meshData.node;
|
||
var worldMatrix = IsNodeValid(bpNode, pos, tankIsMoving, DiagStats);
|
||
if (!worldMatrix) continue;
|
||
DrawWireframeNode(ctx, meshData, worldMatrix, pos);
|
||
DiagStats.renderedNodes++;
|
||
renderedThisTank++;
|
||
}
|
||
} else {
|
||
renderedThisTank = DrawTankOutline(ctx, meshNodes, pos, tankIsMoving, screenW, screenH, DiagStats, color);
|
||
}
|
||
|
||
// 诊断(节流):存活坦克模型退化时输出根因数据,便于一次性定位
|
||
if ((renderedThisTank === 0 && meshNodes.length > 0) || hullCount === 0) {
|
||
LogTankDiag(tankName, tankState, MeshStats, hullCount, turretCount, renderedThisTank);
|
||
}
|
||
// 一个节点都没画出来:画兜底标记,避免坦克完全不可见
|
||
if (renderedThisTank === 0) {
|
||
DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color);
|
||
}
|
||
|
||
// 人机标记:坦克上方写「人机」(同队装备分数相同且 < 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透视模块已加载');
|
||
})();
|