// ============================================================ // 3DTank 工具链 Bundle (自动生成,请勿手动编辑) // 构建: python build_bundle.py // 用法: 全选粘贴到 3DTank 游戏页面控制台 // 操作: Alt+T 打开控制面板 | 左上角 HUD 显示状态 // ============================================================ // ===== hook_gamemode.js ===== // ===== GameMode Hook 脚本 v2(快速获取版)===== // 改进: // 1. 每次 yht 调用都更新引用(解决新战斗中旧引用失效问题) // 2. ght.prototype 方法每次调用都更新 // 3. 定时器主动验证引用有效性,失效则重置 // 4. trySetGameMode 验证坦克列表,避免捕获未初始化实例 // // 原理(新版 main.7cfa4058): // module_87072.$_$.jc = dct 函数(接收 scene,返回 ght 实例) ← 旧版是 $_$.ac,新版键位漂移 // module_87072.$_$.ic = wct 类(ght / GameMode 组件类) ← 旧版是 $_$.zb // module_87072.$_$.kc = gct 函数(接收 scene,返回 GameMode 实体)← 旧版是 $_$.bc // dct(scene).m10b_1 = 坦克列表管理器 // 坦克列表 = dct(scene).m10b_1.j11y() (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; } // ===== 语义字段常量(取不到抽象层时用 fallback 字面名) ===== var FGameModeTankManager = Fld('GameMode.TankManager', 'm10b_1'); // 坦克列表管理器 var MTankList = Mth('GameMode.TankList', 'j11y'); // 坦克列表 console.log('===== GameMode Hook v2 开始 ====='); // 1. 检查 webpack 内部对象 if (!window.__wp_require__) { console.error('[错误] window.__wp_require__ 不存在'); return; } // 2. 获取 module_87072 的导出 var mod = window.__wp_require__(87072); if (!mod || !mod.$_$) { console.error('[错误] module_87072 未加载或无 $_$ 导出'); return; } var ghtClass = mod.$_$.ic; // 新版:wct(旧版 $_$.zb) var yhtFunc = mod.$_$.jc; // 新版:dct(旧版 $_$.ac) console.log('[1] ght 类:', ghtClass ? ghtClass.name : '未找到'); console.log('[1] yht 函数:', typeof yhtFunc === 'function' ? '已找到' : '未找到'); if (!ghtClass) { console.error('[错误] ght 类未导出'); return; } // ===== 辅助:设置 gameMode(无条件设置,坦克列表单独处理)===== function trySetGameMode(gm, source) { if (!gm) return false; var updated = window.__gameMode__ !== gm; window.__gameMode__ = gm; // 尝试获取坦克列表(失败也不影响 gameMode 设置) var tankCount = -1; try { var list = gm[FGameModeTankManager][MTankList](); tankCount = list.e1(); window.__tankList__ = list; } catch(e) { // xzq_1 或 z11b 暂时不可用,不影响 gameMode 设置 } if (updated) { console.log('%c[Hook] ght 已更新 (via ' + source + ') 坦克数:' + tankCount, 'color:#0f0;font-weight:bold'); } return true; } // ===== 3. Hook yht 函数 — 每次调用都更新 ===== if (typeof yhtFunc === 'function' && !window.__yht_hooked__) { window.__yht_hooked__ = true; var origYht = yhtFunc; mod.$_$.jc = function(t) { var result = origYht.call(this, t); if (result) trySetGameMode(result, 'yht'); return result; }; console.log('[2] yht Hook 已安装(每次调用都更新)'); } else if (window.__yht_hooked__) { console.log('[2] yht Hook 已存在'); } // ===== 4. Hook ght.prototype 方法 — 每次调用都更新 ===== if (!window.__ght_proto_hooked__) { window.__ght_proto_hooked__ = true; var proto = ghtClass.prototype; var methodCount = 0; Object.getOwnPropertyNames(proto).forEach(function(name) { if (name === 'constructor') return; var orig = Object.getOwnPropertyDescriptor(proto, name); if (!orig || typeof orig.value !== 'function') return; var origFn = orig.value; methodCount++; proto[name] = function() { trySetGameMode(this, 'ght.' + name); return origFn.apply(this, arguments); }; }); console.log('[3] ght.prototype 方法 Hook 已安装(' + methodCount + ' 个方法)'); } else { console.log('[3] ght.prototype Hook 已存在'); } // ===== 5. 定时器主动验证引用有效性 ===== if (!window.__ght_validator__) { window.__ght_validator__ = true; var validateCount = 0; setInterval(function() { validateCount++; if (window.__gameMode__) { // 验证当前引用是否有效 try { var list = window.__gameMode__[FGameModeTankManager][MTankList](); window.__tankList__ = list; } catch(e) { // 引用失效(新战斗/退出战斗),重置等待重新捕获 console.log('%c[验证] ght 引用失效,重置等待重新捕获', 'color:#f80;font-weight:bold'); window.__gameMode__ = null; window.__tankList__ = null; } } else { // 未捕获,前5次每秒提示 if (validateCount <= 5) { console.log('[验证] 等待 ght 捕获... (' + validateCount + ')'); } } }, 500); console.log('[4] 定时验证器已启动(每500ms)'); } else { console.log('[4] 定时验证器已存在'); } // ===== 6. 状态显示 ===== if (window.__gameMode__) { console.log('[5] 已有 ght 实例:', window.__gameMode__); if (window.__tankList__) { try { console.log(' 当前坦克数量:', window.__tankList__.e1()); } catch(e) {} } } else { console.log('[5] 等待 ght 实例捕获...'); console.log(' 提示: hook 已安装,战斗中触发事件会自动捕获'); } // ===== 7. 辅助函数 ===== window.__listTanks__ = function() { if (!window.__gameMode__) { console.log('ght 实例未捕获,请等待'); return; } var list = window.__gameMode__[FGameModeTankManager][MTankList](); var count = list.e1(); console.log('=== 坦克列表 (' + count + " 个) ==="); for (var i = 0; i < count; i++) { var tank = list.g1(i); console.log('[' + i + ']', tank); } return list; }; window.__probeTank__ = function(idx) { if (!window.__gameMode__) { console.log('ght 实例未捕获'); return; } var list = window.__gameMode__[FGameModeTankManager][MTankList](); var tank = list.g1(idx || 0); console.log('坦克[' + (idx || 0) + ']:', tank); console.log('字段:', Object.keys(tank)); Object.keys(tank).forEach(function(k) { var v = tank[k]; var type = typeof v; if (type === 'number' || type === 'string' || type === 'boolean') { console.log(' ' + k + ' = ' + v + ' (' + type + ')'); } else if (v === null) { console.log(' ' + k + ' = null'); } else if (type === 'object') { console.log(' ' + k + ' = [object] (' + (v.constructor ? v.constructor.name : typeof v) + ')'); } else if (type === 'function') { console.log(' ' + k + ' = [function]'); } }); return tank; }; // 主动刷新/检查 ght 实例 window.__refreshGameMode__ = function() { if (window.__gameMode__) { try { var list = window.__gameMode__[FGameModeTankManager][MTankList](); console.log('%c[刷新] ght 引用有效,坦克数:' + list.e1(), 'color:#0f0;font-weight:bold'); return window.__gameMode__; } catch(e) { console.log('%c[刷新] 旧引用失效,已重置', 'color:#f80;font-weight:bold'); window.__gameMode__ = null; window.__tankList__ = null; } } console.log('[刷新] 等待 hook 捕获...(战斗中触发事件即可)'); return null; }; console.log('\n===== Hook v2 安装完成 ====='); console.log('辅助函数:'); console.log(' __listTanks() - 列出所有坦克'); console.log(' __probeTank(idx) - 探查指定坦克的字段'); console.log(' __refreshGameMode__() - 刷新/检查 ght 实例'); console.log(' window.__gameMode__ - ght 实例(自动更新)'); console.log(' window.__tankList__ - 坦克列表(自动更新)'); })(); // ===== hornet_schema.js ===== // hornet_schema.js — 引擎匿名字段抽象层(字段映射总表) // // 为什么需要它: // 游戏每个构建版本都会把类的字段名整体平移(如 ejt 的 zzq_1 / bzr_1), // 工具里到处硬编码 `comp.zzq_1`、`tank.qz1_1.s2a_1` 这类匿名名, // 版本一换就要全局搜替换,还容易改错(同名复用极严重)。 // 这里把「语义名 → 匿名字段路径」集中成一张表: // · 工具只用语义名(Schema.Get(tank, 'Tank.StateValue')) // · 换版本时只改这张表,并跑一次 SelfCheck() 看哪几项失配 // // 路径语法: // 'a.b.c' 属性链 // 'lz4()' 方法调用(末尾带括号即调用,中间也可混用,如 'm10b_1.j11y()') // // 用法: // var S = window.__hornetSchema__; // S.Get(gm, 'GameMode.TankList') // S.Get(tank, 'Tank.StateValue') // S.Check(gm, 'GameMode') // → { Ok: [...], Missing: [...] } // S.SelfCheck() // 用当前战局对象自检并打印 // // 版本:main.7cfa4058(字段核对自 module_87072 / 61378 / 92980 / 55088) (function () { 'use strict'; // ===== 字段映射总表 ===== // 命名规则:<对象类型>.<语义名> var Fields = { // ---- 向量(O$)---- 'Vec.X': 'f20_1', 'Vec.Y': 'g20_1', 'Vec.Z': 'h20_1', // ---- 战斗实体基类(fe / BattleEntity)---- 'Entity.Id': 'iz1_1', // 实体 ID(坦克/战场唯一) 'Entity.Root': 'qz1_1', // 组件宿主根对象 'Entity.ComponentMap': 's2a_1', // 宿主上的组件 Map 'Entity.Components': 'qz1_1.s2a_1', // Map<组件标识, 组件列表>,取值要 valueList.g1(0) // ---- GameMode(ght,window.__gameMode__)---- 'GameMode.Scene': 'lz4()', // 场景实体 'GameMode.TankManager': 'm10b_1', // 坦克管理器 'GameMode.TankList': 'm10b_1.j11y()', // 坦克列表(e1() 数量 / g1(i) 取) 'GameMode.OwnedTank': 'u10b_1', // 本地玩家拥有的坦克(onTankPossessed) 'GameMode.ObservedTank': 't10b_1', // 当前观察目标(死亡/观战时可能指向敌人) 'GameMode.LocalTeam': 'v10b_1', // 本地队伍(与 u10b_1 同生命周期) // ---- 坦克组件(lxt,含状态机)---- 'Tank.Fsm': 'p10c_1', // 状态机字段(组件识别用) 'Tank.State': 'k10c_1', // 状态枚举对象 'Tank.StateValue': 'k10c_1.k3_1', // 状态数值:0=DEAD 1=DEAD_PHANTOM 2=SEMI_ACTIVE 3=ACTIVE 4=DISABLED 5=TELEPORT 'Tank.IsAlive': 's14j()', // 引擎可见性判定(2/3/5) 'Tank.TeamSource': 'l10c_1', // 状态机里下发的队伍对象 // ---- 队伍组件(GIt)与队伍对象 ---- 'Team.Field': 'b135_1', // 队伍组件上保存队伍对象的字段 'Team.Id': 'k3_1', // 队伍编号:0 / 1 / 2(2 = 中立) 'Team.NeutralId': 2, // ---- 坦克物理组件(ejt,组件名 tankPhysics)---- 'Physics.Position': 'bzr_1', // 世界位置向量(f20_1/g20_1/h20_1) 'Physics.PositionOld1': 'gzh_1', // 旧版位置字段名(兜底候选) 'Physics.PositionOld2': 'pz7_1', // 更旧位置字段名(兜底候选) 'Physics.Size': 'xzq_1', // 车体半尺寸(**不是位置**) 'Physics.Hull': 'zzq_1', // 车体主碰撞形状(x9j_1 === 'hull-main') 'Physics.Body': 'pbk()', // 物理刚体(jM) 'Physics.AddMask': 'n155()', // z9j_1 |= t 'Physics.RemoveMask': 'm155()', // z9j_1 &= ~t 'Physics.BodyShapes': 'pbk().y9i_1', // 刚体上的形状列表(e1()/g1(i)) // ---- 碰撞形状(yi)---- 'Shape.Name': 'x9j_1', 'Shape.Category': 'y9j_1', // bit0=坦克(1) bit3=地雷(8) bit4=地形(16) 'Shape.Mask': 'z9j_1', // 愿意碰撞的分类位 'Shape.Ghost': 'g9k_1', // true = 只探测不产生碰撞响应 'Shape.Body': 'b9k_1', // 形状 → 所属刚体(Fn) 'Shape.Type': 'v9j_1', // 形状类型位组(1/2/4/8/16/32;8 = 三角形,4 = 克隆包装) 'Shape.Clone': 'c9w_1', // 类型 4 的克隆包装:真实几何在 c9w_1 里 'Shape.Vertex0': 'u9x_1', // 三角形顶点 0 'Shape.Vertex1': 'v9x_1', // 三角形顶点 1 'Shape.Vertex2': 'w9x_1', // 三角形顶点 2 'Body.Owner': 'x9i_1', // 刚体 → 拥有者(坦克物理组件 ejt) 'Body.State': 'f9j_1', // 刚体当前物理态 'Body.PositionVec': 's9j_1', // 刚体位置向量(f9j_1.s9j_1) // ---- 碰撞世界(module_55088 的 ki)---- 'CollisionWorld.ShouldCollide': 'q9q()', // 唯一配对过滤:mask + 形状类型 → 生成接触 'CollisionWorld.Overlap': 'x9o()', // 形状相交查询(真形状检测,不是包围盒) // ---- 实体事件总线 / 外力事件(ODt)---- 'Entity.Dispatch': 'kz7()', // 向实体内所有订阅者派发事件 'Event.PushPoint': 'o13i_1', // 外力事件:作用点(O$ 向量) 'Event.PushDir': 'p13i_1', // 外力事件:方向(O$ 向量) 'Event.PushStrength': 'q13i_1', // 外力事件:强度(坦克物理交给 m9k) // ---- 血量组件(Vkt)---- 'Health.Max': 'p12o_1', 'Health.Current': 'q12o_1', // ---- 用户信息组件(yot)---- 'User.Name': 'c12n_1', 'User.Clan': 'e12n_1', // ---- 装备分组件(BattleGearScoreCC)---- 'Gear.Score': 'cjf_1', // ---- 皮肤组件(eUt)---- 'Skin.HullSkin': 'g159_1', // 皮肤对象 'Skin.HullRoot': 'h159_1', // 车身渲染树根(BP 节点) 'Skin.Parts': 'g159_1.g17r_1', // 部件表 'Skin.PartNode': 'j17r_1', // 部件 → BP 节点 // ---- 炮塔皮肤组件(LUt)---- 'Turret.Wrapper': 'a11p_1', // 组件首字段(包装对象) 'Turret.Root': 'e11p_1.a17q_1', // 炮塔 BP 根节点 'Turret.GetRoot': 'u11p()', // ---- BP 场景节点(zl)---- 'Node.Name': 'g35_1', 'Node.Matrix': 'p35_1', // 世界矩阵(列主序,[12][13][14] = 平移) 'Node.MatrixOld': 'n35_1', // 旧版矩阵字段(兜底候选) 'Node.Children': 'r35_1', 'Node.Mesh': 'v3m_1', 'Node.Lod': 'u4x_1', // ---- 相机(场景实体 .jz1_1.iyx_1)---- 'Camera.SceneRef': 'jz1_1', 'Camera.Object': 'iyx_1', // 主相机对象(挂在场景实体的 jz1_1 上) 'Camera.Viewport': 'w36_1', // 视口(h29() 宽 / g29() 高) 'Camera.Main': 'jz1_1.iyx_1', 'Camera.Position': 'jz1_1.iyx_1.t35_1', 'Camera.ViewMatrix': 'jz1_1.iyx_1.d37_1', 'Camera.ClipMatrix': 'jz1_1.iyx_1.g37_1', 'Camera.Fov': 'jz1_1.iyx_1.b37_1', // ---- 相机:单段叶子字段(FieldName 只认单段,这里给同一批字段补叶子别名)---- 'Camera.PositionLeaf': 't35_1', 'Camera.ViewMatrixLeaf': 'd37_1', 'Camera.ClipMatrixLeaf': 'g37_1', 'Camera.FovLeaf': 'b37_1', 'Camera.PositionNode': 'r35_1', // 旧版相机位置字段(与 Node.Children 同名但不通用) 'Camera.Angles': 'u35_1', // 欧拉角(pitch/roll/yaw) 'Camera.Euler': 'u35_1', // 同上(自由视角叫法) 'Camera.Rotation': 'u35_1', // 同上(沙盘叫法) 'Camera.Update': 'o37()', // 相机矩阵刷新 'Camera.ViewportWidth': 'u36_1', 'Camera.ViewportHeight': 'v36_1', 'Camera.RenderWidth': 'u36_1', // 别名 'Camera.RenderHeight': 'v36_1', // 别名 'Camera.FovValue': 'z36_1', 'Camera.Near': 'x36_1', 'Camera.Far': 'y36_1', 'Camera.SceneFx': 'd75_1', // ---- 渲染器 / 绘制流程(module_92980)---- 'Render.Renderer': 'gyw_1', // 场景渲染器 'Render.Drawer': 'd75_1', // Ff 绘制器 'Render.DrawBuffer': 'd75_1', // 别名(透视里按"绘制缓冲"称呼) 'Renderer.Object': 'd75_1', // 别名(沙盘里按"渲染器对象"称呼) 'Render.Gl': 'w39_1', // GL 包装器 'Renderer.Device': 'w39_1', // 别名 'Render.GlNative': 'b74_1', // 原生 WebGL2RenderingContext 'Renderer.Gl': 'b74_1', // 别名 'Render.Device': 's3h_1', // 资源侧设备包装 'Render.DeviceNative': 'f75_1', // 设备里的原生 GL 'Render.Settings': 'y38_1', // 渲染开关组 'Renderer.Flags': 'y38_1', // 别名 'Render.ShadowEnabled': 'e3o_1', 'Renderer.Shadow': 'e3o_1', // 别名 'Render.PostProcessEnabled': 'f3o_1', 'Renderer.PostFx': 'f3o_1', // 别名 'Render.ExtraPassEnabled': 'k3o_1', 'Renderer.FlagAux': 'k3o_1', // 别名 'Render.LayerQueue': 'e39_1', // 渲染层队列(20=Hud 21-24=GUI) 'Renderer.QueueList': 'e39_1', // 别名 'Render.Draw': 'f3t()', // y3s → f3t 主渲染 'Render.DrawingWidth': 'o3c()', 'Render.DrawingHeight': 'n3c()', 'Scene.RootNode': 'fyx_1', // 场景根节点 // ---- 网格资源(Tp / sm)---- 'Mesh.VertexGroups': 'x4w_1', 'Mesh.VertexBuffer': 'h3x_1', 'Mesh.VertexCount': 'w3x_1', 'Mesh.CpuVertexData': 'x3x_1', 'Mesh.GpuBuffer': 'v3x_1', 'Mesh.IndexGroups': 'y4w_1', 'Mesh.IndexBuffer': 'q4w_1', 'Mesh.IndexBufferOld': 'u3p_1', 'Mesh.CpuIndexData': 'p3x_1', 'Mesh.IndexGpuBuffer': 'n3x_1', // ---- 射线检测(碰撞世界的射线接口)---- 'CollisionWorld.Raycast': 'a17p()', 'Ray.Cast': 'a17p()', // 别名 'Ray.StartPoint': 'y178_1', 'Ray.HitPoint': 'y178_1', // 别名 'Ray.EndPoint': 'z178_1', 'Ray.HitNormal': 'z178_1', // 别名 'Ray.Distance': 'a179_1', 'Ray.HitEntity': 'q17o_1', 'Ray.HitShape': 'q17o_1', // 别名(雷达/瞄准把第 4 参数叫 HitShape) 'Ray.HitBody': 'b179_1', // 第 5 参数 'RayFilter.Context': 'l1da_1', 'RayFilter.Query': 'l1da_1', // 别名 // ---- 车体朝向 / 武器(雷达、瞄准警告)---- 'Tank.HullComponent': 'qzv_1', // 车体朝向组件识别字段 'Tank.HullDirection': 'wzl_1', // 车体朝向向量 'Tank.StateId': 'k3_1', // 状态枚举数值(与 Team.Id 同名字段,按用途分开) 'Weapon.Component': 'n11c_1', 'Weapon.ComponentAlt': 'q112_1', 'Weapon.TurretRoot': 'm17a_1', // ---- 炮塔 / 皮肤槽位 ---- 'Turret.InnerWrapper': 'e11p_1', 'Turret.RootNode': 'a17q_1', 'Skin.SlotHull': 'w14w_1', 'Skin.SlotHullLegacy': 't14l_1', 'Skin.SlotHullAlt': 'w14x_1', 'Skin.SlotHullOld': 'p14l_1' }; // ===== module_87072 的导出键($_.xx),每版都可能平移 ===== // 队伍:vd = WIt(t) 取实体队伍;dl = R7(a,b) 同队且非中立 // ⚠ 旧版是 kd / rk;新版 kd 已指向无关构造器 var Exports = { 'Export.TeamOf': ['vd', 'kd'], 'Export.TeamSame': ['dl', 'rk'], 'Export.PhysicsWorld': ['h'] // module_55088.$_$.h = 碰撞世界类 ki }; // ===== 路径解析 ===== function IsCall(Token) { var N = Token.length; return N > 2 && Token.charAt(N - 1) === ')' && Token.charAt(N - 2) === '('; } function ResolvePath(Target, Path) { if (Target === null || Target === undefined || !Path) return undefined; var Tokens = String(Path).split('.'); var Cur = Target; for (var i = 0; i < Tokens.length; i++) { if (Cur === null || Cur === undefined) return undefined; var Tok = Tokens[i]; if (IsCall(Tok)) { var Name = Tok.slice(0, -2); if (typeof Cur[Name] !== 'function') return undefined; try { Cur = Cur[Name](); } catch (e) { return undefined; } } else { Cur = Cur[Tok]; } } return Cur; } // 取字段值:Key 是总表键(如 'Tank.StateValue'),未命中则把 Key 当字面路径 function Get(Target, Key) { var Path = Fields[Key]; return ResolvePath(Target, Path !== undefined ? Path : Key); } // 取字段路径(字符串),用于打印/自检 function PathOf(Key) { return Fields[Key] !== undefined ? Fields[Key] : String(Key); } // 取单段属性名:'Node.Matrix' → 'p35_1';带方法或多段时返回 null // 工具里写成 `Node[FieldName('Node.Matrix') || 'p35_1']`,即「优先用总表,缺表时用字面名」 function FieldName(Key) { var P = Fields[Key]; if (!P || P.indexOf('.') >= 0 || IsCall(P)) return null; return P; } // 取单段方法名:'Physics.RemoveMask' → 'm155' function MethodName(Key) { var P = Fields[Key]; if (!P || !IsCall(P) || P.indexOf('.') >= 0) return null; return P.slice(0, -2); } // 自检:按前缀检查某对象上哪些语义字段能解析 function Check(Target, Prefix) { var Out = { Ok: [], Missing: [] }; Object.keys(Fields).forEach(function (Key) { if (Prefix && Key.indexOf(Prefix + '.') !== 0) return; if (typeof Fields[Key] !== 'string') return; // 常量项(如 Team.NeutralId)不参与自检 var V = Get(Target, Key); if (V === undefined) Out.Missing.push(Key + ' → ' + Fields[Key]); else Out.Ok.push(Key + ' → ' + Fields[Key]); }); return Out; } // 自检项(只查核心字段:别名/扩展项不在样本对象上,查了会误报) var CoreKeys = { Entity: ['Entity.Id', 'Entity.Root', 'Entity.ComponentMap', 'Entity.Components'], GameMode: ['GameMode.Scene', 'GameMode.TankManager', 'GameMode.TankList', 'GameMode.OwnedTank', 'GameMode.ObservedTank', 'GameMode.LocalTeam'], Tank: ['Tank.Fsm', 'Tank.State', 'Tank.StateValue', 'Tank.IsAlive', 'Tank.TeamSource'], Physics: ['Physics.Position', 'Physics.Size', 'Physics.Hull', 'Physics.Body', 'Physics.BodyShapes'], Shape: ['Shape.Name', 'Shape.Category', 'Shape.Mask', 'Shape.Body', 'Body.Owner'], Skin: ['Skin.HullSkin', 'Skin.HullRoot', 'Skin.Parts', 'Skin.PartNode'], Turret: ['Turret.Wrapper', 'Turret.GetRoot'], Node: ['Node.Name', 'Node.Matrix', 'Node.Children', 'Node.Mesh'], Camera: ['Camera.SceneRef', 'Camera.Main', 'Camera.Position', 'Camera.ViewMatrix', 'Camera.ClipMatrix', 'Camera.Fov'] }; // 用当前战局对象做一次全量自检(控制台调用 __hornetSchema__.SelfCheck()) function SelfCheck() { var Gm = window.__gameMode__; var Report = { HasGameMode: !!Gm, Groups: {}, Missing: [] }; if (!Gm) return Report; var Scene = Get(Gm, 'GameMode.Scene'); var TankList = Get(Gm, 'GameMode.TankList'); var Tank = null; try { Tank = Get(Gm, 'GameMode.OwnedTank'); if (!Tank && TankList && typeof TankList.e1 === 'function' && TankList.e1() > 0) Tank = TankList.g1(0); } catch (e) {} var Physics = Tank ? FindPhysics(Tank) : null; var Targets = { Entity: Tank || Scene, GameMode: Gm, Tank: Tank, Physics: Physics, Shape: Physics ? Get(Physics, 'Physics.Hull') : null, Skin: Tank, Turret: Tank, Node: Tank ? Get(Tank, 'Skin.HullRoot') : null, Camera: Scene }; Object.keys(CoreKeys).forEach(function (Prefix) { var Target = Targets[Prefix]; if (!Target) { Report.Groups[Prefix] = '无样本对象'; return; } var Ok = [], Missing = []; CoreKeys[Prefix].forEach(function (Key) { var V = Get(Target, Key); if (V === undefined) Missing.push(Key + ' → ' + PathOf(Key)); else Ok.push(Key); }); Report.Groups[Prefix] = { Ok: Ok, Missing: Missing }; Missing.forEach(function (M) { Report.Missing.push(Prefix + '.' + M); }); }); if (Report.Missing.length) { console.warn('[字段抽象] 失配 ' + Report.Missing.length + ' 项:', Report.Missing); } else { console.log('[字段抽象] 全部核心字段解析正常'); } return Report; } // 找坦克物理组件(与工具内实现同源,放这里方便自检) function FindPhysics(Tank) { if (!Tank) return null; var Map = Get(Tank, 'Entity.Components'); if (!Map || typeof Map.forEach !== 'function') return null; var Found = null; Map.forEach(function (ValueList) { if (Found) return; var Comp = (ValueList && typeof ValueList.g1 === 'function') ? ValueList.g1(0) : ValueList; if (!Comp) return; var Hull = Get(Comp, 'Physics.Hull'); if (Hull && Get(Hull, 'Shape.Name') === 'hull-main') Found = Comp; }); return Found; } // ===== 导出 ===== window.__hornetSchema__ = { Version: 'main.7cfa4058', Fields: Fields, Exports: Exports, Get: Get, PathOf: PathOf, FieldName: FieldName, MethodName: MethodName, Check: Check, SelfCheck: SelfCheck, ResolvePath: ResolvePath, FindPhysics: FindPhysics }; console.log('[3DTank] 字段抽象层已加载(版本 main.7cfa4058,语义字段 ' + Object.keys(Fields).length + ' 项)'); })(); // ===== hornet_lib.js ===== // ===== 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(', ')); })(); // ===== hornet_radar.js ===== // ===== 3DTank雷达 - 2D俯视图雷达 ===== // 在浏览器控制台运行(需先运行 hook_gamemode.js) // 功能:在屏幕右上角显示2D雷达,显示所有坦克的相对位置 (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; } // ===== 匿名字段常量 ===== var FVecX = Fld('Vec.X', 'f20_1'); var FVecY = Fld('Vec.Y', 'g20_1'); var FVecZ = Fld('Vec.Z', 'h20_1'); var FEntityId = Fld('Entity.Id', 'iz1_1'); var FEntityRoot = Fld('Entity.Root', 'qz1_1'); var FEntityComponentMap = Fld('Entity.ComponentMap', 's2a_1'); var FTankHullComponent = Fld('Tank.HullComponent', 'qzv_1'); var FTankHullDirection = Fld('Tank.HullDirection', 'wzl_1'); var MGameModeScene = Mth('GameMode.Scene', 'lz4'); var MCollisionRaycast = Mth('CollisionWorld.Raycast', 'a17p'); var FRayHitPoint = Fld('Ray.HitPoint', 'y178_1'); var FRayHitNormal = Fld('Ray.HitNormal', 'z178_1'); var FRayDistance = Fld('Ray.Distance', 'a179_1'); var FRayHitShape = Fld('Ray.HitShape', 'q17o_1'); var FRayHitBody = Fld('Ray.HitBody', 'b179_1'); var FRayFilterQuery = Fld('RayFilter.Query', 'l1da_1'); function _start() { console.log('===== 3DTank雷达启动 ====='); // 允许在未进入战局 / ght 尚未捕获时启动:先出雷达框,内容等进入战局再出 if (window.__hornetRadar__) { window.__hornetRadar__.stop(); console.log('已停止旧雷达'); } var gm = window.__gameMode__ || null; // ===== 引用公共库(hornet_lib.js) ===== var lib = window.__hornetLib__; if (!lib) { console.error('[雷达] 请先运行 hornet_lib.js'); return; } var teamFns = lib.LoadTeamFunctions(); var ZCt = teamFns.ZCt, m2Fn = teamFns.m2Fn; // 人机标记:ScanBots 定期扫描(同队内装备分数相同且 < 8000)→ IsBot 查询 var ScanBots = lib.ScanBots, IsBot = lib.IsBot; // 组件查找:使用公共库(已修复 falsy 值漏判 bug) var FindComponentByField = lib.FindComponentByField; // 坦克位置:使用公共库 var GetTankPosition = lib.GetTankPosition; // 坦克状态:使用公共库(0=DEAD,1=DEAD_PHANTOM,2=SEMI_ACTIVE,3=ACTIVE,4=DISABLED) var GetTankState = lib.GetTankState; var GetTankHealth = lib.GetTankHealth; var IsTankAlive = lib.IsTankAlive; // ===== 获取坦克方向(Kkt 组件的 dzc_1) ===== function getTankDirection(tank) { var hullComp = FindComponentByField(tank, FTankHullComponent); if (hullComp && hullComp[FTankHullDirection]) { return { x: hullComp[FTankHullDirection][FVecX], y: hullComp[FTankHullDirection][FVecY], z: hullComp[FTankHullDirection][FVecZ] }; } return null; } // ===== 获取相机朝向(新版:直接用场景主相机 scene.iyx_1 的视图矩阵算水平朝向) ===== // 雷达约定:forward = (sin(angle), cos(angle)),x/y 为水平面 function getCameraHeading() { 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 }; } // 玩家坦克:使用公共库 function GetPlayerTank() { return lib.GetPlayerTank(gm); } // 友方判断:使用公共库 function IsFriendly(tank, playerTank) { return lib.IsFriendly(tank, playerTank, gm); } // 画死亡标记(物理体仍存在但已阵亡):红色 × function DrawDeadCross(x, y) { var s = 5; ctx.strokeStyle = '#ff4444'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(x - s, y - s); ctx.lineTo(x + s, y + s); ctx.moveTo(x + s, y - s); ctx.lineTo(x - s, y + s); ctx.stroke(); } // ===== 获取全局碰撞检测器(uMt 组件,挂在 GameMode entity 上) ===== var UMtComponent = null; function getUMtComponent() { if (UMtComponent) return UMtComponent; try { var gmEntity = gm[MGameModeScene](); if (!gmEntity || !gmEntity[FEntityRoot] || !gmEntity[FEntityRoot][FEntityComponentMap]) return null; gmEntity[FEntityRoot][FEntityComponentMap].forEach(function(valueList, key) { if (UMtComponent) return; try { var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList; if (comp && typeof comp[MCollisionRaycast] === 'function') UMtComponent = comp; } catch(e) {} }); if (UMtComponent) console.log('[雷达] uMt 碰撞检测器已获取'); else UMtComponent = false; } catch(e) { console.warn('[雷达] 获取 uMt 失败:', e.message); } return UMtComponent || null; } // mock O$ 向量:使用公共库 var MkVec = lib.MkVec; // ===== 射线检测:从 from 点沿 dir 方向,最大距离 maxDist,返回命中距离(-1=未命中) ===== function raycastDistance(fromPos, dirX, dirY, dirZ, maxDist) { var uMt = getUMtComponent(); if (!uMt) return -1; try { var startVec = MkVec(fromPos.x, fromPos.y, fromPos.z); var dirVec = MkVec(dirX, dirY, dirZ); var result = { [FRayHitPoint]: MkVec(0,0,0), [FRayHitNormal]: MkVec(0,0,0), [FRayDistance]: maxDist, [FRayHitShape]: null, [FRayHitBody]: null, r17o: function(t, n, i, r, e) { this[FRayHitPoint].k26(t); this[FRayHitNormal].k26(n); this[FRayDistance] = i; this[FRayHitShape] = r; this[FRayHitBody] = e; }, d3: function() { this[FRayHitBody] = null; } }; var filter = { [FRayFilterQuery]: { h1: function() { return false; }, d3: function() {} }, w179: function() { return true; }, d3: function() {}, m1da: function() {} }; var hit = uMt[MCollisionRaycast](startVec, dirVec, maxDist, result, filter); return (hit && result[FRayDistance] < maxDist) ? result[FRayDistance] : -1; } catch(e) { return -1; } } // ===== 地形高度采样(分帧,双缓冲) ===== // 双缓冲:重采样时写 newHeights,旧 heights 继续显示,完成后原子切换 // 解决闪烁:重采样期间不清空 heights,ready 保持 true var Terrain = { gridSize: 72, // 72x72 网格 range: 30000, // 采样范围(与雷达范围一致) // 当前显示的缓冲区 heights: null, originX: 0, originY: 0, minHeight: 99999, maxHeight: -99999, // 重采样时的新缓冲区(null=未在重采样) newHeights: null, newOriginX: 0, newOriginY: 0, newMinHeight: 99999, newMaxHeight: -99999, sampling: false, sampleIndex: 0, lastSampleX: 99999, lastSampleY: 99999, samplesPerFrame: 60, rayStartZ: 5000, rayMaxDist: 10000, ready: false }; // 首次初始化(heights 为 null 时调用) function initTerrain(playerPos) { Terrain.heights = new Float32Array(Terrain.gridSize * Terrain.gridSize); for (var i = 0; i < Terrain.heights.length; i++) Terrain.heights[i] = NaN; Terrain.originX = playerPos.x; Terrain.originY = playerPos.y; Terrain.minHeight = 99999; Terrain.maxHeight = -99999; Terrain.sampleIndex = 0; Terrain.sampling = true; Terrain.ready = false; } // 开始重采样(双缓冲:不清空 heights,保持 ready 状态) function startResample(playerPos) { Terrain.newHeights = new Float32Array(Terrain.gridSize * Terrain.gridSize); for (var i = 0; i < Terrain.newHeights.length; i++) Terrain.newHeights[i] = NaN; Terrain.newOriginX = playerPos.x; Terrain.newOriginY = playerPos.y; Terrain.newMinHeight = 99999; Terrain.newMaxHeight = -99999; Terrain.sampleIndex = 0; Terrain.sampling = true; // 不动 heights/originX/originY/min/max/ready —— 旧数据继续显示,避免闪烁 } function updateTerrainSampling(playerPos) { // 首次初始化 if (!Terrain.heights) { initTerrain(playerPos); Terrain.lastSampleX = playerPos.x; Terrain.lastSampleY = playerPos.y; } else if (!Terrain.sampling) { // 玩家移动超过 range/3 重新采样(双缓冲,不阻塞显示) var dx = playerPos.x - Terrain.lastSampleX; var dy = playerPos.y - Terrain.lastSampleY; if (dx*dx + dy*dy > (Terrain.range/3)*(Terrain.range/3)) { Terrain.lastSampleX = playerPos.x; Terrain.lastSampleY = playerPos.y; startResample(playerPos); } } if (!Terrain.sampling) return; var uMt = getUMtComponent(); if (!uMt) { Terrain.sampling = false; return; } var halfRange = Terrain.range / 2; var cellSize = Terrain.range / (Terrain.gridSize - 1); var sampled = 0; // 目标缓冲区:重采样时写 newHeights,首次时写 heights var target = Terrain.newHeights || Terrain.heights; var originX = Terrain.newHeights ? Terrain.newOriginX : Terrain.originX; var originY = Terrain.newHeights ? Terrain.newOriginY : Terrain.originY; var minH = Terrain.newHeights ? Terrain.newMinHeight : Terrain.minHeight; var maxH = Terrain.newHeights ? Terrain.newMaxHeight : Terrain.maxHeight; while (Terrain.sampleIndex < Terrain.gridSize * Terrain.gridSize && sampled < Terrain.samplesPerFrame) { var gx = Terrain.sampleIndex % Terrain.gridSize; var gy = Math.floor(Terrain.sampleIndex / Terrain.gridSize); var wx = originX - halfRange + gx * cellSize; var wy = originY - halfRange + gy * cellSize; // 向下射线检测地面高度 var dist = raycastDistance( { x: wx, y: wy, z: Terrain.rayStartZ }, 0, 0, -1, Terrain.rayMaxDist ); var height = dist > 0 ? (Terrain.rayStartZ - dist) : NaN; target[Terrain.sampleIndex] = height; if (!isNaN(height)) { if (height < minH) minH = height; if (height > maxH) maxH = height; } Terrain.sampleIndex++; sampled++; } // 回写 min/max(基本类型,需手动赋值) if (Terrain.newHeights) { Terrain.newMinHeight = minH; Terrain.newMaxHeight = maxH; } else { Terrain.minHeight = minH; Terrain.maxHeight = maxH; } if (Terrain.sampleIndex >= Terrain.gridSize * Terrain.gridSize) { // 采样完成:原子切换 if (Terrain.newHeights) { Terrain.heights = Terrain.newHeights; Terrain.originX = Terrain.newOriginX; Terrain.originY = Terrain.newOriginY; Terrain.minHeight = Terrain.newMinHeight; Terrain.maxHeight = Terrain.newMaxHeight; Terrain.newHeights = null; console.log('[雷达] 地形重采样完成, 高度范围:', Terrain.minHeight.toFixed(0), '~', Terrain.maxHeight.toFixed(0)); } else { console.log('[雷达] 地形采样完成, 高度范围:', Terrain.minHeight.toFixed(0), '~', Terrain.maxHeight.toFixed(0)); } Terrain.sampling = false; Terrain.ready = true; } } var _terrainCanvas = null; function drawTerrain(ctx, playerPos, playerAngle) { if (!Terrain.ready || !Terrain.heights) return; if (!_terrainCanvas) { _terrainCanvas = document.createElement('canvas'); _terrainCanvas.width = radius * 2; _terrainCanvas.height = radius * 2; } var tCtx = _terrainCanvas.getContext('2d'); var w = radius * 2; var imgData = tCtx.createImageData(w, w); var data = imgData.data; var cosA = Math.cos(playerAngle); var sinA = Math.sin(playerAngle); var halfRange = Terrain.range / 2; var cellSize = Terrain.range / (Terrain.gridSize - 1); var heightRange = Terrain.maxHeight - Terrain.minHeight; if (heightRange < 1) heightRange = 1; var originX = Terrain.originX - halfRange; var originY = Terrain.originY - halfRange; var scale = Terrain.range / (radius * 2); // 雷达像素→世界距离 // 超出网格范围/未采样点的默认暗色(避免空洞) var DEF_R = 15, DEF_G = 20, DEF_B = 35, DEF_A = 180; // 遍历每个雷达像素,反算世界坐标,双线性插值查询高度图 // 海拔配色:蓝(低)→绿(中)→紫(高) for (var py = 0; py < w; py++) { for (var px = 0; px < w; px++) { var ox = px - radius; // 相对中心x var oy = radius - py; // 相对中心y(反转) // 逆旋转(雷达→世界) var rdx = ox * cosA + oy * sinA; var rdy = -ox * sinA + oy * cosA; var wx = playerPos.x + rdx * scale; var wy = playerPos.y + rdy * scale; var idx = (py * w + px) * 4; // 双线性插值查询高度图 var gxf = (wx - originX) / cellSize; var gyf = (wy - originY) / cellSize; var gx0 = Math.floor(gxf); var gy0 = Math.floor(gyf); // 超出网格范围:填充默认暗色(不跳过,解决高度图不全) if (gx0 < 0 || gx0 >= Terrain.gridSize || gy0 < 0 || gy0 >= Terrain.gridSize) { data[idx] = DEF_R; data[idx + 1] = DEF_G; data[idx + 2] = DEF_B; data[idx + 3] = DEF_A; continue; } var gx1 = Math.min(gx0 + 1, Terrain.gridSize - 1); var gy1 = Math.min(gy0 + 1, Terrain.gridSize - 1); var fx = gxf - gx0; var fy = gyf - gy0; // 四个角的高度 var h00 = Terrain.heights[gy0 * Terrain.gridSize + gx0]; var h10 = Terrain.heights[gy0 * Terrain.gridSize + gx1]; var h01 = Terrain.heights[gy1 * Terrain.gridSize + gx0]; var h11 = Terrain.heights[gy1 * Terrain.gridSize + gx1]; // 未采样到的点:填充默认暗色 if (isNaN(h00) || isNaN(h10) || isNaN(h01) || isNaN(h11)) { data[idx] = DEF_R; data[idx + 1] = DEF_G; data[idx + 2] = DEF_B; data[idx + 3] = DEF_A; continue; } // 双线性插值 var h0 = h00 * (1 - fx) + h10 * fx; // y=gy0 行 x 方向插值 var h1 = h01 * (1 - fx) + h11 * fx; // y=gy1 行 x 方向插值 var h = h0 * (1 - fy) + h1 * fy; // y 方向插值 var norm = (h - Terrain.minHeight) / heightRange; // 蓝(50,100,230) → 绿(80,210,90) → 紫(190,80,220) var r, g, b; if (norm < 0.5) { var t = norm * 2; // 0~1 r = Math.floor(50 + (80 - 50) * t); g = Math.floor(100 + (210 - 100) * t); b = Math.floor(230 + (90 - 230) * t); } else { var t = (norm - 0.5) * 2; // 0~1 r = Math.floor(80 + (190 - 80) * t); g = Math.floor(210 + (80 - 210) * t); b = Math.floor(90 + (220 - 90) * t); } data[idx] = r; data[idx + 1] = g; data[idx + 2] = b; data[idx + 3] = 200; } } tCtx.putImageData(imgData, 0, 0); ctx.save(); ctx.globalAlpha = 0.6; ctx.drawImage(_terrainCanvas, cx - radius, cy - radius); ctx.restore(); } // ===== 创建Canvas ===== var canvas = document.createElement('canvas'); canvas.id = 'hornetRadarCanvas'; canvas.width = 320; canvas.height = 320; canvas.style.cssText = 'position:fixed;top:10px;right:10px;z-index:99999;pointer-events:auto;cursor:move;border:2px solid rgba(255,255,255,0.3);border-radius:5px;background:rgba(0,0,0,0.3);box-shadow:0 0 10px rgba(0,0,0,0.3);'; document.body.appendChild(canvas); if (lib.MakeDraggable) lib.MakeDraggable(canvas, 'radar'); var ctx = canvas.getContext('2d'); var cx = canvas.width / 2; var cy = canvas.height / 2; var radius = 150; var radarRange = 30000; // 雷达范围(世界单位),可通过 Alt+/Alt- 调整 var RADAR_MIN = 3000, RADAR_MAX = 60000, RADAR_STEP = 1.5; // 从 localStorage 恢复范围 try { var __savedRR = localStorage.getItem('hornet_radar_range'); if (__savedRR) { var __rr = parseFloat(__savedRR); if (!isNaN(__rr) && __rr >= RADAR_MIN && __rr <= RADAR_MAX) radarRange = __rr; } } catch(e) {} // 透明度 var RadarOpacity = 1.0; try { var __savedRO = localStorage.getItem('hornet_radar_opacity'); if (__savedRO) { var __ro = parseFloat(__savedRO); if (!isNaN(__ro) && __ro >= 0.2 && __ro <= 1) { RadarOpacity = __ro; canvas.style.opacity = String(__ro); } } } catch(e) {} // ===== Alt+/Alt- 调整雷达范围(Alt+ 放大看局部,Alt- 缩小看全场) ===== function adjustRange(zoomIn) { var newRange = zoomIn ? Math.max(RADAR_MIN, radarRange / RADAR_STEP) : Math.min(RADAR_MAX, radarRange * RADAR_STEP); if (newRange === radarRange) return; radarRange = newRange; try { localStorage.setItem('hornet_radar_range', String(radarRange)); } catch(e) {} Terrain.range = newRange; // 强制下一帧 updateTerrainSampling 触发重采样(双缓冲:保持旧数据可见) Terrain.lastSampleX = 99999; Terrain.lastSampleY = 99999; // 不设 ready=false —— 旧 heights 继续显示,避免闪烁 // 若正在采样则中止,用新 range 重新开始 Terrain.newHeights = null; Terrain.sampling = false; console.log('[雷达] 范围' + (zoomIn ? '缩小(放大局部)' : '扩大(看全场)') + ': ' + (radarRange / 100).toFixed(0) + 'm'); } var _keyHandler = function(e) { if (!e.altKey) return; var key = e.key.toLowerCase(); if (key === '+' || key === '=' || e.code === 'NumpadAdd' || e.code === 'Equal') { e.preventDefault(); adjustRange(true); } else if (key === '-' || key === '_' || e.code === 'NumpadSubtract' || e.code === 'Minus') { e.preventDefault(); adjustRange(false); } }; window.addEventListener('keydown', _keyHandler); // ===== 雷达状态 ===== var radar = { canvas: canvas, ctx: ctx, running: true, frameCount: 0, tankCount: 0, playerFound: false, lastPlayerPos: null, // 上一帧玩家位置(用于计算朝向) playerHeading: 0, // 玩家朝向角度(弧度) stop: function() { this.running = false; if (canvas.parentNode) canvas.parentNode.removeChild(canvas); window.removeEventListener('keydown', _keyHandler); console.log('雷达已停止'); }, setRange: function(r) { var n = parseFloat(r); if (!isNaN(n) && n >= RADAR_MIN && n <= RADAR_MAX) { radarRange = n; try { localStorage.setItem('hornet_radar_range', String(n)); } catch(e) {} Terrain.range = n; Terrain.lastSampleX = 99999; Terrain.lastSampleY = 99999; Terrain.newHeights = null; Terrain.sampling = false; console.log('[雷达] 范围设为: ' + (n/100).toFixed(0) + 'm'); } else { console.warn('[雷达] 范围应在 ' + (RADAR_MIN/100) + '~' + (RADAR_MAX/100) + 'm'); } }, getRange: function() { return radarRange; }, setOpacity: function(o) { var n = parseFloat(o); if (!isNaN(n) && n >= 0.2 && n <= 1) { RadarOpacity = n; canvas.style.opacity = String(n); try { localStorage.setItem('hornet_radar_opacity', String(n)); } catch(e) {} } else { console.warn('[雷达] 透明度应在 0.2~1'); } }, getOpacity: function() { return RadarOpacity; } }; window.__hornetRadar__ = radar; // ===== 渲染循环 ===== function render() { if (!radar.running) return; radar.frameCount++; // ===== 新战斗/新地图自刷新:gameMode 实例变化时重置缓存并重新采样地形 ===== var NewGm = window.__gameMode__; if (NewGm && NewGm !== gm) { gm = NewGm; UMtComponent = null; // 碰撞检测器需从新场景重新获取 radar.lastPlayerPos = null; radar.playerHeading = 0; Terrain.heights = null; // 丢弃旧地图高度图,触发重新采样 Terrain.newHeights = null; Terrain.sampling = false; Terrain.ready = false; Terrain.lastSampleX = 99999; Terrain.lastSampleY = 99999; radar.tankCount = 0; console.log('[雷达] 检测到新战斗/新地图,已重置缓存并重新采样地形'); } // 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) { tankList = gm.m10b_1.j11y(); count = tankList.e1(); } radar.tankCount = count; // 人机扫描:装备分数不会每帧变,节流到约每 0.3 秒一次 if (Ready && ScanBots && (Date.now() - (radar._lastBotScan || 0) > 300)) { radar._lastBotScan = Date.now(); ScanBots(); } // 获取玩家坦克(带备用方案) var playerTank = Ready ? GetPlayerTank() : null; radar.playerFound = (playerTank !== null && playerTank !== undefined); // 自由视角激活时,用虚拟相机位置 var fl = window.__hornetFreeLook__; var flActive = fl && typeof fl.IsActive === 'function' && fl.IsActive(); // 获取玩家位置和方向 var playerPos = flActive ? fl.GetPosition() : (playerTank ? GetTankPosition(playerTank) : null); var playerDir = flActive ? fl.GetDirection() : (playerTank ? getTankDirection(playerTank) : null); // 地形高度采样(分帧) if (playerPos) updateTerrainSampling(playerPos); // 清空Canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // 画雷达背景(方形) ctx.fillStyle = 'rgba(0, 20, 0, 0.4)'; ctx.fillRect(cx - radius, cy - radius, radius * 2, radius * 2); ctx.strokeStyle = 'rgba(100, 255, 100, 0.5)'; ctx.lineWidth = 2; ctx.strokeRect(cx - radius, cy - radius, radius * 2, radius * 2); // 画十字线 ctx.beginPath(); ctx.moveTo(cx - radius, cy); ctx.lineTo(cx + radius, cy); ctx.moveTo(cx, cy - radius); ctx.lineTo(cx, cy + radius); ctx.strokeStyle = 'rgba(100, 255, 100, 0.2)'; ctx.lineWidth = 1; ctx.stroke(); // 画距离网格线(方形) for (var r = radius / 3; r < radius; r += radius / 3) { ctx.beginPath(); ctx.moveTo(cx - radius, cy - r); ctx.lineTo(cx + radius, cy - r); ctx.moveTo(cx - radius, cy + r); ctx.lineTo(cx + radius, cy + r); ctx.moveTo(cx - r, cy - radius); ctx.lineTo(cx - r, cy + radius); ctx.moveTo(cx + r, cy - radius); ctx.lineTo(cx + r, cy + radius); ctx.strokeStyle = 'rgba(100, 255, 100, 0.15)'; ctx.stroke(); } // 获取相机朝向(偏航角)——仅在战局内 var camHeading = Ready ? getCameraHeading() : null; var playerAngle = radar.playerHeading; // 自由视角激活时,直接用虚拟相机 yaw(取反:雷达旋转方向与相机yaw相反) if (flActive && playerDir) { playerAngle = -playerDir.yaw; radar.playerHeading = playerAngle; } else if (camHeading && camHeading.yaw !== undefined) { // 使用相机偏航角作为雷达朝向 playerAngle = camHeading.yaw; radar.playerHeading = playerAngle; } else if (playerPos && radar.lastPlayerPos) { // 备用方案:通过玩家位置变化计算朝向 var ddx = playerPos.x - radar.lastPlayerPos.x; var ddy = playerPos.y - radar.lastPlayerPos.y; var moveDist = Math.sqrt(ddx * ddx + ddy * ddy); if (moveDist > 0.5) { playerAngle = Math.atan2(ddx, ddy); radar.playerHeading = playerAngle; } } if (playerPos) radar.lastPlayerPos = playerPos; radar._camHeading = camHeading; // 保存用于调试 // 绘制地形高度图 if (playerPos) drawTerrain(ctx, playerPos, playerAngle); // 绘制所有坦克 var friendlyCount = 0, enemyCount = 0, deadCount = 0, botCount = 0; for (var i = 0; i < count; i++) { var tank = tankList.g1(i); if (!tank) continue; // 跳过玩家自己 if (playerTank && tank === playerTank) continue; var pos = GetTankPosition(tank); if (!pos || !playerPos) continue; // 计算相对位置(x和y是水平面) var dx = pos.x - playerPos.x; var dy = pos.y - playerPos.y; // 旋转到相机朝向(相机朝上=北) var cosA = Math.cos(playerAngle); var sinA = Math.sin(playerAngle); var rx = dx * cosA - dy * sinA; var ry = dx * sinA + dy * cosA; // 缩放到雷达范围 var radarX = cx + (rx / radarRange) * radius; var radarY = cy - (ry / radarRange) * radius; // 注意y轴反转(屏幕y向下) // 限制在雷达方形内 if (radarX < cx - radius) radarX = cx - radius; if (radarX > cx + radius) radarX = cx + radius; if (radarY < cy - radius) radarY = cy - radius; if (radarY > cy + radius) radarY = cy + radius; // 死亡判断(同 3D透视):直接调用引擎的 s14j();血量归零也算死亡(状态偶有滞后) var tankState = GetTankState(tank); var hp = GetTankHealth(tank); if (!IsTankAlive(tank) || (hp && hp.current <= 0)) { DrawDeadCross(radarX, radarY); deadCount++; continue; } // 判断友方/敌方 var friendly = IsFriendly(tank, playerTank); var color = friendly ? '#4488ff' : '#ff4444'; if (friendly) friendlyCount++; else enemyCount++; // 画坦克点:人机用「灰方块 + 黄框」,真人用「实心圆 + 白边」 var bot = (typeof IsBot === 'function') && IsBot(tank); if (bot) botCount++; if (bot) { ctx.beginPath(); ctx.rect(radarX - 5, radarY - 5, 10, 10); ctx.fillStyle = 'rgba(170,170,170,0.55)'; ctx.fill(); ctx.strokeStyle = '#ffd24a'; ctx.lineWidth = 1.5; ctx.stroke(); } else { ctx.beginPath(); ctx.arc(radarX, radarY, 5, 0, 2 * Math.PI); ctx.fillStyle = color; ctx.fill(); ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 1; ctx.stroke(); } } // 画玩家(中心,三角形朝上) ctx.beginPath(); ctx.moveTo(cx, cy - 7); ctx.lineTo(cx - 5, cy + 5); ctx.lineTo(cx + 5, cy + 5); ctx.closePath(); ctx.fillStyle = '#00ff00'; ctx.fill(); ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 1; ctx.stroke(); // 画北向指示 ctx.fillStyle = 'rgba(100, 255, 100, 0.8)'; ctx.font = '10px Arial'; ctx.textAlign = 'center'; ctx.fillText('N', cx, cy - radius + 12); // 等待战局提示(ght 尚未捕获 / 不在战局) if (!Ready) { ctx.fillStyle = 'rgba(255, 220, 120, 0.95)'; ctx.font = 'bold 11px Arial'; ctx.textAlign = 'center'; ctx.fillText('等待战局…', cx, cy + 4); } // 信息文本 ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; ctx.font = '9px Arial'; ctx.textAlign = 'left'; ctx.fillText('Tanks: ' + count + ' (B:' + friendlyCount + ' R:' + enemyCount + ' X:' + deadCount + ' Bot:' + botCount + ')', 5, 12); if (radar.playerFound) { ctx.fillText('Player: ' + (playerTank ? playerTank[FEntityId] : '?'), 5, 24); } else { ctx.fillText('Player: NOT FOUND', 5, 24); } ctx.fillText('Cam: ' + (radar._camHeading ? 'OK' : 'NULL'), 5, 36); ctx.fillText('Angle: ' + (radar.playerHeading * 180 / Math.PI).toFixed(0), 5, 48); ctx.fillText('Range: ' + (radarRange / 100).toFixed(0) + 'm', 5, 60); ctx.fillText('Frame: ' + radar.frameCount, 5, canvas.height - 16); ctx.fillText('Alt+/-: zoom', 5, canvas.height - 5); } catch(e) { ctx.fillStyle = 'rgba(255, 0, 0, 0.8)'; ctx.font = '10px Arial'; ctx.textAlign = 'left'; ctx.fillText('Error: ' + e.message, 5, 15); } requestAnimationFrame(render); } // 启动渲染 requestAnimationFrame(render); console.log('雷达已启动'); console.log('提示: window.__hornetRadar__.stop() 停止雷达'); console.log('提示: window.__hornetRadar__.canvas 获取Canvas元素'); } var Tool = { Start: function() { if (window.__hornetRadar__ && window.__hornetRadar__.running) return; _start(); }, Stop: function() { if (window.__hornetRadar__) window.__hornetRadar__.stop(); }, Status: function() { return (window.__hornetRadar__ && window.__hornetRadar__.running) ? '运行中' : '已停止'; }, GetRunning: function() { return !!(window.__hornetRadar__ && window.__hornetRadar__.running); }, SetRange: function(r) { if (window.__hornetRadar__) window.__hornetRadar__.setRange(r); }, GetRange: function() { return window.__hornetRadar__ ? window.__hornetRadar__.getRange() : 30000; }, SetOpacity: function(o) { if (window.__hornetRadar__) window.__hornetRadar__.setOpacity(o); }, GetOpacity: function() { return window.__hornetRadar__ ? window.__hornetRadar__.getOpacity() : 1.0; } }; window.__hornetTools__ = window.__hornetTools__ || {}; window.__hornetTools__.Radar = Tool; console.log('[3DTank] 雷达模块已加载'); })(); // ===== hornet_3d_wireframe.js ===== // ===== 3DTank3D透视 - 线框/描边双模式 ===== // 在浏览器控制台运行(需先运行 hook_gamemode.js) // 功能:在全屏叠加层上绘制敌方坦克的3D模型 // 模式: // wireframe - 完整线框(绘制所有边) // outline - 屏幕空间轮廓描边(只画外轮廓,合并所有mesh节点) // 切换:window.__hornet3DWireframe__.setMode('outline') (function() { function _start() { console.log('===== 3DTank3D透视启动 ====='); // 允许在未进入战局 / ght 尚未捕获时启动:先出叠加层与状态行,模型等进入战局再出 if (window.__hornet3DWireframe__) { window.__hornet3DWireframe__.stop(); console.log('已停止旧透视'); } if (window.__hornet3DOutline__) { window.__hornet3DOutline__.stop(); console.log('已停止旧描边透视'); } var gm = window.__gameMode__ || null; // ===== 字段抽象层(匿名引擎字段 → 语义名,总表在 hornet_schema.js)===== // 换版本时只改 schema;Fld 返回当前版本的字面名,schema 缺失时退回 fallback var Schema = window.__hornetSchema__ || null; function Fld(Key, Fallback) { var N = Schema ? Schema.FieldName(Key) : null; return N || Fallback; } var FNodeName = Fld('Node.Name', 'g35_1'); // 节点名 var FNodeMatrix = Fld('Node.Matrix', 'p35_1'); // 世界矩阵 var FNodeChildren = Fld('Node.Children', 'r35_1'); // 子节点列表 var FNodeMesh = Fld('Node.Mesh', 'v3m_1'); // mesh 资源(Tp) var FNodeLod = Fld('Node.Lod', 'u4x_1'); // LOD 列表 var FSkinHullSkin = Fld('Skin.HullSkin', 'g159_1'); // 皮肤对象 var FSkinHullRoot = Fld('Skin.HullRoot', 'h159_1'); // 车身渲染树根 var FSkinParts = Fld('Skin.Parts', 'g17r_1'); // 部件表 var FSkinPartNode = Fld('Skin.PartNode', 'j17r_1'); // 部件 → BP 节点 var FNodeMatrixOld = Fld('Node.MatrixOld', 'n35_1'); // 旧版世界矩阵(兜底) // 实体 / 向量 / 战斗实例 var FEntityId = Fld('Entity.Id', 'iz1_1'); // 实体 ID var FEntityRoot = Fld('Entity.Root', 'qz1_1'); // 组件宿主根对象 var FEntityComponentMap = Fld('Entity.ComponentMap', 's2a_1'); // 宿主上的组件 Map var FVecX = Fld('Vec.X', 'f20_1'); // 向量 X var FVecY = Fld('Vec.Y', 'g20_1'); // 向量 Y var FVecZ = Fld('Vec.Z', 'h20_1'); // 向量 Z var FGameModeTankManager = Fld('GameMode.TankManager', 'm10b_1'); // 坦克管理器 var FTeamId = Fld('Team.Id', 'k3_1'); // 队伍编号 // 炮塔皮肤(LUt / CUt 包装) var FTurretWrapper = Fld('Turret.Wrapper', 'a11p_1'); // 炮塔组件包装对象 var FTurretInnerWrapper = Fld('Turret.InnerWrapper', 'e11p_1'); // 包装内的 CUt 包装 var FTurretRootNode = Fld('Turret.RootNode', 'a17q_1'); // 炮塔 BP 根节点 // mesh 资源(Tp / J$ / Y$) var FMeshVertexGroups = Fld('Mesh.VertexGroups', 'x4w_1'); // 顶点组列表 var FMeshVertexBuffer = Fld('Mesh.VertexBuffer', 'h3x_1'); // 顶点缓冲资源 var FMeshGpuBuffer = Fld('Mesh.GpuBuffer', 'v3x_1'); // GPU 缓冲包装 var FMeshVertexCount = Fld('Mesh.VertexCount', 'w3x_1'); // 顶点数 var FMeshCpuVertexData = Fld('Mesh.CpuVertexData', 'x3x_1'); // CPU 端顶点原始数据 var FMeshIndexGroups = Fld('Mesh.IndexGroups', 'y4w_1'); // 索引组列表 var FMeshIndexBuffer = Fld('Mesh.IndexBuffer', 'q4w_1'); // 索引缓冲资源 var FMeshIndexBufferOld = Fld('Mesh.IndexBufferOld', 'u3p_1'); // 旧版索引缓冲字段 var FMeshCpuIndexData = Fld('Mesh.CpuIndexData', 'p3x_1'); // CPU 端索引原始数据 var FMeshIndexGpuBuffer = Fld('Mesh.IndexGpuBuffer', 'n3x_1'); // 索引 GPU 缓冲包装 // 渲染设备 / 原生缓冲 var FRenderDevice = Fld('Render.Device', 's3h_1'); // GL 包装器 var FRenderDeviceNative = Fld('Render.DeviceNative', 'f75_1'); // 原生 WebGL2RenderingContext var FRenderDrawBuffer = Fld('Render.DrawBuffer', 'd75_1'); // 原生 WebGLBuffer // ===== 当前模式 ===== var Mode = 'outline'; // 'wireframe' | 'outline' try { var __savedMode = localStorage.getItem('hornet_wireframe_mode'); if (__savedMode === 'wireframe' || __savedMode === 'outline') Mode = __savedMode; } catch(e) {} // ===== 线宽设置 ===== var LineWidth = 2; try { var __savedLW = localStorage.getItem('hornet_wireframe_linewidth'); if (__savedLW) { var __n = parseFloat(__savedLW); if (!isNaN(__n) && __n >= 0.5 && __n <= 8) LineWidth = __n; } } catch(e) {} // ===== 渲染模式 ===== // 'team' - 队伍模式:所有坦克按队伍渲染(蓝=友方,红=敌方) // 'whitelist' - 白名单:只有名单中的坦克渲染(按队伍颜色),其他不渲染 // 'blacklist' - 黑名单:名单中的坦克不渲染,其他按队伍颜色渲染 var HighlightMode = 'team'; try { var __savedHM = localStorage.getItem('hornet_wireframe_highlight_mode'); if (__savedHM && ['team','whitelist','blacklist'].indexOf(__savedHM) >= 0) { HighlightMode = __savedHM; } } catch(e) {} // ===== 名单 ===== var HighlightList = []; try { var __savedHL = localStorage.getItem('hornet_highlight_list'); if (__savedHL) HighlightList = JSON.parse(__savedHL); } catch(e) {} function SaveHighlightList() { try { localStorage.setItem('hornet_highlight_list', JSON.stringify(HighlightList)); } catch(e) {} } function IsInList(name) { if (!name) return false; for (var i = 0; i < HighlightList.length; i++) { if (HighlightList[i] === name) return true; } return false; } // 判断坦克是否应渲染(根据 HighlightMode) function ShouldRender(name) { switch (HighlightMode) { case 'team': return true; // 全部渲染 case 'whitelist': return IsInList(name); // 名单内渲染 case 'blacklist': return !IsInList(name); // 名单外渲染 default: return true; } } // ===== 引用公共库(hornet_lib.js) ===== var lib = window.__hornetLib__; if (!lib) { console.error('[透视] 请先运行 hornet_lib.js'); return; } var teamFns = lib.LoadTeamFunctions(); var ZCt = teamFns.ZCt, m2Fn = teamFns.m2Fn; // 公共函数别名(调用处不变) var FindComponentByField = lib.FindComponentByField; var GetTankPosition = lib.GetTankPosition; var GetTankState = lib.GetTankState; var IsTankAlive = lib.IsTankAlive; var GetTankName = lib.GetTankName; var GetTankHealth = lib.GetTankHealth; function GetPlayerTank() { return lib.GetPlayerTank(gm); } function IsFriendly(tank, playerTank) { return lib.IsFriendly(tank, playerTank, gm); } function GetCameraInfo() { // 自由视角激活时,用虚拟相机参数(位置/方向跟随鼠标) var fl = window.__hornetFreeLook__; if (fl && typeof fl.IsActive === 'function' && fl.IsActive()) { var pos = fl.GetPosition(); var dir = fl.GetDirection(); if (pos && dir) { return { pos: pos, yaw: dir.yaw, pitch: dir.pitch, fov: 1.0471976 }; } } 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 秒一次) // ===== 热点优化:相机基每帧只算一次(避免逐顶点重复三角函数 + 对象分配)===== var Cam = { fx: 0, fy: 0, fz: 0, rx: 0, ry: 0, rz: 0, ux: 0, uy: 0, uz: 0, 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 Cam.rx = cy; Cam.ry = sy; Cam.rz = 0; // right Cam.ux = Cam.ry * Cam.fz - Cam.rz * Cam.fy; // up = right × forward Cam.uy = Cam.rz * Cam.fx - Cam.rx * Cam.fz; 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; } // 顶点缓冲(全局复用,杜绝每帧每顶点对象分配) var VB_Wx = null, VB_Wy = null, VB_Wz = null; // 世界坐标(法线用) var VB_Sx = null, VB_Sy = null, VB_Ok = null; // 屏幕坐标 + 可投影标志 function EnsureVerts(n) { if (!VB_Wx || VB_Wx.length < n) { var cap = Math.max(n, 2048); VB_Wx = new Float32Array(cap); VB_Wy = new Float32Array(cap); VB_Wz = new Float32Array(cap); VB_Sx = new Float32Array(cap); VB_Sy = new Float32Array(cap); VB_Ok = new Uint8Array(cap); } } // 变换 + 投影一个 mesh 节点的全部顶点到复用缓冲,返回顶点数 function PrepareNodeVerts(meshData, worldMatrix, pos) { var positions = meshData.positions; var stride = meshData.stride || 3; var vc = Math.floor(positions.length / stride); EnsureVerts(vc); var m0 = worldMatrix[0], m1 = worldMatrix[1], m2 = worldMatrix[2], m4 = worldMatrix[4], m5 = worldMatrix[5], m6 = worldMatrix[6], 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; var focal = Cam.focal, halfW = Cam.halfW, halfH = Cam.halfH; var cpx = Cam.px, cpy = Cam.py, cpz = Cam.pz; for (var v = 0; v < vc; v++) { var bx = positions[v * stride], by = positions[v * stride + 1], bz = positions[v * stride + 2]; var wx = m0 * bx + m4 * by + m8 * bz + m12; var wy = m1 * bx + m5 * by + m9 * bz + m13; var wz = m2 * bx + m6 * by + m10 * bz + m14; VB_Wx[v] = wx; VB_Wy[v] = wy; VB_Wz[v] = wz; var ddx = wx - px, ddy = wy - py, ddz = wz - pz; if (ddx * ddx + ddy * ddy + ddz * ddz > 6250000) { VB_Ok[v] = 0; continue; } var relX = wx - cpx, relY = wy - cpy, relZ = wz - cpz; var camZ = relX * cfx + relY * cfy + relZ * cfz; if (camZ <= 0.1) { VB_Ok[v] = 0; continue; } var camX = relX * crx + relY * cry + relZ * crz; var camY = relX * cux + relY * cuy + relZ * cuz; VB_Sx[v] = halfW + (camX / camZ) * focal; VB_Sy[v] = halfH - (camY / camZ) * focal; VB_Ok[v] = 1; } return vc; } // ===== 皮肤组件(新旧差异修正) ===== // 新版(main.7cfa4058): // skin 组件(类 eUt)构造后由 j16r 把皮肤对象存到 **组件自己的 g159_1**, // 皮肤对象.g17r_1 = 车身部件表;每个部件 part.j17r_1 是**单个 BP 节点**(旧版是节点列表)。 // 炮塔根节点 = skinComp.a17q_1(weaponSkin 组件 LUt.u11p() = e13a().a17q_1,e13a 取的就是 skin 组件)。 // 旧写法(w14w_1 / n11c_1 / m17a_1)在新版已被无关类占用(w14w_1=SetReverseBackTurn、m17a_1=数值参数),不再可用。 var SkinCompCache = new WeakMap(); function GetSkinComponent(entity) { if (!entity) return null; var Cached = SkinCompCache.get(entity); if (Cached) return Cached; var comp = FindComponentByField(entity, FSkinHullSkin); if (!comp) comp = FindComponentByField(entity, FTurretRootNode); // 兜底:按结构找 —— 组件字段值是"皮肤对象"(含 g17r_1 部件表) if (!comp) { try { entity[FEntityRoot][FEntityComponentMap].forEach(function (VL) { if (comp) return; var C = (VL && typeof VL.g1 === 'function') ? VL.g1(0) : VL; if (!C) return; for (var K in C) { var V = C[K]; if (V && typeof V === 'object' && V[FSkinParts] && typeof V[FSkinParts].e1 === 'function') { comp = C; return; } } }); } catch (e) {} } if (comp) SkinCompCache.set(entity, comp); return comp; } // 取车身皮肤对象(含 g17r_1 部件表);stats 用于诊断来源 function GetHullSkin(tank, stats) { var comp = GetSkinComponent(tank); if (!comp) { if (stats) stats.hullSource = '无皮肤组件'; return null; } if (comp[FSkinHullSkin] && comp[FSkinHullSkin][FSkinParts]) { if (stats) stats.hullSource = 'g159_1'; return comp[FSkinHullSkin]; } if (stats) stats.hullSource = '有组件但无皮肤'; return null; } // 炮塔 BP 根节点 // 新版:weaponSkin 组件(类 LUt,构造首字段 a11p_1)里 e11p_1 = CUt 包装,其 a17q_1 = 炮塔节点 function GetTurretNode(entity) { var wp = FindComponentByField(entity, FTurretWrapper); if (wp) { if (wp[FTurretInnerWrapper] && wp[FTurretInnerWrapper][FTurretRootNode]) return wp[FTurretInnerWrapper][FTurretRootNode]; if (typeof wp.u11p === 'function') { try { return wp.u11p(); } catch (e) {} } } var comp = GetSkinComponent(entity); if (comp && comp[FTurretRootNode]) return comp[FTurretRootNode]; return null; } // 兼容两种集合:引擎列表类(e1/g1)与迭代器(t/u/v) function ForEachInList(list, fn) { if (!list) return; try { if (typeof list.e1 === 'function' && typeof list.g1 === 'function') { var n = list.e1(); for (var i = 0; i < n; i++) fn(list.g1(i)); return; } if (typeof list.t === 'function') { for (var it = list.t(); it.u();) fn(it.v()); } } catch (e) {} } // ===== 从GPU读取顶点位置数据(新版结构) ===== // 新旧差异(main.7cfa4058): // Tp(类 sm): x4w_1 = 顶点组列表,每组 { h3x_1: 顶点缓冲资源 J$, i3x_1: 属性布局 } // J$: v3x_1 = GPU 缓冲包装(kC,d75_1 = 原生 WebGLBuffer),w3x_1 = 顶点数 // 设备:V$.s3h_1 = GL 包装器 $C,$C.f75_1 = 原生 WebGL2RenderingContext // ⚠ 旧字段 d3x_1 / s3h_1.b74_1 / v3x_1.z73_1 在新版分别是"渲染参数 / 雾色 uniform / uniform 槽位",全错 function GetGlFromResource(Res) { if (!Res) return null; var Dev = Res[FRenderDevice]; return (Dev && Dev[FRenderDeviceNative]) ? Dev[FRenderDeviceNative] : null; } // 把引擎的 CPU 端缓冲区包装转成 TypedArray // J$.x3x_1 / Y$.p3x_1 = 未上传时的原始数据(上传后置 null) // 包装接口:v2u()=底层视图,g2t()=元素偏移,t2t()=元素个数 function ToTypedArray(Src, Kind) { if (!Src) return null; try { if (typeof Src.v2u === 'function') { var V = Src.v2u(); var Start = typeof Src.g2t === 'function' ? Src.g2t() : 0; var Count = typeof Src.t2t === 'function' ? Src.t2t() : (V && V.length); if (ArrayBuffer.isView(V) && Count > 0) { var ByteOff = V.byteOffset + Start * V.BYTES_PER_ELEMENT; if (Kind === 'u32') return new Uint32Array(V.buffer, ByteOff, Count); if (Kind === 'u16') return new Uint16Array(V.buffer, ByteOff, Count); return new Float32Array(V.buffer, ByteOff, Count); } } if (ArrayBuffer.isView(Src)) { if (Kind === 'u16') return (Src instanceof Uint16Array) ? Src : new Uint16Array(Src.buffer, Src.byteOffset, Src.byteLength >> 1); if (Kind === 'u32') return (Src instanceof Uint32Array) ? Src : new Uint32Array(Src.buffer, Src.byteOffset, Src.byteLength >> 2); return (Src instanceof Float32Array) ? Src : new Float32Array(Src.buffer, Src.byteOffset, Src.byteLength >> 2); } if (typeof Src.length === 'number' && Src.length > 0) { return (Kind === 'u16') ? Uint16Array.from(Src) : (Kind === 'u32' ? Uint32Array.from(Src) : Float32Array.from(Src)); } } catch (e) {} return null; } function ReadVertexPositionsFromTp(tp) { if (!tp || !tp[FMeshVertexGroups] || typeof tp[FMeshVertexGroups].e1 !== 'function' || tp[FMeshVertexGroups].e1() === 0) return null; var group = tp[FMeshVertexGroups].g1(0); if (!group) return null; var Buf = group[FMeshVertexBuffer]; // J$ if (!Buf) return null; // ① 优先 CPU 端原始数据:未上传 GPU 的 mesh 也能读到(v3x_1.d75_1 此时为 null) var Cpu = ToTypedArray(Buf[FMeshCpuVertexData], 'f32'); if (Cpu && Cpu.length) return Cpu; // ② GPU 回读(引擎已上传并释放 CPU 数据时) var gl = GetGlFromResource(Buf); var webglBuf = Buf[FMeshGpuBuffer] && Buf[FMeshGpuBuffer][FRenderDrawBuffer]; if (!gl || !webglBuf) return null; gl.bindBuffer(gl.ARRAY_BUFFER, webglBuf); var bufSize = gl.getBufferParameter(gl.ARRAY_BUFFER, gl.BUFFER_SIZE); if (bufSize <= 0) return null; var f32 = new Float32Array(bufSize / 4); gl.getBufferSubData(gl.ARRAY_BUFFER, 0, f32); return f32; } // ===== 从GPU读取索引数据 ===== // y4w_1 = 索引列表,每项 { q4w_1: 索引缓冲资源 Y$ }(旧版是 il.u3p_1) function ReadIndicesFromTp(tp) { if (!tp[FMeshIndexGroups] || typeof tp[FMeshIndexGroups].e1 !== 'function') return null; var meshCount = tp[FMeshIndexGroups].e1(); if (meshCount === 0) return null; var allIndices = []; for (var m = 0; m < meshCount; m++) { var il = tp[FMeshIndexGroups].g1(m); if (!il) continue; var IdxBuf = il[FMeshIndexBuffer] || il[FMeshIndexBufferOld]; if (!IdxBuf) continue; // ① CPU 端索引数据(Y$.p3x_1),未上传时也能用 var Cpu = ToTypedArray(IdxBuf[FMeshCpuIndexData], 'u16'); if (Cpu && Cpu.length) { allIndices.push(Cpu); continue; } // ② GPU 回读 var gl = GetGlFromResource(IdxBuf); var webglBuf = IdxBuf[FMeshIndexGpuBuffer] && IdxBuf[FMeshIndexGpuBuffer][FRenderDrawBuffer]; if (!gl || !webglBuf) continue; gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, webglBuf); var bufSize = gl.getBufferParameter(gl.ELEMENT_ARRAY_BUFFER, gl.BUFFER_SIZE); if (bufSize <= 0) continue; var indices = new Uint16Array(bufSize / 2); gl.getBufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, indices); allIndices.push(indices); } return allIndices; } // ===== GPU 数据缓存(按 Tp 资源引用,避免重复 GPU 读取) ===== var GpuDataCache = new Map(); var GpuCacheFrameCount = 0; var GpuCacheClearInterval = 1800; // 定期清 GPU 数据缓存(getBufferSubData 会停顿 GPU,太频繁会周期性卡顿) var StrideDiagPrinted = new WeakSet(); var GpuFailLogged = new WeakSet(); function GetGpuData(tp) { if (!tp) return null; if (GpuDataCache.has(tp)) return GpuDataCache.get(tp); var positions = ReadVertexPositionsFromTp(tp); var indices = ReadIndicesFromTp(tp); if (positions && indices && indices.length > 0) { // 计算最大索引值,推断顶点 stride var maxIdx = 0; for (var i = 0; i < indices.length; i++) { var idxArr = indices[i]; for (var j = 0; j < idxArr.length; j++) { if (idxArr[j] > maxIdx) maxIdx = idxArr[j]; } } var stride = 0; // 优先用引擎记录的顶点数反推 stride(J$.w3x_1 = 顶点数) try { var G0 = tp[FMeshVertexGroups].g1(0); var Vc = G0 && G0[FMeshVertexBuffer] && G0[FMeshVertexBuffer][FMeshVertexCount]; if (Vc > 0 && positions.length % Vc === 0) stride = Math.round(positions.length / Vc); } catch (e) {} if (!stride || stride < 3 || stride > 20) { stride = 3; var vc3 = Math.floor(positions.length / 3); if (maxIdx >= vc3) { // stride=3 顶点数不够,尝试常见 interleaved 布局 var candidates = [8, 11, 5, 7, 14]; for (var c = 0; c < candidates.length; c++) { var vc = Math.floor(positions.length / candidates[c]); if (maxIdx < vc) { stride = candidates[c]; break; } } } } var data = { positions: positions, indices: indices, stride: stride }; GpuDataCache.set(tp, data); if (!StrideDiagPrinted.has(tp)) { StrideDiagPrinted.add(tp); console.log('[透视][GPU数据] stride=' + stride + ', 缓冲区=' + positions.length + ' float, 最大索引=' + maxIdx + ', 顶点数=' + Math.floor(positions.length / stride)); } return data; } // 采集失败诊断(每个资源只打一次):直接指出卡在顶点、索引还是 GL 上下文 if (!GpuFailLogged.has(tp)) { GpuFailLogged.add(tp); var D = {}; try { var G = tp[FMeshVertexGroups] && typeof tp[FMeshVertexGroups].g1 === 'function' ? tp[FMeshVertexGroups].g1(0) : null; var B = G && G[FMeshVertexBuffer]; var Dev = B && B[FRenderDevice]; var I = tp[FMeshIndexGroups] && typeof tp[FMeshIndexGroups].e1 === 'function' && tp[FMeshIndexGroups].e1() > 0 ? tp[FMeshIndexGroups].g1(0) : null; var IB = I && (I[FMeshIndexBuffer] || I[FMeshIndexBufferOld]); D = { 顶点组: tp[FMeshVertexGroups] && tp[FMeshVertexGroups].e1 ? tp[FMeshVertexGroups].e1() : -1, 顶点缓冲资源: !!B, 缓冲包装: !!(B && B[FMeshGpuBuffer]), 原生Buffer: !!(B && B[FMeshGpuBuffer] && B[FMeshGpuBuffer][FRenderDrawBuffer]), 设备: !!Dev, 原生GL: !!(Dev && Dev[FRenderDeviceNative]), 顶点数: B ? B[FMeshVertexCount] : -1, 位置长度: positions ? positions.length : -1, 索引组: tp[FMeshIndexGroups] && tp[FMeshIndexGroups].e1 ? tp[FMeshIndexGroups].e1() : -1, 索引资源: !!IB, 索引Buffer: !!(IB && IB[FMeshIndexGpuBuffer] && IB[FMeshIndexGpuBuffer][FRenderDrawBuffer]), 索引个数: indices ? indices.length : -1 }; } catch (e) {} console.warn('[透视][GPU] 采集失败', D); } return null; } // ===== 递归收集mesh节点(每帧重新获取 Bp 节点引用) ===== // Seen:跨「车身根 / 部件表 / 炮塔 / 扫描」多路径去重,避免同一节点被画多次 function CollectMeshNodes(bpNode, nodes, depth, group, stats, Seen) { if (!bpNode || depth > 12) return; if (Seen) { if (Seen.has(bpNode)) return; Seen.add(bpNode); } if (stats) stats.seen++; var name = bpNode[FNodeName] || ''; if (bpNode[FNodeMesh]) { if (stats) stats.hasMesh++; var gpuData = GetGpuData(bpNode[FNodeMesh]); if (gpuData) { if (stats) stats.gpuOk++; nodes.push({ name: name, node: bpNode, tp: bpNode[FNodeMesh], positions: gpuData.positions, indices: gpuData.indices, stride: gpuData.stride, group: group }); } else if (stats) stats.gpuFail++; } var LodList = bpNode[FNodeLod]; if (LodList && typeof LodList.e1 === 'function') { try { var lodCount = LodList.e1(); for (var lod = 0; lod < lodCount && lod < 1; lod++) { var lodLevel = LodList.g1(lod); if (!lodLevel) continue; var lodTp = lodLevel[FNodeMesh] || lodLevel.v3m_1; if (!lodTp) continue; var lodGpuData = GetGpuData(lodTp); if (lodGpuData) { if (stats) stats.gpuOk++; nodes.push({ name: name + '_lod' + lod, node: bpNode, tp: lodTp, positions: lodGpuData.positions, indices: lodGpuData.indices, stride: lodGpuData.stride, group: group }); } } } catch(e) {} } // 子节点:新版是 r35_1(zl 基类:this.r35_1 = new lo(),r3k 里递归子节点) // 旧版是 p35_1;新版 p35_1 已变成"世界矩阵",绝不能当子节点列表 var Children = (bpNode[FNodeChildren] && typeof bpNode[FNodeChildren].e1 === 'function') ? bpNode[FNodeChildren] : ((bpNode.p35_1 && typeof bpNode.p35_1.e1 === 'function') ? bpNode.p35_1 : null); if (Children) { ForEachInList(Children, function (child) { if (child) CollectMeshNodes(child, nodes, depth + 1, group, stats, Seen); }); } } // 兜底采集:完全不依赖皮肤组件结构,直接扫实体组件的字段值,找 3D 节点 // 新版节点(zl 基类)特征:g35_1 = 名字字符串,p35_1 = 世界矩阵(长度≥16),r35_1 = 子节点列表 function IsSceneNode(V) { return !!(V && typeof V === 'object' && typeof V[FNodeName] === 'string' && V[FNodeMatrix] && typeof V[FNodeMatrix].length === 'number' && V[FNodeMatrix].length >= 16 && V[FNodeChildren] && typeof V[FNodeChildren] === 'object'); } // 只遍历普通对象:TypedArray / 数组 / Map / Set 的 for-in 会产出成千上万个键,必须跳过 function IsPlainObj(V) { if (!V || typeof V !== 'object') return false; if (Array.isArray(V) || ArrayBuffer.isView(V)) return false; if (typeof Map !== 'undefined' && V instanceof Map) return false; if (typeof Set !== 'undefined' && V instanceof Set) return false; return true; } // ===== 节点归属校验:只认「跟着这辆坦克」的节点 ===== // 兜底扫描是盲扫组件字段,可能翻到共享的场景根/周围物件(表现:非坦克也被画线框)。 // 坦克自身所有部件都在车体附近(整车 < 10m),用世界矩阵平移做距离闸门即可把它们挡掉。 // 只对 group === 'scan' 的节点生效:车身根/炮塔根是可信路径,绝不因距离丢模型。 var NODE_SCAN_RADIUS = 2500; // 25m(单位与引擎世界坐标一致) function NodeTranslation(Node) { if (!Node) return null; var M = Node[FNodeMatrix]; if (!M || typeof M.length !== 'number' || M.length < 16) { M = Node[FNodeMatrixOld]; // 旧版矩阵字段 if (!M || typeof M.length !== 'number' || M.length < 16) return null; } return { x: M[12], y: M[13], z: M[14] }; } // 剔除「非本车」的兜底扫描节点(车身根/炮塔根是可信路径,不参与该过滤) function FilterOwnedNodes(nodes, Pos) { if (!nodes || !Pos || !nodes.length) return nodes; var Out = []; for (var i = 0; i < nodes.length; i++) { var N = nodes[i]; if (N.group === 'scan' && !IsNodeNearPoint(N.node, Pos)) continue; Out.push(N); } return Out; } function IsNodeNearPoint(Node, Pos, Radius) { var T = NodeTranslation(Node); if (!T || !Pos) return true; // 读不到矩阵时不拦(交给后续渲染阶段处理) var dx = T.x - Pos.x, dy = T.y - Pos.y, dz = T.z - Pos.z; var R = Radius || NODE_SCAN_RADIUS; return (dx * dx + dy * dy + dz * dz) <= (R * R); } // 扫描结果按「组件字段路径」缓存(不缓存节点对象:引擎会重建节点树, // 但字段名与组件实例稳定,路径每次都能解析到当前节点) var NodeSrcCache = new WeakMap(); // entity -> [{ Comp, Path: [K1, K2, K3] }] var ScanTick = 0; function ResolveSrc(Src) { var V = Src.Comp; for (var i = 0; i < Src.Path.length; i++) { if (!V || typeof V !== 'object') return null; V = V[Src.Path[i]]; } return V; } function ScanMeshNodes(tank, nodes, stats, Seen, TankPos) { Seen = Seen || new WeakSet(); // ① 有缓存路径:直接解析(开销极小);每 300 帧强制重扫一次,捡起后加载的部件 ScanTick++; var NeedRescan = (ScanTick % 300) === 0; var Cached = NodeSrcCache.get(tank); if (!NeedRescan && Cached && Cached.length) { var AllOk = true; for (var c = 0; c < Cached.length; c++) { var V0 = ResolveSrc(Cached[c]); if (!IsSceneNode(V0)) { AllOk = false; break; } if (!IsNodeNearPoint(V0, TankPos)) continue; // 缓存根跑远了(不是这辆车的)→ 不画 if (!Seen.has(V0)) CollectMeshNodes(V0, nodes, 0, 'scan', stats, Seen); } if (AllOk) return; } // ② 缓存失效或首次:全量扫描并重建路径表 var Srcs = []; function Try(V, Comp, Path) { if (!IsSceneNode(V) || Seen.has(V)) return; // 归属闸门:盲扫可能翻到共享场景根/周围物件,按「是否在车体附近」剔除 if (!IsNodeNearPoint(V, TankPos)) { if (stats) stats.farRejected = (stats.farRejected || 0) + 1; return; } CollectMeshNodes(V, nodes, 0, 'scan', stats, Seen); Srcs.push({ Comp: Comp, Path: Path }); } try { tank[FEntityRoot][FEntityComponentMap].forEach(function (VL) { var comp = (VL && typeof VL.g1 === 'function') ? VL.g1(0) : VL; if (!IsPlainObj(comp)) return; for (var K in comp) { var V = comp[K]; if (!IsPlainObj(V)) continue; if (IsSceneNode(V)) { Try(V, comp, [K]); continue; } // 深度 2/3:节点常包在皮肤对象/包装对象里(如 skin.h159_1、weaponSkin.e11p_1.a17q_1) try { for (var K2 in V) { var V2 = V[K2]; if (!IsPlainObj(V2)) continue; if (IsSceneNode(V2)) { Try(V2, comp, [K, K2]); continue; } for (var K3 in V2) { var V3 = V2[K3]; if (IsPlainObj(V3)) Try(V3, comp, [K, K2, K3]); } } } catch (e) {} } }); } catch (e) {} NodeSrcCache.set(tank, Srcs); } // 结果级去重:同一 mesh 资源 + 同一节点名,只保留一份 // 注意:**不要把世界坐标写进键**——坦克移动时两套实例的平移会差一帧, // 键随位置变化会导致「这帧去重、下帧不去重」→ 模型闪烁。 var TpIds = new WeakMap(); var TpNextId = 1; function DedupNodes(nodes) { if (nodes.length < 2) return nodes; var Keys = new Set(); var Out = []; for (var i = 0; i < nodes.length; i++) { var N = nodes[i]; var Tp = N.tp; var Id = TpIds.get(Tp); if (!Id) { Id = TpNextId++; TpIds.set(Tp, Id); } var Key = Id + ':' + N.name; if (Keys.has(Key)) continue; Keys.add(Key); Out.push(N); } return Out; } // ===== 提取坦克的mesh数据 ===== // 采集策略:**单一路径优先**,绝不把两条路径的结果混在一起(混在一起就是"复制身体") // ① 皮肤组件 h159_1(实际渲染树根)→ ② 部件表 → ③ 组件字段扫描(兜底) function GetTankMeshData(tank, stats, TankPos) { var nodes = []; var Seen = new WeakSet(); var skinComp = GetSkinComponent(tank); var skin = (skinComp && skinComp[FSkinHullSkin] && skinComp[FSkinHullSkin][FSkinParts]) ? skinComp[FSkinHullSkin] : null; if (stats) { stats.hullSource = skin ? 'g159_1' : (skinComp ? '有组件但无皮肤' : '无皮肤组件'); stats.hullParts = (skin && typeof skin[FSkinParts].e1 === 'function') ? skin[FSkinParts].e1() : -1; } // ① 主路径:车身根节点(包含全部部件节点) if (skinComp && skinComp[FSkinHullRoot]) { try { CollectMeshNodes(skinComp[FSkinHullRoot], nodes, 0, 'hull', stats ? stats.hull : null, Seen); } catch (e) {} } // ② 根节点采不到才退回部件表 if (nodes.length === 0 && skin) { try { ForEachInList(skin[FSkinParts], function (part) { if (!part) return; var j = part[FSkinPartNode]; // 新版:单个 BP 节点;旧版:节点列表 if (!j) return; if (typeof j === 'object' && typeof j.e1 === 'function' && typeof j.g1 === 'function') { ForEachInList(j, function (bp) { if (bp) CollectMeshNodes(bp, nodes, 0, 'hull', stats ? stats.hull : null, Seen); }); } else { CollectMeshNodes(j, nodes, 0, 'hull', stats ? stats.hull : null, Seen); } }); } catch(e) {} } // ③ 结构扫描:无条件补充漏采(车身根/部件表往往只覆盖一部分) // 重复实例(皮肤树/场景树同 tp 同名)由 DedupNodes 收掉,不会再出现"两个身体" try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen, TankPos); } catch (e) {} // ④ 炮塔(独立于车身根;重复实例由 DedupNodes 收掉) var turretNode = GetTurretNode(tank); if (stats) stats.turretComp = !!turretNode; if (turretNode) { if (stats) stats.turretRoot = true; try { CollectMeshNodes(turretNode, nodes, 0, 'turret', stats ? stats.turret : null, Seen); } catch(e) {} } return DedupNodes(nodes); } // ===== 残留节点检测(世界矩阵"冻结")→ 拖拽跟随 ===== // 现象:实体已移动,但原处仍留着线框。 // 原因:引擎只为"本帧实际参与渲染"的节点更新世界矩阵,被裁剪/脱离的节点矩阵停在最后位置。 // 处理:检测到「节点矩阵平移长时间不动、而坦克在动」时**不再丢弃**(丢弃会让整辆模型只剩一个点), // 而是用「冻结时刻的相对偏移 + 坦克当前位置」平移这份冻结矩阵,让它跟着坦克走: // 既不在旧位置留残留,也不会因为引擎不更新矩阵而丢掉模型。 // 权衡:拖拽期间朝向沿用最后一次更新(位置正确、朝向可能滞后),对透视足够用。 var NodeMatrixHistory = new WeakMap(); // WeakMap:不持有节点强引用(引擎会重建节点树) var TankPrevPos = new Map(); // tankId → 上一帧位置(含位移滑动平均) var FrozenFrames = 20; // 连续不动帧数阈值(仅在坦克本身移动时才累计) var NodeMatrixScratch = new Float32Array(16); function IsNodeValid(bpNode, pos, tankIsMoving, diagStats) { // 世界矩阵:新版是 p35_1(zl 基类构造里 = hl(),s3k/t3k/u3k 读 p35_1[12..14]) // 旧版是 n35_1;新版 n35_1 已变成布尔标志 var M = bpNode[FNodeMatrix]; if (!M || typeof M.length !== 'number' || M.length < 16) { var OldM = bpNode[FNodeMatrixOld]; M = (OldM && typeof OldM.length === 'number' && OldM.length >= 16) ? OldM : null; } if (!M) return null; if (diagStats) diagStats.totalNodes++; var Hist = NodeMatrixHistory.get(bpNode); if (!Hist) { NodeMatrixHistory.set(bpNode, { x: M[12], y: M[13], z: M[14], still: 0, off: null }); } else { var dx = M[12] - Hist.x, dy = M[13] - Hist.y, dz = M[14] - Hist.z; if (dx * dx + dy * dy + dz * dz > 0.01) { Hist.x = M[12]; Hist.y = M[13]; Hist.z = M[14]; Hist.still = 0; Hist.off = null; // 矩阵恢复更新 → 恢复正常绘制 } else if (tankIsMoving) { Hist.still++; if (Hist.still >= FrozenFrames) { if (!Hist.off) Hist.off = { x: M[12] - pos.x, y: M[13] - pos.y, z: M[14] - pos.z }; for (var k = 0; k < 16; k++) NodeMatrixScratch[k] = M[k]; NodeMatrixScratch[12] = pos.x + Hist.off.x; NodeMatrixScratch[13] = pos.y + Hist.off.y; NodeMatrixScratch[14] = pos.z + Hist.off.z; if (diagStats) diagStats.draggedNodes++; return NodeMatrixScratch; // 冻结但跟随坦克 } } else { Hist.still = 0; } } // 位置检查(节点世界矩阵平移与坦克位置相差过大则丢弃)—— 只对"未冻结"的节点做 var nodeDx = M[12] - pos.x, nodeDy = M[13] - pos.y, nodeDz = M[14] - pos.z; if (nodeDx * nodeDx + nodeDy * nodeDy + nodeDz * nodeDz > 4000000) { if (diagStats) diagStats.filteredNodes++; return null; } return M; } // ===== 线框模式:绘制单个节点的所有边 ===== // 顶点数据来自 PrepareNodeVerts 的复用缓冲;边去重用数值键 Set(避免字符串分配) var _WfEdgeSet = new Set(); function WfAddEdge(ctx, a, b) { var lo = a < b ? a : b, hi = a < b ? b : a; var key = lo * 65536 + hi; if (_WfEdgeSet.has(key)) return; _WfEdgeSet.add(key); ctx.moveTo(VB_Sx[lo], VB_Sy[lo]); ctx.lineTo(VB_Sx[hi], VB_Sy[hi]); } function DrawWireframeNode(ctx, meshData, worldMatrix, pos) { var indicesArr = meshData.indices; var vertexCount = PrepareNodeVerts(meshData, worldMatrix, pos); _WfEdgeSet.clear(); ctx.beginPath(); for (var mi = 0; mi < indicesArr.length; mi++) { var indices = indicesArr[mi]; for (var idx = 0; idx < indices.length; idx += 3) { var i0 = indices[idx], i1 = indices[idx+1], i2 = indices[idx+2]; if (!VB_Ok[i0] || !VB_Ok[i1] || !VB_Ok[i2]) continue; WfAddEdge(ctx, i0, i1); WfAddEdge(ctx, i1, i2); WfAddEdge(ctx, i2, i0); } } ctx.stroke(); } // ===== 兜底标记点:模型采集不到/被过滤时,至少画一个点,避免坦克完全不可见 ===== function DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color) { var p = ProjectPoint(pos, camInfo, screenW, screenH); if (!p) return; ctx.fillStyle = color; ctx.beginPath(); ctx.arc(p.x, p.y, 3, 0, 2 * Math.PI); ctx.fill(); } // ===== 描边模式:屏幕空间光栅化轮廓提取 ===== // 算法(真正的"屏幕几何"方法): // 1. 把所有面向相机的三角形光栅化(扫描线填充)到像素掩码 // 2. 车身和炮塔在屏幕上重叠时,填充区域自动合并 // 3. 找出填充区域的边界像素(填充像素的4邻居中有未填充像素) // 4. 只画边界像素 = 屏幕空间外轮廓 var PixelMask = null; var PixelMaskW = 0; var PixelMaskH = 0; var MaskGen = 0; // 掩码用"代数"标记:本辆坦克填充的像素 = gen,无需在坦克之间清空整块缓冲 function GetPixelMask(w, h) { if (!PixelMask || PixelMaskW !== w || PixelMaskH !== h) { PixelMask = new Uint32Array(w * h); PixelMaskW = w; PixelMaskH = h; MaskGen = 0; } return PixelMask; } // 扫描线光栅化三角形(pixels 中写入 gen 表示"本辆坦克已填充") function RasterizeTriangle(pixels, w, h, gen, x0, y0, x1, y1, x2, y2) { // 按 y 排序三个顶点 if (y0 > y1) { var t = y0; y0 = y1; y1 = t; t = x0; x0 = x1; x1 = t; } if (y1 > y2) { var t = y1; y1 = y2; y2 = t; t = x1; x1 = x2; x2 = t; } if (y0 > y1) { var t = y0; y0 = y1; y1 = t; t = x0; x0 = x1; x1 = t; } var minY = Math.max(0, Math.ceil(y0)); var maxY = Math.min(h - 1, Math.floor(y2)); if (minY > maxY) return; for (var y = minY; y <= maxY; y++) { var xa, xb; if (y < y1) { // 上半部分:y0 -> y1 和 y0 -> y2 xa = (y1 === y0) ? x0 : x0 + (x1 - x0) * (y - y0) / (y1 - y0); xb = (y2 === y0) ? x0 : x0 + (x2 - x0) * (y - y0) / (y2 - y0); } else { // 下半部分:y1 -> y2 和 y0 -> y2 xa = (y2 === y1) ? x1 : x1 + (x2 - x1) * (y - y1) / (y2 - y1); xb = (y2 === y0) ? x0 : x0 + (x2 - x0) * (y - y0) / (y2 - y0); } if (xa > xb) { var t = xa; xa = xb; xb = t; } var minXA = Math.max(0, Math.ceil(xa - 0.5)); var maxXA = Math.min(w - 1, Math.floor(xb + 0.5)); var rowOffset = y * w; for (var x = minXA; x <= maxXA; x++) { pixels[rowOffset + x] = gen; } } } function DrawTankOutline(ctx, meshNodes, pos, tankIsMoving, screenW, screenH, diagStats, color) { var pixels = GetPixelMask(screenW, screenH); var gen = ++MaskGen; if (gen === 0) { pixels.fill(0); gen = ++MaskGen; } // 代数回绕(理论值):整体清一次 // 记录坦克的屏幕边界框(用于后续边界检测优化) var minX = screenW, minY = screenH, maxX = 0, maxY = 0; for (var ni = 0; ni < meshNodes.length; ni++) { var meshData = meshNodes[ni]; var bpNode = meshData.node; var worldMatrix = IsNodeValid(bpNode, pos, tankIsMoving, diagStats); if (!worldMatrix) continue; var indicesArr = meshData.indices; // 顶点变换+投影到复用缓冲(无对象分配) var vertexCount = PrepareNodeVerts(meshData, worldMatrix, pos); for (var v = 0; v < vertexCount; v++) { if (!VB_Ok[v]) continue; var sx = VB_Sx[v], sy = VB_Sy[v]; if (sx < minX) minX = sx; if (sy < minY) minY = sy; if (sx > maxX) maxX = sx; if (sy > maxY) maxY = sy; } // 遍历三角形,光栅化面向相机的 for (var mi = 0; mi < indicesArr.length; mi++) { var indices = indicesArr[mi]; for (var i = 0; i < indices.length; i += 3) { var a = indices[i], b = indices[i+1], c = indices[i+2]; if (!VB_Ok[a] || !VB_Ok[b] || !VB_Ok[c]) continue; // 计算法线判断是否面向相机(世界坐标,来自缓冲) var ax = VB_Wx[a], ay = VB_Wy[a], az = VB_Wz[a]; var bx = VB_Wx[b], by = VB_Wy[b], bz = VB_Wz[b]; var gx = VB_Wx[c], gy = VB_Wy[c], gz = VB_Wz[c]; var e1x = bx - ax, e1y = by - ay, e1z = bz - az; var e2x = gx - ax, e2y = gy - ay, e2z = gz - az; var nx = e1y * e2z - e1z * e2y; var ny = e1z * e2x - e1x * e2z; var nz = e1x * e2y - e1y * e2x; var cx = (ax + bx + gx) / 3; var cy = (ay + by + gy) / 3; var cz = (az + bz + gz) / 3; var dot = nx * (Cam.px - cx) + ny * (Cam.py - cy) + nz * (Cam.pz - cz); if (dot <= 0) continue; // 背向相机,跳过 // 光栅化三角形到像素掩码 RasterizeTriangle(pixels, screenW, screenH, gen, VB_Sx[a], VB_Sy[a], VB_Sx[b], VB_Sy[b], VB_Sx[c], VB_Sy[c]); } } } // 找出边界像素并绘制(只在坦克边界框内遍历;用单条路径 + 横向连续合并,替代逐像素 fillRect) var bx0 = Math.max(0, Math.floor(minX) - 1); var by0 = Math.max(0, Math.floor(minY) - 1); var bx1 = Math.min(screenW - 1, Math.ceil(maxX) + 1); var by1 = Math.min(screenH - 1, Math.ceil(maxY) + 1); ctx.fillStyle = color; ctx.beginPath(); var outlineCount = 0; for (var y = by0; y <= by1; y++) { var rowOffset = y * screenW; var runStart = -1; for (var x = bx0; x <= bx1 + 1; x++) { var isBoundary = false; if (x <= bx1 && pixels[rowOffset + x] === gen) { // 检查4个邻居是否有未填充像素(== gen 即本辆坦克填充) if (x === 0 || pixels[rowOffset + x - 1] !== gen) isBoundary = true; else if (x === screenW - 1 || pixels[rowOffset + x + 1] !== gen) isBoundary = true; else if (y === 0 || pixels[rowOffset - screenW + x] !== gen) isBoundary = true; else if (y === screenH - 1 || pixels[rowOffset + screenW + x] !== gen) isBoundary = true; } if (isBoundary) { if (runStart < 0) runStart = x; outlineCount++; } else if (runStart >= 0) { ctx.rect(runStart, y, x - runStart + 2, 3); // 横向连续的边界像素合并为一条 runStart = -1; } } } ctx.fill(); if (diagStats) diagStats.outlineEdges += outlineCount; return outlineCount; } // ===== 创建全屏Canvas ===== var canvas = document.createElement('canvas'); canvas.id = 'hornet3DWireframeCanvas'; canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:99998;pointer-events:none;'; document.body.appendChild(canvas); var ctx = canvas.getContext('2d'); function ResizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } ResizeCanvas(); window.addEventListener('resize', ResizeCanvas); // ===== 诊断统计 ===== var DiagStats = { totalNodes: 0, filteredNodes: 0, renderedNodes: 0, outlineEdges: 0 }; var LastDegradeLogFrame = -9999; // 退化诊断日志节流 // 退化诊断(节流输出):定位"存活坦克模型没画出来"的根因(皮肤/部件/GPU/炮塔逐级) function LogTankDiag(tankName, tankState, S, hullCount, turretCount, rendered) { if (esp.frameCount - LastDegradeLogFrame <= 120) return; LastDegradeLogFrame = esp.frameCount; console.warn('[透视][退化诊断] ' + tankName + ' state=' + tankState + ' | 皮肤=' + S.hullSource + ' 部件=' + S.hullParts + ' 车身[见/网格/GPU成功/失败]=' + S.hull.seen + '/' + S.hull.hasMesh + '/' + S.hull.gpuOk + '/' + S.hull.gpuFail + ' | 炮塔 组件=' + S.turretComp + ' 根节点=' + S.turretRoot + ' 炮塔[见/网格/GPU成功/失败]=' + S.turret.seen + '/' + S.turret.hasMesh + '/' + S.turret.gpuOk + '/' + S.turret.gpuFail + ' | 收集 车身=' + hullCount + ' 炮塔=' + turretCount + ' 绘制=' + rendered + ' | 本帧 总=' + DiagStats.totalNodes + ' 过滤=' + DiagStats.filteredNodes); } // ===== ESP状态 ===== var esp = { canvas: canvas, ctx: ctx, running: true, frameCount: 0, mode: Mode, setMode: function(m) { if (m === 'wireframe' || m === 'outline') { Mode = m; this.mode = m; try { localStorage.setItem('hornet_wireframe_mode', m); } catch(e) {} console.log('[透视] 模式切换为: ' + m); } else { console.warn('[透视] 未知模式: ' + m + ',支持: wireframe | outline'); } }, getMode: function() { return Mode; }, setLineWidth: function(w) { var n = parseFloat(w); if (!isNaN(n) && n >= 0.5 && n <= 8) { LineWidth = n; try { localStorage.setItem('hornet_wireframe_linewidth', String(n)); } catch(e) {} console.log('[透视] 线宽设置为: ' + n); } else { console.warn('[透视] 线宽应在 0.5 ~ 8 之间,当前: ' + w); } }, getLineWidth: function() { return LineWidth; }, setHighlightMode: function(mode) { var valid = ['team','whitelist','blacklist']; if (valid.indexOf(mode) >= 0) { HighlightMode = mode; try { localStorage.setItem('hornet_wireframe_highlight_mode', mode); } catch(e) {} console.log('[透视] 渲染模式切换为: ' + mode); } else { console.warn('[透视] 未知渲染模式: ' + mode + ',支持: ' + valid.join(', ')); } }, getHighlightMode: function() { return HighlightMode; }, setHighlightList: function(names) { HighlightList = Array.isArray(names) ? names.slice() : []; SaveHighlightList(); console.log('[透视] 高亮名单已更新:', HighlightList.length + '人'); }, addHighlight: function(name) { if (name && HighlightList.indexOf(name) < 0) { HighlightList.push(name); SaveHighlightList(); console.log('[透视] 已添加高亮:', name); } }, removeHighlight: function(name) { var idx = HighlightList.indexOf(name); if (idx >= 0) { HighlightList.splice(idx, 1); SaveHighlightList(); console.log('[透视] 已移除高亮:', name); } }, getHighlightList: function() { return HighlightList.slice(); }, clearHighlight: function() { HighlightList = []; SaveHighlightList(); console.log('[透视] 高亮名单已清空'); }, stop: function() { this.running = false; window.removeEventListener('resize', ResizeCanvas); if (canvas.parentNode) canvas.parentNode.removeChild(canvas); console.log('透视已停止'); } }; window.__hornet3DWireframe__ = esp; // ===== 金箱子标记 ===== // 金箱子是独立实体(不是坦克)。引擎侧标志: // · 补给实体 = entity.j10q_1(bonusData 组件,见 module_87072 的 ctt()) // · 类型枚举 mE(module_10032):GOLD_BOX = 6 // 做法:实体基类的组件创建方法 nyz()(module_61378「BattleEntity.createComponent」)上挂观察钩子, // 发现「带 j10q_1 且类型=6」的实体就登记,之后每帧按它自己的位置画金色标记。 // 钩子目标沿 tank 的原型链找 nyz 的宿主原型 —— 不需要任何导出键。 var GoldBoxes = []; var GoldPending = []; var EntityProtoHooked = false; var GOLD_BOX_TYPE = 6; function FindEnumOf(Obj) { for (var K in Obj) { try { var V = Obj[K]; if (V && typeof V === 'object' && typeof V[FTeamId] === 'number' && typeof V.equals === 'function') return V; } catch (e) {} } return null; } // 引擎口径:有 bonusData 且类型是 GOLD_BOX function IsGoldBoxEntity(Entity) { if (!Entity || !Entity.j10q_1) return false; var Type = FindEnumOf(Entity.j10q_1); return !!(Type && Type[FTeamId] === GOLD_BOX_TYPE); } function RegisterGoldBox(Entity) { for (var i = 0; i < GoldBoxes.length; i++) if (GoldBoxes[i] === Entity) return; GoldBoxes.push(Entity); console.log('[透视] 发现金箱子,已加入标记(当前 ' + GoldBoxes.length + ' 个)'); } function InstallEntityHook(Tank) { if (EntityProtoHooked || !Tank) return; var Proto = Tank; while (Proto && !Object.prototype.hasOwnProperty.call(Proto, 'nyz')) Proto = Object.getPrototypeOf(Proto); if (!Proto || typeof Proto.nyz !== 'function') return; var Orig = Proto.nyz; Proto.nyz = function () { var R = Orig.apply(this, arguments); try { if (IsGoldBoxEntity(this)) RegisterGoldBox(this); else if (this && this.j10q_1 === undefined && GoldPending.length < 300) { GoldPending.push({ e: this, f: esp.frameCount }); // 字段可能稍后才赋值,延后判定 } } catch (E) {} return R; }; EntityProtoHooked = true; console.log('[透视] 已挂补给实体观察钩子(金箱子)'); } // 金箱位置:优先物理组件(与坦克同款 rzf_1),否则退到 bonusData 里的节点世界矩阵/向量 function GetGoldBoxPosition(Entity) { var PhysPos = GetTankPosition(Entity); // 多级探测物理组件位置 if (PhysPos) return PhysPos; var Bonus = Entity.j10q_1; for (var K in Bonus) { try { var V = Bonus[K]; if (!V || typeof V !== 'object') continue; if (V[FNodeMatrixOld] && V[FNodeMatrixOld].length >= 15) { var M = V[FNodeMatrixOld]; return { x: M[12], y: M[13], z: M[14] }; } if (typeof V[FVecX] === 'number' && typeof V[FVecY] === 'number' && typeof V[FVecZ] === 'number') { return { x: V[FVecX], y: V[FVecY], z: V[FVecZ] }; } } catch (e) {} } return null; } // 金色菱形 + 十字 + 「金箱」标签(穿透显示,不遮挡判断) function DrawGoldBoxMarker(Pos, ScreenW, ScreenH) { var X = Pos.x - Cam.px, Y = Pos.y - Cam.py, Z = Pos.z - Cam.pz; var CamZ = X * Cam.fx + Y * Cam.fy + Z * Cam.fz; if (CamZ <= 0.1) return false; var CamX = X * Cam.rx + Y * Cam.ry + Z * Cam.rz; var CamY = X * Cam.ux + Y * Cam.uy + Z * Cam.uz; var Sx = Cam.halfW + (CamX / CamZ) * Cam.focal; var Sy = Cam.halfH - (CamY / CamZ) * Cam.focal; if (Sx < -60 || Sy < -60 || Sx > ScreenW + 60 || Sy > ScreenH + 60) return false; var R = 12; ctx.save(); ctx.strokeStyle = '#ffd700'; ctx.fillStyle = 'rgba(255, 215, 0, 0.22)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(Sx, Sy - R); ctx.lineTo(Sx + R, Sy); ctx.lineTo(Sx, Sy + R); ctx.lineTo(Sx - R, Sy); ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.moveTo(Sx - R - 7, Sy); ctx.lineTo(Sx + R + 7, Sy); ctx.moveTo(Sx, Sy - R - 7); ctx.lineTo(Sx, Sy + R + 7); ctx.stroke(); ctx.fillStyle = '#ffd700'; ctx.font = 'bold 12px Consolas,monospace'; ctx.textAlign = 'center'; ctx.fillText('金箱', Sx, Sy - R - 9); ctx.restore(); return true; } // ===== 冻结节点"拖拽跟随"诊断(节流 2s)===== var LastFrozenLogAt = 0; function LogDragDiag(Stats) { if (!Stats.draggedNodes) return; var Now = Date.now(); if (Now - LastFrozenLogAt < 2000) return; LastFrozenLogAt = Now; console.log('[透视][冻结] 本帧拖拽跟随节点 ' + Stats.draggedNodes + '/' + Stats.totalNodes + '(引擎未更新这些节点矩阵,已按坦克位移平移,避免旧位置残留/模型丢失)'); } // ===== 渲染循环 ===== function Render() { if (!esp.running) return; esp.frameCount++; // 换图自刷新:gameMode 实例变化时重绑并清空该地图的缓存 var NewGm = window.__gameMode__; if (NewGm && NewGm !== gm) { gm = NewGm; GpuDataCache.clear(); // 旧地图的 GPU 几何数据失效 TankPrevPos.clear(); // 位置历史(判断"在动")失效 GoldBoxes.length = 0; // 上一局的箱子实体全部作废 GoldPending.length = 0; GpuCacheFrameCount = 0; console.log('[透视] 检测到新战斗,已重置缓存'); } // 旧实例已失效、新实例尚未捕获:清屏并显示等待提示,避免继续用悬空引用 if (!window.__gameMode__) { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = 'rgba(255, 220, 120, 0.9)'; ctx.font = 'bold 12px Consolas,monospace'; ctx.textAlign = 'left'; ctx.fillText('[' + Mode.toUpperCase() + '] 等待战局…(ght 未捕获)', 5, canvas.height - 5); requestAnimationFrame(Render); return; } // 定期清空 GPU 数据缓存 GpuCacheFrameCount++; if (GpuCacheFrameCount >= GpuCacheClearInterval) { GpuDataCache.clear(); GpuCacheFrameCount = 0; } DiagStats = { totalNodes: 0, filteredNodes: 0, draggedNodes: 0, renderedNodes: 0, outlineEdges: 0, goldBoxes: 0 }; try { var screenW = canvas.width; var screenH = canvas.height; ctx.clearRect(0, 0, screenW, screenH); var camInfo = GetCameraInfo(); if (!camInfo) { requestAnimationFrame(Render); return; } var playerTank = GetPlayerTank(); if (!playerTank) { requestAnimationFrame(Render); return; } // 相机位置为0时使用玩家位置 if (Math.abs(camInfo.pos.x) < 0.1 && Math.abs(camInfo.pos.y) < 0.1 && Math.abs(camInfo.pos.z) < 0.1) { var pPos = GetTankPosition(playerTank); if (pPos) camInfo.pos = { x: pPos.x, y: pPos.y, z: pPos.z + 100 }; } UpdateCam(camInfo, screenW, screenH); // 每帧只算一次相机基(需在相机位置兜底之后),供顶点变换/投影复用 var tankList = gm[FGameModeTankManager].j11y(); var count = tankList.e1(); var drawnCount = 0, botDrawn = 0; // 人机扫描(装备分数不会每帧变,节流到约每 0.3 秒一次) if (ScanBots && (Date.now() - LastBotScanAt > 300)) { LastBotScanAt = Date.now(); ScanBots(); } for (var i = 0; i < count; i++) { var tank = tankList.g1(i); if (!tank || tank === playerTank) continue; if (!EntityProtoHooked) InstallEntityHook(tank); // 借坦克实例找到实体基类原型,挂补给观察钩子 var pos = GetTankPosition(tank); // 多级探测物理组件位置(字段名每个构建会平移) if (!pos) continue; var tankState = GetTankState(tank); // 直接调用引擎自己的可见性判定 s14j()(SEMI_ACTIVE / ACTIVE / TELEPORT 才画) if (!IsTankAlive(tank)) continue; // 辅助判死:状态字段有时滞后一帧(爆毁动画期间仍可见),血量归零则不再画 var hp = GetTankHealth(tank); if (hp && hp.current <= 0) continue; // 本帧该坦克是否在移动(供「残留节点」检测:坦克在动而节点矩阵不动 = 旧位置遗留) // 用位移的指数滑动平均,而不是「单帧位移 > 10 单位」——后者对慢速移动几乎永远为假, // 会让残留检测形同虚设(残留漏掉的根因就在这里) var tankId = tank[FEntityId]; var prevPos = TankPrevPos.get(tankId); var tankMove = 0; if (prevPos) { var pdx = pos.x - prevPos.x, pdy = pos.y - prevPos.y, pdz = pos.z - prevPos.z; tankMove = (prevPos.move || 0) * 0.85 + Math.sqrt(pdx * pdx + pdy * pdy + pdz * pdz); } var tankIsMoving = tankMove > 0.5; // 0.5 单位/帧 ≈ 0.3 m/s;静置坦克的抖动低于此值 TankPrevPos.set(tankId, { x: pos.x, y: pos.y, z: pos.z, move: tankMove }); // 距离检查(50000 = 500米) var dx = pos.x - camInfo.pos.x; var dy = pos.y - camInfo.pos.y; var dz = pos.z - camInfo.pos.z; var distance = Math.sqrt(dx*dx + dy*dy + dz*dz); if (distance > 50000) continue; var friendly = IsFriendly(tank, playerTank); var tankName = GetTankName(tank) || ('ID:' + tank[FEntityId]); // 根据 HighlightMode 判断是否渲染该坦克 if (!ShouldRender(tankName)) continue; // 颜色按队伍:友方=蓝,敌方=红 var color = friendly ? 'rgba(68, 136, 255, 0.6)' : 'rgba(255, 68, 68, 0.6)'; // 3D模型渲染(仅在mesh数据可用时) var MeshStats = { hullSource: '', hullParts: 0, turretComp: false, turretRoot: false, hull: { seen: 0, hasMesh: 0, gpuOk: 0, gpuFail: 0 }, turret: { seen: 0, hasMesh: 0, gpuOk: 0, gpuFail: 0 }, scan: { seen: 0, hasMesh: 0, gpuOk: 0, gpuFail: 0, farRejected: 0 } }; var meshNodes = GetTankMeshData(tank, MeshStats, pos); meshNodes = FilterOwnedNodes(meshNodes, pos); // 只保留跟着这辆车动的节点(挡掉非坦克物件) if (!meshNodes || meshNodes.length === 0) { LogTankDiag(tankName, tankState, MeshStats, 0, 0, 0); DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color); // 兜底:至少可见 drawnCount++; continue; } // 统计各分组节点数(诊断用) var hullCount = 0, turretCount = 0; for (var gi = 0; gi < meshNodes.length; gi++) { if (meshNodes[gi].group === 'turret') turretCount++; else hullCount++; } // 车身/炮塔任一有 mesh 就画模型;两者都为空才退回标记点 // (旧逻辑要求必须先拿到炮塔节点,炮塔字段漂移时会连带把车身模型也吞掉) if (hullCount === 0 && turretCount === 0) { LogTankDiag(tankName, tankState, MeshStats, hullCount, turretCount, 0); DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color); drawnCount++; continue; } ctx.strokeStyle = color; ctx.lineWidth = LineWidth; var renderedThisTank = 0; if (Mode === 'wireframe') { for (var ni = 0; ni < meshNodes.length; ni++) { var meshData = meshNodes[ni]; var bpNode = meshData.node; var worldMatrix = IsNodeValid(bpNode, pos, tankIsMoving, DiagStats); if (!worldMatrix) continue; DrawWireframeNode(ctx, meshData, worldMatrix, pos); DiagStats.renderedNodes++; renderedThisTank++; } } else { renderedThisTank = DrawTankOutline(ctx, meshNodes, pos, tankIsMoving, screenW, screenH, DiagStats, color); } // 诊断(节流):存活坦克模型退化时输出根因数据,便于一次性定位 if ((renderedThisTank === 0 && meshNodes.length > 0) || hullCount === 0) { LogTankDiag(tankName, tankState, MeshStats, hullCount, turretCount, renderedThisTank); } // 一个节点都没画出来:画兜底标记,避免坦克完全不可见 if (renderedThisTank === 0) { DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color); } // 人机标记:坦克上方写「人机」(同队装备分数相同且 < 8000) if (typeof IsBot === 'function' && IsBot(tank)) { 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'; ctx.textAlign = 'center'; ctx.lineWidth = 3; ctx.strokeStyle = 'rgba(0,0,0,0.85)'; ctx.strokeText('人机', botPt.x, botPt.y); // 先描黑边,保证任何背景下都看得清 ctx.fillStyle = '#ffd24a'; ctx.fillText('人机', botPt.x, botPt.y); ctx.restore(); botDrawn++; } } drawnCount++; } // ===== 金箱子:清理失效项 → 评估候选 → 画金色标记 ===== var goldDrawn = 0; for (var gci = GoldBoxes.length - 1; gci >= 0; gci--) { if (!IsGoldBoxEntity(GoldBoxes[gci])) GoldBoxes.splice(gci, 1); // 被拾取/销毁 } for (var gpi = GoldPending.length - 1; gpi >= 0; gpi--) { var PendingIt = GoldPending[gpi]; if (esp.frameCount - PendingIt.f > 90) { GoldPending.splice(gpi, 1); continue; } if (IsGoldBoxEntity(PendingIt.e)) { RegisterGoldBox(PendingIt.e); GoldPending.splice(gpi, 1); } } for (var gbi = 0; gbi < GoldBoxes.length; gbi++) { var GoldPos = GetGoldBoxPosition(GoldBoxes[gbi]); if (GoldPos && DrawGoldBoxMarker(GoldPos, screenW, screenH)) goldDrawn++; } DiagStats.goldBoxes = goldDrawn; // 屏幕信息 ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; ctx.font = '10px Arial'; ctx.textAlign = 'left'; var infoText = '[' + Mode.toUpperCase() + '] | ' + drawnCount + ' tanks'; if (Mode === 'outline') { infoText += ' | Edges: ' + DiagStats.outlineEdges; } else { infoText += ' | Nodes: ' + DiagStats.renderedNodes + '/' + DiagStats.totalNodes; } infoText += ' | Frame: ' + esp.frameCount; if (DiagStats.draggedNodes > 0) infoText += ' | 拖拽:' + DiagStats.draggedNodes; if (DiagStats.goldBoxes > 0) infoText += ' | 金箱:' + DiagStats.goldBoxes; ctx.fillText(infoText, 5, screenH - 5); LogDragDiag(DiagStats); } catch(e) { console.error('[透视] 错误:', e.message, e.stack ? e.stack.split('\n')[1] : ''); } requestAnimationFrame(Render); } // 启动渲染 requestAnimationFrame(Render); console.log('3D透视已启动(双模式: wireframe | outline)'); console.log('当前模式: ' + Mode); console.log('提示: window.__hornet3DWireframe__.setMode("outline") 切换到描边模式'); console.log('提示: window.__hornet3DWireframe__.setMode("wireframe") 切换到线框模式'); console.log('提示: window.__hornet3DWireframe__.addHighlight("玩家名") 添加高亮(金色标记)'); console.log('提示: window.__hornet3DWireframe__.stop() 停止透视'); } var Tool = { Start: function() { if (window.__hornet3DWireframe__ && window.__hornet3DWireframe__.running) return; _start(); }, Stop: function() { if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.stop(); }, Status: function() { return (window.__hornet3DWireframe__ && window.__hornet3DWireframe__.running) ? ('运行中(' + window.__hornet3DWireframe__.mode + ')') : '已停止'; }, GetRunning: function() { return !!(window.__hornet3DWireframe__ && window.__hornet3DWireframe__.running); }, SetMode: function(m) { if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.setMode(m); }, GetMode: function() { return window.__hornet3DWireframe__ ? window.__hornet3DWireframe__.getMode() : null; }, SetLineWidth: function(w) { if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.setLineWidth(w); }, GetLineWidth: function() { return window.__hornet3DWireframe__ ? window.__hornet3DWireframe__.getLineWidth() : 2; }, SetHighlightMode: function(mode) { if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.setHighlightMode(mode); }, GetHighlightMode: function() { return window.__hornet3DWireframe__ ? window.__hornet3DWireframe__.getHighlightMode() : 'team'; }, SetHighlightList: function(names) { if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.setHighlightList(names); }, AddHighlight: function(name) { if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.addHighlight(name); }, RemoveHighlight: function(name) { if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.removeHighlight(name); }, GetHighlightList: function() { return window.__hornet3DWireframe__ ? window.__hornet3DWireframe__.getHighlightList() : []; }, ClearHighlight: function() { if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.clearHighlight(); } }; window.__hornetTools__ = window.__hornetTools__ || {}; window.__hornetTools__.Wireframe = Tool; console.log('[3DTank] 3D透视模块已加载'); })(); // ===== hornet_rearview.js ===== // ===== 3DTank后视镜 - 原生渲染方案 ===== // 在浏览器控制台运行(需先运行 hook_gamemode.js) // 功能:复用游戏原生渲染器(Ff.y3s)+ FBO 重定向,在小 canvas 中渲染玩家背后的 3D 场景 // 原理:Hook gl.bindFramebuffer 重定向到自定义 FBO,用后视相机参数调用游戏渲染器,readback 显示 (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; } // ===== 场景 / 渲染器 ===== var FSceneRef = Fld('Camera.SceneRef', 'jz1_1'); // 场景实体上的相机引用 var FRenderer = Fld('Render.Renderer', 'gyw_1'); // 渲染器实例 var FDrawer = Fld('Render.Drawer', 'd75_1'); // Ff 绘制器实例 var FRootNode = Fld('Scene.RootNode', 'fyx_1'); // 场景根节点 var FCameraObject = Fld('Camera.Object', 'iyx_1'); // 主相机对象 var FGl = Fld('Render.Gl', 'w39_1'); // GL 包装对象(o3c/n3c) var FGlNative = Fld('Render.GlNative', 'b74_1'); // 原生 WebGL 上下文 // ===== 相机 ===== var FAngles = Fld('Camera.Angles', 'u35_1'); // 朝向角(pitch/roll/yaw) var FPosition = Fld('Camera.Position', 'r35_1'); // 位置向量 var FFov = Fld('Camera.Fov', 'b37_1'); // 视图矩阵数据 var FViewportWidth = Fld('Camera.ViewportWidth', 'u36_1'); // 渲染宽 var FViewportHeight = Fld('Camera.ViewportHeight', 'v36_1');// 渲染高 var FFovValue = Fld('Camera.FovValue', 'z36_1'); // 视场角 var FNear = Fld('Camera.Near', 'x36_1'); // 近裁剪面 var FFar = Fld('Camera.Far', 'y36_1'); // 远裁剪面 var MCameraUpdate = Mth('Camera.Update', 'o37'); // 重算相机矩阵 var FX = Fld('Vec.X', 'f20_1'); var FY = Fld('Vec.Y', 'g20_1'); var FZ = Fld('Vec.Z', 'h20_1'); // ===== 渲染开关 / 队列 ===== var FSettings = Fld('Render.Settings', 'y38_1'); // 渲染开关集合 var FShadowEnabled = Fld('Render.ShadowEnabled', 'e3o_1'); // 阴影开关 var FPostProcessEnabled = Fld('Render.PostProcessEnabled', 'f3o_1'); // 后处理开关 var FExtraPassEnabled = Fld('Render.ExtraPassEnabled', 'k3o_1'); // 额外 pass 开关 var FLayerQueue = Fld('Render.LayerQueue', 'e39_1'); // 渲染层队列 var MDrawScene = Mth('Render.Draw', 'f3t'); // 渲染主场景 var MGetDrawingWidth = Mth('Render.DrawingWidth', 'o3c'); // drawingBufferWidth var MGetDrawingHeight = Mth('Render.DrawingHeight', 'n3c'); // drawingBufferHeight function _start() { console.log('===== 3DTank后视镜(原生渲染版)启动 ====='); if (!window.__gameMode__) { console.error('请先运行 hook_gamemode.js'); return; } if (window.__hornetRearview__) { window.__hornetRearview__.Stop(); console.log('已停止旧后视镜'); } var gm = window.__gameMode__; // ===== 引用公共库(hornet_lib.js) ===== var lib = window.__hornetLib__; if (!lib) { console.error('[后视] 请先运行 hornet_lib.js'); return; } var teamFns = lib.LoadTeamFunctions(); var ZCt = teamFns.ZCt, m2Fn = teamFns.m2Fn; // 公共函数别名(调用处不变) var FindComponentByField = lib.FindComponentByField; var GetTankPosition = lib.GetTankPosition; var GetTankState = lib.GetTankState; var IsTankAlive = lib.IsTankAlive; var GetTankName = lib.GetTankName; var GetTankHealth = lib.GetTankHealth; function GetPlayerTank() { return lib.GetPlayerTank(gm); } function IsFriendly(tank, playerTank) { return lib.IsFriendly(tank, playerTank, gm); } function GetCameraInfo() { return lib.GetCameraInfo(gm); } // ===== 配置 ===== var Config = { width: 400, height: 240, fov: 1.2, maxRange: 15000, pitchOffset: -0.05, cameraHeight: 250, near: 50, far: 30000, refreshInterval: 0 // 每帧渲染(0=每帧,用 PBO 异步回读避免 GPU 停顿) }; // 从 localStorage 恢复 FOV 和透明度 try { var __savedFov = localStorage.getItem('hornet_rearview_fov'); if (__savedFov) { var __fv = parseFloat(__savedFov); if (!isNaN(__fv) && __fv >= 0.5 && __fv <= 2.5) Config.fov = __fv; } } catch(e) {} var RearviewOpacity = 1.0; try { var __savedROp = localStorage.getItem('hornet_rearview_opacity'); if (__savedROp) { var __rop = parseFloat(__savedROp); if (!isNaN(__rop) && __rop >= 0.2 && __rop <= 1) RearviewOpacity = __rop; } } catch(e) {} // ===== UI 层禁用(防止技能条等 UI 混入后视 FBO)===== // ff.x38_1 是渲染层队列数组,索引对应层: // 20=Hud, 21=GuiBack, 22=GuiMain, 23=GuiFront, 24=DebugGui // 替换为空假对象,使 z3k 填充和渲染都跳过这些层 var uiLayerIndices = [20, 21, 22, 23, 24]; var fakeQueue = { e1: function() { return 0; }, g1: function(i) { return null; }, w2a: function(x) {}, d3: function() {}, ii: function(x) {} }; // ===== 获取游戏渲染对象 ===== var scene = null, ff = null, rootNode = null, mainCamera = null, gl = null; var initError = null; try { scene = gm.lz4()[FSceneRef]; console.log('[后视] 场景对象:', scene); // hC 渲染器实例(jr.lye_1) var renderer = scene[FRenderer]; console.log('[后视] 渲染器:', renderer); // Ff 绘制器实例(hC.r74_1) ff = renderer[FDrawer]; console.log('[后视] Ff绘制器:', ff); // 场景根节点(jr.kyf_1) rootNode = scene[FRootNode]; console.log('[后视] 根节点:', rootNode); // 主相机 Ha 实例(jr.nyf_1) mainCamera = scene[FCameraObject]; console.log('[后视] 主相机:', mainCamera); // 游戏 gl context(Ff.p39_1.p73_1 = BS.p73_1) gl = ff[FGl][FGlNative]; console.log('[后视] GL上下文:', gl); if (!ff || !rootNode || !mainCamera || !gl) { throw new Error('关键字段缺失'); } // 探测相机字段 console.log('[后视] 相机u35_1:', mainCamera[FAngles]); console.log('[后视] 相机r35_1:', mainCamera[FPosition]); console.log('[后视] 相机b37_1:', mainCamera[FFov]); if (!mainCamera[FAngles] || !mainCamera[FPosition]) { throw new Error('相机字段缺失'); } } catch(e) { initError = e; console.error('[后视] 初始化失败:', e.message, e.stack); } // ===== 创建 UI canvas(显示后视画面 + UI 标记)===== var uiCanvas = document.createElement('canvas'); uiCanvas.width = Config.width; uiCanvas.height = Config.height; uiCanvas.style.cssText = 'position:fixed;top:10px;right:10px;width:' + Config.width + 'px;height:' + Config.height + 'px;z-index:99999;border:2px solid rgba(120,200,255,0.6);border-radius:6px;pointer-events:auto;cursor:move;box-shadow:0 0 12px rgba(0,0,0,0.6);opacity:' + RearviewOpacity + ';'; document.body.appendChild(uiCanvas); if (lib.MakeDraggable) lib.MakeDraggable(uiCanvas, 'rearview'); var uiCtx = uiCanvas.getContext('2d'); // ===== UI 绘制函数 ===== function DrawText(text, x, y, color, font, align, baseline) { uiCtx.font = font || '12px monospace'; uiCtx.textAlign = align || 'left'; uiCtx.textBaseline = baseline || 'middle'; uiCtx.fillStyle = color || 'rgba(255,255,255,1)'; uiCtx.fillText(text, x, y); } function DrawTextOutlined(text, x, y, color, font, align, baseline) { uiCtx.font = font || '12px monospace'; uiCtx.textAlign = align || 'left'; uiCtx.textBaseline = baseline || 'middle'; uiCtx.strokeStyle = 'rgba(0,0,0,0.85)'; uiCtx.lineWidth = 3; uiCtx.strokeText(text, x, y); uiCtx.fillStyle = color || 'rgba(255,255,255,1)'; uiCtx.fillText(text, x, y); } // ===== FBO 创建(后视镜渲染目标)===== var rw = Config.width, rh = Config.height; var rearFbo = null, rearTex = null, rearDepthRb = null; var pixels = new Uint8Array(rw * rh * 4); var imageData = uiCtx.createImageData(rw, rh); // PBO 双缓冲异步回读(避免 readPixels 同步 GPU 停顿) var pbos = [null, null]; var pboIndex = 0; var pboReady = false; // 第一帧没有前帧数据,跳过回读 var pboSupported = false; // Uint32 视图用于批量设置 alpha=255(修复树叶等半透明边缘) var pixels32 = new Uint32Array(pixels.buffer); if (gl && !initError) { try { rearFbo = gl.createFramebuffer(); rearTex = gl.createTexture(); rearDepthRb = gl.createRenderbuffer(); // 颜色纹理 gl.bindTexture(gl.TEXTURE_2D, rearTex); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, rw, rh, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); // 深度缓冲 gl.bindRenderbuffer(gl.RENDERBUFFER, rearDepthRb); gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, rw, rh); // 组装 FBO gl.bindFramebuffer(gl.FRAMEBUFFER, rearFbo); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, rearTex, 0); gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, rearDepthRb); var fboStatus = gl.checkFramebufferStatus(gl.FRAMEBUFFER); console.log('[后视] FBO状态:', fboStatus, '(完整=' + gl.FRAMEBUFFER_COMPLETE + ')'); gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.bindTexture(gl.TEXTURE_2D, null); gl.bindRenderbuffer(gl.RENDERBUFFER, null); if (fboStatus !== gl.FRAMEBUFFER_COMPLETE) { throw new Error('FBO不完整: ' + fboStatus); } // 创建 PBO 双缓冲(WebGL2 PIXEL_PACK_BUFFER) try { var byteSize = rw * rh * 4; for (var pi = 0; pi < 2; pi++) { pbos[pi] = gl.createBuffer(); gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbos[pi]); gl.bufferData(gl.PIXEL_PACK_BUFFER, byteSize, gl.STREAM_READ); } gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null); pboSupported = true; console.log('[后视] PBO 双缓冲已创建(异步回读)'); } catch(pboErr) { console.warn('[后视] PBO 不支持,回退到同步 readPixels:', pboErr.message); pboSupported = false; } } catch(e) { initError = e; console.error('[后视] FBO创建失败:', e.message); } } // ===== Hook gl.bindFramebuffer(后视渲染时重定向到我们的 FBO)===== var rearviewRendering = false; var mainFbo = null; var origBindFramebuffer = null; var origH3c = null, origG3c = null; if (gl && !initError) { origBindFramebuffer = gl.bindFramebuffer.bind(gl); gl.bindFramebuffer = function(target, fbo) { // 记录主 FBO(非后视渲染时的绑定) if (!rearviewRendering && target === gl.FRAMEBUFFER) { mainFbo = fbo; } // 后视渲染时重定向到我们的 FBO if (rearviewRendering && target === gl.FRAMEBUFFER && fbo !== rearFbo) { return origBindFramebuffer(target, rearFbo); } return origBindFramebuffer(target, fbo); }; console.log('[后视] gl.bindFramebuffer 已 Hook'); // Hook BS.h3c/g3c:后视渲染时返回 FBO 尺寸(修复 screenSize/aspect ratio) // y3s 会调用 BS.h3c()/g3c() 获取 drawingBufferWidth/Height 设置相机 l36_1/m36_1 // 不 Hook 的话 aspect ratio 会用游戏 canvas 尺寸(如1920x1080),导致画面比例错误 origH3c = ff[FGl][MGetDrawingWidth].bind(ff[FGl]); origG3c = ff[FGl][MGetDrawingHeight].bind(ff[FGl]); ff[FGl][MGetDrawingWidth] = function() { return rearviewRendering ? rw : origH3c(); }; ff[FGl][MGetDrawingHeight] = function() { return rearviewRendering ? rh : origG3c(); }; console.log('[后视] BS.o3c/n3c 已 Hook'); } // ===== 渲染后视镜 ===== var frameCount = 0; var running = true; var debugInfo = { frame: 0, tankCount: 0, renderedTanks: 0, lastError: null }; function RenderRearview() { if (!running || initError || !ff || !mainCamera || !rootNode || !gl || !rearFbo) return; frameCount++; debugInfo.frame = frameCount; // 频率控制 if (frameCount % (Config.refreshInterval + 1) !== 0) return; var playerTank = GetPlayerTank(); if (!playerTank) { uiCtx.clearRect(0, 0, rw, rh); DrawText('未找到玩家坦克', rw/2, rh/2, 'rgba(255,200,80,1)', '13px monospace', 'center'); return; } var playerPos = GetTankPosition(playerTank); if (!playerPos) { uiCtx.clearRect(0, 0, rw, rh); DrawText('无位置数据', rw/2, rh/2, 'rgba(255,200,80,1)', '13px monospace', 'center'); return; } var camInfo = GetCameraInfo(); if (!camInfo) { uiCtx.clearRect(0, 0, rw, rh); DrawText('无相机数据', rw/2, rh/2, 'rgba(255,200,80,1)', '13px monospace', 'center'); return; } // 后视方向修复:camInfo.yaw = $r.lym_1.h20_1 是 roll(不是 yaw!) // o37: forward.y = d[5] = cos(pitch)*cos(roll) + sin(pitch)*sin(yaw)*sin(roll) // yaw=π 时 sin(yaw)=0,故后视 forward.y = cos(rearPitch)*cos(roll) // roll = camInfo.yaw+π → cos(camInfo.yaw+π) = -cos(camInfo.yaw) // 故后视 forward.y = -cos(rearPitch)*cos(camInfo.yaw),主视为 +cos(pitch)*cos(camInfo.yaw) // → 后视水平方向 = -主视水平方向(朝后看 ✓,与 rearPitch 符号无关) // o37: forward.z = d[9] = sin(pitch)*cos(yaw) // 后视 yaw=π: forward.z = -sin(rearPitch) // 要后视与主视同垂直方向(低头时都朝下看地形):rearPitch = -camInfo.pitch // → 后视 forward.z = -sin(-camInfo.pitch) = sin(camInfo.pitch) = 主视 forward.z ✓ // up 向量:rearPitch=-camInfo.pitch 时 up.z<0(朝下),FBO 画面颠倒 // GL readPixels(自底向上) + putImageData(自顶向下) = Y 翻转,使显示画面正立 ✓ var rearPitch = -camInfo.pitch; // 后视相机位置:玩家位置 + 高度偏移 var camPosX = playerPos.x; var camPosY = playerPos.y; var camPosZ = playerPos.z + Config.cameraHeight; // ===== 保存主相机参数 ===== var savedPitch = mainCamera[FAngles][FX]; var savedYaw = mainCamera[FAngles][FY]; var savedRoll = mainCamera[FAngles][FZ]; var savedX = mainCamera[FPosition][FX]; var savedY = mainCamera[FPosition][FY]; var savedZ = mainCamera[FPosition][FZ]; var savedL36 = mainCamera[FViewportWidth]; var savedM36 = mainCamera[FViewportHeight]; var savedS36 = mainCamera[FFovValue]; var savedQ36 = mainCamera[FNear]; var savedR36 = mainCamera[FFar]; // 临时暂停自由视角的 h37 hook,避免覆盖后视镜相机参数 var fl = window.__hornetFreeLook__; if (fl && typeof fl.SetSuspended === 'function') fl.SetSuspended(true); try { // ===== 设置后视相机参数 ===== // g=π 使水平方向反向(朝后看),h=yaw+π 保持水平视角正确 // f=-pitch 使垂直方向与主视一致(低头看后方地形) // up 朝下 → FBO 画面颠倒 → GL readback Y 翻转修正 mainCamera[FAngles][FX] = rearPitch; mainCamera[FAngles][FY] = Math.PI; mainCamera[FAngles][FZ] = camInfo.yaw + Math.PI; mainCamera[FPosition][FX] = camPosX; mainCamera[FPosition][FY] = camPosY; mainCamera[FPosition][FZ] = camPosZ; mainCamera[FViewportWidth] = rw; mainCamera[FViewportHeight] = rh; mainCamera[FFovValue] = Config.fov; mainCamera[FNear] = Config.near; mainCamera[FFar] = Config.far; // 更新相机矩阵(viewMatrix, projectionMatrix, viewProjectionMatrix) mainCamera[MCameraUpdate](); // 保存后视视图矩阵副本(用于 UI 标记投影,finally 会恢复相机所以需提前保存) var rearViewMatrix = new Float32Array(mainCamera[FFov]); // ===== 保存并禁用阴影/后处理(避免它们切换 FBO 覆盖 rearFbo)===== // p38_1.b3o_1 控制 z39_1(阴影),c3o_1 控制 c3a_1/d3a_1(后处理),z3n_1 控制 f3a_1 // 禁用后 y3s 只执行主场景渲染 z3k,跳过所有 FBO 切换的 pass var savedX3n = ff[FSettings][FShadowEnabled]; var savedY3n = ff[FSettings][FPostProcessEnabled]; var savedZ3n = ff[FSettings][FExtraPassEnabled]; ff[FSettings][FShadowEnabled] = false; ff[FSettings][FPostProcessEnabled] = false; ff[FSettings][FExtraPassEnabled] = false; // ===== 禁用 UI 渲染层(防止技能条等 UI 混入后视 FBO)===== // 保存 UI 层队列,替换为空假对象,y3s 后恢复 var savedUiQueues = {}; for (var ui = 0; ui < uiLayerIndices.length; ui++) { var li = uiLayerIndices[ui]; savedUiQueues[li] = ff[FLayerQueue][li]; ff[FLayerQueue][li] = fakeQueue; } // ===== 渲染到 FBO ===== rearviewRendering = true; // 清除 FBO origBindFramebuffer(gl.FRAMEBUFFER, rearFbo); gl.viewport(0, 0, rw, rh); gl.clearColor(0.15, 0.18, 0.22, 1.0); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // 调用游戏原生渲染器(已禁用阴影/后处理/UI层,只渲染 3D 场景) ff[MDrawScene](rootNode, mainCamera); rearviewRendering = false; // ===== Readback FBO 到 canvas(PBO 异步回读,避免 GPU 管线停顿)===== origBindFramebuffer(gl.FRAMEBUFFER, rearFbo); if (pboSupported) { // PBO 异步回读:当前帧发起异步 readPixels 到 PBO[current],读取上一帧的 PBO[previous] 数据 var curPbo = pboIndex; var prevPbo = 1 - pboIndex; // 发起当前帧的异步 readPixels(写入 PBO,不阻塞 GPU) gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbos[curPbo]); gl.readPixels(0, 0, rw, rh, gl.RGBA, gl.UNSIGNED_BYTE, 0); // 读取上一帧的 PBO 数据(此时 GPU 应已完成) if (pboReady) { gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbos[prevPbo]); gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, pixels); } gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null); pboIndex = prevPbo; pboReady = true; } else { // 回退:同步 readPixels gl.readPixels(0, 0, rw, rh, gl.RGBA, gl.UNSIGNED_BYTE, pixels); } // 显示画面(g=π 渲染已自带 Y 翻转,直接复制) // 修复半透明边缘:强制 alpha=255(树叶等 alpha 混合会导致 canvas 透明) for (var ai = 0; ai < pixels32.length; ai++) { pixels32[ai] = pixels32[ai] | 0xFF000000; // little-endian: 高字节=alpha } imageData.data.set(pixels); uiCtx.putImageData(imageData, 0, 0); // 恢复主 FBO origBindFramebuffer(gl.FRAMEBUFFER, mainFbo); } catch(e) { debugInfo.lastError = e.message; console.error('[后视] 渲染失败:', e.message, e.stack); uiCtx.clearRect(0, 0, rw, rh); DrawText('渲染错误: ' + e.message, rw/2, rh/2, 'rgba(255,100,100,1)', '11px monospace', 'center'); } finally { // ===== 确保 rearviewRendering 已重置 ===== rearviewRendering = false; // ===== 恢复阴影/后处理(异常时也要恢复)===== try { if (typeof savedX3n !== 'undefined') { ff[FSettings][FShadowEnabled] = savedX3n; ff[FSettings][FPostProcessEnabled] = savedY3n; ff[FSettings][FExtraPassEnabled] = savedZ3n; } } catch(e) { console.error('[后视] 恢复阴影/后处理失败:', e.message); } // ===== 恢复 UI 渲染层(异常时也要恢复,否则游戏 UI 永久失效)===== try { if (savedUiQueues) { for (var ui2 = 0; ui2 < uiLayerIndices.length; ui2++) { var li2 = uiLayerIndices[ui2]; ff[FLayerQueue][li2] = savedUiQueues[li2]; } } } catch(e) { console.error('[后视] 恢复 UI 层失败:', e.message); } // ===== 恢复主相机参数 ===== try { mainCamera[FAngles][FX] = savedPitch; mainCamera[FAngles][FY] = savedYaw; mainCamera[FAngles][FZ] = savedRoll; mainCamera[FPosition][FX] = savedX; mainCamera[FPosition][FY] = savedY; mainCamera[FPosition][FZ] = savedZ; mainCamera[FViewportWidth] = savedL36; mainCamera[FViewportHeight] = savedM36; mainCamera[FFovValue] = savedS36; mainCamera[FNear] = savedQ36; mainCamera[FFar] = savedR36; mainCamera[MCameraUpdate](); } catch(e) { console.error('[后视] 恢复相机失败:', e.message); } // 恢复自由视角的 h37 hook if (fl && typeof fl.SetSuspended === 'function') fl.SetSuspended(false); } // ===== 绘制 UI 标记(坦克位置标记)===== if (rearViewMatrix) { DrawUIMarkers(playerTank, playerPos, camInfo, rearViewMatrix); } } // ===== UI 标记绘制 ===== function DrawUIMarkers(playerTank, playerPos, camInfo, viewMatrix) { var tankList = gm.m10b_1.j11y(); var count = tankList.e1(); debugInfo.tankCount = count; var visibleTanks = 0; for (var i = 0; i < count; i++) { var tank = tankList.g1(i); if (!tank || tank === playerTank) continue; if (!IsTankAlive(tank)) continue; var pos = GetTankPosition(tank); if (!pos) continue; // 使用视图矩阵投影(与 3D 渲染一致) // o37 变换前矩阵行:right(d[0,4,8]), forward(d[1,5,9]), up(d[2,6,10]) // Ua 变换后行:right, up, -forward // 故变换后相机空间:camX=右,camY=上(up·rel),camZ=深度(-forward·rel,负=前方) // 后视相机 up 朝下:世界"上"rel.z>0 → camY<0;渲染+FBO readback 使世界"上"显示在 canvas 顶部 // marker 用 + 号匹配:camY<0(上方) → +负 → screen.y= -0.1) continue; // 剔除相机后方(深度非负) var depth = -camZ; // 正值=距离 var focalLength = rh / (2 * Math.tan(Config.fov / 2)); var screen = { x: rw / 2 + (camX / depth) * focalLength, y: rh / 2 + (camY / depth) * focalLength, // +号匹配 GL readback Y 翻转后的画面 depth: depth }; if (screen.x < 0 || screen.x > rw || screen.y < 0 || screen.y > rh) continue; if (screen.depth > Config.maxRange) continue; visibleTanks++; var friendly = IsFriendly(tank, playerTank); var color = friendly ? 'rgba(80,180,255,1)' : 'rgba(255,80,60,1)'; var bgColor = friendly ? 'rgba(40,90,140,0.7)' : 'rgba(140,40,30,0.7)'; // 方框标记 uiCtx.strokeStyle = color; uiCtx.lineWidth = 1.5; uiCtx.strokeRect(screen.x - 6, screen.y - 6, 12, 12); // 名称 var name = GetTankName(tank); if (name) { DrawTextOutlined(name, screen.x, screen.y - 12, color, '10px monospace', 'center'); } // 血量条 var health = GetTankHealth(tank); if (health) { var barW = 24, barH = 3; var barX = screen.x - barW / 2, barY = screen.y + 8; uiCtx.fillStyle = 'rgba(0,0,0,0.6)'; uiCtx.fillRect(barX, barY, barW, barH); var hpColor = health.percent > 0.6 ? 'rgba(80,255,80,1)' : health.percent > 0.3 ? 'rgba(255,200,60,1)' : 'rgba(255,80,60,1)'; uiCtx.fillStyle = hpColor; uiCtx.fillRect(barX, barY, barW * health.percent, barH); } } debugInfo.renderedTanks = visibleTanks; // 边框 + 标题 uiCtx.strokeStyle = 'rgba(120,200,255,0.4)'; uiCtx.lineWidth = 1; uiCtx.strokeRect(0, 0, rw, rh); DrawTextOutlined('后视镜', 8, 12, 'rgba(120,200,255,0.9)', '11px monospace', 'left'); DrawTextOutlined(visibleTanks + '/' + count, rw - 8, 12, 'rgba(180,180,180,0.9)', '11px monospace', 'right'); if (debugInfo.lastError) { DrawTextOutlined('ERR: ' + debugInfo.lastError.substring(0, 30), 8, rh - 8, 'rgba(255,100,100,0.9)', '10px monospace', 'left'); } } // ===== 主循环 ===== function Loop() { if (!running) return; // 换图自刷新:gameMode 实例变化时整体重启,重新获取场景/相机/GL hook var NewGm = window.__gameMode__; if (NewGm && NewGm !== gm) { console.log('[后视] 检测到新战斗,正在重启后视镜...'); Stop(); _start(); return; } requestAnimationFrame(Loop); // 旧实例已失效、新实例尚未捕获:跳过本帧 if (!window.__gameMode__) return; RenderRearview(); } // ===== 停止函数 ===== function Stop() { running = false; console.log('[后视] 停止后视镜'); // 恢复 gl.bindFramebuffer if (origBindFramebuffer && gl) { gl.bindFramebuffer = origBindFramebuffer; console.log('[后视] gl.bindFramebuffer 已恢复'); } // 恢复 BS.h3c/g3c if (origH3c && ff && ff[FGl]) { ff[FGl][MGetDrawingWidth] = origH3c; ff[FGl][MGetDrawingHeight] = origG3c; console.log('[后视] BS.o3c/n3c 已恢复'); } // 清理 FBO if (gl && rearFbo) gl.deleteFramebuffer(rearFbo); if (gl && rearTex) gl.deleteTexture(rearTex); if (gl && rearDepthRb) gl.deleteRenderbuffer(rearDepthRb); // 清理 PBO if (gl && pbos[0]) gl.deleteBuffer(pbos[0]); if (gl && pbos[1]) gl.deleteBuffer(pbos[1]); // 移除 canvas if (uiCanvas && uiCanvas.parentNode) { uiCanvas.parentNode.removeChild(uiCanvas); } window.__hornetRearview__ = null; } // ===== 启动 ===== window.__hornetRearview__ = { Stop: Stop, Config: Config, setFov: function(f) { var n = parseFloat(f); if (!isNaN(n) && n >= 0.5 && n <= 2.5) { Config.fov = n; try { localStorage.setItem('hornet_rearview_fov', String(n)); } catch(e) {} console.log('[后视] FOV 设为: ' + n); } else { console.warn('[后视] FOV 应在 0.5~2.5'); } }, getFov: function() { return Config.fov; }, setOpacity: function(o) { var n = parseFloat(o); if (!isNaN(n) && n >= 0.2 && n <= 1) { RearviewOpacity = n; uiCanvas.style.opacity = String(n); try { localStorage.setItem('hornet_rearview_opacity', String(n)); } catch(e) {} } else { console.warn('[后视] 透明度应在 0.2~1'); } }, getOpacity: function() { return RearviewOpacity; } }; if (initError) { uiCtx.clearRect(0, 0, rw, rh); DrawText('初始化失败:', rw/2, rh/2 - 10, 'rgba(255,100,100,1)', '13px monospace', 'center'); DrawText(initError.message, rw/2, rh/2 + 10, 'rgba(255,200,80,1)', '11px monospace', 'center'); console.error('[后视] 初始化失败,请检查字段访问'); return; } console.log('[后视] 原生渲染后视镜已启动'); console.log('[后视] 配置: ' + rw + 'x' + rh + ', FOV=' + Config.fov + ', 间隔=' + Config.refreshInterval); Loop(); } var Tool = { Start: function() { if (window.__hornetRearview__) return; _start(); }, Stop: function() { if (window.__hornetRearview__) window.__hornetRearview__.Stop(); }, Status: function() { return window.__hornetRearview__ ? '运行中' : '已停止'; }, GetRunning: function() { return !!window.__hornetRearview__; }, SetFov: function(f) { if (window.__hornetRearview__) window.__hornetRearview__.setFov(f); }, GetFov: function() { return window.__hornetRearview__ ? window.__hornetRearview__.getFov() : 1.2; }, SetOpacity: function(o) { if (window.__hornetRearview__) window.__hornetRearview__.setOpacity(o); }, GetOpacity: function() { return window.__hornetRearview__ ? window.__hornetRearview__.getOpacity() : 1.0; } }; window.__hornetTools__ = window.__hornetTools__ || {}; window.__hornetTools__.Rearview = Tool; console.log('[3DTank] 后视镜模块已加载'); })(); // ===== hornet_aim_warning.js ===== // ===== 3DTank瞄准警告 - 敌方坦克瞄准线预判 ===== // 在浏览器控制台运行(需先运行 hook_gamemode.js) // 功能:当敌方坦克炮塔朝向玩家时,立刻显示HUD警告并标记方向 // 独立轻量级脚本,不依赖3D透视渲染 (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; } // ===== 匿名字段常量 ===== var FEntityId = Fld('Entity.Id', 'iz1_1'); var FEntityRoot = Fld('Entity.Root', 'qz1_1'); var FEntityComponentMap = Fld('Entity.ComponentMap', 's2a_1'); var MGameModeScene = Mth('GameMode.Scene', 'lz4'); var MCollisionRaycast = Mth('CollisionWorld.Raycast', 'a17p'); var FWeaponComponent = Fld('Weapon.Component', 'n11c_1'); var FWeaponComponentAlt = Fld('Weapon.ComponentAlt', 'q112_1'); var FWeaponTurretRoot = Fld('Weapon.TurretRoot', 'm17a_1'); var FNodeMatrix = Fld('Node.MatrixOld', 'n35_1'); var FRayHitPoint = Fld('Ray.HitPoint', 'y178_1'); var FRayHitNormal = Fld('Ray.HitNormal', 'z178_1'); var FRayDistance = Fld('Ray.Distance', 'a179_1'); var FRayHitShape = Fld('Ray.HitShape', 'q17o_1'); var FRayHitBody = Fld('Ray.HitBody', 'b179_1'); var FRayFilterQuery = Fld('RayFilter.Query', 'l1da_1'); function _start() { console.log('===== 3DTank瞄准警告启动 ====='); // 允许在未进入战局 / ght 尚未捕获时启动:先出状态行,威胁判定等进入战局再算 if (window.__hornetAimWarning__) { window.__hornetAimWarning__.Stop(); console.log('已停止旧瞄准警告'); } var gm = window.__gameMode__ || null; // ===== 设置参数(localStorage 持久化)===== // Sensitivity: 灵敏度系数,>1 更灵敏(阈值更大,更易触发),<1 更迟钝 var Sensitivity = 1.0; try { var __savedSens = localStorage.getItem('hornet_aim_sensitivity'); if (__savedSens) { var __s = parseFloat(__savedSens); if (!isNaN(__s) && __s >= 0.3 && __s <= 3.0) Sensitivity = __s; } } catch(e) {} // SoundEnabled: 警告声音开关 var SoundEnabled = false; try { var __savedSE = localStorage.getItem('hornet_aim_sound'); if (__savedSE === 'true') SoundEnabled = true; } catch(e) {} var _audioCtx = null; var _lastBeepTime = 0; function PlayBeep() { if (!SoundEnabled) return; var now = Date.now(); if (now - _lastBeepTime < 300) return; // 最多每300ms响一次 _lastBeepTime = now; try { if (!_audioCtx) _audioCtx = new (window.AudioContext || window.webkitAudioContext)(); var osc = _audioCtx.createOscillator(); var gain = _audioCtx.createGain(); osc.frequency.value = 880; osc.type = 'square'; gain.gain.value = 0.08; osc.connect(gain); gain.connect(_audioCtx.destination); osc.start(); gain.gain.exponentialRampToValueAtTime(0.001, _audioCtx.currentTime + 0.15); osc.stop(_audioCtx.currentTime + 0.15); } catch(e) {} } // ===== 引用公共库(hornet_lib.js) ===== var lib = window.__hornetLib__; if (!lib) { console.error('[瞄准] 请先运行 hornet_lib.js'); return; } var teamFns = lib.LoadTeamFunctions(); var ZCt = teamFns.ZCt, m2Fn = teamFns.m2Fn; // 公共函数别名(调用处不变) var FindComponentByField = lib.FindComponentByField; var GetTankPosition = lib.GetTankPosition; var GetTankState = lib.GetTankState; var IsTankAlive = lib.IsTankAlive; // ===== 获取 TDt 武器皮肤组件(炮塔) ===== function GetTDtComponent(entity) { var comp = FindComponentByField(entity, FWeaponComponent); if (comp) return comp; comp = FindComponentByField(entity, FWeaponComponentAlt); return comp; } // 坦克名称/血量:使用公共库 var GetTankName = lib.GetTankName; var GetTankHealth = lib.GetTankHealth; // ===== 获取炮塔瞄准角度 ===== // 从炮塔 Bp 节点世界矩阵(n35_1)提取水平面前向方向 // 返回 { angle: 炮塔朝向与"敌方→玩家"方向的夹角, distance: 距离, turretAngle: 炮塔朝向角度, turretDir: {x,y} } function GetTurretAimAngle(tank, enemyPos, playerPos) { var tdt = GetTDtComponent(tank); if (!tdt || !tdt[FWeaponComponent] || !tdt[FWeaponComponent][FWeaponTurretRoot]) return null; var bpNode = tdt[FWeaponComponent][FWeaponTurretRoot]; var m = bpNode[FNodeMatrix]; if (!m) return null; // 敌方→玩家方向(x,y 水平面) var dx = playerPos.x - enemyPos.x; var dy = playerPos.y - enemyPos.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < 100) return null; // 太近不判断 var tx = dx / dist, ty = dy / dist; // 尝试 X 轴(m0,m1) 和 Y 轴(m4,m5),取最小夹角 var minAngle = Math.PI; var turretAngle = 0; var turretDir = { x: 1, y: 0 }; var axes = [ { x: m[0], y: m[1] }, { x: m[4], y: m[5] } ]; for (var ai = 0; ai < axes.length; ai++) { var ax = axes[ai].x, ay = axes[ai].y; var len = Math.sqrt(ax * ax + ay * ay); if (len < 0.001) continue; var nx = ax / len, ny = ay / len; var dot = nx * tx + ny * ty; var ang = Math.acos(Math.max(-1, Math.min(1, dot))); if (ang < minAngle) { minAngle = ang; turretAngle = Math.atan2(ny, nx); turretDir = { x: nx, y: ny }; } } return { angle: minAngle, distance: dist, turretAngle: turretAngle, turretDir: turretDir }; } // ===== 全局碰撞检测器(uMt 组件,挂在 GameMode entity 上) ===== var UMtComponent = null; function GetUMtComponent() { if (UMtComponent) return UMtComponent; try { var gmEntity = gm[MGameModeScene](); if (!gmEntity || !gmEntity[FEntityRoot] || !gmEntity[FEntityRoot][FEntityComponentMap]) return null; gmEntity[FEntityRoot][FEntityComponentMap].forEach(function(valueList, key) { if (UMtComponent) return; try { var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList; if (comp && typeof comp[MCollisionRaycast] === 'function') UMtComponent = comp; } catch(e) {} }); if (UMtComponent) console.log('[瞄准] uMt 碰撞检测器已获取'); else UMtComponent = false; } catch(e) { console.warn('[瞄准] 获取 uMt 组件失败:', e.message); } return UMtComponent || null; } // mock O$ 向量:使用公共库 var MkVec = lib.MkVec; // ===== 射线检测视线遮挡(缓存:每10帧更新) ===== var RaycastCache = new Map(); var RaycastFailCount = 0; function IsLineOfSightBlocked(enemyPos, playerPos, tankId, frame) { var cached = RaycastCache.get(tankId); if (cached && (frame - cached.frame) < 10) return cached.blocked; var uMt = GetUMtComponent(); if (!uMt) return false; var blocked = false; try { var dx = playerPos.x - enemyPos.x; var dy = playerPos.y - enemyPos.y; var dz = playerPos.z - enemyPos.z; var dist = Math.sqrt(dx*dx + dy*dy + dz*dz); if (dist < 200) { RaycastCache.set(tankId, { frame: frame, blocked: false }); return false; } var ndx = dx / dist, ndy = dy / dist, ndz = dz / dist; var startVec = MkVec(enemyPos.x, enemyPos.y, enemyPos.z + 200); var dirVec = MkVec(ndx, ndy, ndz); var maxDist = dist - 300; var result = { [FRayHitPoint]: MkVec(0,0,0), [FRayHitNormal]: MkVec(0,0,0), [FRayDistance]: maxDist, [FRayHitShape]: null, [FRayHitBody]: null, r17o: function(t, n, i, r, e) { this[FRayHitPoint].k26(t); this[FRayHitNormal].k26(n); this[FRayDistance] = i; this[FRayHitShape] = r; this[FRayHitBody] = e; }, d3: function() { this[FRayHitBody] = null; } }; var filter = { [FRayFilterQuery]: { h1: function() { return false; }, d3: function() {} }, w179: function() { return true; }, d3: function() { this[FRayFilterQuery].d3(); }, m1da: function() {} }; var hit = uMt[MCollisionRaycast](startVec, dirVec, maxDist, result, filter); blocked = !!hit && result[FRayDistance] < (maxDist - 50); RaycastFailCount = 0; } catch(e) { RaycastFailCount++; if (RaycastFailCount <= 3) console.warn('[瞄准] 射线检测异常:', e.message); blocked = false; } RaycastCache.set(tankId, { frame: frame, blocked: blocked }); return blocked; } // 玩家坦克/友方/相机/投影:使用公共库 function GetPlayerTank() { return lib.GetPlayerTank(gm); } function IsFriendly(tank, playerTank) { return lib.IsFriendly(tank, playerTank, gm); } function GetCameraInfo() { return lib.GetCameraInfo(gm); } var Project3D = lib.Project3D; // ===== 创建 Canvas ===== var canvas = document.createElement('canvas'); canvas.id = 'hornetAimWarningCanvas'; canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:99998;pointer-events:none;'; document.body.appendChild(canvas); var ctx = canvas.getContext('2d'); function ResizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } ResizeCanvas(); window.addEventListener('resize', ResizeCanvas); // ===== 警告闪烁状态 ===== var WarningPulse = 0; // 闪烁动画相位 // ===== 死亡检测状态 ===== var PlayerPrevHealth = -1; // 上一帧血量(-1=未初始化) var DeathInfo = null; // 死亡信息 { killerName, distance, expected, time, threats } var DeathDisplayTime = 360; // 死亡信息显示帧数(约6秒) var DeathStats = { total: 0, expected: 0, unexpected: 0 }; // 死亡统计 // ===== 主循环 ===== var running = true; var frameCount = 0; function Loop() { if (!running) return; frameCount++; requestAnimationFrame(Loop); // 换图自刷新:gameMode 实例变化时重绑并清空该地图的缓存 var NewGm = window.__gameMode__; if (NewGm && NewGm !== gm) { gm = NewGm; UMtComponent = null; // 碰撞检测器需从新场景重新获取 RaycastCache.clear(); // 旧地图的视线遮挡缓存失效 DeathInfo = null; // 清掉旧地图的死亡提示 console.log('[瞄准] 检测到新战斗,已重置缓存'); } // ght 尚未捕获 / 不在战局:清屏并显示等待提示 if (!window.__gameMode__) { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = 'rgba(255, 220, 120, 0.85)'; ctx.font = '11px Arial'; ctx.textAlign = 'left'; ctx.fillText('[瞄准警告] 等待战局…(ght 未捕获)', 5, 18); return; } var screenW = canvas.width; var screenH = canvas.height; ctx.clearRect(0, 0, screenW, screenH); var playerTank = GetPlayerTank(); if (!playerTank) return; var playerPos = GetTankPosition(playerTank); if (!playerPos) return; var camInfo = GetCameraInfo(); if (!camInfo) return; // 相机位置为0时使用玩家位置 if (Math.abs(camInfo.pos.x) < 0.1 && Math.abs(camInfo.pos.y) < 0.1 && Math.abs(camInfo.pos.z) < 0.1) { camInfo.pos = { x: playerPos.x, y: playerPos.y, z: playerPos.z + 100 }; } var tankList = gm.m10b_1.j11y(); var count = tankList.e1(); var threats = []; // 遍历所有敌方坦克,检测炮塔朝向 for (var i = 0; i < count; i++) { var tank = tankList.g1(i); if (!tank || tank === playerTank) continue; if (!IsTankAlive(tank)) continue; var pos = GetTankPosition(tank); if (!pos) continue; // 只检测敌方 if (IsFriendly(tank, playerTank)) continue; var aimInfo = GetTurretAimAngle(tank, pos, playerPos); if (!aimInfo) continue; // 瞄准阈值随距离变化:近距离~11度(0.20rad),远距离~3度 // 公式:0.20 / (1 + distance/8000),乘以灵敏度系数 var aimThreshold = 0.20 / (1 + aimInfo.distance / 8000) * Sensitivity; if (aimInfo.angle >= aimThreshold) continue; // 墙体阻隔判断 var blocked = IsLineOfSightBlocked(pos, playerPos, tank[FEntityId], frameCount); if (blocked) continue; // 记录威胁 var proj = Project3D(pos, camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH); var tankName = GetTankName(tank) || ('ID:' + tank[FEntityId]); threats.push({ pos: pos, distance: aimInfo.distance, angle: aimInfo.angle, turretDir: aimInfo.turretDir, screenProj: proj, tankId: tank[FEntityId], tankName: tankName }); } // ===== 死亡检测 ===== var playerHealth = GetTankHealth(playerTank); var playerCurHealth = playerHealth ? playerHealth.current : 0; // 检测血量从 >0 变为 0 = 死亡 if (PlayerPrevHealth > 0 && playerCurHealth <= 0) { // 死亡时,从 threats 列表找最近的瞄准者作为可能的击杀者 var killer = null; var minDist = Infinity; for (var ki = 0; ki < threats.length; ki++) { if (threats[ki].distance < minDist) { minDist = threats[ki].distance; killer = threats[ki]; } } DeathInfo = { killerName: killer ? killer.tankName : null, killerDistance: killer ? killer.distance : 0, expected: threats.length > 0, // 有瞄准者 = 意料之内 threatCount: threats.length, time: frameCount }; DeathStats.total++; if (DeathInfo.expected) DeathStats.expected++; else DeathStats.unexpected++; console.log('[瞄准] 玩家死亡! ' + (killer ? '被 ' + killer.tankName + ' 击杀' : '被偷击杀') + ' | 意料之' + (threats.length > 0 ? '内' : '外') + ' | 瞄准者: ' + threats.length + ' | 统计: ' + DeathStats.total + '死 (内' + DeathStats.expected + '/外' + DeathStats.unexpected + ')'); } // 复活检测:血量恢复 >0 if (PlayerPrevHealth <= 0 && playerCurHealth > 0) { DeathInfo = null; console.log('[瞄准] 玩家复活,重置死亡信息'); } PlayerPrevHealth = playerCurHealth; // ===== 绘制瞄准线(每个威胁的弹道轨迹) ===== if (threats.length > 0) { PlayBeep(); ctx.strokeStyle = 'rgba(255, 40, 40, 0.5)'; ctx.lineWidth = 2; for (var ti = 0; ti < threats.length; ti++) { var tr = threats[ti]; var startPos = { x: tr.pos.x, y: tr.pos.y, z: tr.pos.z + 200 }; // 炮塔高度 var endPos = { x: playerPos.x, y: playerPos.y, z: playerPos.z }; var segs = 20; var prevScreen = null; ctx.beginPath(); for (var si = 0; si <= segs; si++) { var st = si / segs; var swp = { x: startPos.x + (endPos.x - startPos.x) * st, y: startPos.y + (endPos.y - startPos.y) * st, z: startPos.z + (endPos.z - startPos.z) * st }; var sp = Project3D(swp, camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH); if (sp) { if (prevScreen) ctx.lineTo(sp.x, sp.y); else ctx.moveTo(sp.x, sp.y); prevScreen = sp; } else { prevScreen = null; } } ctx.stroke(); } } // ===== 每个威胁的方向标记 ===== for (var thi = 0; thi < threats.length; thi++) { var threat = threats[thi]; var proj = threat.screenProj; if (proj && proj.x >= 0 && proj.x <= screenW && proj.y >= 0 && proj.y <= screenH) { // ===== 威胁在屏幕内:在威胁位置画红色警告三角形 ===== var mx = proj.x, my = proj.y - 35; ctx.strokeStyle = 'rgba(255, 0, 0, 1)'; ctx.lineWidth = 2.5; ctx.fillStyle = 'rgba(255, 0, 0, 0.4)'; ctx.beginPath(); ctx.moveTo(mx, my - 10); ctx.lineTo(mx - 9, my + 6); ctx.lineTo(mx + 9, my + 6); ctx.closePath(); ctx.fill(); ctx.stroke(); // 感叹号 ctx.fillStyle = 'rgba(255, 255, 255, 1)'; ctx.font = 'bold 13px Arial'; ctx.textAlign = 'center'; ctx.fillText('!', mx, my + 4); // 距离标签 ctx.fillStyle = 'rgba(255, 200, 200, 0.95)'; ctx.font = 'bold 11px Arial'; ctx.fillText(Math.round(threat.distance / 100) + 'm', mx, my + 22); // 坦克名称 ctx.fillStyle = 'rgba(255, 240, 240, 0.95)'; ctx.font = 'bold 11px Arial'; ctx.fillText(threat.tankName, mx, my + 36); } else { // ===== 威胁不在屏幕内:屏幕边缘红色箭头指示方向 ===== var tdx = threat.pos.x - playerPos.x; var tdy = threat.pos.y - playerPos.y; var tlen = Math.sqrt(tdx * tdx + tdy * tdy); if (tlen > 0.001) { var ndx = tdx / tlen, ndy = tdy / tlen; // 计算相对相机偏航角的方向 var fwdX = -Math.sin(camInfo.yaw), fwdY = Math.cos(camInfo.yaw); var rgtX = Math.cos(camInfo.yaw), rgtY = Math.sin(camInfo.yaw); var screenRight = ndx * rgtX + ndy * rgtY; var screenFwd = ndx * fwdX + ndy * fwdY; var sAng = Math.atan2(screenRight, screenFwd); var edgeR = Math.min(screenW, screenH) * 0.42; var ex = screenW / 2 + Math.sin(sAng) * edgeR; var ey = screenH / 2 - Math.cos(sAng) * edgeR; // 限制在屏幕边缘内 ex = Math.max(30, Math.min(screenW - 30, ex)); ey = Math.max(30, Math.min(screenH - 30, ey)); // 画箭头(指向威胁方向) ctx.fillStyle = 'rgba(255, 0, 0, 0.95)'; ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(ex + Math.sin(sAng) * 14, ey - Math.cos(sAng) * 14); ctx.lineTo(ex + Math.sin(sAng + 2.5) * 9, ey - Math.cos(sAng + 2.5) * 9); ctx.lineTo(ex + Math.sin(sAng - 2.5) * 9, ey - Math.cos(sAng - 2.5) * 9); ctx.closePath(); ctx.fill(); ctx.stroke(); // 距离标签 ctx.fillStyle = 'rgba(255, 255, 255, 0.95)'; ctx.font = 'bold 11px Arial'; ctx.textAlign = 'center'; ctx.fillText(Math.round(threat.distance / 100) + 'm', ex, ey + 22); // 坦克名称 ctx.fillStyle = 'rgba(255, 240, 240, 0.95)'; ctx.font = 'bold 10px Arial'; ctx.fillText(threat.tankName, ex, ey + 36); } } } // ===== 左侧中央常显警告框(有威胁时) ===== if (threats.length > 0) { WarningPulse += 0.15; var pulseAlpha = 0.55 + Math.sin(WarningPulse) * 0.2; // 脉动透明度 // 动态高度:标题区 + 威胁列表 var maxListed = 4; // 最多列出4个威胁名称 var listedCount = Math.min(threats.length, maxListed); var boxW = 240; var boxH = 70 + listedCount * 18 + (threats.length > maxListed ? 16 : 0); var boxX = 20; var boxY = screenH / 2 - boxH / 2; // 警告框背景(脉动) ctx.fillStyle = 'rgba(80, 0, 0, ' + pulseAlpha + ')'; ctx.fillRect(boxX, boxY, boxW, boxH); ctx.strokeStyle = 'rgba(255, 0, 0, 1)'; ctx.lineWidth = 2.5; ctx.strokeRect(boxX, boxY, boxW, boxH); // 警告标题 ctx.fillStyle = 'rgba(255, 60, 60, 1)'; ctx.font = 'bold 26px Arial'; ctx.textAlign = 'center'; ctx.fillText('⚠ 被瞄准', boxX + boxW / 2, boxY + 32); ctx.font = 'bold 13px Arial'; ctx.fillStyle = 'rgba(255, 220, 220, 0.95)'; ctx.fillText(threats.length + ' 个威胁锁定你', boxX + boxW / 2, boxY + 54); // 威胁坦克名称列表 ctx.textAlign = 'left'; ctx.font = 'bold 12px Arial'; for (var li = 0; li < listedCount; li++) { var t = threats[li]; var lineY = boxY + 72 + li * 18; // 名称 + 距离 ctx.fillStyle = 'rgba(255, 240, 240, 0.95)'; ctx.fillText('• ' + t.tankName, boxX + 12, lineY); ctx.fillStyle = 'rgba(255, 180, 180, 0.85)'; ctx.textAlign = 'right'; ctx.fillText(Math.round(t.distance / 100) + 'm', boxX + boxW - 12, lineY); ctx.textAlign = 'left'; } // 超出部分 if (threats.length > maxListed) { ctx.fillStyle = 'rgba(255, 200, 200, 0.7)'; ctx.font = 'italic 11px Arial'; ctx.textAlign = 'center'; ctx.fillText('等 ' + (threats.length - maxListed) + ' 个...', boxX + boxW / 2, boxY + 72 + listedCount * 18 + 12); } } // ===== 死亡信息显示(屏幕中央,持续6秒或直到复活) ===== if (DeathInfo && (frameCount - DeathInfo.time) < DeathDisplayTime) { var dBoxW = 400, dBoxH = 120; var dBoxX = screenW / 2 - dBoxW / 2; var dBoxY = screenH / 2 - dBoxH / 2 - 60; // 偏上一点 // 半透明背景 ctx.fillStyle = 'rgba(0, 0, 0, 0.75)'; ctx.fillRect(dBoxX, dBoxY, dBoxW, dBoxH); // 边框(意料之内=红色,意料之外=紫色) var borderColor = DeathInfo.expected ? 'rgba(255, 60, 60, 1)' : 'rgba(200, 80, 255, 1)'; ctx.strokeStyle = borderColor; ctx.lineWidth = 3; ctx.strokeRect(dBoxX, dBoxY, dBoxW, dBoxH); // 标题 ctx.textAlign = 'center'; ctx.fillStyle = 'rgba(255, 80, 80, 1)'; ctx.font = 'bold 26px Arial'; ctx.fillText('你被击杀', dBoxX + dBoxW / 2, dBoxY + 36); // 击杀者信息 ctx.font = 'bold 18px Arial'; if (DeathInfo.killerName) { ctx.fillStyle = 'rgba(255, 220, 220, 1)'; ctx.fillText('击杀者: ' + DeathInfo.killerName, dBoxX + dBoxW / 2, dBoxY + 66); ctx.font = '12px Arial'; ctx.fillStyle = 'rgba(255, 180, 180, 0.8)'; ctx.fillText('距离 ' + Math.round(DeathInfo.killerDistance / 100) + 'm', dBoxX + dBoxW / 2, dBoxY + 86); } else { ctx.fillStyle = 'rgba(220, 180, 255, 1)'; ctx.fillText('未知来源(偷袭)', dBoxX + dBoxW / 2, dBoxY + 66); } // 意料之内/外标签 ctx.font = 'bold 16px Arial'; if (DeathInfo.expected) { ctx.fillStyle = 'rgba(255, 100, 100, 1)'; ctx.fillText('⚠ 意料之内(已在瞄准名单中)', dBoxX + dBoxW / 2, dBoxY + 108); } else { ctx.fillStyle = 'rgba(200, 100, 255, 1)'; ctx.fillText('✦ 意料之外(偷袭!)', dBoxX + dBoxW / 2, dBoxY + 108); } } // ===== 顶部状态栏(始终显示) ===== ctx.fillStyle = 'rgba(255, 255, 255, 0.4)'; ctx.font = '11px Arial'; ctx.textAlign = 'left'; var statusText = '[瞄准警告] 监控中 | 威胁: ' + threats.length + ' | 死亡: ' + DeathStats.total + '(内' + DeathStats.expected + '/外' + DeathStats.unexpected + ')' + ' | Frame: ' + frameCount; ctx.fillText(statusText, 5, 18); } // ===== 启动 ===== Loop(); console.log('[瞄准] 警告系统已启动,监控敌方坦克炮塔朝向'); // ===== 暴露控制接口 ===== window.__hornetAimWarning__ = { running: true, Stop: function() { this.running = false; running = false; window.removeEventListener('resize', ResizeCanvas); if (canvas && canvas.parentNode) canvas.parentNode.removeChild(canvas); console.log('[瞄准] 警告系统已停止'); }, GetDeathStats: function() { return DeathStats; }, ResetDeathStats: function() { DeathStats = { total: 0, expected: 0, unexpected: 0 }; console.log('[瞄准] 死亡统计已重置'); }, setSensitivity: function(s) { var n = parseFloat(s); if (!isNaN(n) && n >= 0.3 && n <= 3.0) { Sensitivity = n; try { localStorage.setItem('hornet_aim_sensitivity', String(n)); } catch(e) {} console.log('[瞄准] 灵敏度设为: ' + n); } else { console.warn('[瞄准] 灵敏度应在 0.3~3.0'); } }, getSensitivity: function() { return Sensitivity; }, setSound: function(on) { SoundEnabled = !!on; try { localStorage.setItem('hornet_aim_sound', SoundEnabled ? 'true' : 'false'); } catch(e) {} console.log('[瞄准] 警告声音: ' + (SoundEnabled ? '开' : '关')); }, getSound: function() { return SoundEnabled; } }; } var Tool = { Start: function() { if (window.__hornetAimWarning__ && window.__hornetAimWarning__.running) return; _start(); }, Stop: function() { if (window.__hornetAimWarning__) window.__hornetAimWarning__.Stop(); }, Status: function() { return (window.__hornetAimWarning__ && window.__hornetAimWarning__.running) ? '运行中' : '已停止'; }, GetRunning: function() { return !!(window.__hornetAimWarning__ && window.__hornetAimWarning__.running); }, SetSensitivity: function(s) { if (window.__hornetAimWarning__) window.__hornetAimWarning__.setSensitivity(s); }, GetSensitivity: function() { return window.__hornetAimWarning__ ? window.__hornetAimWarning__.getSensitivity() : 1.0; }, SetSound: function(on) { if (window.__hornetAimWarning__) window.__hornetAimWarning__.setSound(on); }, GetSound: function() { return window.__hornetAimWarning__ ? window.__hornetAimWarning__.getSound() : false; } }; window.__hornetTools__ = window.__hornetTools__ || {}; window.__hornetTools__.AimWarning = Tool; console.log('[3DTank] 瞄准警告模块已加载'); })(); // ===== hornet_freelook.js ===== // ===== 3DTank自由视角 ===== // 在浏览器控制台运行(需先运行 hook_gamemode.js 和 hornet_lib.js) // 功能:解锁主相机,WASD移动/鼠标右键旋转/QE升降/Shift加速/滚轮调速 // 原理:Hook mainCamera.h37,在相机矩阵更新前覆盖l35_1/k35_1参数 (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; } // ===== 匿名字段常量 ===== var FVecX = Fld('Vec.X', 'f20_1'); var FVecY = Fld('Vec.Y', 'g20_1'); var FVecZ = Fld('Vec.Z', 'h20_1'); var FCameraSceneRef = Fld('Camera.SceneRef', 'jz1_1'); var FCameraObject = Fld('Camera.Object', 'iyx_1'); var FCameraEuler = Fld('Camera.Euler', 'u35_1'); var FCameraPosition = Fld('Node.Children', 'r35_1'); var FViewMatrix = Fld('Camera.Fov', 'b37_1'); var FCameraSceneFx = Fld('Camera.SceneFx', 'd75_1'); var MGameModeScene = Mth('GameMode.Scene', 'lz4'); function _start() { console.log('===== 3DTank自由视角启动 ====='); if (!window.__gameMode__) { console.error('请先运行 hook_gamemode.js'); return; } if (window.__hornetFreeLook__) { window.__hornetFreeLook__.Stop(); console.log('已停止旧自由视角'); } var gm = window.__gameMode__; // ===== 引用公共库 ===== var lib = window.__hornetLib__; if (!lib) { console.error('[自由视角] 请先运行 hornet_lib.js'); return; } function GetPlayerTank() { return lib.GetPlayerTank(gm); } function GetTankPosition(tank) { return lib.GetTankPosition(tank); } // ===== 获取游戏渲染对象 ===== var scene = null, mainCamera = null, ff = null; var initError = null; try { scene = gm[MGameModeScene]()[FCameraSceneRef]; mainCamera = scene[FCameraObject]; ff = scene.gyw_1[FCameraSceneFx]; if (!mainCamera || !mainCamera[FCameraEuler] || !mainCamera[FCameraPosition] || !mainCamera.o37) { throw new Error('相机字段缺失'); } console.log('[自由视角] 主相机:', mainCamera.constructor.name, 'l35=(' + mainCamera[FCameraEuler][FVecX].toFixed(2) + ',' + mainCamera[FCameraEuler][FVecY].toFixed(2) + ',' + mainCamera[FCameraEuler][FVecZ].toFixed(2) + ')', 'k35=(' + mainCamera[FCameraPosition][FVecX].toFixed(0) + ',' + mainCamera[FCameraPosition][FVecY].toFixed(0) + ',' + mainCamera[FCameraPosition][FVecZ].toFixed(0) + ')'); } catch(e) { initError = e; console.error('[自由视角] 初始化失败:', e.message); } if (initError) return; // ===== 自由视角状态 ===== var freeLookActive = false; var running = true; // 自由视角参数(从当前相机参数初始化) var freeYaw = 0, freePitch = 0, freeRoll = 0; var freePosX = 0, freePosY = 0, freePosZ = 0; var moveSpeed = 1200; // 基础移动速度(世界单位/秒) var speedMultiplier = 1; // 滚轮调速倍数(0.2~5.0) // 按键状态 var keys = {}; var mouseSensitivity = 0.005; // 鼠标灵敏度 // ===== Hook mainCamera.h37 ===== var hadOwnH37 = mainCamera.hasOwnProperty('o37'); var origH37 = mainCamera.o37; function applyFreeLookParams() { mainCamera[FCameraEuler][FVecX] = freePitch; // pitch (f) mainCamera[FCameraEuler][FVecY] = freeRoll; // roll (g) 保持初始值 mainCamera[FCameraEuler][FVecZ] = freeYaw; // yaw (h) 鼠标左右控制 mainCamera[FCameraPosition][FVecX] = freePosX; mainCamera[FCameraPosition][FVecY] = freePosY; mainCamera[FCameraPosition][FVecZ] = freePosZ; } // 缓存从 viewMatrix 提取的前方向/右方向(h37 更新后设置) // o37 行向量布局(列主序存储): // 行0 = (d[0], d[4], d[8]) = right 向量 // 行1 = (d[1], d[5], d[9]) = up 向量 // 行2 = (d[2], d[6], d[10]) = -forward 向量 var cachedFwdX = 0, cachedFwdY = 0, cachedRightX = 0, cachedRightY = 0; var cachedInitialized = false; var suspended = false; // 后视镜等模块渲染时临时暂停 hook mainCamera.o37 = function() { if (freeLookActive && !suspended) { applyFreeLookParams(); origH37.call(this); // 从更新后的 viewMatrix (u36_1) 提取前方向和右方向(水平分量 x,y) if (this[FViewMatrix]) { cachedFwdX = -this[FViewMatrix][2]; // forward.x cachedFwdY = -this[FViewMatrix][6]; // forward.y cachedRightX = this[FViewMatrix][0]; // right.x cachedRightY = this[FViewMatrix][4]; // right.y cachedInitialized = true; } } else { return origH37.call(this); } }; console.log('[自由视角] mainCamera.o37 已 Hook'); // ===== 开启/关闭自由视角 ===== function enableFreeLook() { // 从当前相机参数初始化(平滑过渡) // l35_1: f=pitch, g=roll, h=yaw freePitch = mainCamera[FCameraEuler][FVecX]; freeRoll = mainCamera[FCameraEuler][FVecY]; // roll 保持初始值 freeYaw = mainCamera[FCameraEuler][FVecZ]; // yaw 鼠标控制 freePosX = mainCamera[FCameraPosition][FVecX]; freePosY = mainCamera[FCameraPosition][FVecY]; freePosZ = mainCamera[FCameraPosition][FVecZ]; freeLookActive = true; console.log('[自由视角] 已开启 — WASD移动 | 鼠标旋转 | 空格上升/Shift下降 | 滚轮调速 | Alt+F关闭'); updateHud(); } function disableFreeLook() { freeLookActive = false; keys = {}; // 清空按键状态,防止卡死 console.log('[自由视角] 已关闭,恢复游戏相机控制'); updateHud(); } function toggleFreeLook() { if (freeLookActive) disableFreeLook(); else enableFreeLook(); } // ===== 键盘事件(capture阶段拦截,防止游戏接收WASD) ===== // 用 e.code(物理键位)匹配,比 e.key 更稳定,不受 Shift 修饰影响 var keyMap = { 'KeyW': 'w', 'KeyA': 'a', 'KeyS': 's', 'KeyD': 'd', 'KeyQ': 'q', 'KeyE': 'e', 'Space': ' ', 'ShiftLeft': 'shift', 'ShiftRight': 'shift' }; function onKeyDown(e) { // 焦点在输入框/选择框时不拦截(允许面板输入) var ae = document.activeElement; if (ae && (ae.tagName === 'INPUT' || ae.tagName === 'SELECT' || ae.tagName === 'TEXTAREA')) return; // Alt+F 切换(全局,无论是否开启) if (e.altKey && (e.key === 'f' || e.key === 'F')) { e.preventDefault(); e.stopPropagation(); toggleFreeLook(); return; } if (!freeLookActive) return; var key = keyMap[e.code] || e.key.toLowerCase(); if (keyMap[e.code]) { keys[key] = true; e.preventDefault(); e.stopPropagation(); } } function onKeyUp(e) { // 焦点在输入框/选择框时不拦截 var ae = document.activeElement; if (ae && (ae.tagName === 'INPUT' || ae.tagName === 'SELECT' || ae.tagName === 'TEXTAREA')) return; var key = keyMap[e.code] || e.key.toLowerCase(); if (keyMap[e.code]) { keys[key] = false; // 总是清理按键状态,防止卡死 // 只在自由视角激活时拦截事件,暂停时放行给游戏 if (freeLookActive) { e.preventDefault(); e.stopPropagation(); } } } // ===== 鼠标事件(自由视角激活时鼠标移动直接旋转,不按键) ===== function onMouseMove(e) { if (!freeLookActive) return; // 鼠标左移→视角左转(yaw减),鼠标上移→视角上抬(pitch增) freeYaw -= e.movementX * mouseSensitivity; freePitch -= e.movementY * mouseSensitivity; // 限制俯仰角到±80°,避免接近90°时的万向锁翻转 var limit = 80 * Math.PI / 180; if (freePitch > limit) freePitch = limit; if (freePitch < -limit) freePitch = -limit; } function onWheel(e) { if (!freeLookActive) return; e.preventDefault(); e.stopPropagation(); // 滚轮上=加速,下=减速 if (e.deltaY < 0) speedMultiplier = Math.min(5.0, speedMultiplier + 0.2); else speedMultiplier = Math.max(0.2, speedMultiplier - 0.2); updateHud(); } // capture 阶段拦截键盘 window.addEventListener('keydown', onKeyDown, true); window.addEventListener('keyup', onKeyUp, true); document.addEventListener('mousemove', onMouseMove); window.addEventListener('wheel', onWheel, true); // ===== HUD 显示 ===== var hud = document.createElement('div'); hud.id = 'hornetFreeLookHud'; hud.style.cssText = 'position:fixed;bottom:10px;left:10px;z-index:99999;' + 'background:rgba(0,0,0,0.7);color:#8f8;padding:6px 10px;border-radius:4px;' + 'font:12px monospace;pointer-events:none;border:1px solid rgba(120,255,120,0.5);' + 'display:none;user-select:none;'; document.body.appendChild(hud); function updateHud() { if (!freeLookActive) { hud.style.display = 'none'; return; } hud.style.display = 'block'; hud.innerHTML = '[自由视角 ON] 速度:' + (moveSpeed * speedMultiplier).toFixed(0) + '/秒
' + '位置:(' + freePosX.toFixed(0) + ',' + freePosY.toFixed(0) + ',' + freePosZ.toFixed(0) + ')
' + 'Yaw:' + freeYaw.toFixed(2) + ' Pitch:' + freePitch.toFixed(2) + '
' + 'WASD移动 鼠标旋转 空格上升/Shift下降 滚轮调速 Alt+F关闭'; } // ===== 主循环:处理按键 → 更新位置 ===== var rafId = null; var lastTime = performance.now(); function loop(now) { if (!running) return; // 换图自刷新:gameMode 实例变化时整体重启,重新 Hook 新相机的 o37 var NewGm = window.__gameMode__; if (NewGm && NewGm !== gm) { console.log('[自由视角] 检测到新战斗,正在重启以绑定新相机...'); self.Stop(); _start(); return; } // 旧实例已失效、新实例尚未捕获:跳过本帧 if (!window.__gameMode__) { rafId = requestAnimationFrame(loop); return; } var dt = (now - lastTime) / 1000; lastTime = now; if (dt > 0.1) dt = 0.1; // 防止卡顿后大跳 if (freeLookActive) { var speed = moveSpeed * speedMultiplier * dt; // 每秒 moveSpeed 单位 // 相机前方向/右方向:优先用 viewMatrix 提取的真实方向(跟随鼠标朝向) var fwdX, fwdY, rightX, rightY; if (cachedInitialized) { // 从 viewMatrix 提取的水平方向分量(列主序) fwdX = cachedFwdX; fwdY = cachedFwdY; rightX = cachedRightX; rightY = cachedRightY; // 归一化水平方向(pitch 不为0时向量长度<1,保持移动速度一致) var fwdLen = Math.sqrt(fwdX * fwdX + fwdY * fwdY); if (fwdLen > 1e-6) { fwdX /= fwdLen; fwdY /= fwdLen; } var rightLen = Math.sqrt(rightX * rightX + rightY * rightY); if (rightLen > 1e-6) { rightX /= rightLen; rightY /= rightLen; } } else { // fallback: sin/cos(viewMatrix 尚未初始化时) fwdX = -Math.sin(freeYaw); fwdY = -Math.cos(freeYaw); rightX = Math.cos(freeYaw); rightY = -Math.sin(freeYaw); } var moved = false; // W/S 前后(水平移动,可与其他键组合) if (keys['w']) { freePosX += fwdX * speed; freePosY += fwdY * speed; moved = true; } if (keys['s']) { freePosX -= fwdX * speed; freePosY -= fwdY * speed; moved = true; } // A/D 左右(水平移动,可与其他键组合) if (keys['a']) { freePosX -= rightX * speed; freePosY -= rightY * speed; moved = true; } if (keys['d']) { freePosX += rightX * speed; freePosY += rightY * speed; moved = true; } // 空格上升 / Shift 下降(绝对 z 轴,可与 WASD 组合) if (keys[' ']) { freePosZ += speed; moved = true; } if (keys['shift']) { freePosZ -= speed; moved = true; } // Q/E 升降(备用) if (keys['q']) { freePosZ -= speed; moved = true; } if (keys['e']) { freePosZ += speed; moved = true; } if (moved) updateHud(); } rafId = requestAnimationFrame(loop); } rafId = requestAnimationFrame(loop); // ===== 对外接口 ===== var self = { Stop: function() { running = false; if (rafId) cancelAnimationFrame(rafId); disableFreeLook(); // 恢复 h37 if (hadOwnH37) mainCamera.o37 = origH37; else delete mainCamera.o37; // 移除监听 window.removeEventListener('keydown', onKeyDown, true); window.removeEventListener('keyup', onKeyUp, true); document.removeEventListener('mousemove', onMouseMove); window.removeEventListener('wheel', onWheel, true); if (hud.parentNode) hud.parentNode.removeChild(hud); window.__hornetFreeLook__ = null; console.log('[自由视角] 已停止'); }, Status: function() { // 返回字符串供面板 HUD 显示(其他工具也是字符串) if (!running) return '未运行'; return freeLookActive ? '已开启' : '已暂停'; }, GetRunning: function() { return freeLookActive; }, IsActive: function() { return freeLookActive; }, Toggle: function() { toggleFreeLook(); }, Enable: function() { if (!freeLookActive) enableFreeLook(); }, Disable: function() { if (freeLookActive) disableFreeLook(); }, // 供其他模块(雷达/3D透视)获取虚拟相机参数 GetPosition: function() { return freeLookActive ? { x: freePosX, y: freePosY, z: freePosZ } : null; }, GetDirection: function() { return freeLookActive ? { yaw: freeYaw, pitch: freePitch } : null; }, // 供后视镜等模块渲染时临时暂停 h37 hook SetSuspended: function(s) { suspended = s; } }; window.__hornetFreeLook__ = self; window.__hornetTools__ = window.__hornetTools__ || {}; window.__hornetTools__.FreeLook = self; // 启动时自动激活自由视角 enableFreeLook(); console.log('[自由视角] 就绪 — 已激活,Alt+F 切换激活/暂停'); } function _stop() { if (window.__hornetFreeLook__) window.__hornetFreeLook__.Stop(); } function _status() { return window.__hornetFreeLook__ ? window.__hornetFreeLook__.Status() : '未运行'; } function _getRunning() { return !!(window.__hornetFreeLook__ && window.__hornetFreeLook__.GetRunning()); } window.__hornetTools__ = window.__hornetTools__ || {}; window.__hornetTools__.FreeLook = { Start: _start, Stop: _stop, Status: _status, GetRunning: _getRunning }; })(); // ===== hornet_warsandbox.js ===== // ===== 3DTank 战争沙盘 - 真实3D俯瞰渲染 ===== // 在浏览器控制台运行(需先运行 hook_gamemode.js 和 hornet_lib.js) // 功能:80%x90% 可拖拽缩放的全景俯瞰图,使用游戏原生渲染器渲染 3D 场景 // 原理:Hook gl.bindFramebuffer 重定向到自定义 FBO,用俯瞰相机参数调用游戏渲染器 // 操作:拖拽标题移动窗口 | 拖拽地图平移 | 滚轮缩放 | 双击居中 | Alt+M 开关 (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; } // 向量 var FVecX = Fld('Vec.X', 'f20_1'); var FVecY = Fld('Vec.Y', 'g20_1'); var FVecZ = Fld('Vec.Z', 'h20_1'); // 战局 / 场景 var MScene = Mth('GameMode.Scene', 'lz4'); var FCameraSceneRef = Fld('Camera.SceneRef', 'jz1_1'); var FTankManager = Fld('GameMode.TankManager', 'm10b_1'); var MTankList = Mth('GameMode.TankList', 'j11y'); // 渲染器 var FRenderer = Fld('Renderer.Object', 'd75_1'); var FRenderDevice = Fld('Renderer.Device', 'w39_1'); var FRenderGl = Fld('Renderer.Gl', 'b74_1'); var FRenderFlags = Fld('Renderer.Flags', 'y38_1'); var FRenderShadow = Fld('Renderer.Shadow', 'e3o_1'); var FRenderPostFx = Fld('Renderer.PostFx', 'f3o_1'); var FRenderFlagAux = Fld('Renderer.FlagAux', 'k3o_1'); var FRenderQueueList = Fld('Renderer.QueueList', 'e39_1'); // 相机 var FCameraRotation = Fld('Camera.Rotation', 'u35_1'); var FNodeChildren = Fld('Node.Children', 'r35_1'); // 本文件里该字段是相机位置向量(与节点子列表同名复用) var FCameraRenderWidth = Fld('Camera.RenderWidth', 'u36_1'); var FCameraRenderHeight = Fld('Camera.RenderHeight', 'v36_1'); var FCameraFov = Fld('Camera.Fov', 'z36_1'); var FCameraNear = Fld('Camera.Near', 'x36_1'); var FCameraFar = Fld('Camera.Far', 'y36_1'); // 皮肤 / 节点 var FSkinSlotHull = Fld('Skin.SlotHull', 'w14w_1'); var FSkinSlotHullLegacy = Fld('Skin.SlotHullLegacy', 't14l_1'); var FSkinSlotHullAlt = Fld('Skin.SlotHullAlt', 'w14x_1'); var FSkinSlotHullOld = Fld('Skin.SlotHullOld', 'p14l_1'); var FSkinParts = Fld('Skin.Parts', 'g17r_1'); var FSkinPartNode = Fld('Skin.PartNode', 'j17r_1'); var FNodeMatrixOld = Fld('Node.MatrixOld', 'n35_1'); function _start() { console.log('===== 3DTank 战争沙盘(3D俯瞰版)启动 ====='); if (!window.__gameMode__) { console.error('请先运行 hook_gamemode.js'); return; } if (window.__hornetWarSandbox__) { window.__hornetWarSandbox__.Stop(); console.log('已停止旧战争沙盘'); } var gm = window.__gameMode__; var lib = window.__hornetLib__; if (!lib) { console.error('[沙盘] 请先运行 hornet_lib.js'); return; } var teamFns = lib.LoadTeamFunctions(); var FindComponentByField = lib.FindComponentByField; var GetTankPosition = lib.GetTankPosition; var GetTankState = lib.GetTankState; var IsTankAlive = lib.IsTankAlive; var GetTankName = lib.GetTankName; var GetTankHealth = lib.GetTankHealth; function GetPlayerTank() { return lib.GetPlayerTank(gm); } function IsFriendly(tank, playerTank) { return lib.IsFriendly(tank, playerTank, gm); } function getTankDirection(tank) { // 从车体皮肤组件(KMt)的Bp节点世界矩阵提取前向方向 // dzc_1 是局部空间向量且可能未设置,不可靠 // 皮肤字段:新 build 是 w14w_1;旧 build 是 t14l_1(新版 t14l_1 已改成悬停车体状态 Qmt) var hullComp = FindComponentByField(tank, FSkinSlotHull) || FindComponentByField(tank, FSkinSlotHullLegacy) || FindComponentByField(tank, FSkinSlotHullAlt) || FindComponentByField(tank, FSkinSlotHullOld); if (!hullComp) return null; var skin = (hullComp[FSkinSlotHull] && hullComp[FSkinSlotHull][FSkinParts]) ? hullComp[FSkinSlotHull] : (hullComp[FSkinSlotHullLegacy] && hullComp[FSkinSlotHullLegacy][FSkinParts]) ? hullComp[FSkinSlotHullLegacy] : (hullComp[FSkinSlotHullOld] && hullComp[FSkinSlotHullOld][FSkinParts]) ? hullComp[FSkinSlotHullOld] : null; if (!skin) return null; try { var partCount = skin[FSkinParts].e1(); for (var p = 0; p < partCount; p++) { var part = skin[FSkinParts].g1(p); if (!part || !part[FSkinPartNode]) continue; var nodeCount = part[FSkinPartNode].e1(); for (var n = 0; n < nodeCount; n++) { var bpNode = part[FSkinPartNode].g1(n); if (!bpNode || !bpNode[FNodeMatrixOld]) continue; // 不按 g35_1 过滤:它是引擎的细节/淡出权重(0~1),不是幽灵标志, // 过滤会漏掉低细节节点、导致取不到朝向 var m = bpNode[FNodeMatrixOld]; // 列主序世界矩阵:Col0=(m[0],m[1],m[2]) Col1=(m[4],m[5],m[6]) Col2=(m[8],m[9],m[10]) // 游戏坐标:x,y 水平面; z(h20_1)=高度。找"上"列(|z|最大),其余为水平方向 var az0 = Math.abs(m[2]), az1 = Math.abs(m[6]), az2 = Math.abs(m[10]); // 尝试 Col1(Y前向约定,Z-up游戏常见)作为前向,若 Col1 是"上"则用 Col2 var fx, fy; if (az1 < 0.7) { // Col1 不是上方向,用作前向 fx = m[4]; fy = m[5]; } else if (az2 < 0.7) { // Col1 是上,用 Col2 作前向 fx = m[8]; fy = m[9]; } else { // Col0 不是上,用作前向 fx = m[0]; fy = m[1]; } var len = Math.sqrt(fx*fx + fy*fy); if (len > 0.01) { return { x: fx/len, y: fy/len, z: 0 }; } } } } catch(e) {} return null; } // ===== 获取游戏渲染对象 ===== var scene = null, ff = null, rootNode = null, mainCamera = null, gl = null; var initError = null; try { scene = gm[MScene]()[FCameraSceneRef]; ff = scene.gyw_1[FRenderer]; rootNode = scene.fyx_1; mainCamera = scene.iyx_1; gl = ff[FRenderDevice][FRenderGl]; if (!ff || !rootNode || !mainCamera || !gl) throw new Error('关键字段缺失'); } catch(e) { initError = e; console.error('[沙盘] 初始化失败:', e.message); } if (initError) return; // ===== FBO 配置(固定内部分辨率,性能优先)===== var FBO_W = 960, FBO_H = 720; var Config = { fov: 1.0, // 俯瞰 FOV near: 100, far: 80000, minHeight: 3000, // 最小相机高度(放大) maxHeight: 60000, // 最大相机高度(缩小) defaultHeight: 20000 }; // 设置参数(localStorage 持久化) var ShowGrid = true; try { if (localStorage.getItem('hornet_ws_showgrid') === 'false') ShowGrid = false; } catch(e) {} var MarkerSize = 1.0; try { var __savedMS = localStorage.getItem('hornet_ws_marker_size'); if (__savedMS) { var __ms = parseFloat(__savedMS); if (!isNaN(__ms) && __ms >= 0.5 && __ms <= 3.0) MarkerSize = __ms; } } catch(e) {} // ===== 创建 UI ===== var container = document.createElement('div'); container.id = 'hornetWarSandbox'; var winW = Math.floor(window.innerWidth * 0.8); var winH = Math.floor(window.innerHeight * 0.9); container.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:100002;' + 'width:' + winW + 'px;height:' + winH + 'px;' + 'background:rgba(8,12,20,0.82);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);' + 'border:1px solid rgba(125,249,255,0.25);border-radius:12px;' + 'box-shadow:0 12px 48px rgba(0,0,0,0.7);display:flex;flex-direction:column;overflow:hidden;' + 'user-select:none;-webkit-user-select:none;'; document.body.appendChild(container); // ===== 窗口布局持久化(位置 + 尺寸)===== function SaveWinLayout() { try { var r = container.getBoundingClientRect(); localStorage.setItem('hornet_layout_warsandbox', JSON.stringify({ left: Math.round(r.left), top: Math.round(r.top), width: Math.round(r.width), height: Math.round(r.height) })); } catch(e) {} } try { var __wsSaved = localStorage.getItem('hornet_layout_warsandbox'); if (__wsSaved) { var __wsP = JSON.parse(__wsSaved); if (__wsP && typeof __wsP.left === 'number' && typeof __wsP.top === 'number') { container.style.transform = 'none'; container.style.left = Math.max(0, __wsP.left) + 'px'; container.style.top = Math.max(0, __wsP.top) + 'px'; if (typeof __wsP.width === 'number') container.style.width = Math.max(320, __wsP.width) + 'px'; if (typeof __wsP.height === 'number') container.style.height = Math.max(240, __wsP.height) + 'px'; } } } catch(e) {} var titleBar = document.createElement('div'); titleBar.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:10px 16px;' + 'background:rgba(125,249,255,0.06);border-bottom:1px solid rgba(125,249,255,0.15);cursor:move;' + 'font:14px Consolas,monospace;color:#7df9ff;flex-shrink:0;'; titleBar.innerHTML = '战争沙盘'; container.appendChild(titleBar); var ctrlBox = document.createElement('div'); ctrlBox.style.cssText = 'display:flex;gap:10px;align-items:center;'; titleBar.appendChild(ctrlBox); var infoLabel = document.createElement('span'); infoLabel.style.cssText = 'font-size:11px;color:#5a7a8a;'; ctrlBox.appendChild(infoLabel); var closeBtn = document.createElement('button'); closeBtn.textContent = 'X'; closeBtn.style.cssText = 'padding:2px 8px;border:1px solid rgba(255,100,100,0.4);border-radius:4px;' + 'background:rgba(200,50,50,0.3);color:#ff8080;cursor:pointer;font:12px Consolas;'; closeBtn.onclick = function() { _stop(); }; ctrlBox.appendChild(closeBtn); var canvas = document.createElement('canvas'); canvas.style.cssText = 'flex:1;cursor:grab;display:block;'; container.appendChild(canvas); var ctx = canvas.getContext('2d'); function resizeCanvas() { var rect = canvas.getBoundingClientRect(); canvas.width = Math.floor(rect.width); canvas.height = Math.floor(rect.height); } resizeCanvas(); // ===== 右下角缩放手柄:允许自由缩放窗口 ===== var resizeGrip = document.createElement('div'); resizeGrip.style.cssText = 'position:absolute;right:0;bottom:0;width:18px;height:18px;cursor:nwse-resize;z-index:2;' + 'background:linear-gradient(135deg,transparent 45%,rgba(125,249,255,0.35) 45%);' + 'border-bottom-right-radius:12px;'; container.appendChild(resizeGrip); var __rsx = 0, __rsy = 0, __rsw = 0, __rsh = 0; BindPointerDrag(resizeGrip, function(e) { var r = container.getBoundingClientRect(); __rsx = e.clientX; __rsy = e.clientY; __rsw = r.width; __rsh = r.height; return true; }, function(e) { container.style.transform = 'none'; container.style.width = Math.max(320, __rsw + (e.clientX - __rsx)) + 'px'; container.style.height = Math.max(240, __rsh + (e.clientY - __rsy)) + 'px'; resizeCanvas(); }, function() { resizeCanvas(); SaveWinLayout(); }); // ===== FBO 创建 ===== var sandboxFbo = null, sandboxTex = null, sandboxDepthRb = null; var pixels = new Uint8Array(FBO_W * FBO_H * 4); var pixels32 = new Uint32Array(pixels.buffer); var imageData = ctx.createImageData(FBO_W, FBO_H); var tempCanvas = document.createElement('canvas'); tempCanvas.width = FBO_W; tempCanvas.height = FBO_H; var tempCtx = tempCanvas.getContext('2d'); var pbos = [null, null], pboIndex = 0, pboReady = false, pboSupported = false; try { sandboxFbo = gl.createFramebuffer(); sandboxTex = gl.createTexture(); sandboxDepthRb = gl.createRenderbuffer(); gl.bindTexture(gl.TEXTURE_2D, sandboxTex); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, FBO_W, FBO_H, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.bindRenderbuffer(gl.RENDERBUFFER, sandboxDepthRb); gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, FBO_W, FBO_H); gl.bindFramebuffer(gl.FRAMEBUFFER, sandboxFbo); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, sandboxTex, 0); gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, sandboxDepthRb); var status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.bindTexture(gl.TEXTURE_2D, null); gl.bindRenderbuffer(gl.RENDERBUFFER, null); if (status !== gl.FRAMEBUFFER_COMPLETE) throw new Error('FBO不完整: ' + status); // PBO try { var byteSize = FBO_W * FBO_H * 4; for (var i = 0; i < 2; i++) { pbos[i] = gl.createBuffer(); gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbos[i]); gl.bufferData(gl.PIXEL_PACK_BUFFER, byteSize, gl.STREAM_READ); } gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null); pboSupported = true; } catch(e) { pboSupported = false; } console.log('[沙盘] FBO ' + FBO_W + 'x' + FBO_H + ' 已创建' + (pboSupported ? ' (PBO)' : ' (同步)')); } catch(e) { initError = e; console.error('[沙盘] FBO创建失败:', e.message); } if (initError) return; // ===== Hook gl.bindFramebuffer + BS.h3c/g3c ===== var sandboxRendering = false; var mainFbo = null; var origBindFramebuffer = gl.bindFramebuffer.bind(gl); gl.bindFramebuffer = function(target, fbo) { if (!sandboxRendering && target === gl.FRAMEBUFFER) mainFbo = fbo; if (sandboxRendering && target === gl.FRAMEBUFFER && fbo !== sandboxFbo) { return origBindFramebuffer(target, sandboxFbo); } return origBindFramebuffer(target, fbo); }; var origH3c = ff[FRenderDevice].o3c.bind(ff[FRenderDevice]); var origG3c = ff[FRenderDevice].n3c.bind(ff[FRenderDevice]); ff[FRenderDevice].o3c = function() { return sandboxRendering ? FBO_W : origH3c(); }; ff[FRenderDevice].n3c = function() { return sandboxRendering ? FBO_H : origG3c(); }; // UI 层禁用 var uiLayerIndices = [20, 21, 22, 23, 24]; var fakeQueue = { e1: function() { return 0; }, g1: function(i) { return null; }, w2a: function(x) {}, d3: function() {}, ii: function(x) {} }; // ===== 视图状态 ===== var viewCenterX = 0, viewCenterY = 0; var camHeight = Config.defaultHeight; var viewYaw = 0; // 地图旋转(0=北朝上) var initialized = false; // ===== 指针拖动绑定(Pointer Events + setPointerCapture,避免松手后粘手)===== // HandleEl: 触发拖动的元素;OnStart/OnMove/OnEnd 分别处理开始/移动/结束 function BindPointerDrag(HandleEl, OnStart, OnMove, OnEnd) { var Active = false; var PointerId = null; function Finish() { if (!Active) return; Active = false; if (PointerId !== null && HandleEl.releasePointerCapture) { try { HandleEl.releasePointerCapture(PointerId); } catch (Err) {} } PointerId = null; OnEnd(); } if (window.PointerEvent) { HandleEl.addEventListener('pointerdown', function(e) { if (e.button !== 0) return; if (OnStart(e) === false) return; Active = true; PointerId = e.pointerId; try { HandleEl.setPointerCapture(e.pointerId); } catch (Err) {} e.preventDefault(); e.stopPropagation(); }, true); HandleEl.addEventListener('pointermove', function(e) { if (!Active || (PointerId !== null && e.pointerId !== PointerId)) return; if (e.buttons === 0) { Finish(); return; } // 兜底:按键已松开 OnMove(e); e.preventDefault(); }, true); HandleEl.addEventListener('pointerup', Finish, true); HandleEl.addEventListener('pointercancel', Finish, true); HandleEl.addEventListener('lostpointercapture', Finish, true); window.addEventListener('pointerup', Finish, true); } else { HandleEl.addEventListener('mousedown', function(e) { if (e.button !== 0) return; if (OnStart(e) === false) return; Active = true; e.preventDefault(); }, true); document.addEventListener('mousemove', function(e) { if (!Active) return; if (e.buttons === 0) { Finish(); return; } OnMove(e); }, true); document.addEventListener('mouseup', Finish, true); } window.addEventListener('blur', Finish); window.addEventListener('mouseleave', Finish); return { End: Finish, IsActive: function() { return Active; } }; } // ===== 窗口拖动 ===== var dsx = 0, dsy = 0, dwx = 0, dwy = 0; BindPointerDrag(titleBar, function(e) { if (e.target.tagName === 'BUTTON') return false; dsx = e.clientX; dsy = e.clientY; var rect = container.getBoundingClientRect(); dwx = rect.left; dwy = rect.top; container.style.transform = 'none'; container.style.left = dwx + 'px'; container.style.top = dwy + 'px'; }, function(e) { var nx = Math.max(0, Math.min(window.innerWidth - 100, dwx + e.clientX - dsx)); var ny = Math.max(0, Math.min(window.innerHeight - 40, dwy + e.clientY - dsy)); container.style.left = nx + 'px'; container.style.top = ny + 'px'; }, function() { SaveWinLayout(); }); // ===== 地图平移 ===== var psx = 0, psy = 0, pcx = 0, pcy = 0; BindPointerDrag(canvas, function(e) { psx = e.clientX; psy = e.clientY; pcx = viewCenterX; pcy = viewCenterY; canvas.style.cursor = 'grabbing'; }, function(e) { // 俯瞰视图:屏幕像素→世界单位,X/Y 方向缩放不同(FBO 拉伸到 canvas) var ext = getViewExtents(); var dx = (e.clientX - psx) * ext.wppX; var dy = (e.clientY - psy) * ext.wppY; viewCenterX = pcx - dx; viewCenterY = pcy + dy; // y 反转 }, function() { canvas.style.cursor = 'grab'; }); // 滚轮缩放(改变相机高度) canvas.addEventListener('wheel', function(e) { e.preventDefault(); var factor = e.deltaY < 0 ? 0.85 : 1.15; var newHeight = camHeight * factor; if (newHeight < Config.minHeight || newHeight > Config.maxHeight) return; camHeight = newHeight; }, { passive: false }); // 双击居中 canvas.addEventListener('dblclick', function() { var player = GetPlayerTank(); if (player) { var pos = GetTankPosition(player); if (pos) { viewCenterX = pos.x; viewCenterY = pos.y; } } else { var fl = window.__hornetFreeLook__; if (fl && fl.IsActive && fl.IsActive()) { var p = fl.GetPosition(); if (p) { viewCenterX = p.x; viewCenterY = p.y; } } } }); // ===== 世界→屏幕投影(俯瞰视图,用于 2D 叠加层)===== // 注意:FBO(960x720) 被拉伸到 canvas(cw x ch),若两者比例不同则 X/Y 缩放不同 // 相机以 FBO_W/FBO_H 为 aspect 渲染,可见世界范围: // visH = 2*camHeight*tan(fov/2) (世界单位,纵向) // visW = visH * (FBO_W/FBO_H) (世界单位,横向) // FBO 拉伸到 canvas 后: // worldPerPixelX = visW / cw // worldPerPixelY = visH / ch function getViewExtents() { var ch = canvas.height, cw = canvas.width; var visH = 2 * camHeight * Math.tan(Config.fov / 2); var visW = visH * (FBO_W / FBO_H); return { visW: visW, visH: visH, wppX: visW / cw, // X方向 世界单位/像素 wppY: visH / ch // Y方向 世界单位/像素 }; } function worldToScreen(wx, wy) { var ext = getViewExtents(); var cw = canvas.width, ch = canvas.height; return { x: cw / 2 + (wx - viewCenterX) / ext.wppX, y: ch / 2 - (wy - viewCenterY) / ext.wppY }; } // ===== 2D 叠加层绘制 ===== function drawOverlays() { var cw = canvas.width, ch = canvas.height; var playerTank = GetPlayerTank(); // 网格(X/Y 方向缩放不同,用 ext 计算) if (ShowGrid) { ctx.strokeStyle = 'rgba(125,249,255,0.08)'; ctx.lineWidth = 1; var steps = [500, 1000, 2000, 5000, 10000, 20000, 50000]; var step = 5000; var ext = getViewExtents(); // 用较小的 wpp 选择网格间距(保证密度合适) var minWpp = Math.min(ext.wppX, ext.wppY); for (var i = 0; i < steps.length; i++) { if (steps[i] / minWpp > 60) { step = steps[i]; break; } } var left = viewCenterX - (cw/2) * ext.wppX; var right = viewCenterX + (cw/2) * ext.wppX; var top = viewCenterY + (ch/2) * ext.wppY; var bottom = viewCenterY - (ch/2) * ext.wppY; var startX = Math.floor(left/step)*step; var startY = Math.floor(bottom/step)*step; ctx.beginPath(); for (var wx = startX; wx <= right; wx += step) { var p = worldToScreen(wx, 0); ctx.moveTo(p.x, 0); ctx.lineTo(p.x, ch); } for (var wy = startY; wy <= top; wy += step) { var p = worldToScreen(0, wy); ctx.moveTo(0, p.y); ctx.lineTo(cw, p.y); } ctx.stroke(); // 距离标签 ctx.fillStyle = 'rgba(125,249,255,0.35)'; ctx.font = '10px Consolas'; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; for (var wx2 = startX; wx2 <= right; wx2 += step) { var p = worldToScreen(wx2, 0); ctx.fillText((wx2/100).toFixed(0)+'m', p.x+2, 2); } } // 坦克标记 var tanks = []; try { var list = gm[FTankManager][MTankList](); var n = list.e1(); for (var i = 0; i < n; i++) { var t = list.g1(i); if (t) tanks.push(t); } } catch(e) {} var margin = 50; tanks.forEach(function(tank) { var pos = GetTankPosition(tank); if (!pos) return; var p = worldToScreen(pos.x, pos.y); if (p.x < -margin || p.x > cw+margin || p.y < -margin || p.y > ch+margin) return; var isPlayer = (tank === playerTank); var friendly = IsFriendly(tank, playerTank); var alive = IsTankAlive(tank); var state = GetTankState(tank); if (state !== 3 && !isPlayer) return; var color, glow; if (isPlayer) { color = '#ffdd44'; glow = 'rgba(255,221,68,0.7)'; } else if (friendly) { color = '#44ff88'; glow = 'rgba(68,255,136,0.5)'; } else { color = '#ff5566'; glow = 'rgba(255,85,102,0.5)'; } var radius = (isPlayer ? 8 : 6) * MarkerSize; if (!alive) { radius = 4 * MarkerSize; color = '#666'; glow = 'none'; } // 方向指示(真实坦克朝向) var dir = getTankDirection(tank); if (dir && alive) { var dirLen = Math.sqrt(dir.x*dir.x + dir.y*dir.y); if (dirLen > 0.01) { var dx = dir.x / dirLen, dy = dir.y / dirLen; var arrowLen = 22; ctx.strokeStyle = color; ctx.lineWidth = 2.5; ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(p.x + dx * arrowLen, p.y - dy * arrowLen); ctx.stroke(); // 箭头头 var ax = p.x + dx * arrowLen, ay = p.y - dy * arrowLen; var ang = Math.atan2(-dy, dx); ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(ax - 7*Math.cos(ang-0.4), ay - 7*Math.sin(ang-0.4)); ctx.moveTo(ax, ay); ctx.lineTo(ax - 7*Math.cos(ang+0.4), ay - 7*Math.sin(ang+0.4)); ctx.stroke(); } } // 光晕标记 if (alive) { ctx.save(); ctx.shadowColor = glow; ctx.shadowBlur = 12; ctx.fillStyle = color; ctx.beginPath(); ctx.arc(p.x, p.y, radius, 0, Math.PI*2); ctx.fill(); ctx.restore(); } else { ctx.fillStyle = color; ctx.fillRect(p.x-3, p.y-3, 6, 6); } // 玩家外圈 if (isPlayer && alive) { ctx.strokeStyle = 'rgba(255,221,68,0.6)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(p.x, p.y, radius+6, 0, Math.PI*2); ctx.stroke(); } // 名称 var name = GetTankName(tank); if (name) { ctx.font = 'bold 11px Consolas'; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom'; ctx.strokeStyle = 'rgba(0,0,0,0.85)'; ctx.lineWidth = 3; ctx.strokeText(name, p.x, p.y - radius - 5); ctx.fillStyle = alive ? '#fff' : '#666'; ctx.fillText(name, p.x, p.y - radius - 5); } // 血量条 if (alive) { var health = GetTankHealth(tank); if (health) { var pct = health.cur / health.max; if (pct > 1) pct = 1; var barW = 34, barH = 5; var bx = p.x - barW/2, by = p.y + radius + 4; ctx.fillStyle = 'rgba(0,0,0,0.7)'; ctx.fillRect(bx, by, barW, barH); var hpColor = pct > 0.6 ? '#2ecc71' : (pct > 0.3 ? '#f5a623' : '#e05555'); ctx.fillStyle = hpColor; ctx.fillRect(bx, by, barW * pct, barH); ctx.strokeStyle = 'rgba(255,255,255,0.3)'; ctx.lineWidth = 0.5; ctx.strokeRect(bx, by, barW, barH); } } }); // 罗盘 var ccx = cw - 50, ccy = 50, cr = 28; ctx.save(); ctx.strokeStyle = 'rgba(125,249,255,0.4)'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(ccx, ccy, cr, 0, Math.PI*2); ctx.stroke(); ctx.fillStyle = 'rgba(8,12,20,0.6)'; ctx.beginPath(); ctx.arc(ccx, ccy, cr-1, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#ff5566'; ctx.beginPath(); ctx.moveTo(ccx, ccy - cr + 4); ctx.lineTo(ccx - 5, ccy); ctx.lineTo(ccx + 5, ccy); ctx.closePath(); ctx.fill(); ctx.fillStyle = 'rgba(200,210,220,0.6)'; ctx.beginPath(); ctx.moveTo(ccx, ccy + cr - 4); ctx.lineTo(ccx - 5, ccy); ctx.lineTo(ccx + 5, ccy); ctx.closePath(); ctx.fill(); ctx.fillStyle = '#7df9ff'; ctx.font = 'bold 12px Consolas'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('N', ccx, ccy - cr - 8); ctx.restore(); // 中心十字准星 ctx.strokeStyle = 'rgba(125,249,255,0.2)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(cw/2 - 15, ch/2); ctx.lineTo(cw/2 + 15, ch/2); ctx.moveTo(cw/2, ch/2 - 15); ctx.lineTo(cw/2, ch/2 + 15); ctx.stroke(); } // ===== 主渲染循环 ===== var running = true; var rafId = null; var frameCount = 0; function render() { if (!running) return; // 换图自刷新:gameMode 实例变化时整体重启,重新获取场景/相机/GL hook var NewGm = window.__gameMode__; if (NewGm && NewGm !== gm) { console.log('[沙盘] 检测到新战斗,正在重启战争沙盘...'); self.Stop(); _start(); return; } frameCount++; // 旧实例已失效、新实例尚未捕获:跳过本帧 if (!window.__gameMode__) { rafId = requestAnimationFrame(render); return; } var cw = canvas.width, ch = canvas.height; // 初始化视图中心 if (!initialized) { var player = GetPlayerTank(); if (player) { var pos = GetTankPosition(player); if (pos) { viewCenterX = pos.x; viewCenterY = pos.y; initialized = true; } } } // 暂停 FreeLook var fl = window.__hornetFreeLook__; if (fl && typeof fl.SetSuspended === 'function') fl.SetSuspended(true); try { // 保存主相机参数 var sPitch = mainCamera[FCameraRotation][FVecX]; var sRoll = mainCamera[FCameraRotation][FVecY]; var sYaw = mainCamera[FCameraRotation][FVecZ]; var sX = mainCamera[FNodeChildren][FVecX]; var sY = mainCamera[FNodeChildren][FVecY]; var sZ = mainCamera[FNodeChildren][FVecZ]; var sL36 = mainCamera[FCameraRenderWidth], sM36 = mainCamera[FCameraRenderHeight]; var sS36 = mainCamera[FCameraFov], sQ36 = mainCamera[FCameraNear], sR36 = mainCamera[FCameraFar]; // 设置俯瞰相机参数 // pitch=-π/2 朝下看,roll=0 不翻转,yaw=0 北朝上 mainCamera[FCameraRotation][FVecX] = -Math.PI / 2; mainCamera[FCameraRotation][FVecY] = 0; mainCamera[FCameraRotation][FVecZ] = 0; mainCamera[FNodeChildren][FVecX] = viewCenterX; mainCamera[FNodeChildren][FVecY] = viewCenterY; mainCamera[FNodeChildren][FVecZ] = camHeight; mainCamera[FCameraRenderWidth] = FBO_W; mainCamera[FCameraRenderHeight] = FBO_H; mainCamera[FCameraFov] = Config.fov; mainCamera[FCameraNear] = Config.near; mainCamera[FCameraFar] = Config.far; mainCamera.o37(); // 禁用阴影/后处理 var savedX3n = ff[FRenderFlags][FRenderShadow]; var savedY3n = ff[FRenderFlags][FRenderPostFx]; var savedZ3n = ff[FRenderFlags][FRenderFlagAux]; ff[FRenderFlags][FRenderShadow] = false; ff[FRenderFlags][FRenderPostFx] = false; ff[FRenderFlags][FRenderFlagAux] = false; // 禁用 UI 层 var savedUiQueues = {}; for (var ui = 0; ui < uiLayerIndices.length; ui++) { var li = uiLayerIndices[ui]; savedUiQueues[li] = ff[FRenderQueueList][li]; ff[FRenderQueueList][li] = fakeQueue; } // 渲染到 FBO sandboxRendering = true; origBindFramebuffer(gl.FRAMEBUFFER, sandboxFbo); gl.viewport(0, 0, FBO_W, FBO_H); gl.clearColor(0.05, 0.08, 0.12, 1.0); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); ff.f3t(rootNode, mainCamera); sandboxRendering = false; // Readback origBindFramebuffer(gl.FRAMEBUFFER, sandboxFbo); if (pboSupported) { var curPbo = pboIndex, prevPbo = 1 - pboIndex; gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbos[curPbo]); gl.readPixels(0, 0, FBO_W, FBO_H, gl.RGBA, gl.UNSIGNED_BYTE, 0); if (pboReady) { gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbos[prevPbo]); gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, pixels); } gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null); pboIndex = prevPbo; pboReady = true; } else { gl.readPixels(0, 0, FBO_W, FBO_H, gl.RGBA, gl.UNSIGNED_BYTE, pixels); } // 强制 alpha=255 for (var ai = 0; ai < pixels32.length; ai++) pixels32[ai] = pixels32[ai] | 0xFF000000; imageData.data.set(pixels); // Y 翻转显示(roll=0 时 GL readback 产生上下颠倒图像,需翻转) tempCtx.putImageData(imageData, 0, 0); ctx.clearRect(0, 0, cw, ch); ctx.save(); ctx.imageSmoothingEnabled = true; ctx.scale(1, -1); ctx.drawImage(tempCanvas, 0, -ch, cw, ch); ctx.restore(); // 恢复主 FBO origBindFramebuffer(gl.FRAMEBUFFER, mainFbo); // 恢复渲染状态 ff[FRenderFlags][FRenderShadow] = savedX3n; ff[FRenderFlags][FRenderPostFx] = savedY3n; ff[FRenderFlags][FRenderFlagAux] = savedZ3n; for (var ui2 = 0; ui2 < uiLayerIndices.length; ui2++) { ff[FRenderQueueList][uiLayerIndices[ui2]] = savedUiQueues[uiLayerIndices[ui2]]; } // 恢复主相机 mainCamera[FCameraRotation][FVecX] = sPitch; mainCamera[FCameraRotation][FVecY] = sRoll; mainCamera[FCameraRotation][FVecZ] = sYaw; mainCamera[FNodeChildren][FVecX] = sX; mainCamera[FNodeChildren][FVecY] = sY; mainCamera[FNodeChildren][FVecZ] = sZ; mainCamera[FCameraRenderWidth] = sL36; mainCamera[FCameraRenderHeight] = sM36; mainCamera[FCameraFov] = sS36; mainCamera[FCameraNear] = sQ36; mainCamera[FCameraFar] = sR36; mainCamera.o37(); // 绘制 2D 叠加层(坦克标记/网格/罗盘) drawOverlays(); } catch(e) { console.error('[沙盘] 渲染失败:', e.message); sandboxRendering = false; } finally { // 确保 FreeLook 恢复 if (fl && typeof fl.SetSuspended === 'function') fl.SetSuspended(false); } // 信息更新 if (frameCount % 30 === 0) { var ext2 = getViewExtents(); var viewRange = cw * ext2.wppX; // 横向可视范围 infoLabel.textContent = '高度:' + (camHeight/100).toFixed(0) + 'm | 范围:' + (viewRange/100).toFixed(0) + 'm | 拖拽平移 滚轮缩放 双击居中'; } rafId = requestAnimationFrame(render); } window.addEventListener('resize', resizeCanvas); rafId = requestAnimationFrame(render); console.log('[沙盘] 战争沙盘已就绪 — 拖拽地图平移 | 滚轮缩放 | 双击居中玩家 | Alt+M 或 X 关闭'); // ===== 对外接口 ===== var self = { Stop: function() { running = false; if (rafId) cancelAnimationFrame(rafId); // 恢复 hook gl.bindFramebuffer = origBindFramebuffer; ff[FRenderDevice].o3c = origH3c; ff[FRenderDevice].n3c = origG3c; window.removeEventListener('resize', resizeCanvas); if (container.parentNode) container.parentNode.removeChild(container); window.__hornetWarSandbox__ = null; console.log('[沙盘] 战争沙盘已关闭'); }, Status: function() { return running ? '已开启' : '未运行'; }, GetRunning: function() { return running; }, SetShowGrid: function(on) { ShowGrid = !!on; try { localStorage.setItem('hornet_ws_showgrid', ShowGrid ? 'true' : 'false'); } catch(e) {} console.log('[沙盘] 网格: ' + (ShowGrid ? '显示' : '隐藏')); }, GetShowGrid: function() { return ShowGrid; }, SetMarkerSize: function(s) { var n = parseFloat(s); if (!isNaN(n) && n >= 0.5 && n <= 3.0) { MarkerSize = n; try { localStorage.setItem('hornet_ws_marker_size', String(n)); } catch(e) {} console.log('[沙盘] 标记大小: ' + n); } else { console.warn('[沙盘] 标记大小应在 0.5~3.0'); } }, GetMarkerSize: function() { return MarkerSize; } }; window.__hornetWarSandbox__ = self; window.__hornetTools__ = window.__hornetTools__ || {}; window.__hornetTools__.WarSandbox = self; } function _stop() { if (window.__hornetWarSandbox__) window.__hornetWarSandbox__.Stop(); } function _status() { return window.__hornetWarSandbox__ ? window.__hornetWarSandbox__.Status() : '未运行'; } function _getRunning() { return !!(window.__hornetWarSandbox__ && window.__hornetWarSandbox__.GetRunning()); } window.__hornetTools__ = window.__hornetTools__ || {}; window.__hornetTools__.WarSandbox = { Start: _start, Stop: _stop, Status: _status, GetRunning: _getRunning, SetShowGrid: function(on) { if (window.__hornetWarSandbox__) window.__hornetWarSandbox__.SetShowGrid(on); }, GetShowGrid: function() { return window.__hornetWarSandbox__ ? window.__hornetWarSandbox__.GetShowGrid() : true; }, SetMarkerSize: function(s) { if (window.__hornetWarSandbox__) window.__hornetWarSandbox__.SetMarkerSize(s); }, GetMarkerSize: function() { return window.__hornetWarSandbox__ ? window.__hornetWarSandbox__.GetMarkerSize() : 1.0; } }; })(); // ===== hornet_mouse.js ===== // hornet_mouse.js — 鼠标输入稳定器 // 目标:缓解原版「指针锁定下鼠标乱晃 / 难控制」的问题(游戏自身行为,非本工具链引入)。 // // 游戏侧机制(module_03569): // · 锁定:Lh() 先 requestPointerLock({unadjustedMovement:true})(原始输入),**失败则静默回退** // 成不带 unadjustedMovement 的普通锁定 —— 后者会带上 OS 鼠标加速,手感立刻变飘。 // · 视角:mousemove 里直接把 e.movementX / e.movementY 喂给输入管理器 // (fal_1.ia9(qh(), movementX, movementY)),没有任何尖峰/跳变过滤。 // // 本模块做三件事,全部可在面板「鼠标稳定」里开关: // ① 强制原始输入:接管 Element.prototype.requestPointerLock,始终带 unadjustedMovement, // 并把「回退/失败」打到控制台(手感异常的第一嫌疑人)。 // ② 锁后/恢复跳变抑制:指针锁定刚建立、或事件流中断(切窗口/失焦)后恢复的头 DropMs 毫秒内 // 丢弃 mousemove —— 这一两下常带巨大 movementX/Y,会直接把视角甩飞。 // ③ 尖峰钳制:单事件 |movementX| / |movementY| 超过 MaxDelta(0 = 关闭)时截断, // 抑制驱动/焦点抖动造成的瞬时尖峰。 // // 状态:HUD 里显示「鼠标: RAW 48/s 尖峰320 钳0 丢1」,或 // console.log(window.__hornetTools__.Mouse.GetStatus()) (function () { 'use strict'; var lib = window.__hornetLib__; if (!lib) { console.error('[鼠标] 请先加载 hornet_lib.js'); return; } // ===== 配置(localStorage 持久化)===== var CONFIG_KEY = 'hornet_mouse_cfg'; var Config = { ForceRaw: true, // 强制原始输入(unadjustedMovement),关掉则完全不干预指针锁定 DropMs: 120, // 锁定建立 / 事件流恢复后的忽略时长(ms,0 = 不丢事件) GapMs: 250, // 事件间隔超过它视为「恢复」,同样触发忽略窗口(ms) MaxDelta: 0 // 单事件位移钳制阈值(px,0 = 不钳制) }; try { var Saved = JSON.parse(localStorage.getItem(CONFIG_KEY) || '{}'); if (typeof Saved.ForceRaw === 'boolean') Config.ForceRaw = Saved.ForceRaw; if (typeof Saved.DropMs === 'number') Config.DropMs = Saved.DropMs; if (typeof Saved.GapMs === 'number') Config.GapMs = Saved.GapMs; if (typeof Saved.MaxDelta === 'number') Config.MaxDelta = Saved.MaxDelta; } catch (e) {} function SaveConfig() { try { localStorage.setItem(CONFIG_KEY, JSON.stringify(Config)); } catch (e) {} } function Now() { return (window.performance && window.performance.now) ? window.performance.now() : Date.now(); } // ===== 运行状态与统计 ===== var Stat = { running: false, rawMode: '未知', // 原始输入 / 带加速 / 未知 locked: false, dropUntil: 0, // 该时刻前的 mousemove 全部丢弃 dropped: 0, clamped: 0, events: 0, maxAbs: 0, // 本统计窗口内最大单事件位移 lastEventAt: 0, rate: 0, rateEvents: 0, rateWindow: 0, lastAnomalyAt: 0 }; var OrigRequestPointerLock = null; var Attached = false; // ===== ① 指针锁定:强制原始输入 ===== function InstallLockPatch() { if (OrigRequestPointerLock) return; OrigRequestPointerLock = Element.prototype.requestPointerLock; Element.prototype.requestPointerLock = function (Options) { var Opt = Options; if (Config.ForceRaw && !(Opt && Opt.unadjustedMovement)) { Opt = { unadjustedMovement: true }; // 显式请求原始输入(不改调用方对象) } // 只有在「本来没锁」时才算一次真正的锁定(避免已锁定时的重复请求反复刷新忽略窗口, // 那会让鼠标一直被丢弃而失去响应) var WasLocked = !!document.pointerLockElement; Stat.locked = true; if (!WasLocked) { Stat.lastEventAt = 0; // 重新计时,避免把旧的间隔当成「恢复」 Stat.dropUntil = Config.DropMs > 0 ? (Now() + Config.DropMs) : 0; } var P; try { P = OrigRequestPointerLock.call(this, Opt); } catch (Err) { console.error('[鼠标] requestPointerLock 调用异常:', Err && Err.message); return null; } var WantRaw = !!(Opt && Opt.unadjustedMovement); if (P && typeof P.then === 'function') { P.then( function () { Stat.rawMode = WantRaw ? '原始输入' : '带加速'; console.log('[鼠标] 指针锁定成功,模式=' + Stat.rawMode + (WantRaw ? '' : '(带 OS 加速,手感会飘;可在面板勾选「强制原始输入」)')); }, function (Err) { Stat.rawMode = '未知'; console.warn('[鼠标] 指针锁定失败: ' + (Err && Err.message)); }, ); } else { Stat.rawMode = WantRaw ? '原始输入' : '带加速'; } return P; }; } function UninstallLockPatch() { if (!OrigRequestPointerLock) return; Element.prototype.requestPointerLock = OrigRequestPointerLock; OrigRequestPointerLock = null; } // ===== ②③ mousemove 拦截:丢弃跳变 + 钳制尖峰 ===== // 注册在 window 的捕获阶段:一定早于游戏挂在画布/document 上的处理器,丢弃/改写才有效 function OnMouseMoveCapture(e) { Stat.events++; var T = Now(); // 事件流「恢复」判定(切窗口/失焦回来时的第一下常是巨幅位移) if (Config.GapMs > 0 && Stat.lastEventAt && (T - Stat.lastEventAt) > Config.GapMs && Config.DropMs > 0) { Stat.dropUntil = T + Config.DropMs; } Stat.lastEventAt = T; // ② 忽略窗口内:整条事件丢弃(stopImmediatePropagation 阻断游戏处理器) if (Stat.dropUntil && T < Stat.dropUntil) { Stat.dropped++; e.stopImmediatePropagation(); return; } var Dx = e.movementX, Dy = e.movementY; if (typeof Dx !== 'number' || typeof Dy !== 'number') return; var Ax = Math.abs(Dx), Ay = Math.abs(Dy); if (Ax > Stat.maxAbs) Stat.maxAbs = Ax; if (Ay > Stat.maxAbs) Stat.maxAbs = Ay; // ③ 尖峰钳制:改写事件上的 movementX/Y(own property 遮蔽原型 getter) if (Config.MaxDelta > 0 && (Ax > Config.MaxDelta || Ay > Config.MaxDelta)) { var Nx = Ax > Config.MaxDelta ? (Dx > 0 ? Config.MaxDelta : -Config.MaxDelta) : Dx; var Ny = Ay > Config.MaxDelta ? (Dy > 0 ? Config.MaxDelta : -Config.MaxDelta) : Dy; try { Object.defineProperty(e, 'movementX', { value: Nx, configurable: true }); Object.defineProperty(e, 'movementY', { value: Ny, configurable: true }); Stat.clamped++; } catch (Err) { // 属性不可写则退化为丢弃该事件(同样能压住甩飞) Stat.clamped++; e.stopImmediatePropagation(); return; } } // 事件率统计(2s 窗口),尖峰异常时节流提示,帮助定位是「抖动」还是「跳变」 Stat.rateEvents++; if (!Stat.rateWindow) Stat.rateWindow = T; if (T - Stat.rateWindow >= 2000) { Stat.rate = Math.round((Stat.rateEvents * 1000) / (T - Stat.rateWindow)); Stat.rateWindow = T; Stat.rateEvents = 0; var Bad = Stat.maxAbs > 400 && (T - Stat.lastAnomalyAt) > 3000; if (Bad) { Stat.lastAnomalyAt = T; console.warn('[鼠标] 出现大位移尖峰: ' + Stat.maxAbs.toFixed(0) + 'px(事件率 ' + Stat.rate + '/s,累计钳制 ' + Stat.clamped + ' 次)。若频繁甩飞,把「尖峰钳制」调成 250~400 试试'); } Stat.maxAbs = 0; // 只统计窗口内最大值 } } // ===== 启动 / 停止 ===== function _start() { if (Stat.running) return; if (Config.ForceRaw) InstallLockPatch(); if (!Attached) { window.addEventListener('mousemove', OnMouseMoveCapture, true); Attached = true; } Stat.running = true; console.log('[鼠标] 稳定器已启动 强制原始输入=' + Config.ForceRaw + ' 锁后忽略=' + Config.DropMs + 'ms 尖峰钳制=' + (Config.MaxDelta > 0 ? Config.MaxDelta + 'px' : '关')); } function _stop() { if (!Stat.running) return; UninstallLockPatch(); if (Attached) { window.removeEventListener('mousemove', OnMouseMoveCapture, true); Attached = false; } Stat.running = false; Stat.dropped = 0; Stat.clamped = 0; Stat.events = 0; Stat.maxAbs = 0; Stat.dropUntil = 0; Stat.lastEventAt = 0; Stat.rawMode = '未知'; console.log('[鼠标] 稳定器已停止'); } var Api = { config: Config, _start: _start, _stop: _stop, setForceRaw: function (On) { Config.ForceRaw = !!On; SaveConfig(); if (Stat.running) { if (On) InstallLockPatch(); else UninstallLockPatch(); } }, setDropMs: function (Ms) { Config.DropMs = Math.max(0, Number(Ms) || 0); SaveConfig(); }, setMaxDelta: function (Px) { Config.MaxDelta = Math.max(0, Number(Px) || 0); SaveConfig(); }, getStatus: function () { return { running: Stat.running, rawMode: Stat.rawMode, rate: Stat.rate, maxAbs: Math.round(Stat.maxAbs), clamped: Stat.clamped, dropped: Stat.dropped, events: Stat.events }; } }; window.__hornetMouse__ = Api; var Tool = { Start: function () { _start(); }, Stop: function () { _stop(); }, Status: function () { if (!Stat.running) return '已停止'; return Stat.rawMode + ' ' + Stat.rate + '/s 尖峰' + Math.round(Stat.maxAbs) + ' 钳' + Stat.clamped + ' 丢' + Stat.dropped; }, GetRunning: function () { return Stat.running; }, SetForceRaw: function (On) { Api.setForceRaw(On); }, GetForceRaw: function () { return Config.ForceRaw; }, SetDropMs: function (Ms) { Api.setDropMs(Ms); }, GetDropMs: function () { return Config.DropMs; }, SetMaxDelta: function (Px) { Api.setMaxDelta(Px); }, GetMaxDelta: function () { return Config.MaxDelta; }, GetStatus: function () { return Api.getStatus(); } }; window.__hornetTools__ = window.__hornetTools__ || {}; window.__hornetTools__.Mouse = Tool; console.log('[3DTank] 鼠标稳定模块已加载'); })(); // ===== hornet_noclip.js ===== // hornet_noclip.js — 忽略模块(穿坦克 / 忽略栅栏) // 面板「忽略」区域控制: // · 忽略坦克:关闭 / 仅敌方 / 仅友方 / 全部 // · 忽略栅栏:穿墙(只忽略陡峭的静态三角形,地面与缓坡照常) // // 原理(module_55088 碰撞世界 ki): // q9q(a, b) 是全物理世界唯一的「接触配对过滤」: // if (0 === (a.y9j_1 & b.z9j_1) || 0 === (b.y9j_1 & a.y9j_1)) return; // 不生成接触 // 我们包装它,在满足条件时直接返回(等价引擎自己的「不碰撞」分支): // · 坦克↔坦克:一边是本车、另一边是目标阵营的坦克 → 跳过 // · 本车↔静态三角形:三角形法线接近水平(墙/栅栏/悬崖立面)→ 跳过;法线朝上(地面/缓坡)→ 保留 // // 关键点:**不改 mask/category**,所以拾取物触发、地雷、子弹、地形判定都不受影响。 // 形状 → 刚体 → 拥有者: // Shape.b9k_1 = 刚体(Fn);Body.x9i_1 = 拥有者(坦克物理组件 ejt) // 静态三角形(类型 8):顶点 u9x_1 / v9x_1 / w9x_1;本引擎 x,y 为水平面,**z 是高度**。 (function () { 'use strict'; var Lib = window.__hornetLib__; if (!Lib) { console.error('[忽略] 请先加载 hornet_lib.js'); return; } // ===== 字段抽象层(语义名 → 匿名字段,总表在 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; } function ExportKeys(Key, Fallback) { if (Schema && Schema.Exports && Schema.Exports[Key]) return Schema.Exports[Key]; return Fallback; } var FPhysicsHull = Fld('Physics.Hull', 'zzq_1'); // 车体主碰撞形状 var FShapeName = Fld('Shape.Name', 'x9j_1'); // 形状名('hull-main') var FShapeCategory = Fld('Shape.Category', 'y9j_1'); // category 位组 var FShapeType = Fld('Shape.Type', 'v9j_1'); // 形状类型位组(8 = 三角形) var FShapeClone = Fld('Shape.Clone', 'c9w_1'); // 类型 4 包装里的真实形状 var FShapeBody = Fld('Shape.Body', 'b9k_1'); // 形状 → 刚体 var FBodyOwner = Fld('Body.Owner', 'x9i_1'); // 刚体 → 拥有者 var FV0 = Fld('Shape.Vertex0', 'u9x_1'); // 三角形顶点 0 var FV1 = Fld('Shape.Vertex1', 'v9x_1'); // 三角形顶点 1 var FV2 = Fld('Shape.Vertex2', 'w9x_1'); // 三角形顶点 2 var MRemoveMask = Mth('Physics.RemoveMask', 'm155'); // 坦克物理组件识别方法 var MCollide = Mth('CollisionWorld.ShouldCollide', 'q9q'); var MOverlap = Mth('CollisionWorld.Overlap', 'x9o'); // 形状相交查询(真形状,不是包围盒) var WorldKeys = ExportKeys('Export.PhysicsWorld', ['h']); var PHYSICS_MODULE = 55088; var STATIC_CATEGORY_BIT = 16; // 地图静态几何 category 位(s6() 造地形:y9j_1 = 16) var TRIANGLE_TYPE = 8; // yi 子类 wu(三角形)的 v9j_1 var FENCE_NORMAL_Z = 0.5; // |法线 z| 小于它 → 视为墙/栅栏(地面与缓坡保留) var Stat = { running: false, mode: 'all', // 'off' | 'enemy' | 'friendly' | 'all' ignoreFence: false, skippedTank: 0, skippedFence: 0, skippedOverlap: 0, // x9o 形状相交查询被拦下的次数 byType: {}, // cat/type → 被忽略次数(诊断) seen: {}, // cat/type → 本车接触到的非坦克形状次数(诊断) patched: false, overlapPatched: false }; var WorldProto = null; var OrigCollide = null; // 拥有者是否是坦克物理组件(ejt) function IsTankOwner(Owner) { if (!Owner || typeof Owner !== 'object') return false; if (typeof Owner[MRemoveMask] !== 'function') return false; var Hull = Owner[FPhysicsHull]; return !!(Hull && Hull[FShapeName] === 'hull-main'); } function OwnerOf(Shape) { if (!Shape || typeof Shape !== 'object') return null; var Body = Shape[FShapeBody]; return Body ? (Body[FBodyOwner] || null) : null; } function IsTankShape(Shape) { return IsTankOwner(OwnerOf(Shape)); } // 本地坦克物理组件(100ms 节流缓存;重生/换车后最多 100ms 生效) var LocalPhys = null; var LocalPhysAt = 0; function GetLocalPhysics() { var Now = Date.now(); if (Now - LocalPhysAt < 100) return LocalPhys; LocalPhysAt = Now; LocalPhys = null; var Gm = window.__gameMode__; if (!Gm) return null; var Tank = null; try { Tank = Gm.u10b_1 || Gm.t10b_1; } catch (e) { Tank = null; } if (!Tank || !Tank.qz1_1 || !Tank.qz1_1.s2a_1) return null; try { Tank.qz1_1.s2a_1.forEach(function (ValueList) { if (LocalPhys) return; var Comp = (ValueList && typeof ValueList.g1 === 'function') ? ValueList.g1(0) : ValueList; if (IsTankOwner(Comp)) LocalPhys = Comp; }); } catch (e) {} return LocalPhys; } function EntityOf(Owner) { if (!Owner || typeof Owner.lz4 !== 'function') return null; try { return Owner.lz4(); } catch (e) { return null; } } // 三角形是否「陡」:法线 z 分量接近 0(墙/栅栏/悬崖立面) // type 4 是「克隆包装」(hu: this.c9w_1 = 原形状.d21()),真实几何在 c9w_1 里,必须先解包 function UnwrapShape(Shape) { var S = Shape, Guard = 0; while (S && S[FShapeType] === 4 && S[FShapeClone] && Guard++ < 4) S = S[FShapeClone]; return S; } function IsTriangle(Shape) { var S = UnwrapShape(Shape); return !!(S && S[FShapeType] === TRIANGLE_TYPE); } // 返回法线 z 分量(-1..1);读不到返回 null function TriangleNormalZ(Shape) { var S = UnwrapShape(Shape); if (!S || S[FShapeType] !== TRIANGLE_TYPE) return null; var A = S[FV0], B = S[FV1], C = S[FV2]; if (!A || !B || !C) return null; if (typeof A.f20_1 !== 'number' || typeof B.f20_1 !== 'number' || typeof C.f20_1 !== 'number') return null; var ABx = B.f20_1 - A.f20_1, ABy = B.g20_1 - A.g20_1, ABz = B.h20_1 - A.h20_1; var ACx = C.f20_1 - A.f20_1, ACy = C.g20_1 - A.g20_1, ACz = C.h20_1 - A.h20_1; var Nx = ABy * ACz - ABz * ACy; var Ny = ABz * ACx - ABx * ACz; var Nz = ABx * ACy - ABy * ACx; var Len = Math.sqrt(Nx * Nx + Ny * Ny + Nz * Nz); if (!(Len > 1e-6)) return null; return Nz / Len; } function IsSteepTriangle(Shape) { var Nz = TriangleNormalZ(Shape); return Nz !== null && Math.abs(Nz) < FENCE_NORMAL_Z; } // 静态几何:category 含 bit4,且不是坦克部件 function IsStaticShape(Shape) { if (!Shape || typeof Shape[FShapeCategory] !== 'number') return false; if ((Shape[FShapeCategory] & STATIC_CATEGORY_BIT) === 0) return false; return !IsTankShape(Shape); } // 三角形竖直跨度(局部坐标):顶点 z 的 max-min // 地面/缓坡:三个顶点几乎同高 → 跨度≈0 // 栅栏/墙:竖直面 → 跨度大(≥1m) function TriangleZRange(Shape) { var S = UnwrapShape(Shape); if (!S || S[FShapeType] !== TRIANGLE_TYPE) return null; var A = S[FV0], B = S[FV1], C = S[FV2]; if (!A || !B || !C) return null; if (typeof A.h20_1 !== 'number' || typeof B.h20_1 !== 'number' || typeof C.h20_1 !== 'number') return null; var Max = Math.max(A.h20_1, B.h20_1, C.h20_1); var Min = Math.min(A.h20_1, B.h20_1, C.h20_1); return Max - Min; } // 「栅栏类」= 静态几何中的**立面三角形**(全部用局部坐标判定,不依赖世界坐标): // · type 4 先解包到真实形状,必须是三角形 // · |法线 z| < 0.6 → 竖直立面(地面法线≈1) // · 竖直跨度 > 100 单位 → 有高度的墙/栅栏(地面/缓坡跨度≈0) function IsFenceLike(Shape) { if (!IsStaticShape(Shape)) return false; if (!IsTriangle(Shape)) return false; var Nz = TriangleNormalZ(Shape); if (Nz === null || Math.abs(Nz) >= 0.6) return false; var Range = TriangleZRange(Shape); return Range !== null && Range > 100; } // 本车形状 / 本车与栅栏形状的配对(q9q 与 x9o 共用同一套判定) function IsLocalTankShape(Shape) { var Local = GetLocalPhysics(); return !!(Local && IsTankShape(Shape) && OwnerOf(Shape) === Local); } // 「本车 ↔ 任意静态形状」:用于形状相交查询(x9o)这条路径。 // 扫掠/查询被挡时返回 false 即可穿过;地面支撑走的是射线(l9m),不经过 x9o。 function IsLocalStaticPair(ShapeA, ShapeB) { return (IsLocalTankShape(ShapeA) && IsStaticShape(ShapeB)) || (IsLocalTankShape(ShapeB) && IsStaticShape(ShapeA)); } // ===== 诊断:识别「正在和什么物体重叠」===== // 碰撞形状本身没有名字(地图三角形是合并的三角汤),所以用两级信息: // ① 形状自身:category / mask / type / 法线 // ② 形状位置附近(默认 7m)场景树里的节点名(如 fence_s / wall_new_3w) var FSceneRoot = Fld('Scene.RootNode', 'fyx_1'); var FNodeName = Fld('Node.Name', 'g35_1'); var FNodeMatrix = Fld('Node.Matrix', 'p35_1'); var FNodeChildren = Fld('Node.Children', 'r35_1'); var FBodyState = Fld('Body.State', 'f9j_1'); var FBodyPosVec = Fld('Body.PositionVec', 's9j_1'); function ShapePoint(Shape) { if (!Shape) return null; // 顶点是刚体局部坐标(常见 (0,0,0) 附近),诊断要用刚体的世界位置 var Body = Shape[FShapeBody]; var State = Body && Body[FBodyState]; var P = State && State[FBodyPosVec]; if (P && typeof P.f20_1 === 'number') return { x: P.f20_1, y: P.g20_1, z: P.h20_1 }; // 兜底:局部顶点重心 var S = UnwrapShape(Shape); if (S && S[FShapeType] === TRIANGLE_TYPE) { var A = S[FV0], B = S[FV1], C = S[FV2]; if (A && B && C) { return { x: (A.f20_1 + B.f20_1 + C.f20_1) / 3, y: (A.g20_1 + B.g20_1 + C.g20_1) / 3, z: (A.h20_1 + B.h20_1 + C.h20_1) / 3 }; } } return null; } function FindNearNodeNames(Point, Radius) { if (!Point) return []; var Gm = window.__gameMode__; if (!Gm) return []; var Scene = null; try { Scene = Gm.lz4()[Fld('Camera.SceneRef', 'jz1_1')]; } catch (e) { return []; } var Root = Scene && Scene[FSceneRoot]; if (!Root) return []; var Names = []; var Seen = typeof Set !== 'undefined' ? new Set() : null; var Visited = 0; var R = Radius || 700; var R2 = R * R; (function Walk(Node, Depth) { if (!Node || Depth > 12 || Visited > 4000) return; if (Seen) { if (Seen.has(Node)) return; Seen.add(Node); } Visited++; var M = Node[FNodeMatrix]; if (M && typeof M.length === 'number' && M.length >= 16 && typeof M[12] === 'number') { var Dx = M[12] - Point.x, Dy = M[13] - Point.y, Dz = M[14] - Point.z; if (Dx * Dx + Dy * Dy + Dz * Dz <= R2) { var Nm = Node[FNodeName]; if (Nm && Names.indexOf(Nm) < 0 && Names.length < 8) Names.push(Nm); } } var Children = Node[FNodeChildren]; if (Children && typeof Children.e1 === 'function') { var N = Children.e1(); for (var i = 0; i < N; i++) { try { Walk(Children.g1(i), Depth + 1); } catch (e) {} } } })(Root, 0); return Names; } // 每个「未跳过」的静态形状只打印一次(按签名去重) var FenceSampleKeys = {}; function RecordFenceSample(Shape) { if (window.__hornetFenceDebug__ !== true) return; var Cat = Shape[FShapeCategory]; var Type = Shape[FShapeType]; var Tri = IsTriangle(Shape); var Nz = TriangleNormalZ(Shape); var Base = UnwrapShape(Shape); var BaseType = Base ? Base[FShapeType] : '-'; var Range = TriangleZRange(Shape); var Key = Cat + '/' + Type + '/' + BaseType + '/' + (Nz === null ? '-' : Nz.toFixed(1)) + '/' + (Range === null ? '-' : Math.round(Range / 50)); if (FenceSampleKeys[Key]) return; FenceSampleKeys[Key] = 1; var Pt = ShapePoint(Shape); var Near = FindNearNodeNames(Pt, 700); console.log('[忽略][重叠物体] cat=' + Cat + ' type=' + Type + ' 解包type=' + BaseType + ' 三角=' + Tri + ' nz=' + (Nz === null ? '-' : Nz.toFixed(2)) + ' 跨度=' + (Range === null ? '-' : Range.toFixed(0)) + ' 位置=' + (Pt ? ('(' + Pt.x.toFixed(0) + ',' + Pt.y.toFixed(0) + ',' + Pt.z.toFixed(0) + ')') : '-') + ' 近邻节点=[' + Near.join(', ') + ']' + ' 当前判定=' + (IsFenceLike(Shape) ? '忽略' : '保留')); } // ===== 判定:这一对形状要不要跳过 ===== function ShouldSkip(ShapeA, ShapeB) { var Local = GetLocalPhysics(); var TankA = IsTankShape(ShapeA); var TankB = IsTankShape(ShapeB); // ① 忽略坦克(按阵营) if (Stat.mode !== 'off' && TankA && TankB) { var OA = OwnerOf(ShapeA); var OB = OwnerOf(ShapeB); if (Local && (OA === Local || OB === Local)) { var OtherOwner = (OA === Local) ? OB : OA; if (Stat.mode === 'all') { Stat.skippedTank++; return true; } var OtherEntity = EntityOf(OtherOwner); if (OtherEntity) { var Friendly = Lib.IsFriendly(OtherEntity, null, window.__gameMode__); if ((Stat.mode === 'enemy' && !Friendly) || (Stat.mode === 'friendly' && Friendly)) { Stat.skippedTank++; return true; } } } } // ② 忽略栅栏(静态障碍;地面/缓坡保留) if (Stat.ignoreFence && Local) { var FenceShape = null; if (TankA && OwnerOf(ShapeA) === Local) FenceShape = ShapeB; else if (TankB && OwnerOf(ShapeB) === Local) FenceShape = ShapeA; // 统计:本车接触到的所有非坦克形状(不论是否跳过)→ cat/type 直方图 if (FenceShape) { var SeenKey = FenceShape[FShapeCategory] + '/' + FenceShape[FShapeType]; Stat.seen[SeenKey] = (Stat.seen[SeenKey] || 0) + 1; } if (FenceShape && IsFenceLike(FenceShape)) { Stat.skippedFence++; var Key = FenceShape[FShapeCategory] + '/' + FenceShape[FShapeType]; Stat.byType[Key] = (Stat.byType[Key] || 0) + 1; return true; } // 诊断:本车与任意非坦克形状配对但没跳过 → 记录样本(含被挡住的栅栏) if (FenceShape) RecordFenceSample(FenceShape); } return false; } // ===== 安装/卸载配对过滤 ===== function GetWorldProto() { if (WorldProto) return WorldProto; var Req = window.__wp_require__; if (typeof Req !== 'function') return null; var Mod = null; try { Mod = Req(PHYSICS_MODULE); } catch (e) { return null; } var Exports = Mod && Mod.$_$; if (!Exports) return null; for (var i = 0; i < WorldKeys.length; i++) { var Klass = Exports[WorldKeys[i]]; if (typeof Klass !== 'function' || !Klass.prototype) continue; if (typeof Klass.prototype[MCollide] !== 'function') continue; WorldProto = Klass.prototype; return WorldProto; } return null; } function InstallPatch() { if (Stat.patched) return true; var Proto = GetWorldProto(); if (!Proto) { console.warn('[忽略] 未找到碰撞世界(module ' + PHYSICS_MODULE + ' 导出键可能已变),本功能无效'); return false; } OrigCollide = Proto[MCollide]; Proto[MCollide] = function (ShapeA, ShapeB) { if (Stat.running) { try { if (ShouldSkip(ShapeA, ShapeB)) return undefined; // 等价引擎的「不碰撞」分支:不生成接触 } catch (e) {} } return OrigCollide.apply(this, arguments); }; Stat.patched = true; console.log('[忽略] 已接管碰撞配对过滤(坦克按阵营 / 陡峭静态三角形)'); // 同时接管「形状相交查询」x9o:真形状检测(不是包围盒),用于扫掠/穿墙判定 if (typeof Proto[MOverlap] === 'function') { var OrigOverlap = Proto[MOverlap]; Proto[MOverlap] = function (ShapeA, ShapeB) { if (Stat.running && Stat.ignoreFence) { try { if (IsLocalStaticPair(ShapeA, ShapeB)) { Stat.skippedOverlap++; return false; // 查询结果:不相交(穿栅栏走这条路径) } } catch (e) {} } return OrigOverlap.apply(this, arguments); }; Stat.overlapPatched = true; console.log('[忽略] 已接管形状相交查询 ' + MOverlap + '(真形状检测)'); } else { console.warn('[忽略] 未找到形状相交查询 ' + MOverlap + ',扫掠路径未接管'); } return true; } // 碰撞世界可能还没加载(车库里提前开):每 0.5s 重试 var RetryTimer = null; function ScheduleRetry() { if (RetryTimer) return; RetryTimer = setInterval(function () { if (!Stat.running) { clearInterval(RetryTimer); RetryTimer = null; return; } if (InstallPatch()) { clearInterval(RetryTimer); RetryTimer = null; } }, 500); } function Apply() { var NeedRun = Stat.mode !== 'off' || Stat.ignoreFence; if (NeedRun && !Stat.running) { Stat.running = true; if (!InstallPatch()) { console.warn('[忽略] 碰撞世界尚未加载,进入等待(加载后自动接管)'); ScheduleRetry(); } } else if (!NeedRun && Stat.running) { Stat.running = false; if (RetryTimer) { clearInterval(RetryTimer); RetryTimer = null; } } } // ===== 对外接口 ===== function SetTankMode(Mode) { if (['off', 'enemy', 'friendly', 'all'].indexOf(Mode) < 0) Mode = 'off'; Stat.mode = Mode; Apply(); try { localStorage.setItem('hornet_ignore_tanks', Mode); } catch (e) {} console.log('[忽略] 忽略坦克 = ' + Mode); } function GetTankMode() { return Stat.mode; } function SetIgnoreFence(On) { Stat.ignoreFence = !!On; Apply(); try { localStorage.setItem('hornet_ignore_fence', On ? '1' : '0'); } catch (e) {} console.log('[忽略] 忽略栅栏 = ' + (On ? '开' : '关')); } function GetIgnoreFence() { return Stat.ignoreFence; } // 兼容旧接口(面板旧版/热键可能还在调用) function _start() { SetTankMode(Stat.mode === 'off' ? 'all' : Stat.mode); } function _stop() { Stat.mode = 'off'; Stat.ignoreFence = false; Apply(); } var Tool = { Start: _start, Stop: _stop, GetRunning: function () { return Stat.running; }, Status: function () { if (!Stat.running) return '已停止'; var Parts = []; if (Stat.mode !== 'off') Parts.push('坦克:' + Stat.mode); if (Stat.ignoreFence) Parts.push('栅栏'); return Parts.length ? Parts.join(' + ') : '已停止'; }, SetTankMode: SetTankMode, GetTankMode: GetTankMode, SetIgnoreFence: SetIgnoreFence, GetIgnoreFence: GetIgnoreFence }; window.__hornetTools__ = window.__hornetTools__ || {}; window.__hornetTools__.NoClip = Tool; window.__hornetNoClip__ = { IsTankOwner: IsTankOwner, IsTankShape: IsTankShape, IsSteepTriangle: IsSteepTriangle, IsFenceLike: IsFenceLike, // 诊断:__hornetNoClip__.DebugFence(true) 后撞一次墙,控制台会打印静态形状样本 DebugFence: function (On) { window.__hornetFenceDebug__ = On !== false; FenceSampleKeys = {}; console.log('[忽略] 栅栏诊断 = ' + (window.__hornetFenceDebug__ ? '开(撞栅栏后看 [忽略][重叠物体])' : '关')); }, GetStat: function () { return { running: Stat.running, mode: Stat.mode, ignoreFence: Stat.ignoreFence, skippedTank: Stat.skippedTank, skippedFence: Stat.skippedFence, skippedOverlap: Stat.skippedOverlap, byType: Stat.byType, seen: Stat.seen, patched: Stat.patched, overlapPatched: Stat.overlapPatched }; }, // 清零统计:便于「平地 1 秒」与「顶住栅栏 1 秒」两次对比 seen 里的 cat/type ResetStat: function () { Stat.skippedTank = 0; Stat.skippedFence = 0; Stat.skippedOverlap = 0; Stat.byType = {}; Stat.seen = {}; FenceSampleKeys = {}; console.log('[忽略] 统计已清零'); } }; console.log('[3DTank] 忽略模块已加载(忽略坦克 / 忽略栅栏)'); })(); // ===== hornet_noknockback.js ===== // hornet_noknockback.js — 拒绝击退(中弹不再被推着走) // 放在控制面板「拒绝击退」开关里,只作用于**本地坦克**。 // // 引擎里「被打中弹开」的完整链路(module_87072 / module_55088): // 碰撞/武器命中 → 目标坦克实体派发 ODt 事件(外力事件): // ODt.o13i_1 = 作用点,ODt.p13i_1 = 方向,ODt.q13i_1 = 强度 // → 坦克物理组件 ejt 在 oz4() 里订阅它(callableName = "applyForce"): // t.pbk().m9k(点, 方向, 强度) // m9k = 在点处施加力,直接进速度/角速度 // → 车体被推开 / 被弹得转圈 // // 为什么在「事件派发」这一层拦: // m9k 同时被悬挂/弹簧(Gbt.s151)和自身开炮后座使用,直接封 m9k 会把悬挂也废掉; // 而 ODt 只承载外力(炮弹/地雷/撞击),拦掉它 → 只失去被推动,悬挂与驾驶完全不受影响。 // // 拦法:给本地坦克实体实例的 kz7(事件派发方法)套一层,遇到外力事件直接丢弃。 // 只影响本客户端本地坦克;自己的开炮后座走的是直接 m9k 调用,不受影响。 (function () { 'use strict'; var Lib = window.__hornetLib__; if (!Lib) { console.error('[拒绝击退] 请先加载 hornet_lib.js'); return; } // ===== 字段抽象层(语义名 → 匿名字段,总表在 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; } var FPushPoint = Fld('Event.PushPoint', 'o13i_1'); // 外力作用点 var FPushDir = Fld('Event.PushDir', 'p13i_1'); // 外力方向 var FPushStrength = Fld('Event.PushStrength', 'q13i_1'); // 外力强度 var MDispatch = Mth('Entity.Dispatch', 'kz7'); // 实体事件派发 var Stat = { running: false, blocked: 0, // 累计拦下的外力事件数 tankId: -1 }; var PatchedTank = null; // 当前已接管的坦克实体 var OrigDispatch = null; // 该实体原来的派发方法 // 外力事件识别(ODt):作用点 + 方向 + 强度 + r13i 写入方法 // ⚠ 不能用 instanceof:ODt 类没有对外导出,且每版类对象都会重建 function IsPushEvent(Event) { if (!Event || typeof Event !== 'object') return false; if (typeof Event[FPushStrength] !== 'number') return false; var Point = Event[FPushPoint]; var Dir = Event[FPushDir]; if (!Point || !Dir) return false; if (typeof Point.f20_1 !== 'number' || typeof Dir.f20_1 !== 'number') return false; return true; } // 本地坦克:拥有中的优先;死亡时退回观察目标(此时不参与战斗,影响可忽略) function GetLocalTank() { var Gm = window.__gameMode__; if (!Gm) return null; try { if (Gm.u10b_1) return Gm.u10b_1; } catch (e) {} try { if (Gm.t10b_1) return Gm.t10b_1; } catch (e) {} return null; } function Unpatch() { if (PatchedTank && OrigDispatch) { try { PatchedTank[MDispatch] = OrigDispatch; } catch (e) {} } PatchedTank = null; OrigDispatch = null; Stat.tankId = -1; } function Patch(Tank) { if (!Tank) return; if (PatchedTank === Tank) return; Unpatch(); var Orig = Tank[MDispatch]; if (typeof Orig !== 'function') return; OrigDispatch = Orig; Tank[MDispatch] = function (Event) { // 外力事件:直接吞掉(坦克物理收不到 → 不产生位移/旋转) if (Stat.running && IsPushEvent(Event)) { Stat.blocked++; return undefined; } return OrigDispatch.apply(this, arguments); }; PatchedTank = Tank; Stat.tankId = (Tank.iz1_1 !== undefined) ? Tank.iz1_1 : -1; console.log('[拒绝击退] 已接管本地坦克事件派发 id=' + Stat.tankId); } function Loop() { if (!Stat.running) return; requestAnimationFrame(Loop); try { var Tank = GetLocalTank(); if (Tank && Tank !== PatchedTank) Patch(Tank); // 换车/重生:实体换了要重新套 if (!Tank && PatchedTank) Unpatch(); // 离开战局:还原 } catch (e) {} } function _start() { if (Stat.running) return; Stat.running = true; Stat.blocked = 0; Patch(GetLocalTank()); requestAnimationFrame(Loop); console.log('[拒绝击退] 已启动(中弹/撞击不再推动本地坦克;悬挂、行驶、自身后座不受影响)'); } function _stop() { if (!Stat.running) return; Stat.running = false; Unpatch(); console.log('[拒绝击退] 已停止,事件派发已还原'); } var Tool = { Start: function () { _start(); }, Stop: function () { _stop(); }, GetRunning: function () { return Stat.running; }, Status: function () { if (!Stat.running) return '已停止'; return PatchedTank ? ('拦截 ' + Stat.blocked + ' 次') : '等待坦克'; } }; window.__hornetTools__ = window.__hornetTools__ || {}; window.__hornetTools__.NoKnockback = Tool; window.__hornetNoKnockback__ = { IsPushEvent: IsPushEvent, GetStat: function () { return { running: Stat.running, blocked: Stat.blocked, tankId: Stat.tankId }; } }; console.log('[3DTank] 拒绝击退模块已加载'); })(); // ===== hornet_panel.js ===== // ===== 3DTank 控制面板 ===== // 功能:Alt+T 切换面板显示,控制各工具开关 // 左上角 HUD 实时显示 GHT 捕获状态和各工具运行状态 // HUD 和面板支持鼠标拖动 // 依赖:hook_gamemode.js + hornet_lib.js + 各 hornet_*.js 工具模块 (function() { console.log('[3DTank] 控制面板加载'); // ===== 清理旧面板 ===== if (window.__hornetPanel__) { window.__hornetPanel__.Destroy(); console.log('已清理旧控制面板'); } // 通用拖动功能 // elem: 要拖动的元素,handle: 触发拖动的元素(默认=elem) // ignoreTags: 点击这些标签时不触发拖动(如 BUTTON) // key: 可选,传入后把窗口位置存到 localStorage(hornet_layout_)并在创建时还原 // 实现要点:Pointer Events + setPointerCapture 把指针"锁"在 handle 上, // pointerup/pointercancel 必定回到 handle;并在 window 捕获阶段加兜底, // 避免面板内 stopPropagation 或游戏画布抢事件导致"松手后仍粘着鼠标"。 // 返回:{ End, IsActive },供隐藏面板时强制结束拖动 function MakeDraggable(elem, handle, ignoreTags, key) { handle = handle || elem; ignoreTags = ignoreTags || ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA']; handle.style.cursor = 'move'; var StorageKey = key ? ('hornet_layout_' + key) : null; function ClampX(v) { return Math.max(0, Math.min(window.innerWidth - 40, v)); } function ClampY(v) { return Math.max(0, Math.min(window.innerHeight - 24, 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.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(); // 移除 transform 居中,改用绝对定位,避免拖动起点跳变 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; handle.style.cursor = 'grabbing'; } function End() { if (!Active) return; Active = false; if (PointerId !== null && handle.releasePointerCapture) { try { handle.releasePointerCapture(PointerId); } catch (Err) {} } PointerId = null; handle.style.cursor = 'move'; SavePos(); } function CanStart(Target) { return !Target || ignoreTags.indexOf(Target.tagName) < 0; } if (window.PointerEvent) { // 捕获阶段监听:先于元素自身的 stopPropagation 执行 handle.addEventListener('pointerdown', function (e) { if (e.button !== 0) return; if (!CanStart(e.target)) return; Begin(e.clientX, e.clientY); PointerId = e.pointerId; try { handle.setPointerCapture(e.pointerId); } catch (Err) {} e.preventDefault(); e.stopPropagation(); }, true); handle.addEventListener('pointermove', function (e) { if (!Active || (PointerId !== null && e.pointerId !== PointerId)) return; // 兜底:按键已经松开(例如在窗口外松手)→ 结束拖动 if (e.buttons === 0) { End(); return; } MoveTo(e.clientX, e.clientY); e.preventDefault(); }, true); handle.addEventListener('pointerup', function () { End(); }, true); handle.addEventListener('pointercancel', function () { End(); }, true); handle.addEventListener('lostpointercapture', function () { End(); }, true); window.addEventListener('pointerup', function () { End(); }, true); } else { // 旧浏览器回退:mouse 事件 + document 捕获阶段监听 handle.addEventListener('mousedown', function (e) { if (e.button !== 0) return; if (!CanStart(e.target)) return; Begin(e.clientX, e.clientY); e.preventDefault(); }, true); 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); document.addEventListener('visibilitychange', function () { if (document.hidden) End(); }); return { End: End, IsActive: function () { return Active; } }; } // ===== 工具列表(动态从 __hornetTools__ 获取)===== function GetToolList() { var tools = window.__hornetTools__ || {}; return [ { key: 'Radar', label: '雷达', obj: tools.Radar }, { key: 'Wireframe', label: '3D透视', obj: tools.Wireframe, hasMode: true }, { key: 'Rearview', label: '后视镜', obj: tools.Rearview }, { key: 'AimWarning', label: '瞄准警告', obj: tools.AimWarning }, { key: 'FreeLook', label: '自由视角', obj: tools.FreeLook }, { key: 'WarSandbox', label: '战争沙盘', obj: tools.WarSandbox }, { key: 'Mouse', label: '鼠标稳定', obj: tools.Mouse } ]; } // ===== 工具启用状态(持久化 + 默认启用)===== // 默认除「自由视角」外全部开启:自由视角会直接接管相机,默认开启过于突兀 // 忽略模块(NoClip / NoKnockback)不在这里:由「忽略」区域单独管理,自带 localStorage 持久化 var ToolKeys = ['Radar', 'Wireframe', 'Rearview', 'AimWarning', 'FreeLook', 'WarSandbox', 'Mouse']; var ToolDefaults = { Radar: true, Wireframe: true, Rearview: true, AimWarning: true, FreeLook: false, WarSandbox: true, Mouse: true }; var ToolEnabled = {}; try { var __savedEnabled = JSON.parse(localStorage.getItem('hornet_tool_enabled') || '{}'); ToolKeys.forEach(function(k) { ToolEnabled[k] = (typeof __savedEnabled[k] === 'boolean') ? __savedEnabled[k] : ToolDefaults[k]; }); } catch(e) { ToolKeys.forEach(function(k) { ToolEnabled[k] = ToolDefaults[k]; }); } // 把各工具当前的运行状态写回持久化(开关时调用) function SaveToolEnabled() { try { var tools = window.__hornetTools__ || {}; ToolKeys.forEach(function(k) { var t = tools[k]; if (t && typeof t.GetRunning === 'function') ToolEnabled[k] = !!t.GetRunning(); }); localStorage.setItem('hornet_tool_enabled', JSON.stringify(ToolEnabled)); } catch(e) {} } // 需要 ght 才能初始化的工具(要 hook 相机 / GL)——等捕获后再启动,避免失败报错刷屏 var GameModeTools = { FreeLook: true, Rearview: true, WarSandbox: true }; // 自动按保存状态开启:覆盖层(雷达/透视/瞄准)无 ght 也能先开并出 UI; // 每 30 帧检查一次,全部进入运行状态即停 var AutoEnableDone = false; var AutoEnableFrame = 0; function TryAutoEnable() { if (AutoEnableDone) return; AutoEnableFrame++; if (AutoEnableFrame % 30 !== 0) return; var HasGm = !!window.__gameMode__; var tools = window.__hornetTools__ || {}; var pending = 0; ToolKeys.forEach(function(k) { if (!ToolEnabled[k]) return; if (GameModeTools[k] && !HasGm) { pending++; return; } // 等 ght 捕获 var t = tools[k]; if (!t || typeof t.Start !== 'function') return; if (typeof t.GetRunning === 'function' && t.GetRunning()) return; pending++; try { t.Start(); } catch(e) {} }); if (pending === 0) { AutoEnableDone = true; UpdateButtons(); console.log('[3DTank] 已按保存状态自动开启工具(覆盖层无 ght 也可先开;后视镜/沙盘/自由视角等 ght 捕获后开)'); } } // ===== HUD(左上角状态显示)—— 已按需求移除:**不挂载到页面**,等于不再显示。 // 注意:保留 hud 对象与 UpdateHud 循环 —— 循环里还负责 TryAutoEnable()(按保存状态自动开启工具)。 var hud = document.createElement('div'); hud.id = '3dtank-hud'; // 原样式 + document.body.appendChild(hud) 已移除(那就是常驻左上角的「3DTank 控制台」小窗) var HudDrag = MakeDraggable(hud, null, null, 'hud'); // ===== 面板(可拖动,Alt+T 切换)===== var panel = document.createElement('div'); panel.id = '3dtank-panel'; // 初始居中:用 left/top 50% + transform,拖动后移除 transform panel.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:100001;' + 'background:rgba(12,16,24,0.72);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);' + 'border:1px solid rgba(125,249,255,0.2);border-radius:12px;' + 'padding:18px 22px;color:#e8eef5;font:13px Consolas,monospace;min-width:340px;' + 'box-shadow:0 8px 32px rgba(0,0,0,0.6),0 0 1px rgba(125,249,255,0.3),inset 0 1px 0 rgba(255,255,255,0.05);' + 'display:none;user-select:none;-webkit-user-select:none;'; // 标题(可拖动手柄) var title = document.createElement('div'); title.textContent = '3DTank 控制面板'; title.style.cssText = 'font-size:16px;font-weight:bold;color:#7df9ff;margin-bottom:4px;' + 'text-shadow:0 0 6px rgba(125,249,255,0.4);' + 'cursor:move;padding-bottom:8px;border-bottom:1px solid rgba(125,249,255,0.15);' + 'letter-spacing:0.5px;'; panel.appendChild(title); var subtitle = document.createElement('div'); subtitle.textContent = 'Alt+T 切换显示/隐藏 | 拖动标题移动面板'; subtitle.style.cssText = 'font-size:11px;color:#5a7a8a;margin:6px 0 14px;'; panel.appendChild(subtitle); // 工具按钮容器 var btnContainer = document.createElement('div'); panel.appendChild(btnContainer); // ===== 3D透视设置子面板(可折叠)===== var wfSettings = document.createElement('div'); wfSettings.style.cssText = 'margin-top:12px;padding-top:10px;border-top:1px solid rgba(125,249,255,0.08);'; var wfHeader = document.createElement('div'); wfHeader.style.cssText = 'display:flex;justify-content:space-between;align-items:center;cursor:pointer;color:#7df9ff;font-weight:bold;font-size:13px;'; wfHeader.innerHTML = '3D透视设置▼'; wfSettings.appendChild(wfHeader); var wfBody = document.createElement('div'); wfBody.style.cssText = 'margin-top:8px;display:block;'; wfSettings.appendChild(wfBody); var wfCollapsed = false; wfHeader.onclick = function() { wfCollapsed = !wfCollapsed; wfBody.style.display = wfCollapsed ? 'none' : 'block'; document.getElementById('wfToggle').textContent = wfCollapsed ? '▶' : '▼'; }; // 行布局辅助 function MakeRow(labelText) { var row = document.createElement('div'); row.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:6px 0;'; var lab = document.createElement('span'); lab.style.cssText = 'color:#b8c8d8;font-size:12px;'; lab.textContent = labelText; row.appendChild(lab); var ctrl = document.createElement('div'); ctrl.style.cssText = 'display:flex;gap:6px;align-items:center;'; row.appendChild(ctrl); wfBody.appendChild(row); return { row: row, ctrl: ctrl }; } // 线宽调节 var lwRow = MakeRow('线宽'); var lwSlider = document.createElement('input'); lwSlider.type = 'range'; lwSlider.min = '0.5'; lwSlider.max = '8'; lwSlider.step = '0.5'; lwSlider.style.cssText = 'width:120px;accent-color:#7df9ff;'; var lwVal = document.createElement('span'); lwVal.style.cssText = 'color:#7df9ff;font-size:11px;min-width:28px;text-align:right;'; lwRow.ctrl.appendChild(lwSlider); lwRow.ctrl.appendChild(lwVal); lwSlider.oninput = function() { var v = parseFloat(lwSlider.value); lwVal.textContent = v.toFixed(1); var tools = window.__hornetTools__ || {}; if (tools.Wireframe) tools.Wireframe.SetLineWidth(v); }; // 渲染模式选择 var hmRow = MakeRow('渲染模式'); var hmSelect = document.createElement('select'); hmSelect.style.cssText = 'padding:3px 6px;border:1px solid rgba(125,249,255,0.25);border-radius:4px;' + 'background:rgba(20,30,40,0.6);color:#e8eef5;font:11px Consolas;cursor:pointer;'; var hmOptions = [ { v: 'team', t: '队伍模式(蓝友/红敌)' }, { v: 'whitelist', t: '白名单(仅名单内渲染)' }, { v: 'blacklist', t: '黑名单(名单内不渲染)' } ]; hmOptions.forEach(function(o) { var opt = document.createElement('option'); opt.value = o.v; opt.textContent = o.t; hmSelect.appendChild(opt); }); hmRow.ctrl.appendChild(hmSelect); hmSelect.onchange = function() { var tools = window.__hornetTools__ || {}; if (tools.Wireframe) tools.Wireframe.SetHighlightMode(hmSelect.value); }; // 名单管理 var hlRow = MakeRow('名单'); var hlInput = document.createElement('input'); hlInput.type = 'text'; hlInput.placeholder = '玩家名'; hlInput.style.cssText = 'width:120px;padding:3px 6px;border:1px solid rgba(125,249,255,0.25);border-radius:4px;' + 'background:rgba(20,30,40,0.6);color:#e8eef5;font:11px Consolas;'; var hlAddBtn = document.createElement('button'); hlAddBtn.textContent = '+'; hlAddBtn.style.cssText = 'padding:3px 8px;border:1px solid rgba(46,204,113,0.4);border-radius:4px;' + 'background:rgba(46,204,113,0.2);color:#2ecc71;cursor:pointer;font:bold 12px Consolas;'; hlRow.ctrl.appendChild(hlInput); hlRow.ctrl.appendChild(hlAddBtn); var hlList = document.createElement('div'); hlList.style.cssText = 'margin-top:6px;max-height:120px;overflow-y:auto;font-size:11px;'; wfBody.appendChild(hlList); function RefreshHlList() { var tools = window.__hornetTools__ || {}; var arr = tools.Wireframe ? tools.Wireframe.GetHighlightList() : []; hlList.innerHTML = ''; if (arr.length === 0) { var empty = document.createElement('div'); empty.style.cssText = 'color:#5a7a8a;padding:4px 0;font-style:italic;'; empty.textContent = '(空)'; hlList.appendChild(empty); return; } arr.forEach(function(name) { var item = document.createElement('div'); item.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:3px 4px;border-bottom:1px solid rgba(125,249,255,0.05);'; var ns = document.createElement('span'); ns.style.cssText = 'color:#b8c8d8;'; ns.textContent = name; item.appendChild(ns); var delBtn = document.createElement('button'); delBtn.textContent = '×'; delBtn.style.cssText = 'padding:1px 6px;border:1px solid rgba(224,85,85,0.4);border-radius:3px;' + 'background:rgba(224,85,85,0.2);color:#e05555;cursor:pointer;font:bold 11px Consolas;'; delBtn.onclick = function() { if (tools.Wireframe) tools.Wireframe.RemoveHighlight(name); RefreshHlList(); }; item.appendChild(delBtn); hlList.appendChild(item); }); } function AddFromInput() { var name = hlInput.value.trim(); if (!name) return; var tools = window.__hornetTools__ || {}; if (tools.Wireframe) tools.Wireframe.AddHighlight(name); hlInput.value = ''; RefreshHlList(); } hlAddBtn.onclick = AddFromInput; hlInput.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); AddFromInput(); } }); var hlClearRow = document.createElement('div'); hlClearRow.style.cssText = 'margin-top:4px;text-align:right;'; var hlClearBtn = document.createElement('button'); hlClearBtn.textContent = '清空名单'; hlClearBtn.style.cssText = 'padding:3px 8px;border:1px solid rgba(224,85,85,0.4);border-radius:4px;' + 'background:rgba(224,85,85,0.15);color:#e05555;cursor:pointer;font:11px Consolas;'; hlClearBtn.onclick = function() { var tools = window.__hornetTools__ || {}; if (tools.Wireframe) tools.Wireframe.ClearHighlight(); RefreshHlList(); }; hlClearRow.appendChild(hlClearBtn); wfBody.appendChild(hlClearRow); panel.appendChild(wfSettings); // ===== 通用可折叠设置子面板辅助函数 ===== function MakeSettingsSection(titleText) { var sec = document.createElement('div'); sec.style.cssText = 'margin-top:10px;padding-top:8px;border-top:1px solid rgba(125,249,255,0.08);'; var hdr = document.createElement('div'); hdr.style.cssText = 'display:flex;justify-content:space-between;align-items:center;cursor:pointer;color:#7df9ff;font-weight:bold;font-size:12px;'; var toggleChar = document.createElement('span'); toggleChar.style.cssText = 'font-size:10px;color:#5a7a8a;'; toggleChar.textContent = '▶'; hdr.innerHTML = '' + titleText + ''; hdr.appendChild(toggleChar); sec.appendChild(hdr); var body = document.createElement('div'); body.style.cssText = 'margin-top:6px;display:none;'; sec.appendChild(body); hdr.onclick = function() { var shown = body.style.display !== 'none'; body.style.display = shown ? 'none' : 'block'; toggleChar.textContent = shown ? '▶' : '▼'; }; panel.appendChild(sec); function row(labelText) { var r = document.createElement('div'); r.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:5px 0;'; var lab = document.createElement('span'); lab.style.cssText = 'color:#b8c8d8;font-size:11px;'; lab.textContent = labelText; r.appendChild(lab); var ctrl = document.createElement('div'); ctrl.style.cssText = 'display:flex;gap:6px;align-items:center;'; r.appendChild(ctrl); body.appendChild(r); return { row: r, ctrl: ctrl }; } function slider(labelText, min, max, step, fmt, onInput) { var r = row(labelText); var s = document.createElement('input'); s.type = 'range'; s.min = String(min); s.max = String(max); s.step = String(step); s.style.cssText = 'width:110px;accent-color:#7df9ff;'; var v = document.createElement('span'); v.style.cssText = 'color:#7df9ff;font-size:10px;min-width:32px;text-align:right;'; s.oninput = function() { var val = parseFloat(s.value); v.textContent = fmt(val); onInput(val); }; r.ctrl.appendChild(s); r.ctrl.appendChild(v); return { set: function(val) { s.value = val; v.textContent = fmt(parseFloat(val)); }, get: function() { return parseFloat(s.value); } }; } function checkbox(labelText, onChange) { var r = row(labelText); var c = document.createElement('input'); c.type = 'checkbox'; c.style.cssText = 'accent-color:#7df9ff;width:16px;height:16px;cursor:pointer;'; c.onchange = function() { onChange(c.checked); }; r.ctrl.appendChild(c); return { set: function(on) { c.checked = !!on; }, get: function() { return c.checked; } }; } return { sec: sec, body: body, row: row, slider: slider, checkbox: checkbox }; } // ===== 雷达设置 ===== var radarSec = MakeSettingsSection('雷达设置'); var radarRangeCtl = radarSec.slider('范围(m)', 30, 600, 10, function(v){ return v.toFixed(0); }, function(v) { var tools = window.__hornetTools__ || {}; if (tools.Radar) tools.Radar.SetRange(v * 100); }); var radarOpacityCtl = radarSec.slider('透明度', 0.2, 1.0, 0.05, function(v){ return v.toFixed(2); }, function(v) { var tools = window.__hornetTools__ || {}; if (tools.Radar) tools.Radar.SetOpacity(v); }); // ===== 鼠标稳定设置 ===== // 强制原始输入:游戏锁定失败会静默回退成带 OS 加速的普通锁定,手感会飘 // 锁后忽略:锁定刚建立 / 失焦恢复后的头几毫秒位移常是巨幅跳变,直接丢弃 // 尖峰钳制:单事件位移超过阈值就截断(0 = 关闭) var mouseSec = MakeSettingsSection('鼠标稳定'); var mouseRawCtl = mouseSec.checkbox('强制原始输入', function(on) { var tools = window.__hornetTools__ || {}; if (tools.Mouse) tools.Mouse.SetForceRaw(on); }); var mouseDropCtl = mouseSec.slider('锁后忽略(ms)', 0, 500, 20, function(v){ return v.toFixed(0); }, function(v) { var tools = window.__hornetTools__ || {}; if (tools.Mouse) tools.Mouse.SetDropMs(v); }); var mouseClampCtl = mouseSec.slider('尖峰钳制(px)', 0, 1000, 25, function(v){ return v.toFixed(0); }, function(v) { var tools = window.__hornetTools__ || {}; if (tools.Mouse) tools.Mouse.SetMaxDelta(v); }); (function () { // 回填鼠标模块里已保存的配置 var tools = window.__hornetTools__ || {}; if (!tools.Mouse) return; mouseRawCtl.set(tools.Mouse.GetForceRaw()); mouseDropCtl.set(tools.Mouse.GetDropMs()); mouseClampCtl.set(tools.Mouse.GetMaxDelta()); })(); // ===== 后视镜设置 ===== var rvSec = MakeSettingsSection('后视镜设置'); var rvFovCtl = rvSec.slider('FOV', 0.5, 2.5, 0.05, function(v){ return v.toFixed(2); }, function(v) { var tools = window.__hornetTools__ || {}; if (tools.Rearview) tools.Rearview.SetFov(v); }); var rvOpacityCtl = rvSec.slider('透明度', 0.2, 1.0, 0.05, function(v){ return v.toFixed(2); }, function(v) { var tools = window.__hornetTools__ || {}; if (tools.Rearview) tools.Rearview.SetOpacity(v); }); // ===== 瞄准警告设置 ===== var awSec = MakeSettingsSection('瞄准警告设置'); var awSensCtl = awSec.slider('灵敏度', 0.3, 3.0, 0.1, function(v){ return v.toFixed(1); }, function(v) { var tools = window.__hornetTools__ || {}; if (tools.AimWarning) tools.AimWarning.SetSensitivity(v); }); var awSoundCtl = awSec.checkbox('警告声音', function(on) { var tools = window.__hornetTools__ || {}; if (tools.AimWarning) tools.AimWarning.SetSound(on); }); // ===== 战争沙盘设置 ===== var wsSec = MakeSettingsSection('战争沙盘设置'); var wsGridCtl = wsSec.checkbox('显示网格', function(on) { var tools = window.__hornetTools__ || {}; if (tools.WarSandbox) tools.WarSandbox.SetShowGrid(on); }); var wsMarkerCtl = wsSec.slider('标记大小', 0.5, 3.0, 0.1, function(v){ return v.toFixed(1); }, function(v) { var tools = window.__hornetTools__ || {}; if (tools.WarSandbox) tools.WarSandbox.SetMarkerSize(v); }); // ===== 电磁炮(RAILGUN / z4t)设置 ===== // 锁定/蓄力时长实时覆盖 window.__hornetLockSec__(游戏内 z4t.c18o() 优先读它), // 拖动滑块**立即生效**,不需要重打包或刷新页面。 // 实测门槛:1.5s / 2.0s / 原版3~5s = 满伤;1.0s / 0.1s = 无伤害 ⇒ 门槛在 1.0~1.5s。 var rgSec = MakeSettingsSection('电磁炮设置'); var rgSecVal = 1.5; function ApplyLock(v) { window.__hornetLockSec__ = (v > 0 ? v : undefined); } function SaveLock(on, v) { try { localStorage.setItem('hornet_lock_on', on ? '1' : '0'); localStorage.setItem('hornet_lock_sec', String(v)); } catch (e) {} } var rgOnCtl = rgSec.checkbox('启用时长覆盖', function(on) { ApplyLock(on ? rgSecVal : 0); SaveLock(on, rgSecVal); }); var rgLockCtl = rgSec.slider('锁定/蓄力(s)', 0.1, 5.0, 0.05, function(v) { return v.toFixed(2); }, function(v) { rgSecVal = v; if (rgOnCtl.get()) ApplyLock(v); SaveLock(rgOnCtl.get(), v); }); (function () { // 回填上次保存的配置;**默认开启覆盖、默认值 1.5s**(实测满伤的最小锁定/蓄力时长) try { var on = localStorage.getItem('hornet_lock_on') !== '0'; var v = parseFloat(localStorage.getItem('hornet_lock_sec')); if (isFinite(v) && v > 0) rgSecVal = v; rgLockCtl.set(rgSecVal); rgOnCtl.set(on); ApplyLock(on ? rgSecVal : 0); } catch (e) {} })(); // ===== 忽略(坦克 / 反弹 / 栅栏)===== // 忽略坦克:关闭 / 仅敌方 / 仅友方 / 全部(由 hornet_noclip.js 的配对过滤实现) // 忽略反弹:对应 hornet_noknockback.js(中弹不再被推开) // 忽略栅栏:只忽略陡峭的静态三角形(墙/栅栏立面),地面与缓坡照常 var igSec = MakeSettingsSection('忽略'); var igTankRow = igSec.row('忽略坦克'); var igTankSel = document.createElement('select'); igTankSel.style.cssText = 'padding:3px 6px;border:1px solid rgba(125,249,255,0.25);border-radius:4px;' + 'background:rgba(20,30,40,0.6);color:#e8eef5;font:11px Consolas;cursor:pointer;'; [ { v: 'off', t: '关闭' }, { v: 'enemy', t: '仅敌方' }, { v: 'friendly', t: '仅友方' }, { v: 'all', t: '全部' } ].forEach(function (o) { var opt = document.createElement('option'); opt.value = o.v; opt.textContent = o.t; igTankSel.appendChild(opt); }); igTankRow.ctrl.appendChild(igTankSel); igTankSel.onchange = function () { var tools = window.__hornetTools__ || {}; if (tools.NoClip) tools.NoClip.SetTankMode(igTankSel.value); }; var igKnockCtl = igSec.checkbox('忽略反弹', function (on) { var tools = window.__hornetTools__ || {}; if (tools.NoKnockback) { on ? tools.NoKnockback.Start() : tools.NoKnockback.Stop(); } try { localStorage.setItem('hornet_ignore_knockback', on ? '1' : '0'); } catch (e) {} }); var igFenceCtl = igSec.checkbox('忽略栅栏', function (on) { var tools = window.__hornetTools__ || {}; if (tools.NoClip) tools.NoClip.SetIgnoreFence(on); }); // 启动时按上次保存的状态恢复 (function () { var tools = window.__hornetTools__ || {}; var Mode = 'off'; try { Mode = localStorage.getItem('hornet_ignore_tanks') || 'off'; } catch (e) {} if (['off', 'enemy', 'friendly', 'all'].indexOf(Mode) < 0) Mode = 'off'; igTankSel.value = Mode; if (tools.NoClip) tools.NoClip.SetTankMode(Mode); var Fence = false; try { Fence = localStorage.getItem('hornet_ignore_fence') === '1'; } catch (e) {} igFenceCtl.set(Fence); if (tools.NoClip) tools.NoClip.SetIgnoreFence(Fence); var Knock = false; try { Knock = localStorage.getItem('hornet_ignore_knockback') === '1'; } catch (e) {} igKnockCtl.set(Knock); if (tools.NoKnockback && Knock) tools.NoKnockback.Start(); })(); // 从工具读取当前设置(打开面板时同步) function SyncWfSettings() { var tools = window.__hornetTools__ || {}; if (tools.Wireframe) { var lw = tools.Wireframe.GetLineWidth(); lwSlider.value = lw; lwVal.textContent = lw.toFixed(1); var hm = tools.Wireframe.GetHighlightMode(); hmSelect.value = hm; } RefreshHlList(); // 同步其他工具设置 if (tools.Radar) { radarRangeCtl.set(tools.Radar.GetRange() / 100); radarOpacityCtl.set(tools.Radar.GetOpacity()); } if (tools.Rearview) { rvFovCtl.set(tools.Rearview.GetFov()); rvOpacityCtl.set(tools.Rearview.GetOpacity()); } if (tools.AimWarning) { awSensCtl.set(tools.AimWarning.GetSensitivity()); awSoundCtl.set(tools.AimWarning.GetSound()); } if (tools.WarSandbox) { wsGridCtl.set(tools.WarSandbox.GetShowGrid()); wsMarkerCtl.set(tools.WarSandbox.GetMarkerSize()); } if (tools.NoClip) { igTankSel.value = tools.NoClip.GetTankMode(); igFenceCtl.set(tools.NoClip.GetIgnoreFence()); } if (tools.NoKnockback) igKnockCtl.set(tools.NoKnockback.GetRunning()); } // 底部状态 var footer = document.createElement('div'); footer.style.cssText = 'margin-top:14px;padding-top:10px;border-top:1px solid rgba(125,249,255,0.08);font-size:11px;color:#5a7a8a;text-align:center;'; footer.textContent = '点击按钮切换 开/关'; panel.appendChild(footer); // 热键显示区域(UpdateHotkeyDisplay 在热键系统定义后调用) var hotkeySection = document.createElement('div'); hotkeySection.style.cssText = 'margin-top:12px;padding-top:10px;border-top:1px solid rgba(125,249,255,0.08);font-size:11px;'; function UpdateHotkeyDisplay() { if (!HotkeyLabels || !Hotkeys) return; var html = '
热键(控制台可改: __hornetPanel__.SetHotkey)
'; Object.keys(HotkeyLabels).forEach(function(k) { html += '
' + '' + HotkeyLabels[k] + '' + '' + Hotkeys[k] + '
'; }); hotkeySection.innerHTML = html; } panel.appendChild(hotkeySection); document.body.appendChild(panel); // 面板通过标题栏拖动 var PanelDrag = MakeDraggable(panel, title, null, 'panel'); // ===== 阻止面板内事件冒泡到 window(防止游戏/工具拦截键盘鼠标)===== // 鼠标事件:面板上所有鼠标事件都阻止冒泡(防止游戏转视角/开炮等) ['mousedown','mouseup','mousemove','wheel','click','dblclick','contextmenu'].forEach(function(evtName) { panel.addEventListener(evtName, function(e) { e.stopPropagation(); }); }); // 键盘事件:只在输入元素上阻止冒泡(保留面板上其他位置的热键功能) ['keydown','keypress','keyup'].forEach(function(evtName) { panel.addEventListener(evtName, function(e) { var tag = e.target.tagName; if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA') { e.stopPropagation(); } }); }); // HUD 也阻止鼠标事件冒泡(防止拖动HUD时游戏响应) ['mousedown','mouseup','mousemove','wheel','click','contextmenu'].forEach(function(evtName) { hud.addEventListener(evtName, function(e) { e.stopPropagation(); }); }); // ===== 构建工具按钮(含模式切换)===== var btnMap = {}; // key -> { row, btn, label, tool, modeBtns } function BuildButtons() { btnContainer.innerHTML = ''; btnMap = {}; var list = GetToolList(); list.forEach(function(t) { var row = document.createElement('div'); row.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid rgba(125,249,255,0.06);'; var label = document.createElement('span'); label.textContent = t.label; label.style.cssText = 'color:#b8c8d8;font-size:13px;'; row.appendChild(label); // 右侧按钮容器 var btnBox = document.createElement('div'); btnBox.style.cssText = 'display:flex;gap:4px;align-items:center;'; var modeBtns = null; // Wireframe 模式切换按钮 if (t.hasMode && t.obj && t.obj.SetMode) { modeBtns = {}; var modes = [ { mode: 'wireframe', text: '线框' }, { mode: 'outline', text: '描边' } ]; modes.forEach(function(mb) { var b = document.createElement('button'); b.textContent = mb.text; b.style.cssText = 'padding:4px 8px;border:1px solid rgba(125,249,255,0.25);border-radius:4px;' + 'font:11px Consolas,monospace;cursor:pointer;color:#e8eef5;min-width:44px;' + 'background:rgba(20,30,40,0.6);transition:all 0.2s;'; b.onclick = function() { if (t.obj) { t.obj.SetMode(mb.mode); UpdateButtons(); } }; btnBox.appendChild(b); modeBtns[mb.mode] = b; }); } var btn = document.createElement('button'); btn.style.cssText = 'padding:6px 16px;border:none;border-radius:6px;font:bold 12px Consolas,monospace;' + 'cursor:pointer;min-width:72px;color:#fff;transition:all 0.2s;' + 'box-shadow:0 2px 8px rgba(0,0,0,0.3);'; btn.onclick = function() { if (!t.obj) { console.warn('[3DTank] 工具 ' + t.key + ' 未加载'); return; } if (t.obj.GetRunning()) { t.obj.Stop(); } else { t.obj.Start(); } UpdateButtons(); SaveToolEnabled(); }; btnBox.appendChild(btn); row.appendChild(btnBox); btnContainer.appendChild(row); btnMap[t.key] = { btn: btn, label: label, tool: t.obj, modeBtns: modeBtns }; }); } BuildButtons(); // ===== 更新按钮状态 ===== function UpdateButtons() { Object.keys(btnMap).forEach(function(key) { var info = btnMap[key]; if (!info.tool) { info.btn.textContent = '未加载'; info.btn.style.background = '#555'; info.btn.disabled = true; return; } var running = info.tool.GetRunning(); info.btn.textContent = running ? '关闭' : '开启'; info.btn.style.background = running ? 'linear-gradient(135deg,#e05555,#c0333d)' : 'linear-gradient(135deg,#2ecc71,#27ae60)'; info.btn.disabled = false; // 更新模式按钮高亮 if (info.modeBtns) { var curMode = null; try { curMode = window.__hornet3DWireframe__ && window.__hornet3DWireframe__.mode; } catch(e) {} Object.keys(info.modeBtns).forEach(function(mk) { var mb = info.modeBtns[mk]; if (mk === curMode && running) { mb.style.background = 'linear-gradient(135deg,#2ecc71,#27ae60)'; mb.style.opacity = '1'; mb.style.color = '#fff'; } else { mb.style.background = 'rgba(20,30,40,0.6)'; mb.style.opacity = '0.6'; mb.style.color = '#8899aa'; } }); } }); } // ===== 更新 HUD(HUD 本体已移除,这里主要保留 TryAutoEnable 与循环)===== function UpdateHud() { requestAnimationFrame(UpdateHud); TryAutoEnable(); // gameMode 捕获后自动按保存状态开启工具 if (!hud.parentNode) return; // HUD 未挂载(已移除)→ 不必拼状态文本/写 DOM var ght = window.__gameMode__; var lines = []; lines.push('3DTank 控制台 [拖动移动]'); lines.push('GHT: ' + (ght ? '已捕获' : '未捕获')); var list = GetToolList(); list.forEach(function(t) { var status = t.obj ? t.obj.Status() : '未加载'; var dot = t.obj && t.obj.GetRunning() ? '●' : '○'; lines.push(dot + ' ' + t.label + ': ' + status); }); hud.textContent = lines.join('\n'); hud.style.color = ght ? '#7df9ff' : '#f5a623'; } UpdateHud(); // ===== 热键系统(localStorage 持久化)===== var DefaultHotkeys = { panel: 'Alt+T', radar: 'Alt+R', wireframe: 'Alt+W', rearview: 'Alt+V', aimWarning: 'Alt+A', freeLook: 'Alt+F', warSandbox: 'Alt+M' }; var HotkeyLabels = { panel: '面板', radar: '雷达', wireframe: '3D透视', rearview: '后视镜', aimWarning: '瞄准警告', freeLook: '自由视角', warSandbox: '战争沙盘' }; var Hotkeys = {}; try { var saved = localStorage.getItem('3dtank_hotkeys'); var parsed = saved ? JSON.parse(saved) : {}; Object.keys(DefaultHotkeys).forEach(function(k) { Hotkeys[k] = parsed[k] || DefaultHotkeys[k]; }); } catch(e) { Object.keys(DefaultHotkeys).forEach(function(k) { Hotkeys[k] = DefaultHotkeys[k]; }); } function SaveHotkeys() { try { localStorage.setItem('3dtank_hotkeys', JSON.stringify(Hotkeys)); } catch(e) {} } function EventToHotkey(e) { var parts = []; if (e.altKey) parts.push('Alt'); if (e.ctrlKey) parts.push('Ctrl'); if (e.shiftKey) parts.push('Shift'); var key = e.key; if (key.length === 1) key = key.toUpperCase(); if (key === ' ') key = 'Space'; parts.push(key); return parts.join('+'); } function HidePanel() { panel.style.display = 'none'; PanelDrag.End(); // 防止隐藏时拖动状态残留(粘手) } // 释放鼠标指针锁定:游戏中鼠标被 Pointer Lock 捕获时指针隐藏、锁定在画布上, // 面板点不到,需退出锁定让光标恢复可交互 function ReleasePointerLock() { try { if (document.exitPointerLock && document.pointerLockElement) { document.exitPointerLock(); } } catch(e) {} } function ShowPanel() { panel.style.display = 'block'; ReleasePointerLock(); UpdateButtons(); SyncWfSettings(); } function TogglePanel() { if (panel.style.display === 'block') { HidePanel(); } else { ShowPanel(); } } function OnHotkey(e) { if (e.key === 'Escape' && panel.style.display === 'block') { HidePanel(); return; } var hk = EventToHotkey(e); if (hk === Hotkeys.panel) { e.preventDefault(); TogglePanel(); return; } var toolMap = { radar: 'Radar', wireframe: 'Wireframe', rearview: 'Rearview', aimWarning: 'AimWarning', freeLook: 'FreeLook', warSandbox: 'WarSandbox' }; Object.keys(toolMap).forEach(function(k) { if (hk === Hotkeys[k]) { e.preventDefault(); var tools = window.__hornetTools__ || {}; var tool = tools[toolMap[k]]; if (tool) { if (tool.GetRunning()) tool.Stop(); else tool.Start(); UpdateButtons(); SaveToolEnabled(); } } }); } // F5 刷新页面(游戏会吞掉 F5)——捕获阶段兜底,先于游戏处理 window.addEventListener('keydown', function(e) { if (e.key === 'F5') { e.preventDefault(); e.stopImmediatePropagation(); location.reload(); } }, true); window.addEventListener('keydown', OnHotkey); // 热键系统定义完成,更新显示 UpdateHotkeyDisplay(); // ===== 暴露接口 ===== window.__hornetPanel__ = { Toggle: TogglePanel, Show: ShowPanel, Hide: HidePanel, UpdateButtons: UpdateButtons, RebuildButtons: BuildButtons, GetHotkeys: function() { var r = {}; Object.keys(Hotkeys).forEach(function(k) { r[k] = Hotkeys[k]; }); return r; }, SetHotkey: function(name, key) { if (DefaultHotkeys[name]) { Hotkeys[name] = key; SaveHotkeys(); UpdateHotkeyDisplay(); console.log('[3DTank] 热键 ' + name + ' 已设为 ' + key); } else { console.warn('[3DTank] 未知热键名: ' + name + ',可用: ' + Object.keys(DefaultHotkeys).join(', ')); } }, ResetHotkeys: function() { Object.keys(DefaultHotkeys).forEach(function(k) { Hotkeys[k] = DefaultHotkeys[k]; }); SaveHotkeys(); UpdateHotkeyDisplay(); console.log('[3DTank] 热键已重置为默认值'); }, Destroy: function() { HudDrag.End(); PanelDrag.End(); if (hud.parentNode) hud.parentNode.removeChild(hud); if (panel.parentNode) panel.parentNode.removeChild(panel); window.removeEventListener('keydown', OnHotkey); } }; console.log('[3DTank] 控制面板已就绪,按 Alt+T 打开面板,HUD/面板/窗口可拖动,热键已加载'); })(); // ===== hornet_prefetch.js ===== // ===== 车库资源预热模块 ===== // 目的:从战局返回车库时,车库界面要一次性加载大量贴图/图标/坦克模型, // 首次全部现场拉取会明显卡顿。这里在**战局空闲时**后台把这些资源拉进浏览器缓存。 // // 预热两批: // ① /play/ —— 车库/大厅 UI 图片(由 resources.json 资源表枚举,本地直出) // ② /resources/<...> —— res.3dtank.com 上最近访问过的模型/贴图 // 清单由本地 serve.js 的 /__prefetch_list__ 提供(按缓存文件 mtime 倒序) // // 策略: // - 只在捕获到 __gameMode__(已在战局)后 8 秒才开始,避开进入战局的加载高峰 // - 串行 + 间隔限速,避免抢占战局带宽 // - 10 分钟内只跑一次(localStorage 记录),可用 __hornetPrefetch__.stop() 手动停止 // - localStorage.hornetPrefetch = '0' 可永久关闭 (function () { var KEY_AT = 'hornetPrefetchAt'; var KEY_OFF = 'hornetPrefetch'; var GAP_STATIC = 40; // UI 图片间隔(ms) var GAP_RES = 30; // 模型/贴图间隔(ms) var State = { running: false, stopped: false, timers: [] }; function LastRunAt() { try { return Number(localStorage.getItem(KEY_AT) || 0) || 0; } catch (e) { return 0; } } function MarkRun() { try { localStorage.setItem(KEY_AT, String(Date.now())); } catch (e) {} } // 串行限速加载;命中缓存的资源不会产生网络请求 function LoadList(List, Gap, Done) { var Index = 0, Ok = 0; function Next() { if (State.stopped) return; if (Index >= List.length) { if (Ok > 0) console.log('[预热] 完成 ' + Ok + '/' + List.length + ' 项'); if (Done) Done(); return; } var Url = List[Index++]; fetch(Url, { cache: 'force-cache' }) .then(function () { Ok++; }) .catch(function () {}) .then(function () { setTimeout(Next, Gap); }); } Next(); } // ① 车库/大厅 UI 图片:从 webpack 资源表枚举 function PreloadStatic() { fetch('/play/static/js/unpacked/resources.json', { cache: 'force-cache' }) .then(function (R) { return R.json(); }) .then(function (Json) { var Map = (Json && Json.resourceMap) || {}; var Seen = {}; var List = []; for (var K in Map) { if (!Object.prototype.hasOwnProperty.call(Map, K)) continue; var V = Map[K]; if (typeof V !== 'string' || Seen[V]) continue; Seen[V] = 1; List.push('/play/' + V); } console.log('[预热] 车库 UI 资源 ' + List.length + ' 项'); LoadList(List, GAP_STATIC, PreloadRes); }) .catch(function () { PreloadRes(); }); } // ② res.3dtank.com 资源:按本地缓存清单(最近访问优先) function PreloadRes() { fetch('/__prefetch_list__?n=600') .then(function (R) { return R.json(); }) .then(function (Json) { var List = (Json && Json.list) || []; if (!List.length) return; console.log('[预热] 战局/车库资源 ' + List.length + ' 项'); LoadList(List, GAP_RES, null); }) .catch(function () {}); } function Run() { if (State.stopped) return; if (Date.now() - LastRunAt() < 10 * 60 * 1000) return; // 10 分钟内只跑一次 MarkRun(); PreloadStatic(); } function _start() { if (State.running) return; State.running = true; State.stopped = false; try { if (localStorage.getItem(KEY_OFF) === '0') { console.log('[预热] 已被 localStorage.hornetPrefetch=0 关闭'); State.running = false; return; } } catch (e) {} console.log('===== 车库资源预热启动 ====='); // 等进入战局:__gameMode__ 由 hook_gamemode.js 捕获 var Wait = setInterval(function () { if (State.stopped) { clearInterval(Wait); return; } if (!window.__gameMode__) return; clearInterval(Wait); setTimeout(Run, 8000); }, 2000); State.timers.push(Wait); window.__hornetPrefetch__ = { running: true, stop: function () { State.stopped = true; for (var I = 0; I < State.timers.length; I++) clearInterval(State.timers[I]); console.log('[预热] 已停止'); } }; } _start(); })();