Files
TankIC/tools/hornet_3d_wireframe.js
T
TermiNexus 4005213226 feat(tools): 新版本工具链移植 + 本地镜像单包加载,中继改为原样直发与异步有序落盘
- hornet 工具链:无 ght 启动;透视冻结节点拖拽跟随、金箱标记、性能优化;雷达死亡红叉;鼠标稳定器;窗口布局持久化

- 本地镜像:切割与单包加载(pack_unpacked.js)+ SW 单包缓存 + F5 兜底刷新;入口更新为 main.9f4e7a89.js

- 中继 wss_server.js:转发改 socket 反压的原样直发(零等待零缓存);落盘改单条有序异步队列(默认开启);新增反压看门狗(默认 60s)
2026-10-11 19:35:54 +08:00

1109 lines
46 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 Cam = {
fx: 0, fy: 0, fz: 0, rx: 0, ry: 0, rz: 0, ux: 0, uy: 0, uz: 0,
focal: 1, halfW: 0, halfH: 0, px: 0, py: 0, pz: 0
};
function UpdateCam(camInfo, screenW, screenH) {
var cp = Math.cos(camInfo.pitch), sp = Math.sin(camInfo.pitch);
var cy = Math.cos(camInfo.yaw), sy = Math.sin(camInfo.yaw);
Cam.fx = cp * (-sy); Cam.fy = cp * cy; Cam.fz = sp; // forward
Cam.rx = cy; Cam.ry = sy; Cam.rz = 0; // right
Cam.ux = Cam.ry * Cam.fz - Cam.rz * Cam.fy; // up = right × forward
Cam.uy = Cam.rz * Cam.fx - Cam.rx * Cam.fz;
Cam.uz = Cam.rx * Cam.fy - Cam.ry * Cam.fx;
Cam.focal = screenH / (2 * Math.tan(camInfo.fov / 2));
Cam.halfW = screenW / 2; Cam.halfH = screenH / 2;
Cam.px = camInfo.pos.x; Cam.py = camInfo.pos.y; Cam.pz = camInfo.pos.z;
}
// 顶点缓冲(全局复用,杜绝每帧每顶点对象分配)
var VB_Wx = null, VB_Wy = null, VB_Wz = null; // 世界坐标(法线用)
var VB_Sx = null, VB_Sy = null, VB_Ok = null; // 屏幕坐标 + 可投影标志
function EnsureVerts(n) {
if (!VB_Wx || VB_Wx.length < n) {
var cap = Math.max(n, 2048);
VB_Wx = new Float32Array(cap); VB_Wy = new Float32Array(cap); VB_Wz = new Float32Array(cap);
VB_Sx = new Float32Array(cap); VB_Sy = new Float32Array(cap);
VB_Ok = new Uint8Array(cap);
}
}
// 变换 + 投影一个 mesh 节点的全部顶点到复用缓冲,返回顶点数
function PrepareNodeVerts(meshData, worldMatrix, pos) {
var positions = meshData.positions;
var stride = meshData.stride || 3;
var vc = Math.floor(positions.length / stride);
EnsureVerts(vc);
var m0 = worldMatrix[0], m1 = worldMatrix[1], m2 = worldMatrix[2],
m4 = worldMatrix[4], m5 = worldMatrix[5], m6 = worldMatrix[6],
m8 = worldMatrix[8], m9 = worldMatrix[9], m10 = worldMatrix[10],
m12 = worldMatrix[12], m13 = worldMatrix[13], m14 = worldMatrix[14];
var px = pos.x, py = pos.y, pz = pos.z;
var cfx = Cam.fx, cfy = Cam.fy, cfz = Cam.fz;
var crx = Cam.rx, cry = Cam.ry, crz = Cam.rz;
var cux = Cam.ux, cuy = Cam.uy, cuz = Cam.uz;
var focal = Cam.focal, halfW = Cam.halfW, halfH = Cam.halfH;
var cpx = Cam.px, cpy = Cam.py, cpz = Cam.pz;
for (var v = 0; v < vc; v++) {
var bx = positions[v * stride], by = positions[v * stride + 1], bz = positions[v * stride + 2];
var wx = m0 * bx + m4 * by + m8 * bz + m12;
var wy = m1 * bx + m5 * by + m9 * bz + m13;
var wz = m2 * bx + m6 * by + m10 * bz + m14;
VB_Wx[v] = wx; VB_Wy[v] = wy; VB_Wz[v] = wz;
var ddx = wx - px, ddy = wy - py, ddz = wz - pz;
if (ddx * ddx + ddy * ddy + ddz * ddz > 6250000) { VB_Ok[v] = 0; continue; }
var relX = wx - cpx, relY = wy - cpy, relZ = wz - cpz;
var camZ = relX * cfx + relY * cfy + relZ * cfz;
if (camZ <= 0.1) { VB_Ok[v] = 0; continue; }
var camX = relX * crx + relY * cry + relZ * crz;
var camY = relX * cux + relY * cuy + relZ * cuz;
VB_Sx[v] = halfW + (camX / camZ) * focal;
VB_Sy[v] = halfH - (camY / camZ) * focal;
VB_Ok[v] = 1;
}
return vc;
}
// ===== 获取 KMt / TDt 渲染组件 =====
// 新 build:车身皮肤组件字段是 w14w_1(kDt(t)=t.w14w_1,缺失报 "skin";RDt.v16b 里赋值)
// 旧 build:u14l_1 / t14l_1 / p14l_1(新版 t14l_1 已改成悬停车体状态 Qmt,务必别再当皮肤用)
function GetKMtComponent(entity) {
var comp = FindComponentByField(entity, 'w14w_1');
if (comp) return comp;
comp = FindComponentByField(entity, 'u14l_1');
if (comp) return comp;
comp = FindComponentByField(entity, 't14l_1');
if (comp) return comp;
comp = FindComponentByField(entity, 'p14l_1');
return comp;
}
// 取车身皮肤对象(含 s17b_1 部件表),跨版本兼容;stats 用于诊断来源
function GetHullSkin(tank, stats) {
var comp = GetKMtComponent(tank);
if (!comp) { if (stats) stats.hullSource = '无组件'; return null; }
if (comp.w14w_1 && comp.w14w_1.s17b_1) { if (stats) stats.hullSource = 'w14w_1'; return comp.w14w_1; } // 新 build
if (comp.t14l_1 && comp.t14l_1.s17b_1) { if (stats) stats.hullSource = 't14l_1'; return comp.t14l_1; } // 旧 build
if (comp.p14l_1 && comp.p14l_1.s17b_1) { if (stats) stats.hullSource = 'p14l_1'; return comp.p14l_1; } // 旧 build
if (stats) stats.hullSource = '有组件但无皮肤';
return null;
}
function GetTDtComponent(entity) {
var comp = FindComponentByField(entity, 'n11c_1');
if (comp) return comp;
comp = FindComponentByField(entity, 'q112_1');
return comp;
}
// ===== 从GPU读取顶点位置数据(缓冲区[0]) =====
function ReadVertexPositionsFromTp(tp) {
if (!tp || !tp.t4w_1) return null;
var tl = tp.t4w_1.g1(0);
if (!tl || !tl.d3x_1) return null;
var vq = tl.d3x_1;
var bs = vq.s3h_1;
if (!bs || !bs.b74_1) return null;
var gl = bs.b74_1;
var webglBuf = vq.r3x_1 && vq.r3x_1.z73_1;
if (!webglBuf) return null;
gl.bindBuffer(gl.ARRAY_BUFFER, webglBuf);
var bufSize = gl.getBufferParameter(gl.ARRAY_BUFFER, gl.BUFFER_SIZE);
if (bufSize <= 0) return null;
var f32 = new Float32Array(bufSize / 4);
gl.getBufferSubData(gl.ARRAY_BUFFER, 0, f32);
return f32;
}
// ===== 从GPU读取索引数据 =====
function ReadIndicesFromTp(tp) {
if (!tp.y4w_1 || typeof tp.y4w_1.e1 !== 'function') return null;
var meshCount = tp.y4w_1.e1();
if (meshCount === 0) return null;
var allIndices = [];
for (var m = 0; m < meshCount; m++) {
var il = tp.y4w_1.g1(m);
if (!il || !il.q3p_1) continue;
var fq = il.q3p_1;
var bs = fq.s3h_1;
if (!bs || !bs.b74_1) continue;
var gl = bs.b74_1;
var webglBuf = fq.j3x_1 && fq.j3x_1.z73_1;
if (!webglBuf) continue;
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, webglBuf);
var bufSize = gl.getBufferParameter(gl.ELEMENT_ARRAY_BUFFER, gl.BUFFER_SIZE);
if (bufSize <= 0) continue;
var indices = new Uint16Array(bufSize / 2);
gl.getBufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, indices);
allIndices.push(indices);
}
return allIndices;
}
// ===== GPU 数据缓存(按 Tp 资源引用,避免重复 GPU 读取) =====
var GpuDataCache = new Map();
var GpuCacheFrameCount = 0;
var GpuCacheClearInterval = 1800; // 定期清 GPU 数据缓存(getBufferSubData 会停顿 GPU,太频繁会周期性卡顿)
var StrideDiagPrinted = new WeakSet();
function GetGpuData(tp) {
if (!tp) return null;
if (GpuDataCache.has(tp)) return GpuDataCache.get(tp);
var positions = ReadVertexPositionsFromTp(tp);
var indices = ReadIndicesFromTp(tp);
if (positions && indices && indices.length > 0) {
// 计算最大索引值,推断顶点 stride
var maxIdx = 0;
for (var i = 0; i < indices.length; i++) {
var idxArr = indices[i];
for (var j = 0; j < idxArr.length; j++) {
if (idxArr[j] > maxIdx) maxIdx = idxArr[j];
}
}
var stride = 3;
var vc3 = Math.floor(positions.length / 3);
if (maxIdx >= vc3) {
// stride=3 顶点数不够,尝试常见 interleaved 布局
var candidates = [8, 11, 5, 7, 14];
for (var c = 0; c < candidates.length; c++) {
var vc = Math.floor(positions.length / candidates[c]);
if (maxIdx < vc) { stride = candidates[c]; break; }
}
}
var data = { positions: positions, indices: indices, stride: stride };
GpuDataCache.set(tp, data);
if (!StrideDiagPrinted.has(tp)) {
StrideDiagPrinted.add(tp);
console.log('[透视][GPU数据] stride=' + stride + ', 缓冲区=' + positions.length + ' float, 最大索引=' + maxIdx + ', 顶点数=' + Math.floor(positions.length / stride));
}
return data;
}
return null;
}
// ===== 递归收集mesh节点(每帧重新获取 Bp 节点引用) =====
function CollectMeshNodes(bpNode, nodes, depth, group, stats) {
if (!bpNode || depth > 5) return;
if (stats) stats.seen++;
var name = bpNode.e35_1 || '';
if (bpNode.t3m_1) {
if (stats) stats.hasMesh++;
var gpuData = GetGpuData(bpNode.t3m_1);
if (gpuData) {
if (stats) stats.gpuOk++;
nodes.push({
name: name, node: bpNode, tp: bpNode.t3m_1,
positions: gpuData.positions, indices: gpuData.indices, stride: gpuData.stride, group: group
});
} else if (stats) stats.gpuFail++;
}
if (bpNode.q4x_1 && typeof bpNode.q4x_1.e1 === 'function') {
try {
var lodCount = bpNode.q4x_1.e1();
for (var lod = 0; lod < lodCount && lod < 1; lod++) {
var lodLevel = bpNode.q4x_1.g1(lod);
if (!lodLevel) continue;
var lodTp = lodLevel.t3m_1 || lodLevel.v3m_1;
if (!lodTp) continue;
var lodGpuData = GetGpuData(lodTp);
if (lodGpuData) {
if (stats) stats.gpuOk++;
nodes.push({
name: name + '_lod' + lod, node: bpNode, tp: lodTp,
positions: lodGpuData.positions, indices: lodGpuData.indices, stride: lodGpuData.stride, group: group
});
}
}
} catch(e) {}
}
if (bpNode.p35_1 && typeof bpNode.p35_1.e1 === 'function') {
var childCount = bpNode.p35_1.e1();
for (var c = 0; c < childCount; c++) {
try {
var child = bpNode.p35_1.g1(c);
if (child) CollectMeshNodes(child, nodes, depth + 1, group, stats);
} catch(e) {}
}
}
}
// ===== 提取坦克的mesh数据 =====
function GetTankMeshData(tank, stats) {
var nodes = [];
var skin = GetHullSkin(tank, stats);
if (skin) {
try {
var partCount = skin.s17b_1.e1();
if (stats) stats.hullParts = partCount;
for (var p = 0; p < partCount; p++) {
var part = skin.s17b_1.g1(p);
if (!part || !part.v17b_1) continue;
var nodeCount = part.v17b_1.e1();
for (var n = 0; n < nodeCount; n++) {
var bpNode = part.v17b_1.g1(n);
if (!bpNode) continue;
CollectMeshNodes(bpNode, nodes, 0, 'hull', stats ? stats.hull : null);
}
}
} catch(e) {}
}
var tdt = GetTDtComponent(tank);
if (stats) stats.turretComp = !!tdt;
if (tdt && tdt.n11c_1 && tdt.n11c_1.m17a_1) {
if (stats) stats.turretRoot = true;
try { CollectMeshNodes(tdt.n11c_1.m17a_1, nodes, 0, 'turret', stats ? stats.turret : null); } catch(e) {}
}
return nodes;
}
// ===== 残留节点检测(世界矩阵"冻结")→ 拖拽跟随 =====
// 现象:实体已移动,但原处仍留着线框。
// 原因:引擎只为"本帧实际参与渲染"的节点更新世界矩阵,被裁剪/脱离的节点矩阵停在最后位置。
// 处理:检测到「节点矩阵平移长时间不动、而坦克在动」时**不再丢弃**(丢弃会让整辆模型只剩一个点),
// 而是用「冻结时刻的相对偏移 + 坦克当前位置」平移这份冻结矩阵,让它跟着坦克走:
// 既不在旧位置留残留,也不会因为引擎不更新矩阵而丢掉模型。
// 权衡:拖拽期间朝向沿用最后一次更新(位置正确、朝向可能滞后),对透视足够用。
var NodeMatrixHistory = new WeakMap(); // WeakMap:不持有节点强引用(引擎会重建节点树)
var TankPrevPos = new Map(); // tankId → 上一帧位置(含位移滑动平均)
var FrozenFrames = 20; // 连续不动帧数阈值(仅在坦克本身移动时才累计)
var NodeMatrixScratch = new Float32Array(16);
function IsNodeValid(bpNode, pos, tankIsMoving, diagStats) {
var M = bpNode.n35_1;
if (!M) return null;
if (diagStats) diagStats.totalNodes++;
var Hist = NodeMatrixHistory.get(bpNode);
if (!Hist) {
NodeMatrixHistory.set(bpNode, { x: M[12], y: M[13], z: M[14], still: 0, off: null });
} else {
var dx = M[12] - Hist.x, dy = M[13] - Hist.y, dz = M[14] - Hist.z;
if (dx * dx + dy * dy + dz * dz > 0.01) {
Hist.x = M[12]; Hist.y = M[13]; Hist.z = M[14];
Hist.still = 0;
Hist.off = null; // 矩阵恢复更新 → 恢复正常绘制
} else if (tankIsMoving) {
Hist.still++;
if (Hist.still >= FrozenFrames) {
if (!Hist.off) Hist.off = { x: M[12] - pos.x, y: M[13] - pos.y, z: M[14] - pos.z };
for (var k = 0; k < 16; k++) NodeMatrixScratch[k] = M[k];
NodeMatrixScratch[12] = pos.x + Hist.off.x;
NodeMatrixScratch[13] = pos.y + Hist.off.y;
NodeMatrixScratch[14] = pos.z + Hist.off.z;
if (diagStats) diagStats.draggedNodes++;
return NodeMatrixScratch; // 冻结但跟随坦克
}
} else {
Hist.still = 0;
}
}
// 位置检查(节点世界矩阵平移与坦克位置相差过大则丢弃)—— 只对"未冻结"的节点做
var nodeDx = M[12] - pos.x, nodeDy = M[13] - pos.y, nodeDz = M[14] - pos.z;
if (nodeDx * nodeDx + nodeDy * nodeDy + nodeDz * nodeDz > 4000000) {
if (diagStats) diagStats.filteredNodes++;
return null;
}
return M;
}
// ===== 线框模式:绘制单个节点的所有边 =====
// 顶点数据来自 PrepareNodeVerts 的复用缓冲;边去重用数值键 Set(避免字符串分配)
var _WfEdgeSet = new Set();
function WfAddEdge(ctx, a, b) {
var lo = a < b ? a : b, hi = a < b ? b : a;
var key = lo * 65536 + hi;
if (_WfEdgeSet.has(key)) return;
_WfEdgeSet.add(key);
ctx.moveTo(VB_Sx[lo], VB_Sy[lo]);
ctx.lineTo(VB_Sx[hi], VB_Sy[hi]);
}
function DrawWireframeNode(ctx, meshData, worldMatrix, pos) {
var indicesArr = meshData.indices;
var vertexCount = PrepareNodeVerts(meshData, worldMatrix, pos);
_WfEdgeSet.clear();
ctx.beginPath();
for (var mi = 0; mi < indicesArr.length; mi++) {
var indices = indicesArr[mi];
for (var idx = 0; idx < indices.length; idx += 3) {
var i0 = indices[idx], i1 = indices[idx+1], i2 = indices[idx+2];
if (!VB_Ok[i0] || !VB_Ok[i1] || !VB_Ok[i2]) continue;
WfAddEdge(ctx, i0, i1);
WfAddEdge(ctx, i1, i2);
WfAddEdge(ctx, i2, i0);
}
}
ctx.stroke();
}
// ===== 兜底标记点:模型采集不到/被过滤时,至少画一个点,避免坦克完全不可见 =====
function DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color) {
var p = Project3D(pos, camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH);
if (!p) return;
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(p.x, p.y, 3, 0, 2 * Math.PI);
ctx.fill();
}
// ===== 描边模式:屏幕空间光栅化轮廓提取 =====
// 算法(真正的"屏幕几何"方法):
// 1. 把所有面向相机的三角形光栅化(扫描线填充)到像素掩码
// 2. 车身和炮塔在屏幕上重叠时,填充区域自动合并
// 3. 找出填充区域的边界像素(填充像素的4邻居中有未填充像素)
// 4. 只画边界像素 = 屏幕空间外轮廓
var PixelMask = null;
var PixelMaskW = 0;
var PixelMaskH = 0;
var MaskGen = 0; // 掩码用"代数"标记:本辆坦克填充的像素 = gen,无需在坦克之间清空整块缓冲
function GetPixelMask(w, h) {
if (!PixelMask || PixelMaskW !== w || PixelMaskH !== h) {
PixelMask = new Uint32Array(w * h);
PixelMaskW = w;
PixelMaskH = h;
MaskGen = 0;
}
return PixelMask;
}
// 扫描线光栅化三角形(pixels 中写入 gen 表示"本辆坦克已填充")
function RasterizeTriangle(pixels, w, h, gen, x0, y0, x1, y1, x2, y2) {
// 按 y 排序三个顶点
if (y0 > y1) { var t = y0; y0 = y1; y1 = t; t = x0; x0 = x1; x1 = t; }
if (y1 > y2) { var t = y1; y1 = y2; y2 = t; t = x1; x1 = x2; x2 = t; }
if (y0 > y1) { var t = y0; y0 = y1; y1 = t; t = x0; x0 = x1; x1 = t; }
var minY = Math.max(0, Math.ceil(y0));
var maxY = Math.min(h - 1, Math.floor(y2));
if (minY > maxY) return;
for (var y = minY; y <= maxY; y++) {
var xa, xb;
if (y < y1) {
// 上半部分:y0 -> y1 和 y0 -> y2
xa = (y1 === y0) ? x0 : x0 + (x1 - x0) * (y - y0) / (y1 - y0);
xb = (y2 === y0) ? x0 : x0 + (x2 - x0) * (y - y0) / (y2 - y0);
} else {
// 下半部分:y1 -> y2 和 y0 -> y2
xa = (y2 === y1) ? x1 : x1 + (x2 - x1) * (y - y1) / (y2 - y1);
xb = (y2 === y0) ? x0 : x0 + (x2 - x0) * (y - y0) / (y2 - y0);
}
if (xa > xb) { var t = xa; xa = xb; xb = t; }
var minXA = Math.max(0, Math.ceil(xa - 0.5));
var maxXA = Math.min(w - 1, Math.floor(xb + 0.5));
var rowOffset = y * w;
for (var x = minXA; x <= maxXA; x++) {
pixels[rowOffset + x] = gen;
}
}
}
function DrawTankOutline(ctx, meshNodes, pos, tankIsMoving, screenW, screenH, diagStats, color) {
var pixels = GetPixelMask(screenW, screenH);
var gen = ++MaskGen;
if (gen === 0) { pixels.fill(0); gen = ++MaskGen; } // 代数回绕(理论值):整体清一次
// 记录坦克的屏幕边界框(用于后续边界检测优化)
var minX = screenW, minY = screenH, maxX = 0, maxY = 0;
for (var ni = 0; ni < meshNodes.length; ni++) {
var meshData = meshNodes[ni];
var bpNode = meshData.node;
var worldMatrix = IsNodeValid(bpNode, pos, tankIsMoving, diagStats);
if (!worldMatrix) continue;
var indicesArr = meshData.indices;
// 顶点变换+投影到复用缓冲(无对象分配)
var vertexCount = PrepareNodeVerts(meshData, worldMatrix, pos);
for (var v = 0; v < vertexCount; v++) {
if (!VB_Ok[v]) continue;
var sx = VB_Sx[v], sy = VB_Sy[v];
if (sx < minX) minX = sx;
if (sy < minY) minY = sy;
if (sx > maxX) maxX = sx;
if (sy > maxY) maxY = sy;
}
// 遍历三角形,光栅化面向相机的
for (var mi = 0; mi < indicesArr.length; mi++) {
var indices = indicesArr[mi];
for (var i = 0; i < indices.length; i += 3) {
var a = indices[i], b = indices[i+1], c = indices[i+2];
if (!VB_Ok[a] || !VB_Ok[b] || !VB_Ok[c]) continue;
// 计算法线判断是否面向相机(世界坐标,来自缓冲)
var ax = VB_Wx[a], ay = VB_Wy[a], az = VB_Wz[a];
var bx = VB_Wx[b], by = VB_Wy[b], bz = VB_Wz[b];
var gx = VB_Wx[c], gy = VB_Wy[c], gz = VB_Wz[c];
var e1x = bx - ax, e1y = by - ay, e1z = bz - az;
var e2x = gx - ax, e2y = gy - ay, e2z = gz - az;
var nx = e1y * e2z - e1z * e2y;
var ny = e1z * e2x - e1x * e2z;
var nz = e1x * e2y - e1y * e2x;
var cx = (ax + bx + gx) / 3;
var cy = (ay + by + gy) / 3;
var cz = (az + bz + gz) / 3;
var dot = nx * (Cam.px - cx) + ny * (Cam.py - cy) + nz * (Cam.pz - cz);
if (dot <= 0) continue; // 背向相机,跳过
// 光栅化三角形到像素掩码
RasterizeTriangle(pixels, screenW, screenH, gen, VB_Sx[a], VB_Sy[a], VB_Sx[b], VB_Sy[b], VB_Sx[c], VB_Sy[c]);
}
}
}
// 找出边界像素并绘制(只在坦克边界框内遍历;用单条路径 + 横向连续合并,替代逐像素 fillRect)
var bx0 = Math.max(0, Math.floor(minX) - 1);
var by0 = Math.max(0, Math.floor(minY) - 1);
var bx1 = Math.min(screenW - 1, Math.ceil(maxX) + 1);
var by1 = Math.min(screenH - 1, Math.ceil(maxY) + 1);
ctx.fillStyle = color;
ctx.beginPath();
var outlineCount = 0;
for (var y = by0; y <= by1; y++) {
var rowOffset = y * screenW;
var runStart = -1;
for (var x = bx0; x <= bx1 + 1; x++) {
var isBoundary = false;
if (x <= bx1 && pixels[rowOffset + x] === gen) {
// 检查4个邻居是否有未填充像素(== gen 即本辆坦克填充)
if (x === 0 || pixels[rowOffset + x - 1] !== gen) isBoundary = true;
else if (x === screenW - 1 || pixels[rowOffset + x + 1] !== gen) isBoundary = true;
else if (y === 0 || pixels[rowOffset - screenW + x] !== gen) isBoundary = true;
else if (y === screenH - 1 || pixels[rowOffset + screenW + x] !== gen) isBoundary = true;
}
if (isBoundary) {
if (runStart < 0) runStart = x;
outlineCount++;
} else if (runStart >= 0) {
ctx.rect(runStart, y, x - runStart + 2, 3); // 横向连续的边界像素合并为一条
runStart = -1;
}
}
}
ctx.fill();
if (diagStats) diagStats.outlineEdges += outlineCount;
return outlineCount;
}
// ===== 创建全屏Canvas =====
var canvas = document.createElement('canvas');
canvas.id = 'hornet3DWireframeCanvas';
canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:99998;pointer-events:none;';
document.body.appendChild(canvas);
var ctx = canvas.getContext('2d');
function ResizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
ResizeCanvas();
window.addEventListener('resize', ResizeCanvas);
// ===== 诊断统计 =====
var DiagStats = { totalNodes: 0, filteredNodes: 0, renderedNodes: 0, outlineEdges: 0 };
var LastDegradeLogFrame = -9999; // 退化诊断日志节流
// 退化诊断(节流输出):定位"存活坦克模型没画出来"的根因(皮肤/部件/GPU/炮塔逐级)
function LogTankDiag(tankName, tankState, S, hullCount, turretCount, rendered) {
if (esp.frameCount - LastDegradeLogFrame <= 120) return;
LastDegradeLogFrame = esp.frameCount;
console.warn('[透视][退化诊断] ' + tankName + ' state=' + tankState +
' | 皮肤=' + S.hullSource + ' 部件=' + S.hullParts +
' 车身[见/网格/GPU成功/失败]=' + S.hull.seen + '/' + S.hull.hasMesh + '/' + S.hull.gpuOk + '/' + S.hull.gpuFail +
' | 炮塔 组件=' + S.turretComp + ' 根节点=' + S.turretRoot +
' 炮塔[见/网格/GPU成功/失败]=' + S.turret.seen + '/' + S.turret.hasMesh + '/' + S.turret.gpuOk + '/' + S.turret.gpuFail +
' | 收集 车身=' + hullCount + ' 炮塔=' + turretCount + ' 绘制=' + rendered +
' | 本帧 总=' + DiagStats.totalNodes + ' 过滤=' + DiagStats.filteredNodes);
}
// ===== ESP状态 =====
var esp = {
canvas: canvas,
ctx: ctx,
running: true,
frameCount: 0,
mode: Mode,
setMode: function(m) {
if (m === 'wireframe' || m === 'outline') {
Mode = m;
this.mode = m;
try { localStorage.setItem('hornet_wireframe_mode', m); } catch(e) {}
console.log('[透视] 模式切换为: ' + m);
} else {
console.warn('[透视] 未知模式: ' + m + ',支持: wireframe | outline');
}
},
getMode: function() { return Mode; },
setLineWidth: function(w) {
var n = parseFloat(w);
if (!isNaN(n) && n >= 0.5 && n <= 8) {
LineWidth = n;
try { localStorage.setItem('hornet_wireframe_linewidth', String(n)); } catch(e) {}
console.log('[透视] 线宽设置为: ' + n);
} else {
console.warn('[透视] 线宽应在 0.5 ~ 8 之间,当前: ' + w);
}
},
getLineWidth: function() { return LineWidth; },
setHighlightMode: function(mode) {
var valid = ['team','whitelist','blacklist'];
if (valid.indexOf(mode) >= 0) {
HighlightMode = mode;
try { localStorage.setItem('hornet_wireframe_highlight_mode', mode); } catch(e) {}
console.log('[透视] 渲染模式切换为: ' + mode);
} else {
console.warn('[透视] 未知渲染模式: ' + mode + ',支持: ' + valid.join(', '));
}
},
getHighlightMode: function() { return HighlightMode; },
setHighlightList: function(names) {
HighlightList = Array.isArray(names) ? names.slice() : [];
SaveHighlightList();
console.log('[透视] 高亮名单已更新:', HighlightList.length + '人');
},
addHighlight: function(name) {
if (name && HighlightList.indexOf(name) < 0) {
HighlightList.push(name);
SaveHighlightList();
console.log('[透视] 已添加高亮:', name);
}
},
removeHighlight: function(name) {
var idx = HighlightList.indexOf(name);
if (idx >= 0) {
HighlightList.splice(idx, 1);
SaveHighlightList();
console.log('[透视] 已移除高亮:', name);
}
},
getHighlightList: function() {
return HighlightList.slice();
},
clearHighlight: function() {
HighlightList = [];
SaveHighlightList();
console.log('[透视] 高亮名单已清空');
},
stop: function() {
this.running = false;
window.removeEventListener('resize', ResizeCanvas);
if (canvas.parentNode) canvas.parentNode.removeChild(canvas);
console.log('透视已停止');
}
};
window.__hornet3DWireframe__ = esp;
// ===== 金箱子标记 =====
// 金箱子是独立实体(不是坦克)。引擎侧标志:
// · 补给实体 = entity.j10q_1(bonusData 组件,见 module_87072 的 ctt())
// · 类型枚举 mE(module_10032):GOLD_BOX = 6
// 做法:实体基类的组件创建方法 nyz()(module_61378「BattleEntity.createComponent」)上挂观察钩子,
// 发现「带 j10q_1 且类型=6」的实体就登记,之后每帧按它自己的位置画金色标记。
// 钩子目标沿 tank 的原型链找 nyz 的宿主原型 —— 不需要任何导出键。
var GoldBoxes = [];
var GoldPending = [];
var EntityProtoHooked = false;
var GOLD_BOX_TYPE = 6;
function FindEnumOf(Obj) {
for (var K in Obj) {
try {
var V = Obj[K];
if (V && typeof V === 'object' && typeof V.k3_1 === 'number' && typeof V.equals === 'function') return V;
} catch (e) {}
}
return null;
}
// 引擎口径:有 bonusData 且类型是 GOLD_BOX
function IsGoldBoxEntity(Entity) {
if (!Entity || !Entity.j10q_1) return false;
var Type = FindEnumOf(Entity.j10q_1);
return !!(Type && Type.k3_1 === GOLD_BOX_TYPE);
}
function RegisterGoldBox(Entity) {
for (var i = 0; i < GoldBoxes.length; i++) if (GoldBoxes[i] === Entity) return;
GoldBoxes.push(Entity);
console.log('[透视] 发现金箱子,已加入标记(当前 ' + GoldBoxes.length + ' 个)');
}
function InstallEntityHook(Tank) {
if (EntityProtoHooked || !Tank) return;
var Proto = Tank;
while (Proto && !Object.prototype.hasOwnProperty.call(Proto, 'nyz')) Proto = Object.getPrototypeOf(Proto);
if (!Proto || typeof Proto.nyz !== 'function') return;
var Orig = Proto.nyz;
Proto.nyz = function () {
var R = Orig.apply(this, arguments);
try {
if (IsGoldBoxEntity(this)) RegisterGoldBox(this);
else if (this && this.j10q_1 === undefined && GoldPending.length < 300) {
GoldPending.push({ e: this, f: esp.frameCount }); // 字段可能稍后才赋值,延后判定
}
} catch (E) {}
return R;
};
EntityProtoHooked = true;
console.log('[透视] 已挂补给实体观察钩子(金箱子)');
}
// 金箱位置:优先物理组件(与坦克同款 gzh_1),否则退到 bonusData 里的节点世界矩阵/向量
function GetGoldBoxPosition(Entity) {
var Phys = FindComponentByField(Entity, 'gzh_1');
if (Phys && Phys.gzh_1) {
return { x: Phys.gzh_1.f20_1, y: Phys.gzh_1.g20_1, z: Phys.gzh_1.h20_1 };
}
var Bonus = Entity.j10q_1;
for (var K in Bonus) {
try {
var V = Bonus[K];
if (!V || typeof V !== 'object') continue;
if (V.n35_1 && V.n35_1.length >= 15) {
var M = V.n35_1;
return { x: M[12], y: M[13], z: M[14] };
}
if (typeof V.f20_1 === 'number' && typeof V.g20_1 === 'number' && typeof V.h20_1 === 'number') {
return { x: V.f20_1, y: V.g20_1, z: V.h20_1 };
}
} catch (e) {}
}
return null;
}
// 金色菱形 + 十字 + 「金箱」标签(穿透显示,不遮挡判断)
function DrawGoldBoxMarker(Pos, ScreenW, ScreenH) {
var X = Pos.x - Cam.px, Y = Pos.y - Cam.py, Z = Pos.z - Cam.pz;
var CamZ = X * Cam.fx + Y * Cam.fy + Z * Cam.fz;
if (CamZ <= 0.1) return false;
var CamX = X * Cam.rx + Y * Cam.ry + Z * Cam.rz;
var CamY = X * Cam.ux + Y * Cam.uy + Z * Cam.uz;
var Sx = Cam.halfW + (CamX / CamZ) * Cam.focal;
var Sy = Cam.halfH - (CamY / CamZ) * Cam.focal;
if (Sx < -60 || Sy < -60 || Sx > ScreenW + 60 || Sy > ScreenH + 60) return false;
var R = 12;
ctx.save();
ctx.strokeStyle = '#ffd700';
ctx.fillStyle = 'rgba(255, 215, 0, 0.22)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(Sx, Sy - R);
ctx.lineTo(Sx + R, Sy);
ctx.lineTo(Sx, Sy + R);
ctx.lineTo(Sx - R, Sy);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(Sx - R - 7, Sy); ctx.lineTo(Sx + R + 7, Sy);
ctx.moveTo(Sx, Sy - R - 7); ctx.lineTo(Sx, Sy + R + 7);
ctx.stroke();
ctx.fillStyle = '#ffd700';
ctx.font = 'bold 12px Consolas,monospace';
ctx.textAlign = 'center';
ctx.fillText('金箱', Sx, Sy - R - 9);
ctx.restore();
return true;
}
// ===== 冻结节点"拖拽跟随"诊断(节流 2s)=====
var LastFrozenLogAt = 0;
function LogDragDiag(Stats) {
if (!Stats.draggedNodes) return;
var Now = Date.now();
if (Now - LastFrozenLogAt < 2000) return;
LastFrozenLogAt = Now;
console.log('[透视][冻结] 本帧拖拽跟随节点 ' + Stats.draggedNodes + '/' + Stats.totalNodes +
'(引擎未更新这些节点矩阵,已按坦克位移平移,避免旧位置残留/模型丢失)');
}
// ===== 渲染循环 =====
function Render() {
if (!esp.running) return;
esp.frameCount++;
// 换图自刷新:gameMode 实例变化时重绑并清空该地图的缓存
var NewGm = window.__gameMode__;
if (NewGm && NewGm !== gm) {
gm = NewGm;
GpuDataCache.clear(); // 旧地图的 GPU 几何数据失效
TankPrevPos.clear(); // 位置历史(判断"在动")失效
GoldBoxes.length = 0; // 上一局的箱子实体全部作废
GoldPending.length = 0;
GpuCacheFrameCount = 0;
console.log('[透视] 检测到新战斗,已重置缓存');
}
// 旧实例已失效、新实例尚未捕获:清屏并显示等待提示,避免继续用悬空引用
if (!window.__gameMode__) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'rgba(255, 220, 120, 0.9)';
ctx.font = 'bold 12px Consolas,monospace';
ctx.textAlign = 'left';
ctx.fillText('[' + Mode.toUpperCase() + '] 等待战局…(ght 未捕获)', 5, canvas.height - 5);
requestAnimationFrame(Render);
return;
}
// 定期清空 GPU 数据缓存
GpuCacheFrameCount++;
if (GpuCacheFrameCount >= GpuCacheClearInterval) {
GpuDataCache.clear();
GpuCacheFrameCount = 0;
}
DiagStats = { totalNodes: 0, filteredNodes: 0, draggedNodes: 0, renderedNodes: 0, outlineEdges: 0, goldBoxes: 0 };
try {
var screenW = canvas.width;
var screenH = canvas.height;
ctx.clearRect(0, 0, screenW, screenH);
var camInfo = GetCameraInfo();
if (!camInfo) {
requestAnimationFrame(Render);
return;
}
var playerTank = GetPlayerTank();
if (!playerTank) {
requestAnimationFrame(Render);
return;
}
// 相机位置为0时使用玩家位置
if (Math.abs(camInfo.pos.x) < 0.1 && Math.abs(camInfo.pos.y) < 0.1 && Math.abs(camInfo.pos.z) < 0.1) {
var pPos = GetTankPosition(playerTank);
if (pPos) camInfo.pos = { x: pPos.x, y: pPos.y, z: pPos.z + 100 };
}
UpdateCam(camInfo, screenW, screenH); // 每帧只算一次相机基(需在相机位置兜底之后),供顶点变换/投影复用
var tankList = gm.q100_1.r11l();
var count = tankList.e1();
var drawnCount = 0;
for (var i = 0; i < count; i++) {
var tank = tankList.g1(i);
if (!tank || tank === playerTank) continue;
if (!EntityProtoHooked) InstallEntityHook(tank); // 借坦克实例找到实体基类原型,挂补给观察钩子
var physicsComp = FindComponentByField(tank, 'gzh_1');
if (!physicsComp || !physicsComp.gzh_1) continue;
var tankState = GetTankState(tank);
// 可见坦克 = ACTIVE(3) 或 SEMI_ACTIVE(2)(引擎 y147() 判定),0/1/4 才跳过
if (tankState !== 2 && tankState !== 3) continue;
var pos = {
x: physicsComp.gzh_1.f20_1,
y: physicsComp.gzh_1.g20_1,
z: physicsComp.gzh_1.h20_1
};
// 本帧该坦克是否在移动(供「残留节点」检测:坦克在动而节点矩阵不动 = 旧位置遗留)
// 用位移的指数滑动平均,而不是「单帧位移 > 10 单位」——后者对慢速移动几乎永远为假,
// 会让残留检测形同虚设(残留漏掉的根因就在这里)
var tankId = tank.hyt_1;
var prevPos = TankPrevPos.get(tankId);
var tankMove = 0;
if (prevPos) {
var pdx = pos.x - prevPos.x, pdy = pos.y - prevPos.y, pdz = pos.z - prevPos.z;
tankMove = (prevPos.move || 0) * 0.85 + Math.sqrt(pdx * pdx + pdy * pdy + pdz * pdz);
}
var tankIsMoving = tankMove > 0.5; // 0.5 单位/帧 ≈ 0.3 m/s;静置坦克的抖动低于此值
TankPrevPos.set(tankId, { x: pos.x, y: pos.y, z: pos.z, move: tankMove });
// 距离检查(50000 = 500米)
var dx = pos.x - camInfo.pos.x;
var dy = pos.y - camInfo.pos.y;
var dz = pos.z - camInfo.pos.z;
var distance = Math.sqrt(dx*dx + dy*dy + dz*dz);
if (distance > 50000) continue;
var friendly = IsFriendly(tank, playerTank);
var tankName = GetTankName(tank) || ('ID:' + tank.hyt_1);
// 根据 HighlightMode 判断是否渲染该坦克
if (!ShouldRender(tankName)) continue;
// 颜色按队伍:友方=蓝,敌方=红
var color = friendly ? 'rgba(68, 136, 255, 0.6)' : 'rgba(255, 68, 68, 0.6)';
// 3D模型渲染(仅在mesh数据可用时)
var MeshStats = {
hullSource: '', hullParts: 0, turretComp: false, turretRoot: false,
hull: { seen: 0, hasMesh: 0, gpuOk: 0, gpuFail: 0 },
turret: { seen: 0, hasMesh: 0, gpuOk: 0, gpuFail: 0 }
};
var meshNodes = GetTankMeshData(tank, MeshStats);
if (!meshNodes || meshNodes.length === 0) {
LogTankDiag(tankName, tankState, MeshStats, 0, 0, 0);
DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color); // 兜底:至少可见
drawnCount++;
continue;
}
// 统计各分组节点数(诊断用)
var hullCount = 0, turretCount = 0;
for (var gi = 0; gi < meshNodes.length; gi++) {
if (meshNodes[gi].group === 'turret') turretCount++; else hullCount++;
}
if (turretCount === 0) { // 没有炮塔节点则不画模型,仅画兜底标记
LogTankDiag(tankName, tankState, MeshStats, hullCount, turretCount, 0);
DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color);
drawnCount++;
continue;
}
ctx.strokeStyle = color;
ctx.lineWidth = LineWidth;
var renderedThisTank = 0;
if (Mode === 'wireframe') {
for (var ni = 0; ni < meshNodes.length; ni++) {
var meshData = meshNodes[ni];
var bpNode = meshData.node;
var worldMatrix = IsNodeValid(bpNode, pos, tankIsMoving, DiagStats);
if (!worldMatrix) continue;
DrawWireframeNode(ctx, meshData, worldMatrix, pos);
DiagStats.renderedNodes++;
renderedThisTank++;
}
} else {
renderedThisTank = DrawTankOutline(ctx, meshNodes, pos, tankIsMoving, screenW, screenH, DiagStats, color);
}
// 诊断(节流):存活坦克模型退化时输出根因数据,便于一次性定位
if ((renderedThisTank === 0 && meshNodes.length > 0) || hullCount === 0) {
LogTankDiag(tankName, tankState, MeshStats, hullCount, turretCount, renderedThisTank);
}
// 一个节点都没画出来:画兜底标记,避免坦克完全不可见
if (renderedThisTank === 0) {
DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color);
}
drawnCount++;
}
// ===== 金箱子:清理失效项 → 评估候选 → 画金色标记 =====
var goldDrawn = 0;
for (var gci = GoldBoxes.length - 1; gci >= 0; gci--) {
if (!IsGoldBoxEntity(GoldBoxes[gci])) GoldBoxes.splice(gci, 1); // 被拾取/销毁
}
for (var gpi = GoldPending.length - 1; gpi >= 0; gpi--) {
var PendingIt = GoldPending[gpi];
if (esp.frameCount - PendingIt.f > 90) { GoldPending.splice(gpi, 1); continue; }
if (IsGoldBoxEntity(PendingIt.e)) { RegisterGoldBox(PendingIt.e); GoldPending.splice(gpi, 1); }
}
for (var gbi = 0; gbi < GoldBoxes.length; gbi++) {
var GoldPos = GetGoldBoxPosition(GoldBoxes[gbi]);
if (GoldPos && DrawGoldBoxMarker(GoldPos, screenW, screenH)) goldDrawn++;
}
DiagStats.goldBoxes = goldDrawn;
// 屏幕信息
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.font = '10px Arial';
ctx.textAlign = 'left';
var infoText = '[' + Mode.toUpperCase() + '] | ' + drawnCount + ' tanks';
if (Mode === 'outline') {
infoText += ' | Edges: ' + DiagStats.outlineEdges;
} else {
infoText += ' | Nodes: ' + DiagStats.renderedNodes + '/' + DiagStats.totalNodes;
}
infoText += ' | Frame: ' + esp.frameCount;
if (DiagStats.draggedNodes > 0) infoText += ' | 拖拽:' + DiagStats.draggedNodes;
if (DiagStats.goldBoxes > 0) infoText += ' | 金箱:' + DiagStats.goldBoxes;
ctx.fillText(infoText, 5, screenH - 5);
LogDragDiag(DiagStats);
} catch(e) {
console.error('[透视] 错误:', e.message, e.stack ? e.stack.split('\n')[1] : '');
}
requestAnimationFrame(Render);
}
// 启动渲染
requestAnimationFrame(Render);
console.log('3D透视已启动(双模式: wireframe | outline)');
console.log('当前模式: ' + Mode);
console.log('提示: window.__hornet3DWireframe__.setMode("outline") 切换到描边模式');
console.log('提示: window.__hornet3DWireframe__.setMode("wireframe") 切换到线框模式');
console.log('提示: window.__hornet3DWireframe__.addHighlight("玩家名") 添加高亮(金色标记)');
console.log('提示: window.__hornet3DWireframe__.stop() 停止透视');
}
var Tool = {
Start: function() {
if (window.__hornet3DWireframe__ && window.__hornet3DWireframe__.running) return;
_start();
},
Stop: function() {
if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.stop();
},
Status: function() {
return (window.__hornet3DWireframe__ && window.__hornet3DWireframe__.running) ? ('运行中(' + window.__hornet3DWireframe__.mode + ')') : '已停止';
},
GetRunning: function() {
return !!(window.__hornet3DWireframe__ && window.__hornet3DWireframe__.running);
},
SetMode: function(m) {
if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.setMode(m);
},
GetMode: function() {
return window.__hornet3DWireframe__ ? window.__hornet3DWireframe__.getMode() : null;
},
SetLineWidth: function(w) {
if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.setLineWidth(w);
},
GetLineWidth: function() {
return window.__hornet3DWireframe__ ? window.__hornet3DWireframe__.getLineWidth() : 2;
},
SetHighlightMode: function(mode) {
if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.setHighlightMode(mode);
},
GetHighlightMode: function() {
return window.__hornet3DWireframe__ ? window.__hornet3DWireframe__.getHighlightMode() : 'team';
},
SetHighlightList: function(names) {
if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.setHighlightList(names);
},
AddHighlight: function(name) {
if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.addHighlight(name);
},
RemoveHighlight: function(name) {
if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.removeHighlight(name);
},
GetHighlightList: function() {
return window.__hornet3DWireframe__ ? window.__hornet3DWireframe__.getHighlightList() : [];
},
ClearHighlight: function() {
if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.clearHighlight();
}
};
window.__hornetTools__ = window.__hornetTools__ || {};
window.__hornetTools__.Wireframe = Tool;
console.log('[3DTank] 3D透视模块已加载');
})();