refactor(schema): 新增字段抽象层,透视滤除非本车节点并重做敌我判定
新增 hornet_schema.js 集中维护匿名引擎字段映射与导出键,支持 SelfCheck 自检失配;透视兜底扫描增加车体距离闸门,只保留跟着本车动的节点;敌我改为按队伍编号(k3_1)每帧比对,中立时沿用上次结果,取消一次判定永久锁定;穿坦克改用抽象字段名。
This commit is contained in:
1 parent
6e79057856
commit
972928e848
7 files changed
+1341
-269
No files matched your search
@@ -212,6 +212,258 @@
|
||||
})();
|
||||
|
||||
|
||||
// ===== 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 = {
|
||||
// ---- 战斗实体基类(fe / BattleEntity)----
|
||||
'Entity.Id': 'iz1_1', // 实体 ID(坦克/战场唯一)
|
||||
'Entity.Components': 'qz1_1.s2a_1', // Map<组件标识, 组件列表>,取值要 valueList.g1(0)
|
||||
|
||||
// ---- GameMode(ght,window.__gameMode__)----
|
||||
'GameMode.Scene': 'lz4()', // 场景实体
|
||||
'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.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 = 只探测不产生碰撞响应
|
||||
|
||||
// ---- 血量组件(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.Children': 'r35_1',
|
||||
'Node.Mesh': 'v3m_1',
|
||||
'Node.Lod': 'u4x_1',
|
||||
|
||||
// ---- 相机(场景实体 .jz1_1.iyx_1)----
|
||||
'Camera.SceneRef': 'jz1_1',
|
||||
'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'
|
||||
};
|
||||
|
||||
// ===== module_87072 的导出键($_.xx),每版都可能平移 =====
|
||||
// 队伍:vd = WIt(t) 取实体队伍;dl = R7(a,b) 同队且非中立
|
||||
// ⚠ 旧版是 kd / rk;新版 kd 已指向无关构造器
|
||||
var Exports = {
|
||||
'Export.TeamOf': ['vd', 'kd'],
|
||||
'Export.TeamSame': ['dl', 'rk']
|
||||
};
|
||||
|
||||
// ===== 路径解析 =====
|
||||
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;
|
||||
var V = Get(Target, Key);
|
||||
if (V === undefined) Out.Missing.push(Key + ' → ' + Fields[Key]);
|
||||
else Out.Ok.push(Key + ' → ' + Fields[Key]);
|
||||
});
|
||||
return Out;
|
||||
}
|
||||
|
||||
// 用当前战局对象做一次全量自检(控制台调用 __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 Targets = {
|
||||
Entity: Tank || Scene,
|
||||
GameMode: Gm,
|
||||
Tank: Tank,
|
||||
Physics: Tank ? FindPhysics(Tank) : null,
|
||||
Shape: null,
|
||||
Skin: Tank,
|
||||
Turret: Tank,
|
||||
Node: null,
|
||||
Camera: Scene
|
||||
};
|
||||
|
||||
Object.keys(Targets).forEach(function (Prefix) {
|
||||
if (!Targets[Prefix]) { Report.Groups[Prefix] = '无样本对象'; return; }
|
||||
var R = Check(Targets[Prefix], Prefix);
|
||||
Report.Groups[Prefix] = R;
|
||||
R.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)
|
||||
@@ -228,6 +480,18 @@
|
||||
// ===== 缓存队伍判断函数(ZCt/m2Fn) =====
|
||||
var ZCt = null, m2Fn = null;
|
||||
|
||||
// ===== 字段抽象层引用(字段名统一在 hornet_schema.js 里维护) =====
|
||||
// 取不到抽象层时退回本地常量,保证单独加载本文件也能工作
|
||||
var Schema = window.__hornetSchema__ || null;
|
||||
function FieldOf(Key, Fallback) {
|
||||
if (!Schema) return Fallback;
|
||||
var P = Schema.PathOf(Key);
|
||||
return (P && P.indexOf('.') < 0 && P.indexOf('(') < 0) ? P : Fallback;
|
||||
}
|
||||
var TEAM_FIELD = FieldOf('Team.Field', 'b135_1'); // 队伍组件上的队伍对象字段
|
||||
var TEAM_ID_FIELD = FieldOf('Team.Id', 'k3_1'); // 队伍编号:0/1/2(2=中立)
|
||||
var NEUTRAL_TEAM_ID = 2;
|
||||
|
||||
// 加载队伍判断函数(从 module_87072 导出)
|
||||
// 新旧差异(main.7cfa4058):
|
||||
// 取队伍 $_$.vd = WIt(t) → g$(HIt(), t).b135_1(坦克无队伍时返回中立 q$())
|
||||
@@ -471,80 +735,92 @@
|
||||
// 只在「本地坦克实体变了」(换局/换车)时才重新缓存。
|
||||
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, TEAM_FIELD);
|
||||
return Comp ? Comp[TEAM_FIELD] : null;
|
||||
}
|
||||
|
||||
// 读队伍编号:0 / 1 = 两个阵营,2 = 中立(q$());读不到返回 -1
|
||||
// ⚠ 用数值编号比较,而不是引擎 equals/R7:编号是稳定语义
|
||||
// (module_87072 里 P7()/$ct() 都按 k3_1 分派)
|
||||
function GetTeamId(Team) {
|
||||
if (!Team) return -1;
|
||||
var Id = Team[TEAM_ID_FIELD];
|
||||
return (typeof Id === 'number') ? Id : -1;
|
||||
}
|
||||
|
||||
function GetLocalTeamId() { return GetTeamId(LocalTeam); }
|
||||
function GetTankTeamId(tank) { return GetTeamId(ReadTeamRaw(tank)); }
|
||||
|
||||
function RememberLocalTeam(gm) {
|
||||
if (!ZCt || !m2Fn) LoadTeamFunctions();
|
||||
if (!ZCt) return;
|
||||
var t = null;
|
||||
try { t = gm.u10b_1; } catch (e) { t = null; }
|
||||
if (t) {
|
||||
var Id = t.iz1_1;
|
||||
if (LocalTeam && LocalTeamTankId === Id) return;
|
||||
try {
|
||||
var Team = ZCt(t);
|
||||
if (Team && (!m2Fn || m2Fn(Team, Team))) { LocalTeam = Team; LocalTeamTankId = Id; }
|
||||
} catch (e) {}
|
||||
var 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.v10b_1;
|
||||
if (G && (!m2Fn || m2Fn(G, G))) LocalTeam = G;
|
||||
var GId = GetTeamId(G);
|
||||
if (GId >= 0 && GId !== NEUTRAL_TEAM_ID) LocalTeam = G;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// ===== 判断是否友方 =====
|
||||
// 队伍同步有延迟:坦克刚出现时队伍是「中立」,直接比较会先判成敌人、同步后再变队友(颜色跳变)。
|
||||
// 处理:① 每辆坦克缓存最近一次**非中立**队伍;② 无法判定时沿用上一次判定结果。
|
||||
var TankTeamCache = new WeakMap(); // tank -> 非中立队伍对象
|
||||
// 规则:队伍编号相同且非中立 → 友方;中立/未下发 → 沿用该坦克上次判定(默认敌人)。
|
||||
// ⚠ 不再「一次判定永久锁定」:锁定会在基准短暂错误时把整局颜色固化错,
|
||||
// 这里改成每帧重算 + 中立抖动时沿用上次结果,既不会闪烁也不会锁死。
|
||||
var TankFriendCache = new WeakMap(); // tank -> 上一次的友方判定
|
||||
var TeamLockLogged = new WeakSet();
|
||||
function ResolveTankTeam(tank) {
|
||||
if (!ZCt) return null;
|
||||
var Team = null;
|
||||
try { Team = ZCt(tank); } catch (e) { Team = null; }
|
||||
if (Team && (!m2Fn || m2Fn(Team, Team))) { // 同队且非中立 = 有效队伍
|
||||
TankTeamCache.set(tank, Team);
|
||||
return Team;
|
||||
}
|
||||
return TankTeamCache.get(tank) || null;
|
||||
}
|
||||
var TeamJudgeLogged = new WeakSet();
|
||||
|
||||
function IsFriendly(tank, playerTank, gm) {
|
||||
if (!tank) return false;
|
||||
// 队伍函数可能因为工具早于游戏模块加载而没取到,这里按需补加载
|
||||
if (!ZCt || !m2Fn) LoadTeamFunctions();
|
||||
if (gm) RememberLocalTeam(gm);
|
||||
// 一旦在「基准队伍可靠」时判过,就锁定本局结果,战中不再改变
|
||||
var Locked = TankFriendCache.get(tank);
|
||||
if (Locked !== undefined) return Locked;
|
||||
if (ZCt && m2Fn) {
|
||||
try {
|
||||
var tankTeam = ResolveTankTeam(tank);
|
||||
// 可靠基准(用于锁定):本地拥有坦克的队伍 / GameMode 上的本地队伍字段
|
||||
var LockBase = LocalTeam;
|
||||
if (!LockBase && gm) { try { LockBase = gm.v10b_1; } catch (e) {} }
|
||||
// 观战目标只作临时参考,绝不用它锁定(它可能是敌人)
|
||||
var BaseTeam = LockBase || (playerTank ? ResolveTankTeam(playerTank) : null);
|
||||
if (tankTeam && BaseTeam) {
|
||||
var Friendly = m2Fn(tankTeam, BaseTeam);
|
||||
if (LockBase) {
|
||||
TankFriendCache.set(tank, Friendly);
|
||||
if (Friendly && !TeamLockLogged.has(tank)) {
|
||||
TeamLockLogged.add(tank);
|
||||
try {
|
||||
console.log('[敌我] 锁定为友方 id=' + tank.iz1_1 + ' name=' + (GetTankName(tank) || '?'));
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
return Friendly;
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
var TankId = GetTeamId(ReadTeamRaw(tank));
|
||||
if (TankId < 0 || TankId === NEUTRAL_TEAM_ID) {
|
||||
var Last = TankFriendCache.get(tank); // 队伍还没同步:保持上一帧颜色
|
||||
return Last === undefined ? false : Last;
|
||||
}
|
||||
// 基准还没就绪:先按敌人显示,但**不锁定**,等基准可用后再定
|
||||
return false;
|
||||
|
||||
// 可靠基准:本地拥有坦克的队伍(缓存)→ GameMode 上的本地队伍字段
|
||||
// 观战目标(t10b_1)可能是敌人,绝不作为基准
|
||||
var BaseTeam = LocalTeam;
|
||||
if (!BaseTeam && gm) { try { BaseTeam = gm.v10b_1; } 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.iz1_1 + ' 队伍=' + 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)
|
||||
@@ -831,6 +1107,10 @@
|
||||
IsBot: IsBot,
|
||||
GetPlayerTank: GetPlayerTank,
|
||||
IsFriendly: IsFriendly,
|
||||
ReadTeamRaw: ReadTeamRaw,
|
||||
GetTeamId: GetTeamId,
|
||||
GetTankTeamId: GetTankTeamId,
|
||||
GetLocalTeamId: GetLocalTeamId,
|
||||
GetCameraInfo: GetCameraInfo,
|
||||
GetUMtComponent: GetUMtComponent,
|
||||
MkVec: MkVec,
|
||||
@@ -1616,6 +1896,23 @@
|
||||
|
||||
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 Mode = 'outline'; // 'wireframe' | 'outline'
|
||||
try {
|
||||
@@ -1826,7 +2123,7 @@
|
||||
if (!entity) return null;
|
||||
var Cached = SkinCompCache.get(entity);
|
||||
if (Cached) return Cached;
|
||||
var comp = FindComponentByField(entity, 'g159_1');
|
||||
var comp = FindComponentByField(entity, FSkinHullSkin);
|
||||
if (!comp) comp = FindComponentByField(entity, 'a17q_1');
|
||||
// 兜底:按结构找 —— 组件字段值是"皮肤对象"(含 g17r_1 部件表)
|
||||
if (!comp) {
|
||||
@@ -1837,7 +2134,7 @@
|
||||
if (!C) return;
|
||||
for (var K in C) {
|
||||
var V = C[K];
|
||||
if (V && typeof V === 'object' && V.g17r_1 && typeof V.g17r_1.e1 === 'function') { comp = C; return; }
|
||||
if (V && typeof V === 'object' && V[FSkinParts] && typeof V[FSkinParts].e1 === 'function') { comp = C; return; }
|
||||
}
|
||||
});
|
||||
} catch (e) {}
|
||||
@@ -1850,7 +2147,7 @@
|
||||
function GetHullSkin(tank, stats) {
|
||||
var comp = GetSkinComponent(tank);
|
||||
if (!comp) { if (stats) stats.hullSource = '无皮肤组件'; return null; }
|
||||
if (comp.g159_1 && comp.g159_1.g17r_1) { if (stats) stats.hullSource = 'g159_1'; return comp.g159_1; }
|
||||
if (comp[FSkinHullSkin] && comp[FSkinHullSkin][FSkinParts]) { if (stats) stats.hullSource = 'g159_1'; return comp[FSkinHullSkin]; }
|
||||
if (stats) stats.hullSource = '有组件但无皮肤';
|
||||
return null;
|
||||
}
|
||||
@@ -2061,25 +2358,26 @@
|
||||
Seen.add(bpNode);
|
||||
}
|
||||
if (stats) stats.seen++;
|
||||
var name = bpNode.g35_1 || '';
|
||||
if (bpNode.v3m_1) {
|
||||
var name = bpNode[FNodeName] || '';
|
||||
if (bpNode[FNodeMesh]) {
|
||||
if (stats) stats.hasMesh++;
|
||||
var gpuData = GetGpuData(bpNode.v3m_1);
|
||||
var gpuData = GetGpuData(bpNode[FNodeMesh]);
|
||||
if (gpuData) {
|
||||
if (stats) stats.gpuOk++;
|
||||
nodes.push({
|
||||
name: name, node: bpNode, tp: bpNode.v3m_1,
|
||||
name: name, node: bpNode, tp: bpNode[FNodeMesh],
|
||||
positions: gpuData.positions, indices: gpuData.indices, stride: gpuData.stride, group: group
|
||||
});
|
||||
} else if (stats) stats.gpuFail++;
|
||||
}
|
||||
if (bpNode.u4x_1 && typeof bpNode.u4x_1.e1 === 'function') {
|
||||
var LodList = bpNode[FNodeLod];
|
||||
if (LodList && typeof LodList.e1 === 'function') {
|
||||
try {
|
||||
var lodCount = bpNode.u4x_1.e1();
|
||||
var lodCount = LodList.e1();
|
||||
for (var lod = 0; lod < lodCount && lod < 1; lod++) {
|
||||
var lodLevel = bpNode.u4x_1.g1(lod);
|
||||
var lodLevel = LodList.g1(lod);
|
||||
if (!lodLevel) continue;
|
||||
var lodTp = lodLevel.v3m_1 || lodLevel.v3m_1;
|
||||
var lodTp = lodLevel[FNodeMesh] || lodLevel.v3m_1;
|
||||
if (!lodTp) continue;
|
||||
var lodGpuData = GetGpuData(lodTp);
|
||||
if (lodGpuData) {
|
||||
@@ -2094,7 +2392,7 @@
|
||||
}
|
||||
// 子节点:新版是 r35_1(zl 基类:this.r35_1 = new lo(),r3k 里递归子节点)
|
||||
// 旧版是 p35_1;新版 p35_1 已变成"世界矩阵",绝不能当子节点列表
|
||||
var Children = (bpNode.r35_1 && typeof bpNode.r35_1.e1 === 'function') ? bpNode.r35_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) {
|
||||
@@ -2107,9 +2405,9 @@
|
||||
// 新版节点(zl 基类)特征:g35_1 = 名字字符串,p35_1 = 世界矩阵(长度≥16),r35_1 = 子节点列表
|
||||
function IsSceneNode(V) {
|
||||
return !!(V && typeof V === 'object' &&
|
||||
typeof V.g35_1 === 'string' &&
|
||||
V.p35_1 && typeof V.p35_1.length === 'number' && V.p35_1.length >= 16 &&
|
||||
V.r35_1 && typeof V.r35_1 === '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 会产出成千上万个键,必须跳过
|
||||
@@ -2121,6 +2419,43 @@
|
||||
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.n35_1; // 旧版矩阵字段
|
||||
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] }]
|
||||
@@ -2135,7 +2470,7 @@
|
||||
return V;
|
||||
}
|
||||
|
||||
function ScanMeshNodes(tank, nodes, stats, Seen) {
|
||||
function ScanMeshNodes(tank, nodes, stats, Seen, TankPos) {
|
||||
Seen = Seen || new WeakSet();
|
||||
// ① 有缓存路径:直接解析(开销极小);每 300 帧强制重扫一次,捡起后加载的部件
|
||||
ScanTick++;
|
||||
@@ -2146,6 +2481,7 @@
|
||||
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;
|
||||
@@ -2154,6 +2490,8 @@
|
||||
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 });
|
||||
}
|
||||
@@ -2208,25 +2546,25 @@
|
||||
// ===== 提取坦克的mesh数据 =====
|
||||
// 采集策略:**单一路径优先**,绝不把两条路径的结果混在一起(混在一起就是"复制身体")
|
||||
// ① 皮肤组件 h159_1(实际渲染树根)→ ② 部件表 → ③ 组件字段扫描(兜底)
|
||||
function GetTankMeshData(tank, stats) {
|
||||
function GetTankMeshData(tank, stats, TankPos) {
|
||||
var nodes = [];
|
||||
var Seen = new WeakSet();
|
||||
var skinComp = GetSkinComponent(tank);
|
||||
var skin = (skinComp && skinComp.g159_1 && skinComp.g159_1.g17r_1) ? skinComp.g159_1 : null;
|
||||
var skin = (skinComp && skinComp[FSkinHullSkin] && skinComp[FSkinHullSkin][FSkinParts]) ? skinComp[FSkinHullSkin] : null;
|
||||
if (stats) {
|
||||
stats.hullSource = skin ? 'g159_1' : (skinComp ? '有组件但无皮肤' : '无皮肤组件');
|
||||
stats.hullParts = (skin && typeof skin.g17r_1.e1 === 'function') ? skin.g17r_1.e1() : -1;
|
||||
stats.hullParts = (skin && typeof skin[FSkinParts].e1 === 'function') ? skin[FSkinParts].e1() : -1;
|
||||
}
|
||||
// ① 主路径:车身根节点(包含全部部件节点)
|
||||
if (skinComp && skinComp.h159_1) {
|
||||
try { CollectMeshNodes(skinComp.h159_1, nodes, 0, 'hull', stats ? stats.hull : null, Seen); } catch (e) {}
|
||||
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.g17r_1, function (part) {
|
||||
ForEachInList(skin[FSkinParts], function (part) {
|
||||
if (!part) return;
|
||||
var j = part.j17r_1; // 新版:单个 BP 节点;旧版:节点列表
|
||||
var j = part[FSkinPartNode]; // 新版:单个 BP 节点;旧版:节点列表
|
||||
if (!j) return;
|
||||
if (typeof j === 'object' && typeof j.e1 === 'function' && typeof j.g1 === 'function') {
|
||||
ForEachInList(j, function (bp) {
|
||||
@@ -2240,7 +2578,7 @@
|
||||
}
|
||||
// ③ 结构扫描:无条件补充漏采(车身根/部件表往往只覆盖一部分)
|
||||
// 重复实例(皮肤树/场景树同 tp 同名)由 DedupNodes 收掉,不会再出现"两个身体"
|
||||
try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {}
|
||||
try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen, TankPos); } catch (e) {}
|
||||
// ④ 炮塔(独立于车身根;重复实例由 DedupNodes 收掉)
|
||||
var turretNode = GetTurretNode(tank);
|
||||
if (stats) stats.turretComp = !!turretNode;
|
||||
@@ -2843,9 +3181,11 @@
|
||||
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 }
|
||||
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);
|
||||
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); // 兜底:至少可见
|
||||
@@ -5839,6 +6179,24 @@
|
||||
// 坦克 category 位(bit0)
|
||||
var TANK_CATEGORY_BIT = 1;
|
||||
|
||||
// ===== 字段抽象层(语义名 → 匿名字段,总表在 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 FPhysicsHull = Fld('Physics.Hull', 'zzq_1'); // 车体主碰撞形状
|
||||
var FShapeName = Fld('Shape.Name', 'x9j_1'); // 形状名('hull-main')
|
||||
var FShapeMask = Fld('Shape.Mask', 'z9j_1'); // 碰撞掩码
|
||||
var FBodyShapes = 'y9i_1'; // 刚体上的形状列表(pbk().y9i_1)
|
||||
var MAddMask = Mth('Physics.AddMask', 'n155');
|
||||
var MRemoveMask = Mth('Physics.RemoveMask', 'm155');
|
||||
var MGetBody = Mth('Physics.Body', 'pbk');
|
||||
|
||||
var Stat = {
|
||||
running: false,
|
||||
shapes: 0, // 本帧处理的碰撞形状数
|
||||
@@ -5853,10 +6211,10 @@
|
||||
// 特征:zzq_1 是车体主碰撞形状(x9j_1 === "hull-main"),且带 n155 / m155 / pbk
|
||||
function IsTankPhysics(Comp) {
|
||||
if (!Comp) return false;
|
||||
if (typeof Comp.n155 !== 'function' || typeof Comp.m155 !== 'function') return false;
|
||||
if (typeof Comp.pbk !== 'function') return false;
|
||||
var Hull = Comp.zzq_1;
|
||||
return !!(Hull && Hull.x9j_1 === 'hull-main');
|
||||
if (typeof Comp[MAddMask] !== 'function' || typeof Comp[MRemoveMask] !== 'function') return false;
|
||||
if (typeof Comp[MGetBody] !== 'function') return false;
|
||||
var Hull = Comp[FPhysicsHull];
|
||||
return !!(Hull && Hull[FShapeName] === 'hull-main');
|
||||
}
|
||||
|
||||
// 在实体上找坦克物理组件(不走 FindComponentByField:zzq_1 字段名在其它类里也有复用)
|
||||
@@ -5888,7 +6246,7 @@
|
||||
function ReleaseSnapshot() {
|
||||
MaskSnapshot.forEach(function (Mask, Shape) {
|
||||
try {
|
||||
if (Shape && typeof Mask === 'number') Shape.z9j_1 = Mask;
|
||||
if (Shape && typeof Mask === 'number') Shape[FShapeMask] = Mask;
|
||||
} catch (e) {}
|
||||
});
|
||||
MaskSnapshot.clear();
|
||||
@@ -5911,8 +6269,8 @@
|
||||
}
|
||||
|
||||
var Body = null;
|
||||
try { Body = Phys.pbk(); } catch (e) { Body = null; }
|
||||
var Shapes = Body && Body.y9i_1;
|
||||
try { Body = Phys[MGetBody](); } catch (e) { Body = null; }
|
||||
var Shapes = Body && Body[FBodyShapes];
|
||||
if (!Shapes || typeof Shapes.e1 !== 'function' || typeof Shapes.g1 !== 'function') return;
|
||||
|
||||
var Count = Shapes.e1() | 0;
|
||||
@@ -5920,10 +6278,10 @@
|
||||
for (var i = 0; i < Count; i++) {
|
||||
var Shape = null;
|
||||
try { Shape = Shapes.g1(i); } catch (e) { Shape = null; }
|
||||
if (!Shape || typeof Shape.z9j_1 !== 'number') continue;
|
||||
if (!Shape || typeof Shape[FShapeMask] !== 'number') continue;
|
||||
// 首次见到该形状时记录原始 mask,保证 Stop 能精确还原
|
||||
if (!MaskSnapshot.has(Shape)) MaskSnapshot.set(Shape, Shape.z9j_1);
|
||||
Shape.z9j_1 = Shape.z9j_1 & ~TANK_CATEGORY_BIT;
|
||||
if (!MaskSnapshot.has(Shape)) MaskSnapshot.set(Shape, Shape[FShapeMask]);
|
||||
Shape[FShapeMask] = Shape[FShapeMask] & ~TANK_CATEGORY_BIT;
|
||||
Touched++;
|
||||
}
|
||||
Stat.shapes = Touched;
|
||||
|
||||
+444
-86
@@ -212,6 +212,258 @@
|
||||
})();
|
||||
|
||||
|
||||
// ===== 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 = {
|
||||
// ---- 战斗实体基类(fe / BattleEntity)----
|
||||
'Entity.Id': 'iz1_1', // 实体 ID(坦克/战场唯一)
|
||||
'Entity.Components': 'qz1_1.s2a_1', // Map<组件标识, 组件列表>,取值要 valueList.g1(0)
|
||||
|
||||
// ---- GameMode(ght,window.__gameMode__)----
|
||||
'GameMode.Scene': 'lz4()', // 场景实体
|
||||
'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.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 = 只探测不产生碰撞响应
|
||||
|
||||
// ---- 血量组件(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.Children': 'r35_1',
|
||||
'Node.Mesh': 'v3m_1',
|
||||
'Node.Lod': 'u4x_1',
|
||||
|
||||
// ---- 相机(场景实体 .jz1_1.iyx_1)----
|
||||
'Camera.SceneRef': 'jz1_1',
|
||||
'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'
|
||||
};
|
||||
|
||||
// ===== module_87072 的导出键($_.xx),每版都可能平移 =====
|
||||
// 队伍:vd = WIt(t) 取实体队伍;dl = R7(a,b) 同队且非中立
|
||||
// ⚠ 旧版是 kd / rk;新版 kd 已指向无关构造器
|
||||
var Exports = {
|
||||
'Export.TeamOf': ['vd', 'kd'],
|
||||
'Export.TeamSame': ['dl', 'rk']
|
||||
};
|
||||
|
||||
// ===== 路径解析 =====
|
||||
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;
|
||||
var V = Get(Target, Key);
|
||||
if (V === undefined) Out.Missing.push(Key + ' → ' + Fields[Key]);
|
||||
else Out.Ok.push(Key + ' → ' + Fields[Key]);
|
||||
});
|
||||
return Out;
|
||||
}
|
||||
|
||||
// 用当前战局对象做一次全量自检(控制台调用 __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 Targets = {
|
||||
Entity: Tank || Scene,
|
||||
GameMode: Gm,
|
||||
Tank: Tank,
|
||||
Physics: Tank ? FindPhysics(Tank) : null,
|
||||
Shape: null,
|
||||
Skin: Tank,
|
||||
Turret: Tank,
|
||||
Node: null,
|
||||
Camera: Scene
|
||||
};
|
||||
|
||||
Object.keys(Targets).forEach(function (Prefix) {
|
||||
if (!Targets[Prefix]) { Report.Groups[Prefix] = '无样本对象'; return; }
|
||||
var R = Check(Targets[Prefix], Prefix);
|
||||
Report.Groups[Prefix] = R;
|
||||
R.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)
|
||||
@@ -228,6 +480,18 @@
|
||||
// ===== 缓存队伍判断函数(ZCt/m2Fn) =====
|
||||
var ZCt = null, m2Fn = null;
|
||||
|
||||
// ===== 字段抽象层引用(字段名统一在 hornet_schema.js 里维护) =====
|
||||
// 取不到抽象层时退回本地常量,保证单独加载本文件也能工作
|
||||
var Schema = window.__hornetSchema__ || null;
|
||||
function FieldOf(Key, Fallback) {
|
||||
if (!Schema) return Fallback;
|
||||
var P = Schema.PathOf(Key);
|
||||
return (P && P.indexOf('.') < 0 && P.indexOf('(') < 0) ? P : Fallback;
|
||||
}
|
||||
var TEAM_FIELD = FieldOf('Team.Field', 'b135_1'); // 队伍组件上的队伍对象字段
|
||||
var TEAM_ID_FIELD = FieldOf('Team.Id', 'k3_1'); // 队伍编号:0/1/2(2=中立)
|
||||
var NEUTRAL_TEAM_ID = 2;
|
||||
|
||||
// 加载队伍判断函数(从 module_87072 导出)
|
||||
// 新旧差异(main.7cfa4058):
|
||||
// 取队伍 $_$.vd = WIt(t) → g$(HIt(), t).b135_1(坦克无队伍时返回中立 q$())
|
||||
@@ -471,80 +735,92 @@
|
||||
// 只在「本地坦克实体变了」(换局/换车)时才重新缓存。
|
||||
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, TEAM_FIELD);
|
||||
return Comp ? Comp[TEAM_FIELD] : null;
|
||||
}
|
||||
|
||||
// 读队伍编号:0 / 1 = 两个阵营,2 = 中立(q$());读不到返回 -1
|
||||
// ⚠ 用数值编号比较,而不是引擎 equals/R7:编号是稳定语义
|
||||
// (module_87072 里 P7()/$ct() 都按 k3_1 分派)
|
||||
function GetTeamId(Team) {
|
||||
if (!Team) return -1;
|
||||
var Id = Team[TEAM_ID_FIELD];
|
||||
return (typeof Id === 'number') ? Id : -1;
|
||||
}
|
||||
|
||||
function GetLocalTeamId() { return GetTeamId(LocalTeam); }
|
||||
function GetTankTeamId(tank) { return GetTeamId(ReadTeamRaw(tank)); }
|
||||
|
||||
function RememberLocalTeam(gm) {
|
||||
if (!ZCt || !m2Fn) LoadTeamFunctions();
|
||||
if (!ZCt) return;
|
||||
var t = null;
|
||||
try { t = gm.u10b_1; } catch (e) { t = null; }
|
||||
if (t) {
|
||||
var Id = t.iz1_1;
|
||||
if (LocalTeam && LocalTeamTankId === Id) return;
|
||||
try {
|
||||
var Team = ZCt(t);
|
||||
if (Team && (!m2Fn || m2Fn(Team, Team))) { LocalTeam = Team; LocalTeamTankId = Id; }
|
||||
} catch (e) {}
|
||||
var 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.v10b_1;
|
||||
if (G && (!m2Fn || m2Fn(G, G))) LocalTeam = G;
|
||||
var GId = GetTeamId(G);
|
||||
if (GId >= 0 && GId !== NEUTRAL_TEAM_ID) LocalTeam = G;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// ===== 判断是否友方 =====
|
||||
// 队伍同步有延迟:坦克刚出现时队伍是「中立」,直接比较会先判成敌人、同步后再变队友(颜色跳变)。
|
||||
// 处理:① 每辆坦克缓存最近一次**非中立**队伍;② 无法判定时沿用上一次判定结果。
|
||||
var TankTeamCache = new WeakMap(); // tank -> 非中立队伍对象
|
||||
// 规则:队伍编号相同且非中立 → 友方;中立/未下发 → 沿用该坦克上次判定(默认敌人)。
|
||||
// ⚠ 不再「一次判定永久锁定」:锁定会在基准短暂错误时把整局颜色固化错,
|
||||
// 这里改成每帧重算 + 中立抖动时沿用上次结果,既不会闪烁也不会锁死。
|
||||
var TankFriendCache = new WeakMap(); // tank -> 上一次的友方判定
|
||||
var TeamLockLogged = new WeakSet();
|
||||
function ResolveTankTeam(tank) {
|
||||
if (!ZCt) return null;
|
||||
var Team = null;
|
||||
try { Team = ZCt(tank); } catch (e) { Team = null; }
|
||||
if (Team && (!m2Fn || m2Fn(Team, Team))) { // 同队且非中立 = 有效队伍
|
||||
TankTeamCache.set(tank, Team);
|
||||
return Team;
|
||||
}
|
||||
return TankTeamCache.get(tank) || null;
|
||||
}
|
||||
var TeamJudgeLogged = new WeakSet();
|
||||
|
||||
function IsFriendly(tank, playerTank, gm) {
|
||||
if (!tank) return false;
|
||||
// 队伍函数可能因为工具早于游戏模块加载而没取到,这里按需补加载
|
||||
if (!ZCt || !m2Fn) LoadTeamFunctions();
|
||||
if (gm) RememberLocalTeam(gm);
|
||||
// 一旦在「基准队伍可靠」时判过,就锁定本局结果,战中不再改变
|
||||
var Locked = TankFriendCache.get(tank);
|
||||
if (Locked !== undefined) return Locked;
|
||||
if (ZCt && m2Fn) {
|
||||
try {
|
||||
var tankTeam = ResolveTankTeam(tank);
|
||||
// 可靠基准(用于锁定):本地拥有坦克的队伍 / GameMode 上的本地队伍字段
|
||||
var LockBase = LocalTeam;
|
||||
if (!LockBase && gm) { try { LockBase = gm.v10b_1; } catch (e) {} }
|
||||
// 观战目标只作临时参考,绝不用它锁定(它可能是敌人)
|
||||
var BaseTeam = LockBase || (playerTank ? ResolveTankTeam(playerTank) : null);
|
||||
if (tankTeam && BaseTeam) {
|
||||
var Friendly = m2Fn(tankTeam, BaseTeam);
|
||||
if (LockBase) {
|
||||
TankFriendCache.set(tank, Friendly);
|
||||
if (Friendly && !TeamLockLogged.has(tank)) {
|
||||
TeamLockLogged.add(tank);
|
||||
try {
|
||||
console.log('[敌我] 锁定为友方 id=' + tank.iz1_1 + ' name=' + (GetTankName(tank) || '?'));
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
return Friendly;
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
var TankId = GetTeamId(ReadTeamRaw(tank));
|
||||
if (TankId < 0 || TankId === NEUTRAL_TEAM_ID) {
|
||||
var Last = TankFriendCache.get(tank); // 队伍还没同步:保持上一帧颜色
|
||||
return Last === undefined ? false : Last;
|
||||
}
|
||||
// 基准还没就绪:先按敌人显示,但**不锁定**,等基准可用后再定
|
||||
return false;
|
||||
|
||||
// 可靠基准:本地拥有坦克的队伍(缓存)→ GameMode 上的本地队伍字段
|
||||
// 观战目标(t10b_1)可能是敌人,绝不作为基准
|
||||
var BaseTeam = LocalTeam;
|
||||
if (!BaseTeam && gm) { try { BaseTeam = gm.v10b_1; } 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.iz1_1 + ' 队伍=' + 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)
|
||||
@@ -831,6 +1107,10 @@
|
||||
IsBot: IsBot,
|
||||
GetPlayerTank: GetPlayerTank,
|
||||
IsFriendly: IsFriendly,
|
||||
ReadTeamRaw: ReadTeamRaw,
|
||||
GetTeamId: GetTeamId,
|
||||
GetTankTeamId: GetTankTeamId,
|
||||
GetLocalTeamId: GetLocalTeamId,
|
||||
GetCameraInfo: GetCameraInfo,
|
||||
GetUMtComponent: GetUMtComponent,
|
||||
MkVec: MkVec,
|
||||
@@ -1616,6 +1896,23 @@
|
||||
|
||||
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 Mode = 'outline'; // 'wireframe' | 'outline'
|
||||
try {
|
||||
@@ -1826,7 +2123,7 @@
|
||||
if (!entity) return null;
|
||||
var Cached = SkinCompCache.get(entity);
|
||||
if (Cached) return Cached;
|
||||
var comp = FindComponentByField(entity, 'g159_1');
|
||||
var comp = FindComponentByField(entity, FSkinHullSkin);
|
||||
if (!comp) comp = FindComponentByField(entity, 'a17q_1');
|
||||
// 兜底:按结构找 —— 组件字段值是"皮肤对象"(含 g17r_1 部件表)
|
||||
if (!comp) {
|
||||
@@ -1837,7 +2134,7 @@
|
||||
if (!C) return;
|
||||
for (var K in C) {
|
||||
var V = C[K];
|
||||
if (V && typeof V === 'object' && V.g17r_1 && typeof V.g17r_1.e1 === 'function') { comp = C; return; }
|
||||
if (V && typeof V === 'object' && V[FSkinParts] && typeof V[FSkinParts].e1 === 'function') { comp = C; return; }
|
||||
}
|
||||
});
|
||||
} catch (e) {}
|
||||
@@ -1850,7 +2147,7 @@
|
||||
function GetHullSkin(tank, stats) {
|
||||
var comp = GetSkinComponent(tank);
|
||||
if (!comp) { if (stats) stats.hullSource = '无皮肤组件'; return null; }
|
||||
if (comp.g159_1 && comp.g159_1.g17r_1) { if (stats) stats.hullSource = 'g159_1'; return comp.g159_1; }
|
||||
if (comp[FSkinHullSkin] && comp[FSkinHullSkin][FSkinParts]) { if (stats) stats.hullSource = 'g159_1'; return comp[FSkinHullSkin]; }
|
||||
if (stats) stats.hullSource = '有组件但无皮肤';
|
||||
return null;
|
||||
}
|
||||
@@ -2061,25 +2358,26 @@
|
||||
Seen.add(bpNode);
|
||||
}
|
||||
if (stats) stats.seen++;
|
||||
var name = bpNode.g35_1 || '';
|
||||
if (bpNode.v3m_1) {
|
||||
var name = bpNode[FNodeName] || '';
|
||||
if (bpNode[FNodeMesh]) {
|
||||
if (stats) stats.hasMesh++;
|
||||
var gpuData = GetGpuData(bpNode.v3m_1);
|
||||
var gpuData = GetGpuData(bpNode[FNodeMesh]);
|
||||
if (gpuData) {
|
||||
if (stats) stats.gpuOk++;
|
||||
nodes.push({
|
||||
name: name, node: bpNode, tp: bpNode.v3m_1,
|
||||
name: name, node: bpNode, tp: bpNode[FNodeMesh],
|
||||
positions: gpuData.positions, indices: gpuData.indices, stride: gpuData.stride, group: group
|
||||
});
|
||||
} else if (stats) stats.gpuFail++;
|
||||
}
|
||||
if (bpNode.u4x_1 && typeof bpNode.u4x_1.e1 === 'function') {
|
||||
var LodList = bpNode[FNodeLod];
|
||||
if (LodList && typeof LodList.e1 === 'function') {
|
||||
try {
|
||||
var lodCount = bpNode.u4x_1.e1();
|
||||
var lodCount = LodList.e1();
|
||||
for (var lod = 0; lod < lodCount && lod < 1; lod++) {
|
||||
var lodLevel = bpNode.u4x_1.g1(lod);
|
||||
var lodLevel = LodList.g1(lod);
|
||||
if (!lodLevel) continue;
|
||||
var lodTp = lodLevel.v3m_1 || lodLevel.v3m_1;
|
||||
var lodTp = lodLevel[FNodeMesh] || lodLevel.v3m_1;
|
||||
if (!lodTp) continue;
|
||||
var lodGpuData = GetGpuData(lodTp);
|
||||
if (lodGpuData) {
|
||||
@@ -2094,7 +2392,7 @@
|
||||
}
|
||||
// 子节点:新版是 r35_1(zl 基类:this.r35_1 = new lo(),r3k 里递归子节点)
|
||||
// 旧版是 p35_1;新版 p35_1 已变成"世界矩阵",绝不能当子节点列表
|
||||
var Children = (bpNode.r35_1 && typeof bpNode.r35_1.e1 === 'function') ? bpNode.r35_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) {
|
||||
@@ -2107,9 +2405,9 @@
|
||||
// 新版节点(zl 基类)特征:g35_1 = 名字字符串,p35_1 = 世界矩阵(长度≥16),r35_1 = 子节点列表
|
||||
function IsSceneNode(V) {
|
||||
return !!(V && typeof V === 'object' &&
|
||||
typeof V.g35_1 === 'string' &&
|
||||
V.p35_1 && typeof V.p35_1.length === 'number' && V.p35_1.length >= 16 &&
|
||||
V.r35_1 && typeof V.r35_1 === '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 会产出成千上万个键,必须跳过
|
||||
@@ -2121,6 +2419,43 @@
|
||||
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.n35_1; // 旧版矩阵字段
|
||||
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] }]
|
||||
@@ -2135,7 +2470,7 @@
|
||||
return V;
|
||||
}
|
||||
|
||||
function ScanMeshNodes(tank, nodes, stats, Seen) {
|
||||
function ScanMeshNodes(tank, nodes, stats, Seen, TankPos) {
|
||||
Seen = Seen || new WeakSet();
|
||||
// ① 有缓存路径:直接解析(开销极小);每 300 帧强制重扫一次,捡起后加载的部件
|
||||
ScanTick++;
|
||||
@@ -2146,6 +2481,7 @@
|
||||
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;
|
||||
@@ -2154,6 +2490,8 @@
|
||||
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 });
|
||||
}
|
||||
@@ -2208,25 +2546,25 @@
|
||||
// ===== 提取坦克的mesh数据 =====
|
||||
// 采集策略:**单一路径优先**,绝不把两条路径的结果混在一起(混在一起就是"复制身体")
|
||||
// ① 皮肤组件 h159_1(实际渲染树根)→ ② 部件表 → ③ 组件字段扫描(兜底)
|
||||
function GetTankMeshData(tank, stats) {
|
||||
function GetTankMeshData(tank, stats, TankPos) {
|
||||
var nodes = [];
|
||||
var Seen = new WeakSet();
|
||||
var skinComp = GetSkinComponent(tank);
|
||||
var skin = (skinComp && skinComp.g159_1 && skinComp.g159_1.g17r_1) ? skinComp.g159_1 : null;
|
||||
var skin = (skinComp && skinComp[FSkinHullSkin] && skinComp[FSkinHullSkin][FSkinParts]) ? skinComp[FSkinHullSkin] : null;
|
||||
if (stats) {
|
||||
stats.hullSource = skin ? 'g159_1' : (skinComp ? '有组件但无皮肤' : '无皮肤组件');
|
||||
stats.hullParts = (skin && typeof skin.g17r_1.e1 === 'function') ? skin.g17r_1.e1() : -1;
|
||||
stats.hullParts = (skin && typeof skin[FSkinParts].e1 === 'function') ? skin[FSkinParts].e1() : -1;
|
||||
}
|
||||
// ① 主路径:车身根节点(包含全部部件节点)
|
||||
if (skinComp && skinComp.h159_1) {
|
||||
try { CollectMeshNodes(skinComp.h159_1, nodes, 0, 'hull', stats ? stats.hull : null, Seen); } catch (e) {}
|
||||
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.g17r_1, function (part) {
|
||||
ForEachInList(skin[FSkinParts], function (part) {
|
||||
if (!part) return;
|
||||
var j = part.j17r_1; // 新版:单个 BP 节点;旧版:节点列表
|
||||
var j = part[FSkinPartNode]; // 新版:单个 BP 节点;旧版:节点列表
|
||||
if (!j) return;
|
||||
if (typeof j === 'object' && typeof j.e1 === 'function' && typeof j.g1 === 'function') {
|
||||
ForEachInList(j, function (bp) {
|
||||
@@ -2240,7 +2578,7 @@
|
||||
}
|
||||
// ③ 结构扫描:无条件补充漏采(车身根/部件表往往只覆盖一部分)
|
||||
// 重复实例(皮肤树/场景树同 tp 同名)由 DedupNodes 收掉,不会再出现"两个身体"
|
||||
try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {}
|
||||
try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen, TankPos); } catch (e) {}
|
||||
// ④ 炮塔(独立于车身根;重复实例由 DedupNodes 收掉)
|
||||
var turretNode = GetTurretNode(tank);
|
||||
if (stats) stats.turretComp = !!turretNode;
|
||||
@@ -2843,9 +3181,11 @@
|
||||
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 }
|
||||
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);
|
||||
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); // 兜底:至少可见
|
||||
@@ -5839,6 +6179,24 @@
|
||||
// 坦克 category 位(bit0)
|
||||
var TANK_CATEGORY_BIT = 1;
|
||||
|
||||
// ===== 字段抽象层(语义名 → 匿名字段,总表在 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 FPhysicsHull = Fld('Physics.Hull', 'zzq_1'); // 车体主碰撞形状
|
||||
var FShapeName = Fld('Shape.Name', 'x9j_1'); // 形状名('hull-main')
|
||||
var FShapeMask = Fld('Shape.Mask', 'z9j_1'); // 碰撞掩码
|
||||
var FBodyShapes = 'y9i_1'; // 刚体上的形状列表(pbk().y9i_1)
|
||||
var MAddMask = Mth('Physics.AddMask', 'n155');
|
||||
var MRemoveMask = Mth('Physics.RemoveMask', 'm155');
|
||||
var MGetBody = Mth('Physics.Body', 'pbk');
|
||||
|
||||
var Stat = {
|
||||
running: false,
|
||||
shapes: 0, // 本帧处理的碰撞形状数
|
||||
@@ -5853,10 +6211,10 @@
|
||||
// 特征:zzq_1 是车体主碰撞形状(x9j_1 === "hull-main"),且带 n155 / m155 / pbk
|
||||
function IsTankPhysics(Comp) {
|
||||
if (!Comp) return false;
|
||||
if (typeof Comp.n155 !== 'function' || typeof Comp.m155 !== 'function') return false;
|
||||
if (typeof Comp.pbk !== 'function') return false;
|
||||
var Hull = Comp.zzq_1;
|
||||
return !!(Hull && Hull.x9j_1 === 'hull-main');
|
||||
if (typeof Comp[MAddMask] !== 'function' || typeof Comp[MRemoveMask] !== 'function') return false;
|
||||
if (typeof Comp[MGetBody] !== 'function') return false;
|
||||
var Hull = Comp[FPhysicsHull];
|
||||
return !!(Hull && Hull[FShapeName] === 'hull-main');
|
||||
}
|
||||
|
||||
// 在实体上找坦克物理组件(不走 FindComponentByField:zzq_1 字段名在其它类里也有复用)
|
||||
@@ -5888,7 +6246,7 @@
|
||||
function ReleaseSnapshot() {
|
||||
MaskSnapshot.forEach(function (Mask, Shape) {
|
||||
try {
|
||||
if (Shape && typeof Mask === 'number') Shape.z9j_1 = Mask;
|
||||
if (Shape && typeof Mask === 'number') Shape[FShapeMask] = Mask;
|
||||
} catch (e) {}
|
||||
});
|
||||
MaskSnapshot.clear();
|
||||
@@ -5911,8 +6269,8 @@
|
||||
}
|
||||
|
||||
var Body = null;
|
||||
try { Body = Phys.pbk(); } catch (e) { Body = null; }
|
||||
var Shapes = Body && Body.y9i_1;
|
||||
try { Body = Phys[MGetBody](); } catch (e) { Body = null; }
|
||||
var Shapes = Body && Body[FBodyShapes];
|
||||
if (!Shapes || typeof Shapes.e1 !== 'function' || typeof Shapes.g1 !== 'function') return;
|
||||
|
||||
var Count = Shapes.e1() | 0;
|
||||
@@ -5920,10 +6278,10 @@
|
||||
for (var i = 0; i < Count; i++) {
|
||||
var Shape = null;
|
||||
try { Shape = Shapes.g1(i); } catch (e) { Shape = null; }
|
||||
if (!Shape || typeof Shape.z9j_1 !== 'number') continue;
|
||||
if (!Shape || typeof Shape[FShapeMask] !== 'number') continue;
|
||||
// 首次见到该形状时记录原始 mask,保证 Stop 能精确还原
|
||||
if (!MaskSnapshot.has(Shape)) MaskSnapshot.set(Shape, Shape.z9j_1);
|
||||
Shape.z9j_1 = Shape.z9j_1 & ~TANK_CATEGORY_BIT;
|
||||
if (!MaskSnapshot.has(Shape)) MaskSnapshot.set(Shape, Shape[FShapeMask]);
|
||||
Shape[FShapeMask] = Shape[FShapeMask] & ~TANK_CATEGORY_BIT;
|
||||
Touched++;
|
||||
}
|
||||
Stat.shapes = Touched;
|
||||
|
||||
+12
-11
@@ -16,17 +16,18 @@ UNPACKED_OUTPUT = os.path.join(UNPACKED_DIR, "hornet_bundle.js")
|
||||
# 按依赖顺序加载
|
||||
FILES = [
|
||||
"hook_gamemode.js", # 1. Hook 捕获 GameMode(异步)
|
||||
"hornet_lib.js", # 2. 公共库(软警告,不阻塞加载)
|
||||
"hornet_radar.js", # 3. 雷达模块 -> __hornetTools__.Radar
|
||||
"hornet_3d_wireframe.js",# 4. 3D透视模块 -> __hornetTools__.Wireframe
|
||||
"hornet_rearview.js", # 5. 后视镜模块 -> __hornetTools__.Rearview
|
||||
"hornet_aim_warning.js", # 6. 瞄准警告模块 -> __hornetTools__.AimWarning
|
||||
"hornet_freelook.js", # 7. 自由视角模块 -> __hornetTools__.FreeLook
|
||||
"hornet_warsandbox.js", # 8. 战争沙盘模块 -> __hornetTools__.WarSandbox
|
||||
"hornet_mouse.js", # 9. 鼠标输入稳定器 -> __hornetTools__.Mouse
|
||||
"hornet_noclip.js", # 10. 穿坦克(忽略玩家碰撞)-> __hornetTools__.NoClip
|
||||
"hornet_panel.js", # 11. 控制面板(Alt+T + HUD)
|
||||
"hornet_prefetch.js", # 12. 车库资源预热(战局空闲时后台加载车库所需资源)
|
||||
"hornet_schema.js", # 2. 字段抽象层(匿名引擎字段 → 语义名,换版本只改这里)
|
||||
"hornet_lib.js", # 3. 公共库(软警告,不阻塞加载)
|
||||
"hornet_radar.js", # 4. 雷达模块 -> __hornetTools__.Radar
|
||||
"hornet_3d_wireframe.js",# 5. 3D透视模块 -> __hornetTools__.Wireframe
|
||||
"hornet_rearview.js", # 6. 后视镜模块 -> __hornetTools__.Rearview
|
||||
"hornet_aim_warning.js", # 7. 瞄准警告模块 -> __hornetTools__.AimWarning
|
||||
"hornet_freelook.js", # 8. 自由视角模块 -> __hornetTools__.FreeLook
|
||||
"hornet_warsandbox.js", # 9. 战争沙盘模块 -> __hornetTools__.WarSandbox
|
||||
"hornet_mouse.js", # 10. 鼠标输入稳定器 -> __hornetTools__.Mouse
|
||||
"hornet_noclip.js", # 11. 穿坦克(忽略玩家碰撞)-> __hornetTools__.NoClip
|
||||
"hornet_panel.js", # 12. 控制面板(Alt+T + HUD)
|
||||
"hornet_prefetch.js", # 13. 车库资源预热(战局空闲时后台加载车库所需资源)
|
||||
]
|
||||
|
||||
HEADER = """// ============================================================
|
||||
|
||||
@@ -22,6 +22,23 @@
|
||||
|
||||
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 Mode = 'outline'; // 'wireframe' | 'outline'
|
||||
try {
|
||||
@@ -232,7 +249,7 @@
|
||||
if (!entity) return null;
|
||||
var Cached = SkinCompCache.get(entity);
|
||||
if (Cached) return Cached;
|
||||
var comp = FindComponentByField(entity, 'g159_1');
|
||||
var comp = FindComponentByField(entity, FSkinHullSkin);
|
||||
if (!comp) comp = FindComponentByField(entity, 'a17q_1');
|
||||
// 兜底:按结构找 —— 组件字段值是"皮肤对象"(含 g17r_1 部件表)
|
||||
if (!comp) {
|
||||
@@ -243,7 +260,7 @@
|
||||
if (!C) return;
|
||||
for (var K in C) {
|
||||
var V = C[K];
|
||||
if (V && typeof V === 'object' && V.g17r_1 && typeof V.g17r_1.e1 === 'function') { comp = C; return; }
|
||||
if (V && typeof V === 'object' && V[FSkinParts] && typeof V[FSkinParts].e1 === 'function') { comp = C; return; }
|
||||
}
|
||||
});
|
||||
} catch (e) {}
|
||||
@@ -256,7 +273,7 @@
|
||||
function GetHullSkin(tank, stats) {
|
||||
var comp = GetSkinComponent(tank);
|
||||
if (!comp) { if (stats) stats.hullSource = '无皮肤组件'; return null; }
|
||||
if (comp.g159_1 && comp.g159_1.g17r_1) { if (stats) stats.hullSource = 'g159_1'; return comp.g159_1; }
|
||||
if (comp[FSkinHullSkin] && comp[FSkinHullSkin][FSkinParts]) { if (stats) stats.hullSource = 'g159_1'; return comp[FSkinHullSkin]; }
|
||||
if (stats) stats.hullSource = '有组件但无皮肤';
|
||||
return null;
|
||||
}
|
||||
@@ -467,25 +484,26 @@
|
||||
Seen.add(bpNode);
|
||||
}
|
||||
if (stats) stats.seen++;
|
||||
var name = bpNode.g35_1 || '';
|
||||
if (bpNode.v3m_1) {
|
||||
var name = bpNode[FNodeName] || '';
|
||||
if (bpNode[FNodeMesh]) {
|
||||
if (stats) stats.hasMesh++;
|
||||
var gpuData = GetGpuData(bpNode.v3m_1);
|
||||
var gpuData = GetGpuData(bpNode[FNodeMesh]);
|
||||
if (gpuData) {
|
||||
if (stats) stats.gpuOk++;
|
||||
nodes.push({
|
||||
name: name, node: bpNode, tp: bpNode.v3m_1,
|
||||
name: name, node: bpNode, tp: bpNode[FNodeMesh],
|
||||
positions: gpuData.positions, indices: gpuData.indices, stride: gpuData.stride, group: group
|
||||
});
|
||||
} else if (stats) stats.gpuFail++;
|
||||
}
|
||||
if (bpNode.u4x_1 && typeof bpNode.u4x_1.e1 === 'function') {
|
||||
var LodList = bpNode[FNodeLod];
|
||||
if (LodList && typeof LodList.e1 === 'function') {
|
||||
try {
|
||||
var lodCount = bpNode.u4x_1.e1();
|
||||
var lodCount = LodList.e1();
|
||||
for (var lod = 0; lod < lodCount && lod < 1; lod++) {
|
||||
var lodLevel = bpNode.u4x_1.g1(lod);
|
||||
var lodLevel = LodList.g1(lod);
|
||||
if (!lodLevel) continue;
|
||||
var lodTp = lodLevel.v3m_1 || lodLevel.v3m_1;
|
||||
var lodTp = lodLevel[FNodeMesh] || lodLevel.v3m_1;
|
||||
if (!lodTp) continue;
|
||||
var lodGpuData = GetGpuData(lodTp);
|
||||
if (lodGpuData) {
|
||||
@@ -500,7 +518,7 @@
|
||||
}
|
||||
// 子节点:新版是 r35_1(zl 基类:this.r35_1 = new lo(),r3k 里递归子节点)
|
||||
// 旧版是 p35_1;新版 p35_1 已变成"世界矩阵",绝不能当子节点列表
|
||||
var Children = (bpNode.r35_1 && typeof bpNode.r35_1.e1 === 'function') ? bpNode.r35_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) {
|
||||
@@ -513,9 +531,9 @@
|
||||
// 新版节点(zl 基类)特征:g35_1 = 名字字符串,p35_1 = 世界矩阵(长度≥16),r35_1 = 子节点列表
|
||||
function IsSceneNode(V) {
|
||||
return !!(V && typeof V === 'object' &&
|
||||
typeof V.g35_1 === 'string' &&
|
||||
V.p35_1 && typeof V.p35_1.length === 'number' && V.p35_1.length >= 16 &&
|
||||
V.r35_1 && typeof V.r35_1 === '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 会产出成千上万个键,必须跳过
|
||||
@@ -527,6 +545,43 @@
|
||||
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.n35_1; // 旧版矩阵字段
|
||||
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] }]
|
||||
@@ -541,7 +596,7 @@
|
||||
return V;
|
||||
}
|
||||
|
||||
function ScanMeshNodes(tank, nodes, stats, Seen) {
|
||||
function ScanMeshNodes(tank, nodes, stats, Seen, TankPos) {
|
||||
Seen = Seen || new WeakSet();
|
||||
// ① 有缓存路径:直接解析(开销极小);每 300 帧强制重扫一次,捡起后加载的部件
|
||||
ScanTick++;
|
||||
@@ -552,6 +607,7 @@
|
||||
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;
|
||||
@@ -560,6 +616,8 @@
|
||||
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 });
|
||||
}
|
||||
@@ -614,25 +672,25 @@
|
||||
// ===== 提取坦克的mesh数据 =====
|
||||
// 采集策略:**单一路径优先**,绝不把两条路径的结果混在一起(混在一起就是"复制身体")
|
||||
// ① 皮肤组件 h159_1(实际渲染树根)→ ② 部件表 → ③ 组件字段扫描(兜底)
|
||||
function GetTankMeshData(tank, stats) {
|
||||
function GetTankMeshData(tank, stats, TankPos) {
|
||||
var nodes = [];
|
||||
var Seen = new WeakSet();
|
||||
var skinComp = GetSkinComponent(tank);
|
||||
var skin = (skinComp && skinComp.g159_1 && skinComp.g159_1.g17r_1) ? skinComp.g159_1 : null;
|
||||
var skin = (skinComp && skinComp[FSkinHullSkin] && skinComp[FSkinHullSkin][FSkinParts]) ? skinComp[FSkinHullSkin] : null;
|
||||
if (stats) {
|
||||
stats.hullSource = skin ? 'g159_1' : (skinComp ? '有组件但无皮肤' : '无皮肤组件');
|
||||
stats.hullParts = (skin && typeof skin.g17r_1.e1 === 'function') ? skin.g17r_1.e1() : -1;
|
||||
stats.hullParts = (skin && typeof skin[FSkinParts].e1 === 'function') ? skin[FSkinParts].e1() : -1;
|
||||
}
|
||||
// ① 主路径:车身根节点(包含全部部件节点)
|
||||
if (skinComp && skinComp.h159_1) {
|
||||
try { CollectMeshNodes(skinComp.h159_1, nodes, 0, 'hull', stats ? stats.hull : null, Seen); } catch (e) {}
|
||||
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.g17r_1, function (part) {
|
||||
ForEachInList(skin[FSkinParts], function (part) {
|
||||
if (!part) return;
|
||||
var j = part.j17r_1; // 新版:单个 BP 节点;旧版:节点列表
|
||||
var j = part[FSkinPartNode]; // 新版:单个 BP 节点;旧版:节点列表
|
||||
if (!j) return;
|
||||
if (typeof j === 'object' && typeof j.e1 === 'function' && typeof j.g1 === 'function') {
|
||||
ForEachInList(j, function (bp) {
|
||||
@@ -646,7 +704,7 @@
|
||||
}
|
||||
// ③ 结构扫描:无条件补充漏采(车身根/部件表往往只覆盖一部分)
|
||||
// 重复实例(皮肤树/场景树同 tp 同名)由 DedupNodes 收掉,不会再出现"两个身体"
|
||||
try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {}
|
||||
try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen, TankPos); } catch (e) {}
|
||||
// ④ 炮塔(独立于车身根;重复实例由 DedupNodes 收掉)
|
||||
var turretNode = GetTurretNode(tank);
|
||||
if (stats) stats.turretComp = !!turretNode;
|
||||
@@ -1249,9 +1307,11 @@
|
||||
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 }
|
||||
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);
|
||||
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); // 兜底:至少可见
|
||||
|
||||
+78
-50
@@ -13,6 +13,18 @@
|
||||
// ===== 缓存队伍判断函数(ZCt/m2Fn) =====
|
||||
var ZCt = null, m2Fn = null;
|
||||
|
||||
// ===== 字段抽象层引用(字段名统一在 hornet_schema.js 里维护) =====
|
||||
// 取不到抽象层时退回本地常量,保证单独加载本文件也能工作
|
||||
var Schema = window.__hornetSchema__ || null;
|
||||
function FieldOf(Key, Fallback) {
|
||||
if (!Schema) return Fallback;
|
||||
var P = Schema.PathOf(Key);
|
||||
return (P && P.indexOf('.') < 0 && P.indexOf('(') < 0) ? P : Fallback;
|
||||
}
|
||||
var TEAM_FIELD = FieldOf('Team.Field', 'b135_1'); // 队伍组件上的队伍对象字段
|
||||
var TEAM_ID_FIELD = FieldOf('Team.Id', 'k3_1'); // 队伍编号:0/1/2(2=中立)
|
||||
var NEUTRAL_TEAM_ID = 2;
|
||||
|
||||
// 加载队伍判断函数(从 module_87072 导出)
|
||||
// 新旧差异(main.7cfa4058):
|
||||
// 取队伍 $_$.vd = WIt(t) → g$(HIt(), t).b135_1(坦克无队伍时返回中立 q$())
|
||||
@@ -256,80 +268,92 @@
|
||||
// 只在「本地坦克实体变了」(换局/换车)时才重新缓存。
|
||||
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, TEAM_FIELD);
|
||||
return Comp ? Comp[TEAM_FIELD] : null;
|
||||
}
|
||||
|
||||
// 读队伍编号:0 / 1 = 两个阵营,2 = 中立(q$());读不到返回 -1
|
||||
// ⚠ 用数值编号比较,而不是引擎 equals/R7:编号是稳定语义
|
||||
// (module_87072 里 P7()/$ct() 都按 k3_1 分派)
|
||||
function GetTeamId(Team) {
|
||||
if (!Team) return -1;
|
||||
var Id = Team[TEAM_ID_FIELD];
|
||||
return (typeof Id === 'number') ? Id : -1;
|
||||
}
|
||||
|
||||
function GetLocalTeamId() { return GetTeamId(LocalTeam); }
|
||||
function GetTankTeamId(tank) { return GetTeamId(ReadTeamRaw(tank)); }
|
||||
|
||||
function RememberLocalTeam(gm) {
|
||||
if (!ZCt || !m2Fn) LoadTeamFunctions();
|
||||
if (!ZCt) return;
|
||||
var t = null;
|
||||
try { t = gm.u10b_1; } catch (e) { t = null; }
|
||||
if (t) {
|
||||
var Id = t.iz1_1;
|
||||
if (LocalTeam && LocalTeamTankId === Id) return;
|
||||
try {
|
||||
var Team = ZCt(t);
|
||||
if (Team && (!m2Fn || m2Fn(Team, Team))) { LocalTeam = Team; LocalTeamTankId = Id; }
|
||||
} catch (e) {}
|
||||
var 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.v10b_1;
|
||||
if (G && (!m2Fn || m2Fn(G, G))) LocalTeam = G;
|
||||
var GId = GetTeamId(G);
|
||||
if (GId >= 0 && GId !== NEUTRAL_TEAM_ID) LocalTeam = G;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// ===== 判断是否友方 =====
|
||||
// 队伍同步有延迟:坦克刚出现时队伍是「中立」,直接比较会先判成敌人、同步后再变队友(颜色跳变)。
|
||||
// 处理:① 每辆坦克缓存最近一次**非中立**队伍;② 无法判定时沿用上一次判定结果。
|
||||
var TankTeamCache = new WeakMap(); // tank -> 非中立队伍对象
|
||||
// 规则:队伍编号相同且非中立 → 友方;中立/未下发 → 沿用该坦克上次判定(默认敌人)。
|
||||
// ⚠ 不再「一次判定永久锁定」:锁定会在基准短暂错误时把整局颜色固化错,
|
||||
// 这里改成每帧重算 + 中立抖动时沿用上次结果,既不会闪烁也不会锁死。
|
||||
var TankFriendCache = new WeakMap(); // tank -> 上一次的友方判定
|
||||
var TeamLockLogged = new WeakSet();
|
||||
function ResolveTankTeam(tank) {
|
||||
if (!ZCt) return null;
|
||||
var Team = null;
|
||||
try { Team = ZCt(tank); } catch (e) { Team = null; }
|
||||
if (Team && (!m2Fn || m2Fn(Team, Team))) { // 同队且非中立 = 有效队伍
|
||||
TankTeamCache.set(tank, Team);
|
||||
return Team;
|
||||
}
|
||||
return TankTeamCache.get(tank) || null;
|
||||
}
|
||||
var TeamJudgeLogged = new WeakSet();
|
||||
|
||||
function IsFriendly(tank, playerTank, gm) {
|
||||
if (!tank) return false;
|
||||
// 队伍函数可能因为工具早于游戏模块加载而没取到,这里按需补加载
|
||||
if (!ZCt || !m2Fn) LoadTeamFunctions();
|
||||
if (gm) RememberLocalTeam(gm);
|
||||
// 一旦在「基准队伍可靠」时判过,就锁定本局结果,战中不再改变
|
||||
var Locked = TankFriendCache.get(tank);
|
||||
if (Locked !== undefined) return Locked;
|
||||
if (ZCt && m2Fn) {
|
||||
try {
|
||||
var tankTeam = ResolveTankTeam(tank);
|
||||
// 可靠基准(用于锁定):本地拥有坦克的队伍 / GameMode 上的本地队伍字段
|
||||
var LockBase = LocalTeam;
|
||||
if (!LockBase && gm) { try { LockBase = gm.v10b_1; } catch (e) {} }
|
||||
// 观战目标只作临时参考,绝不用它锁定(它可能是敌人)
|
||||
var BaseTeam = LockBase || (playerTank ? ResolveTankTeam(playerTank) : null);
|
||||
if (tankTeam && BaseTeam) {
|
||||
var Friendly = m2Fn(tankTeam, BaseTeam);
|
||||
if (LockBase) {
|
||||
TankFriendCache.set(tank, Friendly);
|
||||
if (Friendly && !TeamLockLogged.has(tank)) {
|
||||
TeamLockLogged.add(tank);
|
||||
try {
|
||||
console.log('[敌我] 锁定为友方 id=' + tank.iz1_1 + ' name=' + (GetTankName(tank) || '?'));
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
return Friendly;
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
var TankId = GetTeamId(ReadTeamRaw(tank));
|
||||
if (TankId < 0 || TankId === NEUTRAL_TEAM_ID) {
|
||||
var Last = TankFriendCache.get(tank); // 队伍还没同步:保持上一帧颜色
|
||||
return Last === undefined ? false : Last;
|
||||
}
|
||||
// 基准还没就绪:先按敌人显示,但**不锁定**,等基准可用后再定
|
||||
return false;
|
||||
|
||||
// 可靠基准:本地拥有坦克的队伍(缓存)→ GameMode 上的本地队伍字段
|
||||
// 观战目标(t10b_1)可能是敌人,绝不作为基准
|
||||
var BaseTeam = LocalTeam;
|
||||
if (!BaseTeam && gm) { try { BaseTeam = gm.v10b_1; } 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.iz1_1 + ' 队伍=' + 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)
|
||||
@@ -616,6 +640,10 @@
|
||||
IsBot: IsBot,
|
||||
GetPlayerTank: GetPlayerTank,
|
||||
IsFriendly: IsFriendly,
|
||||
ReadTeamRaw: ReadTeamRaw,
|
||||
GetTeamId: GetTeamId,
|
||||
GetTankTeamId: GetTankTeamId,
|
||||
GetLocalTeamId: GetLocalTeamId,
|
||||
GetCameraInfo: GetCameraInfo,
|
||||
GetUMtComponent: GetUMtComponent,
|
||||
MkVec: MkVec,
|
||||
|
||||
+28
-10
@@ -28,6 +28,24 @@
|
||||
// 坦克 category 位(bit0)
|
||||
var TANK_CATEGORY_BIT = 1;
|
||||
|
||||
// ===== 字段抽象层(语义名 → 匿名字段,总表在 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 FPhysicsHull = Fld('Physics.Hull', 'zzq_1'); // 车体主碰撞形状
|
||||
var FShapeName = Fld('Shape.Name', 'x9j_1'); // 形状名('hull-main')
|
||||
var FShapeMask = Fld('Shape.Mask', 'z9j_1'); // 碰撞掩码
|
||||
var FBodyShapes = 'y9i_1'; // 刚体上的形状列表(pbk().y9i_1)
|
||||
var MAddMask = Mth('Physics.AddMask', 'n155');
|
||||
var MRemoveMask = Mth('Physics.RemoveMask', 'm155');
|
||||
var MGetBody = Mth('Physics.Body', 'pbk');
|
||||
|
||||
var Stat = {
|
||||
running: false,
|
||||
shapes: 0, // 本帧处理的碰撞形状数
|
||||
@@ -42,10 +60,10 @@
|
||||
// 特征:zzq_1 是车体主碰撞形状(x9j_1 === "hull-main"),且带 n155 / m155 / pbk
|
||||
function IsTankPhysics(Comp) {
|
||||
if (!Comp) return false;
|
||||
if (typeof Comp.n155 !== 'function' || typeof Comp.m155 !== 'function') return false;
|
||||
if (typeof Comp.pbk !== 'function') return false;
|
||||
var Hull = Comp.zzq_1;
|
||||
return !!(Hull && Hull.x9j_1 === 'hull-main');
|
||||
if (typeof Comp[MAddMask] !== 'function' || typeof Comp[MRemoveMask] !== 'function') return false;
|
||||
if (typeof Comp[MGetBody] !== 'function') return false;
|
||||
var Hull = Comp[FPhysicsHull];
|
||||
return !!(Hull && Hull[FShapeName] === 'hull-main');
|
||||
}
|
||||
|
||||
// 在实体上找坦克物理组件(不走 FindComponentByField:zzq_1 字段名在其它类里也有复用)
|
||||
@@ -77,7 +95,7 @@
|
||||
function ReleaseSnapshot() {
|
||||
MaskSnapshot.forEach(function (Mask, Shape) {
|
||||
try {
|
||||
if (Shape && typeof Mask === 'number') Shape.z9j_1 = Mask;
|
||||
if (Shape && typeof Mask === 'number') Shape[FShapeMask] = Mask;
|
||||
} catch (e) {}
|
||||
});
|
||||
MaskSnapshot.clear();
|
||||
@@ -100,8 +118,8 @@
|
||||
}
|
||||
|
||||
var Body = null;
|
||||
try { Body = Phys.pbk(); } catch (e) { Body = null; }
|
||||
var Shapes = Body && Body.y9i_1;
|
||||
try { Body = Phys[MGetBody](); } catch (e) { Body = null; }
|
||||
var Shapes = Body && Body[FBodyShapes];
|
||||
if (!Shapes || typeof Shapes.e1 !== 'function' || typeof Shapes.g1 !== 'function') return;
|
||||
|
||||
var Count = Shapes.e1() | 0;
|
||||
@@ -109,10 +127,10 @@
|
||||
for (var i = 0; i < Count; i++) {
|
||||
var Shape = null;
|
||||
try { Shape = Shapes.g1(i); } catch (e) { Shape = null; }
|
||||
if (!Shape || typeof Shape.z9j_1 !== 'number') continue;
|
||||
if (!Shape || typeof Shape[FShapeMask] !== 'number') continue;
|
||||
// 首次见到该形状时记录原始 mask,保证 Stop 能精确还原
|
||||
if (!MaskSnapshot.has(Shape)) MaskSnapshot.set(Shape, Shape.z9j_1);
|
||||
Shape.z9j_1 = Shape.z9j_1 & ~TANK_CATEGORY_BIT;
|
||||
if (!MaskSnapshot.has(Shape)) MaskSnapshot.set(Shape, Shape[FShapeMask]);
|
||||
Shape[FShapeMask] = Shape[FShapeMask] & ~TANK_CATEGORY_BIT;
|
||||
Touched++;
|
||||
}
|
||||
Stat.shapes = Touched;
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
// 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 = {
|
||||
// ---- 战斗实体基类(fe / BattleEntity)----
|
||||
'Entity.Id': 'iz1_1', // 实体 ID(坦克/战场唯一)
|
||||
'Entity.Components': 'qz1_1.s2a_1', // Map<组件标识, 组件列表>,取值要 valueList.g1(0)
|
||||
|
||||
// ---- GameMode(ght,window.__gameMode__)----
|
||||
'GameMode.Scene': 'lz4()', // 场景实体
|
||||
'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.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 = 只探测不产生碰撞响应
|
||||
|
||||
// ---- 血量组件(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.Children': 'r35_1',
|
||||
'Node.Mesh': 'v3m_1',
|
||||
'Node.Lod': 'u4x_1',
|
||||
|
||||
// ---- 相机(场景实体 .jz1_1.iyx_1)----
|
||||
'Camera.SceneRef': 'jz1_1',
|
||||
'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'
|
||||
};
|
||||
|
||||
// ===== module_87072 的导出键($_.xx),每版都可能平移 =====
|
||||
// 队伍:vd = WIt(t) 取实体队伍;dl = R7(a,b) 同队且非中立
|
||||
// ⚠ 旧版是 kd / rk;新版 kd 已指向无关构造器
|
||||
var Exports = {
|
||||
'Export.TeamOf': ['vd', 'kd'],
|
||||
'Export.TeamSame': ['dl', 'rk']
|
||||
};
|
||||
|
||||
// ===== 路径解析 =====
|
||||
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;
|
||||
var V = Get(Target, Key);
|
||||
if (V === undefined) Out.Missing.push(Key + ' → ' + Fields[Key]);
|
||||
else Out.Ok.push(Key + ' → ' + Fields[Key]);
|
||||
});
|
||||
return Out;
|
||||
}
|
||||
|
||||
// 用当前战局对象做一次全量自检(控制台调用 __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 Targets = {
|
||||
Entity: Tank || Scene,
|
||||
GameMode: Gm,
|
||||
Tank: Tank,
|
||||
Physics: Tank ? FindPhysics(Tank) : null,
|
||||
Shape: null,
|
||||
Skin: Tank,
|
||||
Turret: Tank,
|
||||
Node: null,
|
||||
Camera: Scene
|
||||
};
|
||||
|
||||
Object.keys(Targets).forEach(function (Prefix) {
|
||||
if (!Targets[Prefix]) { Report.Groups[Prefix] = '无样本对象'; return; }
|
||||
var R = Check(Targets[Prefix], Prefix);
|
||||
Report.Groups[Prefix] = R;
|
||||
R.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 + ' 项)');
|
||||
})();
|
||||
Reference in new issue
Block a user