// ===== 3DTank公共库 - 工具文件共享辅助函数 ===== // 在浏览器控制台运行(需先运行 hook_gamemode.js) // 功能:提供坦克查询、队伍判断、相机信息、3D投影等公共函数 // 用法:各工具文件通过 var lib = window.__hornetLib__ 访问 (function() { // ===== 字段抽象层(语义名 → 匿名字段,总表在 hornet_schema.js)===== var Schema = window.__hornetSchema__ || null; function Fld(Key, Fallback) { var N = Schema ? Schema.FieldName(Key) : null; return N || Fallback; } function Mth(Key, Fallback) { var N = Schema ? Schema.MethodName(Key) : null; return N || Fallback; } console.log('===== 3DTank公共库加载 ====='); if (!window.__gameMode__) { console.warn('[公共库] window.__gameMode__ 暂未捕获,工具启动时将失败。hook_gamemode.js 已安装 Hook,战斗中会自动捕获'); } // ===== 缓存队伍判断函数(ZCt/m2Fn) ===== var ZCt = null, m2Fn = null; // ===== 语义字段常量(取不到抽象层时用 fallback 字面名,保证单独加载也能工作) ===== // 实体 var FEntityRoot = Fld('Entity.Root', 'qz1_1'); // 组件宿主根对象 var FEntityComponentMap = Fld('Entity.ComponentMap', 's2a_1'); // 宿主上的组件 Map var FEntityId = Fld('Entity.Id', 'iz1_1'); // 实体 ID // 向量 var FVecX = Fld('Vec.X', 'f20_1'); var FVecY = Fld('Vec.Y', 'g20_1'); var FVecZ = Fld('Vec.Z', 'h20_1'); // 队伍 var FTeamField = Fld('Team.Field', 'b135_1'); // 队伍组件上的队伍对象字段 var FTeamId = Fld('Team.Id', 'k3_1'); // 队伍编号:0/1/2(2=中立) var NEUTRAL_TEAM_ID = 2; // 物理 var FPhysicsPosition = Fld('Physics.Position', 'bzr_1'); // 世界位置向量 var FPhysicsPositionOld1 = Fld('Physics.PositionOld1', 'gzh_1'); // 旧版位置字段名(兜底候选) var FPhysicsPositionOld2 = Fld('Physics.PositionOld2', 'pz7_1'); // 更旧位置字段名(兜底候选) // 坦克状态 var FTankFsm = Fld('Tank.Fsm', 'p10c_1'); // 状态机字段(组件识别用) var FTankState = Fld('Tank.State', 'k10c_1'); // 状态枚举对象 var FTankStateValue = Fld('Tank.StateValue', 'k3_1'); // 状态数值 // 用户信息 / 血量 / 装备分 var FUserName = Fld('User.Name', 'c12n_1'); var FUserClan = Fld('User.Clan', 'e12n_1'); var FHealthMax = Fld('Health.Max', 'p12o_1'); var FHealthCurrent = Fld('Health.Current', 'q12o_1'); var FGearScore = Fld('Gear.Score', 'cjf_1'); // GameMode var FGameModeTankManager = Fld('GameMode.TankManager', 'm10b_1'); var FGameModeOwnedTank = Fld('GameMode.OwnedTank', 'u10b_1'); var FGameModeObservedTank = Fld('GameMode.ObservedTank', 't10b_1'); var FGameModeLocalTeam = Fld('GameMode.LocalTeam', 'v10b_1'); // 相机 var FCameraSceneRef = Fld('Camera.SceneRef', 'jz1_1'); var FCameraObject = Fld('Camera.Object', 'iyx_1'); var FCameraPosition = Fld('Camera.Position', 't35_1'); var FCameraViewMatrix = Fld('Camera.ViewMatrix', 'd37_1'); var FCameraClipMatrix = Fld('Camera.ClipMatrix', 'g37_1'); var FCameraViewport = Fld('Camera.Viewport', 'w36_1'); var FCameraFov = Fld('Camera.Fov', 'b37_1'); // 射线检测结果 / 过滤器 var FRayStartPoint = Fld('Ray.StartPoint', 'y178_1'); var FRayEndPoint = Fld('Ray.EndPoint', 'z178_1'); var FRayDistance = Fld('Ray.Distance', 'a179_1'); var FRayHitEntity = Fld('Ray.HitEntity', 'q17o_1'); var FRayHitShape = Fld('Ray.HitShape', 'b179_1'); var FRayFilterContext = Fld('RayFilter.Context', 'l1da_1'); // 方法 var MGameModeScene = Mth('GameMode.Scene', 'lz4'); // 场景实体 var MTankList = Mth('GameMode.TankList', 'j11y'); // 坦克列表 var MIsTankAlive = Mth('Tank.IsAlive', 's14j'); // 引擎可见性判定 var MRaycast = Mth('Ray.Cast', 'a17p'); // 全局射线检测 // 加载队伍判断函数(从 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[FEntityRoot] || !entity[FEntityRoot][FEntityComponentMap]) return null; var result = null; entity[FEntityRoot][FEntityComponentMap].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 = [FPhysicsPosition, FPhysicsPositionOld1, FPhysicsPositionOld2]; 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[FVecX] === 'number' && typeof V[FVecY] === 'number' && typeof V[FVecZ] === 'number') { // 数值有效性校验:NaN/超大值一律视为读取失败(避免雷达画到画布外) if (isFinite(V[FVecX]) && isFinite(V[FVecY]) && isFinite(V[FVecZ]) && Math.abs(V[FVecX]) < 1e7 && Math.abs(V[FVecY]) < 1e7 && Math.abs(V[FVecZ]) < 1e7) { return { x: V[FVecX], y: V[FVecY], z: V[FVecZ] }; } } } return null; } // ===== 坦克组件(lxt / TankComponent)===== // 特征:有 k10c_1(状态枚举)与 p10c_1(fsm) function GetTankComponent(tank) { if (!tank || !tank[FEntityRoot] || !tank[FEntityRoot][FEntityComponentMap]) return null; var Found = null; tank[FEntityRoot][FEntityComponentMap].forEach(function(valueList, key) { if (Found) return; try { var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList; if (comp && comp[FTankFsm] && comp[FTankState] !== 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[FTankState]) ? C[FTankState][FTankStateValue] : -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[MIsTankAlive] === 'function') return !!C[MIsTankAlive](); } 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, FUserName); if (!userComp) return null; var name = userComp[FUserName] || ''; var clanTag = userComp[FUserClan] || ''; return clanTag ? (clanTag + ' ' + name) : name; } // ===== 获取坦克血量(Vkt 血量组件) ===== // f122_1 = 最大血量(max), g122_1 = 当前血量(current, 死亡时=0) function GetTankHealth(tank) { var healthComp = FindComponentByField(tank, FHealthCurrent); if (!healthComp) return null; var max = healthComp[FHealthMax] || 0; var cur = healthComp[FHealthCurrent] || 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, FGearScore); if (!c || typeof c[FGearScore] !== 'number') return null; return c[FGearScore]; } // ===== 人机识别 ===== // 规则:**同一队伍**内,**装备分数相同**的坦克(且分数 < 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[FGameModeTankManager]) return BotSet; var playerTank = GetPlayerTank(gm); var list, n; try { list = gm[FGameModeTankManager][MTankList](); 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[MGameModeScene]()[FCameraSceneRef]; } catch (e) { R.errors.push('scene: ' + e.message); } var list = null; try { list = g[FGameModeTankManager][MTankList](); 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][FVecX] === 'number') { Src = K; break; } } R.posHit[Src] = (R.posHit[Src] || 0) + 1; } else { R.posFail++; // 失败时把该坦克组件的字段名与"候选向量"打印出来,便于定位 try { var Fields = []; T[FEntityRoot][FEntityComponentMap].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[FVecX] === '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[FGameModeOwnedTank]; if (t && (!checkPosition || GetTankPosition(t))) return t; t = gm[FGameModeObservedTank]; if (t && (!checkPosition || GetTankPosition(t))) return t; try { var tl = gm[FGameModeTankManager][MTankList](); 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; // 读队伍对象:优先引擎导出函数 WIt($_$.vd),失败则直接读队伍组件字段 b135_1 // WIt(t) 的实现就是 g$(HIt(), t).b135_1 —— 所以直接读组件字段与引擎同源, // 导出键漂移时这条路仍然有效 function ReadTeamRaw(tank) { if (!tank) return null; if (!ZCt) LoadTeamFunctions(); if (ZCt) { try { var T = ZCt(tank); if (T) return T; } catch (e) {} } var Comp = FindComponentByField(tank, FTeamField); return Comp ? Comp[FTeamField] : null; } // 读队伍编号:0 / 1 = 两个阵营,2 = 中立(q$());读不到返回 -1 // ⚠ 用数值编号比较,而不是引擎 equals/R7:编号是稳定语义 // (module_87072 里 P7()/$ct() 都按 k3_1 分派) function GetTeamId(Team) { if (!Team) return -1; var Id = Team[FTeamId]; return (typeof Id === 'number') ? Id : -1; } function GetLocalTeamId() { return GetTeamId(LocalTeam); } function GetTankTeamId(tank) { return GetTeamId(ReadTeamRaw(tank)); } function RememberLocalTeam(gm) { var t = null; try { t = gm[FGameModeOwnedTank]; } catch (e) { t = null; } if (t) { var Id = t[FEntityId]; if (LocalTeam && LocalTeamTankId === Id) return; var Team = ReadTeamRaw(t); var TeamId = GetTeamId(Team); if (TeamId >= 0 && TeamId !== NEUTRAL_TEAM_ID) { LocalTeam = Team; LocalTeamTankId = Id; } return; } // u10b_1 为空(死亡/观战):保持已有缓存;若还没缓存过,再试 GameMode 上的本地队伍字段 if (LocalTeam) return; try { var G = gm[FGameModeLocalTeam]; var GId = GetTeamId(G); if (GId >= 0 && GId !== NEUTRAL_TEAM_ID) LocalTeam = G; } catch (e) {} } // ===== 判断是否友方 ===== // 规则:队伍编号相同且非中立 → 友方;中立/未下发 → 沿用该坦克上次判定(默认敌人)。 // ⚠ 不再「一次判定永久锁定」:锁定会在基准短暂错误时把整局颜色固化错, // 这里改成每帧重算 + 中立抖动时沿用上次结果,既不会闪烁也不会锁死。 var TankFriendCache = new WeakMap(); // tank -> 上一次的友方判定 var TeamJudgeLogged = new WeakSet(); function IsFriendly(tank, playerTank, gm) { if (!tank) return false; if (gm) RememberLocalTeam(gm); var TankId = GetTeamId(ReadTeamRaw(tank)); if (TankId < 0 || TankId === NEUTRAL_TEAM_ID) { var Last = TankFriendCache.get(tank); // 队伍还没同步:保持上一帧颜色 return Last === undefined ? false : Last; } // 可靠基准:本地拥有坦克的队伍(缓存)→ GameMode 上的本地队伍字段 // 观战目标(t10b_1)可能是敌人,绝不作为基准 var BaseTeam = LocalTeam; if (!BaseTeam && gm) { try { BaseTeam = gm[FGameModeLocalTeam]; } catch (e) {} } var BaseId = GetTeamId(BaseTeam); if (BaseId < 0 || BaseId === NEUTRAL_TEAM_ID) { var Last2 = TankFriendCache.get(tank); // 基准不可靠(极早期):沿用上次 return Last2 === undefined ? false : Last2; } var Friendly = (TankId === BaseId); TankFriendCache.set(tank, Friendly); if (!TeamJudgeLogged.has(tank)) { TeamJudgeLogged.add(tank); try { console.log('[敌我] id=' + tank[FEntityId] + ' 队伍=' + TankId + ' 本地=' + BaseId + ' → ' + (Friendly ? '友方' : '敌方') + ' name=' + (GetTankName(tank) || '?')); } catch (e) {} } return Friendly; } // ===== 获取相机信息(场景主相机 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[MGameModeScene]()[FCameraSceneRef]; var cam = scene && scene[FCameraObject]; if (!cam || !cam[FCameraPosition]) return null; var P = cam[FCameraPosition]; var pos = { x: P[FVecX], y: P[FVecY], z: P[FVecZ] }; var V = cam[FCameraViewMatrix]; 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[FCameraViewport].h29(); viewH = cam[FCameraViewport].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[FCameraFov] || 1.0471976, matrix: cam[FCameraClipMatrix] || 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[MGameModeScene](); if (!gmEntity || !gmEntity[FEntityRoot] || !gmEntity[FEntityRoot][FEntityComponentMap]) return null; var uMt = null; gmEntity[FEntityRoot][FEntityComponentMap].forEach(function(valueList, key) { if (uMt) return; try { var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList; if (comp && typeof comp[MRaycast] === 'function') uMt = comp; } catch(e) {} }); return uMt; } catch(e) { return null; } } // ===== 构造 mock O$ 向量(用于射线检测接口) ===== function MkVec(x, y, z) { return { [FVecX]: x || 0, [FVecY]: y || 0, [FVecZ]: z || 0, k26: function(t) { this[FVecX] = t[FVecX]; this[FVecY] = t[FVecY]; this[FVecZ] = t[FVecZ]; }, n23: function(x, y, z) { this[FVecX] = x; this[FVecY] = y; this[FVecZ] = 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 = { [FRayStartPoint]: MkVec(0, 0, 0), [FRayEndPoint]: MkVec(0, 0, 0), [FRayDistance]: maxDist, [FRayHitEntity]: null, [FRayHitShape]: null, r17o: function(t, n, i, r, e) { this[FRayStartPoint].k26(t); this[FRayEndPoint].k26(n); this[FRayDistance] = i; this[FRayHitEntity] = r; this[FRayHitShape] = e; }, d3: function() { this[FRayHitShape] = null; } }; var filter = { [FRayFilterContext]: { h1: function() { return false; }, d3: function() {} }, w179: function() { return true; }, d3: function() { this[FRayFilterContext].d3(); }, m1da: function() {} }; var hit = uMt[MRaycast](startVec, dirVec, maxDist, result, filter); return (hit && result[FRayDistance] < maxDist) ? result[FRayDistance] : -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_)并在创建时还原 // 实现要点: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, ReadTeamRaw: ReadTeamRaw, GetTeamId: GetTeamId, GetTankTeamId: GetTankTeamId, GetLocalTeamId: GetLocalTeamId, GetCameraInfo: GetCameraInfo, GetUMtComponent: GetUMtComponent, MkVec: MkVec, RaycastDistance: RaycastDistance, Project3D: Project3D, Project3DMatrix: Project3DMatrix, TransformVertex: TransformVertex, MakeDraggable: MakeDraggable }; console.log('[公共库] 已加载,可用函数:', Object.keys(window.__hornetLib__).join(', ')); })();