Files
TankIC/tools/hornet_lib.js
T

555 lines
22 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.
// ===== 3DTank公共库 - 工具文件共享辅助函数 =====
// 在浏览器控制台运行(需先运行 hook_gamemode.js)
// 功能:提供坦克查询、队伍判断、相机信息、3D投影等公共函数
// 用法:各工具文件通过 var lib = window.__hornetLib__ 访问
(function() {
console.log('===== 3DTank公共库加载 =====');
if (!window.__gameMode__) {
console.warn('[公共库] window.__gameMode__ 暂未捕获,工具启动时将失败。hook_gamemode.js 已安装 Hook,战斗中会自动捕获');
}
// ===== 缓存队伍判断函数(ZCt/m2Fn) =====
var ZCt = null, m2Fn = null;
// 加载队伍判断函数(从 module_87072 导出)
// ZCt (mod.$_$.kd): 从坦克对象获取队伍ID
// m2 (mod.$_$.rk): 判断两个队伍ID是否相同且非中立 = t.equals(n) && !t.equals(E$())
function LoadTeamFunctions() {
if (ZCt && m2Fn) return { ZCt: ZCt, m2Fn: m2Fn };
try {
var mod87072 = window.__wp_require__(87072);
if (mod87072 && mod87072.$_$) {
ZCt = mod87072.$_$.kd;
m2Fn = mod87072.$_$.rk;
console.log('[公共库] 队伍判断函数已加载: ZCt=' + (typeof ZCt) + ', m2=' + (typeof m2Fn));
}
} catch(e) {
console.warn('[公共库] 加载队伍判断函数失败:', e.message);
}
return { ZCt: ZCt, m2Fn: m2Fn };
}
// ===== 从 entity 获取包含指定字段的组件 =====
// 遍历 entity.wyj_1.r2a_1(Map<组件标识, 组件列表>),value 是列表类(s_),g1(0) 获取实际组件
// 注意:用 !== undefined 检查字段存在性,避免 falsy 值(0/""/null/false)漏判
function FindComponentByField(entity, fieldName) {
if (!entity || !entity.qz1_1 || !entity.qz1_1.s2a_1) return null;
var result = null;
entity.qz1_1.s2a_1.forEach(function(valueList, key) {
if (result) return;
try {
var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList;
if (comp && comp[fieldName] !== undefined) result = comp;
} catch(e) {}
});
return result;
}
// ===== 获取坦克位置(物理组件里的位置向量) =====
// 新旧差异(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 = ['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; }
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 };
}
}
}
return null;
}
// ===== 获取坦克状态值(Yjt 组件的 vzr_1.k3_1) =====
// 状态枚举:0=DEAD, 1=DEAD_PHANTOM, 2=SEMI_ACTIVE, 3=ACTIVE, 4=DISABLED
// Yjt 特征:有 azs_1(状态机)和 vzr_1(状态枚举)字段
function GetTankState(tank) {
if (!tank || !tank.qz1_1 || !tank.qz1_1.s2a_1) return -1;
var tankComp = null;
tank.qz1_1.s2a_1.forEach(function(valueList, key) {
if (tankComp) return;
try {
var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList;
if (comp && comp.p10c_1 && comp.k10c_1 !== undefined) tankComp = comp;
} catch(e) {}
});
if (tankComp && tankComp.k10c_1) return tankComp.k10c_1.k3_1;
return -1;
}
// 可见/战斗中的坦克 = ACTIVE(3) 或 SEMI_ACTIVE(2)
// 依据:引擎用 tankComponent.s14j()(= SEMI_ACTIVE || ACTIVE)决定该坦克节点是否渲染,
// 只认 ACTIVE 会漏掉半活跃状态(出生/过渡)的坦克
function IsTankAlive(tank) {
var s = GetTankState(tank);
return s === 2 || s === 3;
}
// ===== 获取坦克名称(yot 用户信息组件) =====
// s120_1 = userName, u120_1 = clanTag(可为null), t120_1 = userRank
function GetTankName(tank) {
var userComp = FindComponentByField(tank, 'c12n_1');
if (!userComp) return null;
var name = userComp.c12n_1 || '';
var clanTag = userComp.e12n_1 || '';
return clanTag ? (clanTag + ' ' + name) : name;
}
// ===== 获取坦克血量(Vkt 血量组件) =====
// f122_1 = 最大血量(max), g122_1 = 当前血量(current, 死亡时=0)
function GetTankHealth(tank) {
var healthComp = FindComponentByField(tank, 'q12o_1');
if (!healthComp) return null;
var max = healthComp.p12o_1 || 0;
var cur = healthComp.q12o_1 || 0;
if (max <= 0) return null;
return { current: cur, max: max, percent: cur / max };
}
// ===== 装备分数(战斗 GS:BattleGearScoreCC.score 落在组件的 cjf_1 上) =====
// 来源:module_85183 的 BattleGearScoreCC(score = cjf_1);战斗内每辆坦克各有一份
function GetGearScore(tank) {
var c = FindComponentByField(tank, 'cjf_1');
if (!c || typeof c.cjf_1 !== 'number') return null;
return c.cjf_1;
}
// ===== 人机识别 =====
// 规则:**同一队伍**内,**装备分数相同**的坦克(且分数 < maxScore,默认 8000)判为人机。
// 组内唯一的分数不算(真人装备各不相同,机器人往往整队同款)。
// 用法:工具定期调用 ScanBots()(约每 0.3 秒一次即可),再用 IsBot(tank) 查询。
var BotSet = new Set();
function ScanBots(opt) {
opt = opt || {};
var MaxScore = typeof opt.maxScore === 'number' ? opt.maxScore : 8000;
BotSet = new Set();
var gm = window.__gameMode__;
if (!gm || !gm.m10b_1) return BotSet;
var playerTank = GetPlayerTank(gm);
var list, n;
try { list = gm.m10b_1.j11y(); n = list.e1(); } catch (e) { return BotSet; }
var Groups = { ally: {}, enemy: {} }; // 组 → { 分数 → [坦克...] }
for (var i = 0; i < n; i++) {
var tank = list.g1(i);
if (!tank) continue;
var gs = GetGearScore(tank);
if (gs === null || gs >= MaxScore) continue; // 只统计低于阈值的
var g = IsFriendly(tank, playerTank, gm) ? 'ally' : 'enemy';
(Groups[g][gs] = Groups[g][gs] || []).push(tank);
}
['ally', 'enemy'].forEach(function (g) {
var Bag = Groups[g];
Object.keys(Bag).forEach(function (gs) {
var Arr = Bag[gs];
if (Arr.length >= 2) Arr.forEach(function (t) { BotSet.add(t); }); // 同队同分 → 人机
});
});
return BotSet;
}
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以在死亡状态控制)
function GetPlayerTank(gm, checkPosition) {
if (checkPosition === undefined) checkPosition = true;
var t = gm.t10b_1;
if (t && (!checkPosition || GetTankPosition(t))) return t;
t = gm.u10b_1;
if (t && (!checkPosition || GetTankPosition(t))) return t;
try {
var tl = gm.m10b_1.j11y();
if (tl.e1() > 0) {
t = tl.g1(0);
if (t && (!checkPosition || GetTankPosition(t))) return t;
}
} catch(e) {}
return null;
}
// ===== 判断是否友方 =====
// 策略1(优先):ZCt 获取双方队伍ID,m2 比较(同队且非中立)
// 策略2(备用):比较 gm.gzr_1(本地队伍ID)与坦克队伍ID
function IsFriendly(tank, playerTank, gm) {
if (!tank || !playerTank) return false;
if (ZCt && m2Fn) {
try {
var tankTeam = ZCt(tank);
var playerTeam = ZCt(playerTank);
if (tankTeam && playerTeam) return m2Fn(tankTeam, playerTeam);
} catch(e) {}
}
try {
if (gm && gm.v10b_1 && ZCt) {
var team = ZCt(tank);
if (team && gm.v10b_1) {
return team.equals(gm.v10b_1) && m2Fn ? m2Fn(team, gm.v10b_1) : team.equals(gm.v10b_1);
}
}
} catch(e) {}
return false;
}
// ===== 获取相机信息(场景主相机 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) {
try {
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;
}
}
// ===== 获取全局碰撞检测器(uMt 组件,挂在 GameMode entity 上) =====
// g16x(startVec, dirVec, maxDist, resultObj, filterObj) = 全局射线检测
function GetUMtComponent(gm) {
try {
var gmEntity = gm.lz4();
if (!gmEntity || !gmEntity.qz1_1 || !gmEntity.qz1_1.s2a_1) return null;
var uMt = null;
gmEntity.qz1_1.s2a_1.forEach(function(valueList, key) {
if (uMt) return;
try {
var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList;
if (comp && typeof comp.a17p === 'function') uMt = comp;
} catch(e) {}
});
return uMt;
} catch(e) {
return null;
}
}
// ===== 构造 mock O$ 向量(用于射线检测接口) =====
function MkVec(x, y, z) {
return {
f20_1: x || 0, g20_1: y || 0, h20_1: z || 0,
k26: function(t) { this.f20_1 = t.f20_1; this.g20_1 = t.g20_1; this.h20_1 = t.h20_1; },
n23: function(x, y, z) { this.f20_1 = x; this.g20_1 = y; this.h20_1 = z; return this; }
};
}
// ===== 射线检测:返回命中距离(-1=未命中/异常) =====
// 从 fromPos 沿 (dirX,dirY,dirZ) 方向,最大距离 maxDist
function RaycastDistance(gm, fromPos, dirX, dirY, dirZ, maxDist) {
var uMt = GetUMtComponent(gm);
if (!uMt) return -1;
try {
var startVec = MkVec(fromPos.x, fromPos.y, fromPos.z);
var dirVec = MkVec(dirX, dirY, dirZ);
var result = {
y178_1: MkVec(0, 0, 0),
z178_1: MkVec(0, 0, 0),
a179_1: maxDist,
q17o_1: null,
b179_1: null,
r17o: function(t, n, i, r, e) {
this.y178_1.k26(t);
this.z178_1.k26(n);
this.a179_1 = i;
this.q17o_1 = r;
this.b179_1 = e;
},
d3: function() { this.b179_1 = null; }
};
var filter = {
l1da_1: { h1: function() { return false; }, d3: function() {} },
w179: function() { return true; },
d3: function() { this.l1da_1.d3(); },
m1da: function() {}
};
var hit = uMt.a17p(startVec, dirVec, maxDist, result, filter);
return (hit && result.a179_1 < maxDist) ? result.a179_1 : -1;
} catch(e) {
return -1;
}
}
// ===== 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) {
var cp = Math.cos(pitch);
var sp = Math.sin(pitch);
var cy = Math.cos(yaw);
var sy = Math.sin(yaw);
var forward = { x: cp * (-sy), y: cp * cy, z: sp };
var right = { x: cy, y: sy, z: 0 };
var up = {
x: right.y * forward.z - right.z * forward.y,
y: right.z * forward.x - right.x * forward.z,
z: right.x * forward.y - right.y * forward.x
};
var relX = worldPos.x - camPos.x;
var relY = worldPos.y - camPos.y;
var relZ = worldPos.z - camPos.z;
var camX = relX * right.x + relY * right.y + relZ * right.z;
var camY = relX * up.x + relY * up.y + relZ * up.z;
var camZ = relX * forward.x + relY * forward.y + relZ * forward.z;
if (camZ <= 0.1) return null;
var focalLength = screenH / (2 * Math.tan(fov / 2));
var screenX = screenW / 2 + (camX / camZ) * focalLength;
var screenY = screenH / 2 - (camY / camZ) * focalLength;
return { x: screenX, y: screenY, depth: camZ };
}
// ===== 用世界变换矩阵变换顶点(列主序4x4矩阵) =====
function TransformVertex(matrix, x, y, z) {
return {
x: matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12],
y: matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13],
z: matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14]
};
}
// ===== 让元素可拖动(左键拖动,改变 left/top) =====
// 适用于固定定位的小窗口(雷达、后视镜等)
// key: 可选,传入后会把窗口位置存到 localStorage(hornet_layout_<key>)并在创建时还原
// 实现要点:Pointer Events + setPointerCapture,保证 pointerup/pointercancel 必定回到元素,
// 并在 window 捕获阶段兜底,避免游戏画布抢事件后"松手仍粘着鼠标"。
function MakeDraggable(elem, key) {
if (!elem) return null;
elem.style.cursor = 'move';
var StorageKey = key ? ('hornet_layout_' + key) : null;
function ClampX(v) { return Math.max(0, Math.min(window.innerWidth - 50, v)); }
function ClampY(v) { return Math.max(0, Math.min(window.innerHeight - 30, v)); }
// 还原上次保存的位置
if (StorageKey) {
try {
var __saved = localStorage.getItem(StorageKey);
if (__saved) {
var __p = JSON.parse(__saved);
if (__p && typeof __p.left === 'number' && typeof __p.top === 'number') {
elem.style.right = 'auto';
elem.style.transform = 'none';
elem.style.left = ClampX(__p.left) + 'px';
elem.style.top = ClampY(__p.top) + 'px';
}
}
} catch(e) {}
}
function SavePos() {
if (!StorageKey) return;
try {
var r = elem.getBoundingClientRect();
localStorage.setItem(StorageKey, JSON.stringify({ left: Math.round(r.left), top: Math.round(r.top) }));
} catch(e) {}
}
var Active = false;
var PointerId = null;
var GrabX = 0;
var GrabY = 0;
function MoveTo(ClientX, ClientY) {
elem.style.left = ClampX(ClientX - GrabX) + 'px';
elem.style.top = ClampY(ClientY - GrabY) + 'px';
}
function Begin(ClientX, ClientY) {
var Rect = elem.getBoundingClientRect();
// 移除 right 定位,改用 left
elem.style.right = 'auto';
elem.style.transform = 'none';
elem.style.left = Rect.left + 'px';
elem.style.top = Rect.top + 'px';
GrabX = ClientX - Rect.left;
GrabY = ClientY - Rect.top;
Active = true;
elem.style.cursor = 'grabbing';
}
function End() {
if (!Active) return;
Active = false;
if (PointerId !== null && elem.releasePointerCapture) {
try { elem.releasePointerCapture(PointerId); } catch (Err) {}
}
PointerId = null;
elem.style.cursor = 'move';
SavePos();
}
if (window.PointerEvent) {
elem.addEventListener('pointerdown', function (e) {
if (e.button !== 0) return;
Begin(e.clientX, e.clientY);
PointerId = e.pointerId;
try { elem.setPointerCapture(e.pointerId); } catch (Err) {}
e.preventDefault();
});
elem.addEventListener('pointermove', function (e) {
if (!Active || (PointerId !== null && e.pointerId !== PointerId)) return;
if (e.buttons === 0) { End(); return; }
MoveTo(e.clientX, e.clientY);
});
elem.addEventListener('pointerup', function () { End(); });
elem.addEventListener('pointercancel', function () { End(); });
elem.addEventListener('lostpointercapture', function () { End(); });
window.addEventListener('pointerup', function () { End(); }, true);
} else {
elem.addEventListener('mousedown', function (e) {
if (e.button !== 0) return;
Begin(e.clientX, e.clientY);
e.preventDefault();
});
document.addEventListener('mousemove', function (e) {
if (!Active) return;
if (e.buttons === 0) { End(); return; }
MoveTo(e.clientX, e.clientY);
}, true);
document.addEventListener('mouseup', function () { End(); }, true);
}
window.addEventListener('blur', End);
window.addEventListener('mouseleave', End);
return { End: End, IsActive: function () { return Active; } };
}
// ===== 导出公共函数到 window.__hornetLib__ =====
window.__hornetLib__ = {
LoadTeamFunctions: LoadTeamFunctions,
FindComponentByField: FindComponentByField,
GetTankPosition: GetTankPosition,
GetTankState: GetTankState,
IsTankAlive: IsTankAlive,
GetTankName: GetTankName,
GetTankHealth: GetTankHealth,
SelfDiagnose: SelfDiagnose,
GetGearScore: GetGearScore,
ScanBots: ScanBots,
GetBotSet: GetBotSet,
IsBot: IsBot,
GetPlayerTank: GetPlayerTank,
IsFriendly: IsFriendly,
GetCameraInfo: GetCameraInfo,
GetUMtComponent: GetUMtComponent,
MkVec: MkVec,
RaycastDistance: RaycastDistance,
Project3D: Project3D,
Project3DMatrix: Project3DMatrix,
TransformVertex: TransformVertex,
MakeDraggable: MakeDraggable
};
console.log('[公共库] 已加载,可用函数:', Object.keys(window.__hornetLib__).join(', '));
})();