Files
TankIC/tools/hornet_lib.js
T
TermiNexus 9506648877 fix(lib): 位置探测增加矩阵式兜底([12],[13],[14] 平移分量)
- 旧版位置来自物理组件的世界变换矩阵(K$ 实例,非普通向量),新版字段平移后无法靠名字命中

- 兜底 2:遍历组件找长度 >=15 的矩阵对象,取 [12]/[13]/[14] 作为平移(引擎惯用布局)
2026-10-17 16:29:06 +08:00

540 lines
21 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;
}
// ===== 获取坦克位置(物理组件里的位置向量) =====
// 物理组件的字段名每个构建都可能平移,这里按"含 f20_1/g20_1/h20_1 的 O$ 向量"做多级探测:
// ejt(tankPhysics)里可能的候选字段都试一遍,命中即用。
// 返回 {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'];
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 };
}
}
// 兜底 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;
}
// ===== 获取坦克状态值(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); }
// ===== 获取玩家坦克 =====
// 优先 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;
}
// ===== 获取相机信息($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)
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
}
};
}
}
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 投影(用于 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,
GetGearScore: GetGearScore,
ScanBots: ScanBots,
GetBotSet: GetBotSet,
IsBot: IsBot,
GetPlayerTank: GetPlayerTank,
IsFriendly: IsFriendly,
GetCameraInfo: GetCameraInfo,
GetUMtComponent: GetUMtComponent,
MkVec: MkVec,
RaycastDistance: RaycastDistance,
Project3D: Project3D,
TransformVertex: TransformVertex,
MakeDraggable: MakeDraggable
};
console.log('[公共库] 已加载,可用函数:', Object.keys(window.__hornetLib__).join(', '));
})();