Files
TankIC/tools/hornet_lib.js
T

618 lines
25 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 导出)
// 新旧差异(main.7cfa4058):
// 取队伍 $_$.vd = WIt(t) → g$(HIt(), t).b135_1(坦克无队伍时返回中立 q$())
// 同队判定 $_$.dl = R7(a, b) → a.equals(b) && !a.equals(q$())
// ⚠ 旧键 $_$.kd / $_$.rk 在新版已指向无关的构造器类(CCt / FIn),用了会"敌我不分"
var TEAM_GET_KEYS = ['vd', 'kd'];
var TEAM_EQ_KEYS = ['dl', 'rk'];
function LoadTeamFunctions() {
if (ZCt && m2Fn) return { ZCt: ZCt, m2Fn: m2Fn };
try {
var mod87072 = window.__wp_require__(87072);
if (mod87072 && mod87072.$_$) {
for (var i = 0; i < TEAM_GET_KEYS.length && !ZCt; i++) {
var G = mod87072.$_$[TEAM_GET_KEYS[i]];
if (typeof G === 'function') ZCt = G;
}
for (var j = 0; j < TEAM_EQ_KEYS.length && !m2Fn; j++) {
var E = mod87072.$_$[TEAM_EQ_KEYS[j]];
if (typeof E === 'function') m2Fn = E;
}
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;
}
// ===== 坦克组件(lxt / TankComponent)=====
// 特征:有 k10c_1(状态枚举)与 p10c_1(fsm)
function GetTankComponent(tank) {
if (!tank || !tank.qz1_1 || !tank.qz1_1.s2a_1) return null;
var Found = null;
tank.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 && comp.p10c_1 && comp.k10c_1 !== undefined) Found = comp;
} catch(e) {}
});
return Found;
}
// ===== 获取坦克状态值 =====
// 状态枚举:0=DEAD, 1=DEAD_PHANTOM, 2=SEMI_ACTIVE, 3=ACTIVE, 4=DISABLED, 5=TELEPORT
function GetTankState(tank) {
var C = GetTankComponent(tank);
return (C && C.k10c_1) ? C.k10c_1.k3_1 : -1;
}
// 可见/战斗中的坦克:**直接调用引擎自己的判定 s14j()**
// module_87072: lxt.s14j() = Fmt(k10c_1) = SEMI_ACTIVE(2) || ACTIVE(3) || TELEPORT(5)
// 旧实现只认 2/3,会漏掉传送态;死亡态 0/1/4 会被正确排除
function IsTankAlive(tank) {
var C = GetTankComponent(tank);
try { if (C && typeof C.s14j === 'function') return !!C.s14j(); } catch (e) {}
var s = GetTankState(tank);
return s === 2 || s === 3 || s === 5;
}
// ===== 获取坦克名称(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;
}
// ===== 获取玩家坦克 =====
// 本地拥有的坦克优先(u10b_1 = onTankPossessed 设置的那辆)。
// ⚠ 旧顺序先取 t10b_1(当前观察目标):玩家死亡后观察目标会切到别人(甚至敌人),
// 用它当队伍基准会导致「敌我一会儿对一会儿反」。
// checkPosition: 是否检查位置有效性(默认 true)
function GetPlayerTank(gm, checkPosition) {
if (checkPosition === undefined) checkPosition = true;
var t = gm.u10b_1;
if (t && (!checkPosition || GetTankPosition(t))) return t;
t = gm.t10b_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;
}
// 本地队伍缓存:死亡后 u10b_1 会被清空(onTankUnpossessed),不能再用"当前观察目标"推断。
// ⚠ 这里**不跟随 gm 变化重置**:hook 重新捕获 GameMode 时 gm 会换实例,
// 若此时重置,死亡/观战状态下就会拿观战目标当基准 → 敌我整体翻转。
// 只在「本地坦克实体变了」(换局/换车)时才重新缓存。
var LocalTeam = null;
var LocalTeamTankId = null;
function RememberLocalTeam(gm) {
if (!ZCt) return;
var t = null;
try { t = gm.u10b_1; } catch (e) { t = null; }
if (!t) return; // 死亡/观战:保持已有缓存
var Id = t.iz1_1;
if (LocalTeam && LocalTeamTankId === Id) return;
try {
var Team = ZCt(t);
if (Team && (!m2Fn || m2Fn(Team, Team))) { LocalTeam = Team; LocalTeamTankId = Id; }
} catch (e) {}
}
// ===== 判断是否友方 =====
// 队伍同步有延迟:坦克刚出现时队伍是「中立」,直接比较会先判成敌人、同步后再变队友(颜色跳变)。
// 处理:① 每辆坦克缓存最近一次**非中立**队伍;② 无法判定时沿用上一次判定结果。
var TankTeamCache = new WeakMap(); // tank -> 非中立队伍对象
var TankFriendCache = new WeakMap(); // tank -> 上一次的友方判定
var TeamLockLogged = new WeakSet();
function ResolveTankTeam(tank) {
if (!ZCt) return null;
var Team = null;
try { Team = ZCt(tank); } catch (e) { Team = null; }
if (Team && (!m2Fn || m2Fn(Team, Team))) { // 同队且非中立 = 有效队伍
TankTeamCache.set(tank, Team);
return Team;
}
return TankTeamCache.get(tank) || null;
}
function IsFriendly(tank, playerTank, gm) {
if (!tank) return false;
if (gm) RememberLocalTeam(gm);
// 一旦在「本地队伍可靠」时判过,就锁定本局结果,避免任何同步抖动导致颜色翻转
var Locked = TankFriendCache.get(tank);
if (Locked !== undefined) return Locked;
if (ZCt && m2Fn) {
try {
var tankTeam = ResolveTankTeam(tank);
var BaseTeam = LocalTeam;
if (!BaseTeam && playerTank) BaseTeam = ResolveTankTeam(playerTank);
if (tankTeam && BaseTeam) {
var Friendly = m2Fn(tankTeam, BaseTeam);
if (LocalTeam) {
TankFriendCache.set(tank, Friendly);
// 诊断:首次锁定为友方时记一条,便于核对有没有把敌人锁成队友
if (Friendly && !TeamLockLogged.has(tank)) {
TeamLockLogged.add(tank);
try {
console.log('[敌我] 锁定为友方 id=' + tank.iz1_1 + ' name=' + (GetTankName(tank) || '?'));
} catch (e) {}
}
}
return Friendly;
}
} 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(', '));
})();