diff --git a/3dtank.com/play/static/js/unpacked/hornet_bundle.js b/3dtank.com/play/static/js/unpacked/hornet_bundle.js index 87ef1c7..2477a47 100644 --- a/3dtank.com/play/static/js/unpacked/hornet_bundle.js +++ b/3dtank.com/play/static/js/unpacked/hornet_bundle.js @@ -263,60 +263,29 @@ } // ===== 获取坦克位置(物理组件里的位置向量) ===== - // 物理组件的字段名每个构建都可能平移,这里按"含 f20_1/g20_1/h20_1 的 O$ 向量"做多级探测: - // ejt(tankPhysics)里可能的候选字段都试一遍,命中即用。 + // 新旧差异(main.7cfa4058 vs 旧版): + // 物理组件类 ejt(组件名 "tankPhysics")的字段整体平移,构造函数字段序列一一对应: + // 旧 vjt: zzg_1 azh_1 czh_1 dzh_1 ezh_1 fzh_1 **gzh_1** hzh_1 izh_1 ... + // 新 ejt: uzq_1 vzq_1 xzq_1 yzq_1 zzq_1 azr_1 **bzr_1** czr_1 dzr_1 ... + // njt() 里 `bzr_1.l27(上一物理态位置, 当前物理态位置, 插值)` —— bzr_1 就是世界位置。 + // ⚠ xzq_1 是车体半尺寸(用于包围盒/出生点:`-xzq_1.h20_1/2 + 10`),绝不能当位置用。 // 返回 {x, y, z},x/y 水平面,z 高度 - var POS_FIELDS = ['xzq_1', 'uzq_1', 'azr_1', 'bzr_1', 'dzr_1', 'izr_1', 'jzr_1', 'rzf_1', 'gzh_1', 'pz7_1']; + var POS_FIELDS = ['bzr_1', 'gzh_1', 'pz7_1']; function GetTankPosition(tank) { for (var i = 0; i < POS_FIELDS.length; i++) { var Key = POS_FIELDS[i]; var Comp = null; try { Comp = FindComponentByField(tank, Key); } catch (e) { Comp = null; } - if (Comp && Comp[Key] && typeof Comp[Key].f20_1 === 'number') { - return { x: Comp[Key].f20_1, y: Comp[Key].g20_1, z: Comp[Key].h20_1 }; + var V = Comp && Comp[Key]; + if (V && typeof V.f20_1 === 'number' && typeof V.g20_1 === 'number' && typeof V.h20_1 === 'number') { + // 数值有效性校验:NaN/超大值一律视为读取失败(避免雷达画到画布外) + if (isFinite(V.f20_1) && isFinite(V.g20_1) && isFinite(V.h20_1) && + Math.abs(V.f20_1) < 1e7 && Math.abs(V.g20_1) < 1e7 && Math.abs(V.h20_1) < 1e7) { + return { x: V.f20_1, y: V.g20_1, z: V.h20_1 }; + } } } - // 兜底 1:遍历坦克全部组件,找任意"带 O$ 向量"的组件 - var Found = null; - try { - tank.qz1_1.s2a_1.forEach(function (valueList) { - if (Found) return; - var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList; - if (!comp) return; - for (var K in comp) { - var V = comp[K]; - if (V && typeof V === 'object' && typeof V.f20_1 === 'number' && typeof V.g20_1 === 'number' && typeof V.h20_1 === 'number') { - if (Math.abs(V.f20_1) < 1e6 && Math.abs(V.g20_1) < 1e6 && Math.abs(V.h20_1) < 1e6) { - Found = { x: V.f20_1, y: V.g20_1, z: V.h20_1 }; - return; - } - } - } - }); - } catch (e) {} - if (Found) return Found; - - // 兜底 2:矩阵式位置 —— 引擎的世界矩阵通常是长度 ≥16 的数组,[12],[13],[14] 是平移分量 - try { - tank.qz1_1.s2a_1.forEach(function (valueList) { - if (Found) return; - var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList; - if (!comp) return; - for (var K in comp) { - var M = comp[K]; - if (M && typeof M === 'object' && typeof M.length === 'number' && M.length >= 15) { - var X = M[12], Y = M[13], Z = M[14]; - if (typeof X === 'number' && typeof Y === 'number' && typeof Z === 'number' && - Math.abs(X) < 1e6 && Math.abs(Y) < 1e6 && Math.abs(Z) < 1e6 && - (X !== 0 || Y !== 0 || Z !== 0)) { - Found = { x: X, y: Y, z: Z }; - return; - } - } - } - }); - } catch (e) {} - return Found; + return null; } // ===== 获取坦克状态值(Yjt 组件的 vzr_1.k3_1) ===== @@ -408,6 +377,56 @@ function GetBotSet() { return BotSet; } function IsBot(tank) { return BotSet.has(tank); } + // ===== 自动诊断:一次性打印关键链路状态(供新版字段平移后快速定位) ===== + function SelfDiagnose() { + var R = { gm: false, tanks: -1, posHit: {}, posFail: 0, camera: null, scene: false, errors: [] }; + var g = window.__gameMode__; + R.gm = !!g; + if (!g) return R; + try { R.scene = !!g.lz4().jz1_1; } catch (e) { R.errors.push('scene: ' + e.message); } + var list = null; + try { list = g.m10b_1.j11y(); R.tanks = list.e1(); } catch (e) { R.errors.push('tankList: ' + e.message); } + if (list) { + var N = Math.min(list.e1(), 8); + for (var i = 0; i < N; i++) { + var T = null; + try { T = list.g1(i); } catch (e) { continue; } + if (!T) continue; + var P = GetTankPosition(T); + if (P) { + // 记录命中来源(候选字段名 或 fallback) + var Src = 'fallback'; + for (var j = 0; j < POS_FIELDS.length; j++) { + var K = POS_FIELDS[j], C = null; + try { C = FindComponentByField(T, K); } catch (e) { C = null; } + if (C && C[K] && typeof C[K].f20_1 === 'number') { Src = K; break; } + } + R.posHit[Src] = (R.posHit[Src] || 0) + 1; + } else { + R.posFail++; + // 失败时把该坦克组件的字段名与"候选向量"打印出来,便于定位 + try { + var Fields = []; + T.qz1_1.s2a_1.forEach(function (VL) { + var C = (VL && typeof VL.g1 === 'function') ? VL.g1(0) : VL; + if (!C) return; + for (var K2 in C) { + var V2 = C[K2]; + if (V2 && typeof V2 === 'object') { + var Kind = (typeof V2.f20_1 === 'number') ? 'O$' : (typeof V2.length === 'number' && V2.length >= 15 ? 'M' : ''); + if (Kind) Fields.push(K2 + ':' + Kind); + } + } + }); + R.errors.push('tank' + i + ' 组件字段向量: ' + Fields.slice(0, 40).join(',')); + } catch (e2) { R.errors.push('tank' + i + ' dump err: ' + e2.message); } + } + } + } + try { R.camera = GetCameraInfo(g); } catch (e) { R.errors.push('camera: ' + e.message); } + return R; + } + // ===== 获取玩家坦克 ===== // 优先 ezr_1(当前观察坦克),备用 fzr_1(本地控制坦克),再备用坦克列表第一个 // checkPosition: 是否检查位置有效性(默认true;测试脚本传false以在死亡状态控制) @@ -450,70 +469,46 @@ return false; } - // ===== 获取相机信息($r/gq 相机组件) ===== - // 遍历 GameMode entity 和所有坦克 entity 查找活跃相机组件 - // 策略1:查找 $r/gq 相机组件(有 kym_1 位置 和 lym_1 方向) - // 策略2:查找 k7 相机控制器(有 qz9_1 和 jzh_1),从 jzh_1 获取相机组件 - // 返回 {yaw, pitch, fov, pos:{x,y,z}} - // 注意:lym_1.h20_1 是 roll(绕Z),lym_1.f20_1 是 pitch(绕X) + // ===== 获取相机信息(场景主相机 scene.iyx_1) ===== + // 新旧差异:旧版是找挂在实体上的相机组件(kym_1/lym_1),新版这些字段已被别的类占用; + // 新版直接暴露主相机对象:gm.lz4().jz1_1(场景).iyx_1(相机,类 Jf/lc) + // 相机位置 = t35_1(O$ 向量) + // 视图旋转 = d37_1(列主序 4x4,行·点) + // 视投影矩阵 = g37_1(world → clip,行·点,w>0 为可见) + // 视口 = w36_1.h29()/g29(),FOV = b37_1 + // 朝向:视图矩阵第 2 列 = 相机 +Z 轴,相机看向 -Z → forward = -(d37_1[2],[6],[10]) + // 返回 {pos, yaw, pitch, fov, heading, matrix, viewW, viewH} + // yaw/pitch 供 Project3D 使用(forward = (-sin(yaw), cos(yaw), sin(pitch))) + // heading 供雷达使用(forward = (sin(heading), cos(heading)),水平面) function GetCameraInfo(gm) { - var entities = []; try { - var gameScene = gm.lz4().jz1_1; - var gmEntity = gm.lz4(); - if (gmEntity) entities.push(gmEntity); - } catch(e) {} - try { - var tankList = gm.m10b_1.j11y(); - var tCount = tankList.e1(); - for (var i = 0; i < tCount; i++) { - var t = tankList.g1(i); - if (t) entities.push(t); - } - } catch(e) {} - - for (var i = 0; i < entities.length; i++) { - var entity = entities[i]; - if (!entity || !entity.qz1_1 || !entity.qz1_1.s2a_1) continue; - var found = null; - entity.qz1_1.s2a_1.forEach(function(valueList, key) { - if (found) return; - try { - var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList; - if (!comp) return; - // 策略1:$r/gq 相机组件 - if (comp.cyw_1 && comp.dyw_1 && - comp.cyw_1.f20_1 !== undefined && comp.dyw_1.f20_1 !== undefined) { - if (Math.abs(comp.dyw_1.h20_1) > 0.001 || Math.abs(comp.dyw_1.f20_1) > 0.001) { - found = comp; - return; - } - } - // 策略2:k7 相机控制器 - if (comp.jzj_1 && comp.zzi_1) { - var camComp = comp.zzi_1; - if (camComp && camComp.cyw_1 && camComp.dyw_1 && - (Math.abs(camComp.dyw_1.h20_1) > 0.001 || Math.abs(camComp.dyw_1.f20_1) > 0.001)) { - found = camComp; - return; - } - } - } catch(e) {} - }); - if (found) { - return { - yaw: found.dyw_1.h20_1, - pitch: found.dyw_1.f20_1, - fov: found.byw_1 || 1.0471976, - pos: { - x: found.cyw_1.f20_1, - y: found.cyw_1.g20_1, - z: found.cyw_1.h20_1 - } - }; + var scene = gm.lz4().jz1_1; + var cam = scene && scene.iyx_1; + if (!cam || !cam.t35_1) return null; + var P = cam.t35_1; + var pos = { x: P.f20_1, y: P.g20_1, z: P.h20_1 }; + var V = cam.d37_1; + var fx = 0, fy = 1, fz = 0; + if (V && typeof V.length === 'number' && V.length >= 11) { + fx = -V[2]; fy = -V[6]; fz = -V[10]; + var len = Math.sqrt(fx * fx + fy * fy + fz * fz) || 1; + fx /= len; fy /= len; fz /= len; } + var viewW = 0, viewH = 0; + try { viewW = cam.w36_1.h29(); viewH = cam.w36_1.g29(); } catch (e) {} + return { + pos: pos, + yaw: Math.atan2(-fx, fy), + pitch: Math.asin(Math.max(-1, Math.min(1, fz))), + heading: Math.atan2(fx, fy), + fov: cam.b37_1 || 1.0471976, + matrix: cam.g37_1 || null, + viewW: viewW, + viewH: viewH + }; + } catch (e) { + return null; } - return null; } // ===== 获取全局碰撞检测器(uMt 组件,挂在 GameMode entity 上) ===== @@ -581,6 +576,24 @@ } } + // ===== 3D 到 2D 投影(矩阵版,最可靠) ===== + // 直接用相机自带的 world→clip 矩阵(scene.iyx_1.g37_1),不依赖任何欧拉角约定。 + // 矩阵按列主序存放、按"行·点"使用(与引擎 z37/a38 一致)。 + function Project3DMatrix(camInfo, worldPos, screenW, screenH) { + var M = camInfo && camInfo.matrix; + if (!M || typeof M.length !== 'number' || M.length < 16) return null; + var px = worldPos.x, py = worldPos.y, pz = worldPos.z; + var cx = M[0] * px + M[4] * py + M[8] * pz + M[12]; + var cy = M[1] * px + M[5] * py + M[9] * pz + M[13]; + var cw = M[3] * px + M[7] * py + M[11] * pz + M[15]; + if (!(cw > 0.0001)) return null; + return { + x: (cx / cw * 0.5 + 0.5) * screenW, + y: (1 - (cy / cw * 0.5 + 0.5)) * screenH, + depth: cw + }; + } + // ===== 3D 到 2D 投影(用于 UI 标记) ===== // 标准透视投影:forward/right/up 三轴正交基 function Project3D(worldPos, camPos, yaw, pitch, fov, screenW, screenH) { @@ -735,6 +748,7 @@ IsTankAlive: IsTankAlive, GetTankName: GetTankName, GetTankHealth: GetTankHealth, + SelfDiagnose: SelfDiagnose, GetGearScore: GetGearScore, ScanBots: ScanBots, GetBotSet: GetBotSet, @@ -746,6 +760,7 @@ MkVec: MkVec, RaycastDistance: RaycastDistance, Project3D: Project3D, + Project3DMatrix: Project3DMatrix, TransformVertex: TransformVertex, MakeDraggable: MakeDraggable }; @@ -804,79 +819,13 @@ return null; } - // ===== 获取相机方向(gq/$r 组件的 lym_1.h20_1 = 偏航角) ===== - // 相机组件可能在 GameMode entity、玩家坦克或任何坦克上 - // 策略1:查找 $r/gq 相机组件(有 kym_1 和 lym_1 字段) - // 策略2:查找 k7 相机控制器(有 qz9_1 和 jzh_1 字段),从 jzh_1 获取相机组件 + // ===== 获取相机朝向(新版:直接用场景主相机 scene.iyx_1 的视图矩阵算水平朝向) ===== + // 雷达约定:forward = (sin(angle), cos(angle)),x/y 为水平面 function getCameraHeading() { - var entities = []; - - // 1. GameMode entity - try { - var gameScene = gm.lz4().jz1_1; - var gmEntity = gm.lz4(); - if (gmEntity) entities.push(gmEntity); - } catch(e) {} - - // 2. 所有坦克列表中的 entity - try { - var tankList = gm.m10b_1.j11y(); - var tCount = tankList.e1(); - for (var i = 0; i < tCount; i++) { - var t = tankList.g1(i); - if (t) entities.push(t); - } - } catch(e) {} - - // 在每个 entity 上查找相机组件 - for (var i = 0; i < entities.length; i++) { - var entity = entities[i]; - if (!entity || !entity.qz1_1 || !entity.qz1_1.s2a_1) continue; - - var found = null; - entity.qz1_1.s2a_1.forEach(function(valueList, key) { - if (found) return; - try { - var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList; - if (!comp) return; - - // 策略1:查找 $r/gq 相机组件(有 kym_1 和 lym_1 字段) - if (comp.cyw_1 && comp.dyw_1 && - comp.cyw_1.f20_1 !== undefined && comp.dyw_1.f20_1 !== undefined) { - // 检查是否是活跃的相机(lym_1 不全为0) - if (Math.abs(comp.dyw_1.h20_1) > 0.001 || Math.abs(comp.dyw_1.f20_1) > 0.001) { - found = comp; - return; - } - } - - // 策略2:查找 k7 相机控制器(有 qz9_1 和 jzh_1 字段) - if (comp.jzj_1 && comp.zzi_1) { - var camComp = comp.zzi_1; - if (camComp && camComp.cyw_1 && camComp.dyw_1 && - (Math.abs(camComp.dyw_1.h20_1) > 0.001 || Math.abs(camComp.dyw_1.f20_1) > 0.001)) { - found = camComp; - return; - } - } - } catch(e) {} - }); - - if (found) { - // lym_1.h20_1 = -atan2(t, n) = 偏航角 - // lym_1.f20_1 = atan2(i, e) = 俯仰角 - return { - yaw: -found.dyw_1.h20_1, // 偏航角(转为正向) - pitch: found.dyw_1.f20_1, // 俯仰角 - pos: { - x: found.cyw_1.f20_1, - y: found.cyw_1.g20_1, - z: found.cyw_1.h20_1 - } - }; - } - } - return null; + var ci = null; + try { ci = lib.GetCameraInfo(gm); } catch (e) { ci = null; } + if (!ci) return null; + return { yaw: ci.heading, pitch: ci.pitch, pos: ci.pos }; } // 玩家坦克:使用公共库 @@ -1312,6 +1261,12 @@ // ght 尚未捕获 / 不在战局:仍画静态雷达框,内容留空(等进入战局自动出) var Ready = !!gm; + // 进战局后打印一次自诊断(位置/相机链路),便于新版字段平移时快速定位 + if (Ready && !radar._diagPrinted) { + radar._diagPrinted = true; + try { console.log('[雷达] 自诊断:', lib.SelfDiagnose()); } catch (e) {} + } + try { var tankList = null, count = 0; if (Ready) { @@ -1667,6 +1622,15 @@ 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 秒一次) @@ -1677,6 +1641,13 @@ 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 @@ -1686,6 +1657,7 @@ 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; } @@ -1712,6 +1684,32 @@ 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; @@ -2015,7 +2013,7 @@ // ===== 兜底标记点:模型采集不到/被过滤时,至少画一个点,避免坦克完全不可见 ===== function DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color) { - var p = Project3D(pos, camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH); + var p = ProjectPoint(pos, camInfo, screenW, screenH); if (!p) return; ctx.fillStyle = color; ctx.beginPath(); @@ -2563,8 +2561,8 @@ // 人机标记:坦克上方写「人机」(同队装备分数相同且 < 8000) if (typeof IsBot === 'function' && IsBot(tank)) { - var botPt = Project3D({ x: pos.x, y: pos.y, z: pos.z + 1300 }, - camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH); + 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'; diff --git a/3dtank_bundle.js b/3dtank_bundle.js index 87ef1c7..2477a47 100644 --- a/3dtank_bundle.js +++ b/3dtank_bundle.js @@ -263,60 +263,29 @@ } // ===== 获取坦克位置(物理组件里的位置向量) ===== - // 物理组件的字段名每个构建都可能平移,这里按"含 f20_1/g20_1/h20_1 的 O$ 向量"做多级探测: - // ejt(tankPhysics)里可能的候选字段都试一遍,命中即用。 + // 新旧差异(main.7cfa4058 vs 旧版): + // 物理组件类 ejt(组件名 "tankPhysics")的字段整体平移,构造函数字段序列一一对应: + // 旧 vjt: zzg_1 azh_1 czh_1 dzh_1 ezh_1 fzh_1 **gzh_1** hzh_1 izh_1 ... + // 新 ejt: uzq_1 vzq_1 xzq_1 yzq_1 zzq_1 azr_1 **bzr_1** czr_1 dzr_1 ... + // njt() 里 `bzr_1.l27(上一物理态位置, 当前物理态位置, 插值)` —— bzr_1 就是世界位置。 + // ⚠ xzq_1 是车体半尺寸(用于包围盒/出生点:`-xzq_1.h20_1/2 + 10`),绝不能当位置用。 // 返回 {x, y, z},x/y 水平面,z 高度 - var POS_FIELDS = ['xzq_1', 'uzq_1', 'azr_1', 'bzr_1', 'dzr_1', 'izr_1', 'jzr_1', 'rzf_1', 'gzh_1', 'pz7_1']; + var POS_FIELDS = ['bzr_1', 'gzh_1', 'pz7_1']; function GetTankPosition(tank) { for (var i = 0; i < POS_FIELDS.length; i++) { var Key = POS_FIELDS[i]; var Comp = null; try { Comp = FindComponentByField(tank, Key); } catch (e) { Comp = null; } - if (Comp && Comp[Key] && typeof Comp[Key].f20_1 === 'number') { - return { x: Comp[Key].f20_1, y: Comp[Key].g20_1, z: Comp[Key].h20_1 }; + var V = Comp && Comp[Key]; + if (V && typeof V.f20_1 === 'number' && typeof V.g20_1 === 'number' && typeof V.h20_1 === 'number') { + // 数值有效性校验:NaN/超大值一律视为读取失败(避免雷达画到画布外) + if (isFinite(V.f20_1) && isFinite(V.g20_1) && isFinite(V.h20_1) && + Math.abs(V.f20_1) < 1e7 && Math.abs(V.g20_1) < 1e7 && Math.abs(V.h20_1) < 1e7) { + return { x: V.f20_1, y: V.g20_1, z: V.h20_1 }; + } } } - // 兜底 1:遍历坦克全部组件,找任意"带 O$ 向量"的组件 - var Found = null; - try { - tank.qz1_1.s2a_1.forEach(function (valueList) { - if (Found) return; - var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList; - if (!comp) return; - for (var K in comp) { - var V = comp[K]; - if (V && typeof V === 'object' && typeof V.f20_1 === 'number' && typeof V.g20_1 === 'number' && typeof V.h20_1 === 'number') { - if (Math.abs(V.f20_1) < 1e6 && Math.abs(V.g20_1) < 1e6 && Math.abs(V.h20_1) < 1e6) { - Found = { x: V.f20_1, y: V.g20_1, z: V.h20_1 }; - return; - } - } - } - }); - } catch (e) {} - if (Found) return Found; - - // 兜底 2:矩阵式位置 —— 引擎的世界矩阵通常是长度 ≥16 的数组,[12],[13],[14] 是平移分量 - try { - tank.qz1_1.s2a_1.forEach(function (valueList) { - if (Found) return; - var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList; - if (!comp) return; - for (var K in comp) { - var M = comp[K]; - if (M && typeof M === 'object' && typeof M.length === 'number' && M.length >= 15) { - var X = M[12], Y = M[13], Z = M[14]; - if (typeof X === 'number' && typeof Y === 'number' && typeof Z === 'number' && - Math.abs(X) < 1e6 && Math.abs(Y) < 1e6 && Math.abs(Z) < 1e6 && - (X !== 0 || Y !== 0 || Z !== 0)) { - Found = { x: X, y: Y, z: Z }; - return; - } - } - } - }); - } catch (e) {} - return Found; + return null; } // ===== 获取坦克状态值(Yjt 组件的 vzr_1.k3_1) ===== @@ -408,6 +377,56 @@ function GetBotSet() { return BotSet; } function IsBot(tank) { return BotSet.has(tank); } + // ===== 自动诊断:一次性打印关键链路状态(供新版字段平移后快速定位) ===== + function SelfDiagnose() { + var R = { gm: false, tanks: -1, posHit: {}, posFail: 0, camera: null, scene: false, errors: [] }; + var g = window.__gameMode__; + R.gm = !!g; + if (!g) return R; + try { R.scene = !!g.lz4().jz1_1; } catch (e) { R.errors.push('scene: ' + e.message); } + var list = null; + try { list = g.m10b_1.j11y(); R.tanks = list.e1(); } catch (e) { R.errors.push('tankList: ' + e.message); } + if (list) { + var N = Math.min(list.e1(), 8); + for (var i = 0; i < N; i++) { + var T = null; + try { T = list.g1(i); } catch (e) { continue; } + if (!T) continue; + var P = GetTankPosition(T); + if (P) { + // 记录命中来源(候选字段名 或 fallback) + var Src = 'fallback'; + for (var j = 0; j < POS_FIELDS.length; j++) { + var K = POS_FIELDS[j], C = null; + try { C = FindComponentByField(T, K); } catch (e) { C = null; } + if (C && C[K] && typeof C[K].f20_1 === 'number') { Src = K; break; } + } + R.posHit[Src] = (R.posHit[Src] || 0) + 1; + } else { + R.posFail++; + // 失败时把该坦克组件的字段名与"候选向量"打印出来,便于定位 + try { + var Fields = []; + T.qz1_1.s2a_1.forEach(function (VL) { + var C = (VL && typeof VL.g1 === 'function') ? VL.g1(0) : VL; + if (!C) return; + for (var K2 in C) { + var V2 = C[K2]; + if (V2 && typeof V2 === 'object') { + var Kind = (typeof V2.f20_1 === 'number') ? 'O$' : (typeof V2.length === 'number' && V2.length >= 15 ? 'M' : ''); + if (Kind) Fields.push(K2 + ':' + Kind); + } + } + }); + R.errors.push('tank' + i + ' 组件字段向量: ' + Fields.slice(0, 40).join(',')); + } catch (e2) { R.errors.push('tank' + i + ' dump err: ' + e2.message); } + } + } + } + try { R.camera = GetCameraInfo(g); } catch (e) { R.errors.push('camera: ' + e.message); } + return R; + } + // ===== 获取玩家坦克 ===== // 优先 ezr_1(当前观察坦克),备用 fzr_1(本地控制坦克),再备用坦克列表第一个 // checkPosition: 是否检查位置有效性(默认true;测试脚本传false以在死亡状态控制) @@ -450,70 +469,46 @@ return false; } - // ===== 获取相机信息($r/gq 相机组件) ===== - // 遍历 GameMode entity 和所有坦克 entity 查找活跃相机组件 - // 策略1:查找 $r/gq 相机组件(有 kym_1 位置 和 lym_1 方向) - // 策略2:查找 k7 相机控制器(有 qz9_1 和 jzh_1),从 jzh_1 获取相机组件 - // 返回 {yaw, pitch, fov, pos:{x,y,z}} - // 注意:lym_1.h20_1 是 roll(绕Z),lym_1.f20_1 是 pitch(绕X) + // ===== 获取相机信息(场景主相机 scene.iyx_1) ===== + // 新旧差异:旧版是找挂在实体上的相机组件(kym_1/lym_1),新版这些字段已被别的类占用; + // 新版直接暴露主相机对象:gm.lz4().jz1_1(场景).iyx_1(相机,类 Jf/lc) + // 相机位置 = t35_1(O$ 向量) + // 视图旋转 = d37_1(列主序 4x4,行·点) + // 视投影矩阵 = g37_1(world → clip,行·点,w>0 为可见) + // 视口 = w36_1.h29()/g29(),FOV = b37_1 + // 朝向:视图矩阵第 2 列 = 相机 +Z 轴,相机看向 -Z → forward = -(d37_1[2],[6],[10]) + // 返回 {pos, yaw, pitch, fov, heading, matrix, viewW, viewH} + // yaw/pitch 供 Project3D 使用(forward = (-sin(yaw), cos(yaw), sin(pitch))) + // heading 供雷达使用(forward = (sin(heading), cos(heading)),水平面) function GetCameraInfo(gm) { - var entities = []; try { - var gameScene = gm.lz4().jz1_1; - var gmEntity = gm.lz4(); - if (gmEntity) entities.push(gmEntity); - } catch(e) {} - try { - var tankList = gm.m10b_1.j11y(); - var tCount = tankList.e1(); - for (var i = 0; i < tCount; i++) { - var t = tankList.g1(i); - if (t) entities.push(t); - } - } catch(e) {} - - for (var i = 0; i < entities.length; i++) { - var entity = entities[i]; - if (!entity || !entity.qz1_1 || !entity.qz1_1.s2a_1) continue; - var found = null; - entity.qz1_1.s2a_1.forEach(function(valueList, key) { - if (found) return; - try { - var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList; - if (!comp) return; - // 策略1:$r/gq 相机组件 - if (comp.cyw_1 && comp.dyw_1 && - comp.cyw_1.f20_1 !== undefined && comp.dyw_1.f20_1 !== undefined) { - if (Math.abs(comp.dyw_1.h20_1) > 0.001 || Math.abs(comp.dyw_1.f20_1) > 0.001) { - found = comp; - return; - } - } - // 策略2:k7 相机控制器 - if (comp.jzj_1 && comp.zzi_1) { - var camComp = comp.zzi_1; - if (camComp && camComp.cyw_1 && camComp.dyw_1 && - (Math.abs(camComp.dyw_1.h20_1) > 0.001 || Math.abs(camComp.dyw_1.f20_1) > 0.001)) { - found = camComp; - return; - } - } - } catch(e) {} - }); - if (found) { - return { - yaw: found.dyw_1.h20_1, - pitch: found.dyw_1.f20_1, - fov: found.byw_1 || 1.0471976, - pos: { - x: found.cyw_1.f20_1, - y: found.cyw_1.g20_1, - z: found.cyw_1.h20_1 - } - }; + var scene = gm.lz4().jz1_1; + var cam = scene && scene.iyx_1; + if (!cam || !cam.t35_1) return null; + var P = cam.t35_1; + var pos = { x: P.f20_1, y: P.g20_1, z: P.h20_1 }; + var V = cam.d37_1; + var fx = 0, fy = 1, fz = 0; + if (V && typeof V.length === 'number' && V.length >= 11) { + fx = -V[2]; fy = -V[6]; fz = -V[10]; + var len = Math.sqrt(fx * fx + fy * fy + fz * fz) || 1; + fx /= len; fy /= len; fz /= len; } + var viewW = 0, viewH = 0; + try { viewW = cam.w36_1.h29(); viewH = cam.w36_1.g29(); } catch (e) {} + return { + pos: pos, + yaw: Math.atan2(-fx, fy), + pitch: Math.asin(Math.max(-1, Math.min(1, fz))), + heading: Math.atan2(fx, fy), + fov: cam.b37_1 || 1.0471976, + matrix: cam.g37_1 || null, + viewW: viewW, + viewH: viewH + }; + } catch (e) { + return null; } - return null; } // ===== 获取全局碰撞检测器(uMt 组件,挂在 GameMode entity 上) ===== @@ -581,6 +576,24 @@ } } + // ===== 3D 到 2D 投影(矩阵版,最可靠) ===== + // 直接用相机自带的 world→clip 矩阵(scene.iyx_1.g37_1),不依赖任何欧拉角约定。 + // 矩阵按列主序存放、按"行·点"使用(与引擎 z37/a38 一致)。 + function Project3DMatrix(camInfo, worldPos, screenW, screenH) { + var M = camInfo && camInfo.matrix; + if (!M || typeof M.length !== 'number' || M.length < 16) return null; + var px = worldPos.x, py = worldPos.y, pz = worldPos.z; + var cx = M[0] * px + M[4] * py + M[8] * pz + M[12]; + var cy = M[1] * px + M[5] * py + M[9] * pz + M[13]; + var cw = M[3] * px + M[7] * py + M[11] * pz + M[15]; + if (!(cw > 0.0001)) return null; + return { + x: (cx / cw * 0.5 + 0.5) * screenW, + y: (1 - (cy / cw * 0.5 + 0.5)) * screenH, + depth: cw + }; + } + // ===== 3D 到 2D 投影(用于 UI 标记) ===== // 标准透视投影:forward/right/up 三轴正交基 function Project3D(worldPos, camPos, yaw, pitch, fov, screenW, screenH) { @@ -735,6 +748,7 @@ IsTankAlive: IsTankAlive, GetTankName: GetTankName, GetTankHealth: GetTankHealth, + SelfDiagnose: SelfDiagnose, GetGearScore: GetGearScore, ScanBots: ScanBots, GetBotSet: GetBotSet, @@ -746,6 +760,7 @@ MkVec: MkVec, RaycastDistance: RaycastDistance, Project3D: Project3D, + Project3DMatrix: Project3DMatrix, TransformVertex: TransformVertex, MakeDraggable: MakeDraggable }; @@ -804,79 +819,13 @@ return null; } - // ===== 获取相机方向(gq/$r 组件的 lym_1.h20_1 = 偏航角) ===== - // 相机组件可能在 GameMode entity、玩家坦克或任何坦克上 - // 策略1:查找 $r/gq 相机组件(有 kym_1 和 lym_1 字段) - // 策略2:查找 k7 相机控制器(有 qz9_1 和 jzh_1 字段),从 jzh_1 获取相机组件 + // ===== 获取相机朝向(新版:直接用场景主相机 scene.iyx_1 的视图矩阵算水平朝向) ===== + // 雷达约定:forward = (sin(angle), cos(angle)),x/y 为水平面 function getCameraHeading() { - var entities = []; - - // 1. GameMode entity - try { - var gameScene = gm.lz4().jz1_1; - var gmEntity = gm.lz4(); - if (gmEntity) entities.push(gmEntity); - } catch(e) {} - - // 2. 所有坦克列表中的 entity - try { - var tankList = gm.m10b_1.j11y(); - var tCount = tankList.e1(); - for (var i = 0; i < tCount; i++) { - var t = tankList.g1(i); - if (t) entities.push(t); - } - } catch(e) {} - - // 在每个 entity 上查找相机组件 - for (var i = 0; i < entities.length; i++) { - var entity = entities[i]; - if (!entity || !entity.qz1_1 || !entity.qz1_1.s2a_1) continue; - - var found = null; - entity.qz1_1.s2a_1.forEach(function(valueList, key) { - if (found) return; - try { - var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList; - if (!comp) return; - - // 策略1:查找 $r/gq 相机组件(有 kym_1 和 lym_1 字段) - if (comp.cyw_1 && comp.dyw_1 && - comp.cyw_1.f20_1 !== undefined && comp.dyw_1.f20_1 !== undefined) { - // 检查是否是活跃的相机(lym_1 不全为0) - if (Math.abs(comp.dyw_1.h20_1) > 0.001 || Math.abs(comp.dyw_1.f20_1) > 0.001) { - found = comp; - return; - } - } - - // 策略2:查找 k7 相机控制器(有 qz9_1 和 jzh_1 字段) - if (comp.jzj_1 && comp.zzi_1) { - var camComp = comp.zzi_1; - if (camComp && camComp.cyw_1 && camComp.dyw_1 && - (Math.abs(camComp.dyw_1.h20_1) > 0.001 || Math.abs(camComp.dyw_1.f20_1) > 0.001)) { - found = camComp; - return; - } - } - } catch(e) {} - }); - - if (found) { - // lym_1.h20_1 = -atan2(t, n) = 偏航角 - // lym_1.f20_1 = atan2(i, e) = 俯仰角 - return { - yaw: -found.dyw_1.h20_1, // 偏航角(转为正向) - pitch: found.dyw_1.f20_1, // 俯仰角 - pos: { - x: found.cyw_1.f20_1, - y: found.cyw_1.g20_1, - z: found.cyw_1.h20_1 - } - }; - } - } - return null; + var ci = null; + try { ci = lib.GetCameraInfo(gm); } catch (e) { ci = null; } + if (!ci) return null; + return { yaw: ci.heading, pitch: ci.pitch, pos: ci.pos }; } // 玩家坦克:使用公共库 @@ -1312,6 +1261,12 @@ // ght 尚未捕获 / 不在战局:仍画静态雷达框,内容留空(等进入战局自动出) var Ready = !!gm; + // 进战局后打印一次自诊断(位置/相机链路),便于新版字段平移时快速定位 + if (Ready && !radar._diagPrinted) { + radar._diagPrinted = true; + try { console.log('[雷达] 自诊断:', lib.SelfDiagnose()); } catch (e) {} + } + try { var tankList = null, count = 0; if (Ready) { @@ -1667,6 +1622,15 @@ 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 秒一次) @@ -1677,6 +1641,13 @@ 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 @@ -1686,6 +1657,7 @@ 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; } @@ -1712,6 +1684,32 @@ 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; @@ -2015,7 +2013,7 @@ // ===== 兜底标记点:模型采集不到/被过滤时,至少画一个点,避免坦克完全不可见 ===== function DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color) { - var p = Project3D(pos, camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH); + var p = ProjectPoint(pos, camInfo, screenW, screenH); if (!p) return; ctx.fillStyle = color; ctx.beginPath(); @@ -2563,8 +2561,8 @@ // 人机标记:坦克上方写「人机」(同队装备分数相同且 < 8000) if (typeof IsBot === 'function' && IsBot(tank)) { - var botPt = Project3D({ x: pos.x, y: pos.y, z: pos.z + 1300 }, - camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH); + 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'; diff --git a/tools/hornet_3d_wireframe.js b/tools/hornet_3d_wireframe.js index e0ac75d..61cca6a 100644 --- a/tools/hornet_3d_wireframe.js +++ b/tools/hornet_3d_wireframe.js @@ -107,6 +107,15 @@ 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 秒一次) @@ -117,6 +126,13 @@ 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 @@ -126,6 +142,7 @@ 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; } @@ -152,6 +169,32 @@ 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; @@ -455,7 +498,7 @@ // ===== 兜底标记点:模型采集不到/被过滤时,至少画一个点,避免坦克完全不可见 ===== function DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color) { - var p = Project3D(pos, camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH); + var p = ProjectPoint(pos, camInfo, screenW, screenH); if (!p) return; ctx.fillStyle = color; ctx.beginPath(); @@ -1003,8 +1046,8 @@ // 人机标记:坦克上方写「人机」(同队装备分数相同且 < 8000) if (typeof IsBot === 'function' && IsBot(tank)) { - var botPt = Project3D({ x: pos.x, y: pos.y, z: pos.z + 1300 }, - camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH); + 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'; diff --git a/tools/hornet_lib.js b/tools/hornet_lib.js index fa5ba22..cd478e8 100644 --- a/tools/hornet_lib.js +++ b/tools/hornet_lib.js @@ -48,60 +48,29 @@ } // ===== 获取坦克位置(物理组件里的位置向量) ===== - // 物理组件的字段名每个构建都可能平移,这里按"含 f20_1/g20_1/h20_1 的 O$ 向量"做多级探测: - // ejt(tankPhysics)里可能的候选字段都试一遍,命中即用。 + // 新旧差异(main.7cfa4058 vs 旧版): + // 物理组件类 ejt(组件名 "tankPhysics")的字段整体平移,构造函数字段序列一一对应: + // 旧 vjt: zzg_1 azh_1 czh_1 dzh_1 ezh_1 fzh_1 **gzh_1** hzh_1 izh_1 ... + // 新 ejt: uzq_1 vzq_1 xzq_1 yzq_1 zzq_1 azr_1 **bzr_1** czr_1 dzr_1 ... + // njt() 里 `bzr_1.l27(上一物理态位置, 当前物理态位置, 插值)` —— bzr_1 就是世界位置。 + // ⚠ xzq_1 是车体半尺寸(用于包围盒/出生点:`-xzq_1.h20_1/2 + 10`),绝不能当位置用。 // 返回 {x, y, z},x/y 水平面,z 高度 - var POS_FIELDS = ['xzq_1', 'uzq_1', 'azr_1', 'bzr_1', 'dzr_1', 'izr_1', 'jzr_1', 'rzf_1', 'gzh_1', 'pz7_1']; + var POS_FIELDS = ['bzr_1', 'gzh_1', 'pz7_1']; function GetTankPosition(tank) { for (var i = 0; i < POS_FIELDS.length; i++) { var Key = POS_FIELDS[i]; var Comp = null; try { Comp = FindComponentByField(tank, Key); } catch (e) { Comp = null; } - if (Comp && Comp[Key] && typeof Comp[Key].f20_1 === 'number') { - return { x: Comp[Key].f20_1, y: Comp[Key].g20_1, z: Comp[Key].h20_1 }; + var V = Comp && Comp[Key]; + if (V && typeof V.f20_1 === 'number' && typeof V.g20_1 === 'number' && typeof V.h20_1 === 'number') { + // 数值有效性校验:NaN/超大值一律视为读取失败(避免雷达画到画布外) + if (isFinite(V.f20_1) && isFinite(V.g20_1) && isFinite(V.h20_1) && + Math.abs(V.f20_1) < 1e7 && Math.abs(V.g20_1) < 1e7 && Math.abs(V.h20_1) < 1e7) { + return { x: V.f20_1, y: V.g20_1, z: V.h20_1 }; + } } } - // 兜底 1:遍历坦克全部组件,找任意"带 O$ 向量"的组件 - var Found = null; - try { - tank.qz1_1.s2a_1.forEach(function (valueList) { - if (Found) return; - var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList; - if (!comp) return; - for (var K in comp) { - var V = comp[K]; - if (V && typeof V === 'object' && typeof V.f20_1 === 'number' && typeof V.g20_1 === 'number' && typeof V.h20_1 === 'number') { - if (Math.abs(V.f20_1) < 1e6 && Math.abs(V.g20_1) < 1e6 && Math.abs(V.h20_1) < 1e6) { - Found = { x: V.f20_1, y: V.g20_1, z: V.h20_1 }; - return; - } - } - } - }); - } catch (e) {} - if (Found) return Found; - - // 兜底 2:矩阵式位置 —— 引擎的世界矩阵通常是长度 ≥16 的数组,[12],[13],[14] 是平移分量 - try { - tank.qz1_1.s2a_1.forEach(function (valueList) { - if (Found) return; - var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList; - if (!comp) return; - for (var K in comp) { - var M = comp[K]; - if (M && typeof M === 'object' && typeof M.length === 'number' && M.length >= 15) { - var X = M[12], Y = M[13], Z = M[14]; - if (typeof X === 'number' && typeof Y === 'number' && typeof Z === 'number' && - Math.abs(X) < 1e6 && Math.abs(Y) < 1e6 && Math.abs(Z) < 1e6 && - (X !== 0 || Y !== 0 || Z !== 0)) { - Found = { x: X, y: Y, z: Z }; - return; - } - } - } - }); - } catch (e) {} - return Found; + return null; } // ===== 获取坦克状态值(Yjt 组件的 vzr_1.k3_1) ===== @@ -193,6 +162,56 @@ function GetBotSet() { return BotSet; } function IsBot(tank) { return BotSet.has(tank); } + // ===== 自动诊断:一次性打印关键链路状态(供新版字段平移后快速定位) ===== + function SelfDiagnose() { + var R = { gm: false, tanks: -1, posHit: {}, posFail: 0, camera: null, scene: false, errors: [] }; + var g = window.__gameMode__; + R.gm = !!g; + if (!g) return R; + try { R.scene = !!g.lz4().jz1_1; } catch (e) { R.errors.push('scene: ' + e.message); } + var list = null; + try { list = g.m10b_1.j11y(); R.tanks = list.e1(); } catch (e) { R.errors.push('tankList: ' + e.message); } + if (list) { + var N = Math.min(list.e1(), 8); + for (var i = 0; i < N; i++) { + var T = null; + try { T = list.g1(i); } catch (e) { continue; } + if (!T) continue; + var P = GetTankPosition(T); + if (P) { + // 记录命中来源(候选字段名 或 fallback) + var Src = 'fallback'; + for (var j = 0; j < POS_FIELDS.length; j++) { + var K = POS_FIELDS[j], C = null; + try { C = FindComponentByField(T, K); } catch (e) { C = null; } + if (C && C[K] && typeof C[K].f20_1 === 'number') { Src = K; break; } + } + R.posHit[Src] = (R.posHit[Src] || 0) + 1; + } else { + R.posFail++; + // 失败时把该坦克组件的字段名与"候选向量"打印出来,便于定位 + try { + var Fields = []; + T.qz1_1.s2a_1.forEach(function (VL) { + var C = (VL && typeof VL.g1 === 'function') ? VL.g1(0) : VL; + if (!C) return; + for (var K2 in C) { + var V2 = C[K2]; + if (V2 && typeof V2 === 'object') { + var Kind = (typeof V2.f20_1 === 'number') ? 'O$' : (typeof V2.length === 'number' && V2.length >= 15 ? 'M' : ''); + if (Kind) Fields.push(K2 + ':' + Kind); + } + } + }); + R.errors.push('tank' + i + ' 组件字段向量: ' + Fields.slice(0, 40).join(',')); + } catch (e2) { R.errors.push('tank' + i + ' dump err: ' + e2.message); } + } + } + } + try { R.camera = GetCameraInfo(g); } catch (e) { R.errors.push('camera: ' + e.message); } + return R; + } + // ===== 获取玩家坦克 ===== // 优先 ezr_1(当前观察坦克),备用 fzr_1(本地控制坦克),再备用坦克列表第一个 // checkPosition: 是否检查位置有效性(默认true;测试脚本传false以在死亡状态控制) @@ -235,70 +254,46 @@ return false; } - // ===== 获取相机信息($r/gq 相机组件) ===== - // 遍历 GameMode entity 和所有坦克 entity 查找活跃相机组件 - // 策略1:查找 $r/gq 相机组件(有 kym_1 位置 和 lym_1 方向) - // 策略2:查找 k7 相机控制器(有 qz9_1 和 jzh_1),从 jzh_1 获取相机组件 - // 返回 {yaw, pitch, fov, pos:{x,y,z}} - // 注意:lym_1.h20_1 是 roll(绕Z),lym_1.f20_1 是 pitch(绕X) + // ===== 获取相机信息(场景主相机 scene.iyx_1) ===== + // 新旧差异:旧版是找挂在实体上的相机组件(kym_1/lym_1),新版这些字段已被别的类占用; + // 新版直接暴露主相机对象:gm.lz4().jz1_1(场景).iyx_1(相机,类 Jf/lc) + // 相机位置 = t35_1(O$ 向量) + // 视图旋转 = d37_1(列主序 4x4,行·点) + // 视投影矩阵 = g37_1(world → clip,行·点,w>0 为可见) + // 视口 = w36_1.h29()/g29(),FOV = b37_1 + // 朝向:视图矩阵第 2 列 = 相机 +Z 轴,相机看向 -Z → forward = -(d37_1[2],[6],[10]) + // 返回 {pos, yaw, pitch, fov, heading, matrix, viewW, viewH} + // yaw/pitch 供 Project3D 使用(forward = (-sin(yaw), cos(yaw), sin(pitch))) + // heading 供雷达使用(forward = (sin(heading), cos(heading)),水平面) function GetCameraInfo(gm) { - var entities = []; try { - var gameScene = gm.lz4().jz1_1; - var gmEntity = gm.lz4(); - if (gmEntity) entities.push(gmEntity); - } catch(e) {} - try { - var tankList = gm.m10b_1.j11y(); - var tCount = tankList.e1(); - for (var i = 0; i < tCount; i++) { - var t = tankList.g1(i); - if (t) entities.push(t); - } - } catch(e) {} - - for (var i = 0; i < entities.length; i++) { - var entity = entities[i]; - if (!entity || !entity.qz1_1 || !entity.qz1_1.s2a_1) continue; - var found = null; - entity.qz1_1.s2a_1.forEach(function(valueList, key) { - if (found) return; - try { - var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList; - if (!comp) return; - // 策略1:$r/gq 相机组件 - if (comp.cyw_1 && comp.dyw_1 && - comp.cyw_1.f20_1 !== undefined && comp.dyw_1.f20_1 !== undefined) { - if (Math.abs(comp.dyw_1.h20_1) > 0.001 || Math.abs(comp.dyw_1.f20_1) > 0.001) { - found = comp; - return; - } - } - // 策略2:k7 相机控制器 - if (comp.jzj_1 && comp.zzi_1) { - var camComp = comp.zzi_1; - if (camComp && camComp.cyw_1 && camComp.dyw_1 && - (Math.abs(camComp.dyw_1.h20_1) > 0.001 || Math.abs(camComp.dyw_1.f20_1) > 0.001)) { - found = camComp; - return; - } - } - } catch(e) {} - }); - if (found) { - return { - yaw: found.dyw_1.h20_1, - pitch: found.dyw_1.f20_1, - fov: found.byw_1 || 1.0471976, - pos: { - x: found.cyw_1.f20_1, - y: found.cyw_1.g20_1, - z: found.cyw_1.h20_1 - } - }; + var scene = gm.lz4().jz1_1; + var cam = scene && scene.iyx_1; + if (!cam || !cam.t35_1) return null; + var P = cam.t35_1; + var pos = { x: P.f20_1, y: P.g20_1, z: P.h20_1 }; + var V = cam.d37_1; + var fx = 0, fy = 1, fz = 0; + if (V && typeof V.length === 'number' && V.length >= 11) { + fx = -V[2]; fy = -V[6]; fz = -V[10]; + var len = Math.sqrt(fx * fx + fy * fy + fz * fz) || 1; + fx /= len; fy /= len; fz /= len; } + var viewW = 0, viewH = 0; + try { viewW = cam.w36_1.h29(); viewH = cam.w36_1.g29(); } catch (e) {} + return { + pos: pos, + yaw: Math.atan2(-fx, fy), + pitch: Math.asin(Math.max(-1, Math.min(1, fz))), + heading: Math.atan2(fx, fy), + fov: cam.b37_1 || 1.0471976, + matrix: cam.g37_1 || null, + viewW: viewW, + viewH: viewH + }; + } catch (e) { + return null; } - return null; } // ===== 获取全局碰撞检测器(uMt 组件,挂在 GameMode entity 上) ===== @@ -366,6 +361,24 @@ } } + // ===== 3D 到 2D 投影(矩阵版,最可靠) ===== + // 直接用相机自带的 world→clip 矩阵(scene.iyx_1.g37_1),不依赖任何欧拉角约定。 + // 矩阵按列主序存放、按"行·点"使用(与引擎 z37/a38 一致)。 + function Project3DMatrix(camInfo, worldPos, screenW, screenH) { + var M = camInfo && camInfo.matrix; + if (!M || typeof M.length !== 'number' || M.length < 16) return null; + var px = worldPos.x, py = worldPos.y, pz = worldPos.z; + var cx = M[0] * px + M[4] * py + M[8] * pz + M[12]; + var cy = M[1] * px + M[5] * py + M[9] * pz + M[13]; + var cw = M[3] * px + M[7] * py + M[11] * pz + M[15]; + if (!(cw > 0.0001)) return null; + return { + x: (cx / cw * 0.5 + 0.5) * screenW, + y: (1 - (cy / cw * 0.5 + 0.5)) * screenH, + depth: cw + }; + } + // ===== 3D 到 2D 投影(用于 UI 标记) ===== // 标准透视投影:forward/right/up 三轴正交基 function Project3D(worldPos, camPos, yaw, pitch, fov, screenW, screenH) { @@ -520,6 +533,7 @@ IsTankAlive: IsTankAlive, GetTankName: GetTankName, GetTankHealth: GetTankHealth, + SelfDiagnose: SelfDiagnose, GetGearScore: GetGearScore, ScanBots: ScanBots, GetBotSet: GetBotSet, @@ -530,7 +544,8 @@ GetUMtComponent: GetUMtComponent, MkVec: MkVec, RaycastDistance: RaycastDistance, - Project3D: Project3D, + Project3D: Project3D, + Project3DMatrix: Project3DMatrix, TransformVertex: TransformVertex, MakeDraggable: MakeDraggable }; diff --git a/tools/hornet_radar.js b/tools/hornet_radar.js index 250945e..40c2d20 100644 --- a/tools/hornet_radar.js +++ b/tools/hornet_radar.js @@ -47,79 +47,13 @@ return null; } - // ===== 获取相机方向(gq/$r 组件的 lym_1.h20_1 = 偏航角) ===== - // 相机组件可能在 GameMode entity、玩家坦克或任何坦克上 - // 策略1:查找 $r/gq 相机组件(有 kym_1 和 lym_1 字段) - // 策略2:查找 k7 相机控制器(有 qz9_1 和 jzh_1 字段),从 jzh_1 获取相机组件 + // ===== 获取相机朝向(新版:直接用场景主相机 scene.iyx_1 的视图矩阵算水平朝向) ===== + // 雷达约定:forward = (sin(angle), cos(angle)),x/y 为水平面 function getCameraHeading() { - var entities = []; - - // 1. GameMode entity - try { - var gameScene = gm.lz4().jz1_1; - var gmEntity = gm.lz4(); - if (gmEntity) entities.push(gmEntity); - } catch(e) {} - - // 2. 所有坦克列表中的 entity - try { - var tankList = gm.m10b_1.j11y(); - var tCount = tankList.e1(); - for (var i = 0; i < tCount; i++) { - var t = tankList.g1(i); - if (t) entities.push(t); - } - } catch(e) {} - - // 在每个 entity 上查找相机组件 - for (var i = 0; i < entities.length; i++) { - var entity = entities[i]; - if (!entity || !entity.qz1_1 || !entity.qz1_1.s2a_1) continue; - - var found = null; - entity.qz1_1.s2a_1.forEach(function(valueList, key) { - if (found) return; - try { - var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList; - if (!comp) return; - - // 策略1:查找 $r/gq 相机组件(有 kym_1 和 lym_1 字段) - if (comp.cyw_1 && comp.dyw_1 && - comp.cyw_1.f20_1 !== undefined && comp.dyw_1.f20_1 !== undefined) { - // 检查是否是活跃的相机(lym_1 不全为0) - if (Math.abs(comp.dyw_1.h20_1) > 0.001 || Math.abs(comp.dyw_1.f20_1) > 0.001) { - found = comp; - return; - } - } - - // 策略2:查找 k7 相机控制器(有 qz9_1 和 jzh_1 字段) - if (comp.jzj_1 && comp.zzi_1) { - var camComp = comp.zzi_1; - if (camComp && camComp.cyw_1 && camComp.dyw_1 && - (Math.abs(camComp.dyw_1.h20_1) > 0.001 || Math.abs(camComp.dyw_1.f20_1) > 0.001)) { - found = camComp; - return; - } - } - } catch(e) {} - }); - - if (found) { - // lym_1.h20_1 = -atan2(t, n) = 偏航角 - // lym_1.f20_1 = atan2(i, e) = 俯仰角 - return { - yaw: -found.dyw_1.h20_1, // 偏航角(转为正向) - pitch: found.dyw_1.f20_1, // 俯仰角 - pos: { - x: found.cyw_1.f20_1, - y: found.cyw_1.g20_1, - z: found.cyw_1.h20_1 - } - }; - } - } - return null; + var ci = null; + try { ci = lib.GetCameraInfo(gm); } catch (e) { ci = null; } + if (!ci) return null; + return { yaw: ci.heading, pitch: ci.pitch, pos: ci.pos }; } // 玩家坦克:使用公共库 @@ -555,6 +489,12 @@ // ght 尚未捕获 / 不在战局:仍画静态雷达框,内容留空(等进入战局自动出) var Ready = !!gm; + // 进战局后打印一次自诊断(位置/相机链路),便于新版字段平移时快速定位 + if (Ready && !radar._diagPrinted) { + radar._diagPrinted = true; + try { console.log('[雷达] 自诊断:', lib.SelfDiagnose()); } catch (e) {} + } + try { var tankList = null, count = 0; if (Ready) {