实测 byType={16/4:26530,16/1:4620} 表明被跳过的是非三角形地形代理,导致穿地。现判定只对 type=8 三角形生效:最高点高于车体中心 1m 且 |法线z|<0.6;非三角形静态一律保留。
451 lines
17 KiB
JavaScript
451 lines
17 KiB
JavaScript
// 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 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 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,
|
||
byType: {}, // cat/type → 被忽略次数(诊断)
|
||
patched: 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(墙/栅栏/悬崖立面)
|
||
function IsTriangle(Shape) {
|
||
return !!(Shape && Shape[FShapeType] === TRIANGLE_TYPE);
|
||
}
|
||
|
||
// 返回法线 z 分量(-1..1);读不到返回 null
|
||
function TriangleNormalZ(Shape) {
|
||
if (!IsTriangle(Shape)) return null;
|
||
var A = Shape[FV0], B = Shape[FV1], C = Shape[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);
|
||
}
|
||
|
||
var FPhysicsPosition = Fld('Physics.Position', 'bzr_1'); // 本地坦克世界位置
|
||
|
||
function LocalTankZ() {
|
||
var Phys = GetLocalPhysics();
|
||
var P = Phys && Phys[FPhysicsPosition];
|
||
return (P && typeof P.h20_1 === 'number') ? P.h20_1 : null;
|
||
}
|
||
|
||
function ShapeMaxZ(Shape) {
|
||
if (IsTriangle(Shape)) {
|
||
var A = Shape[FV0], B = Shape[FV1], C = Shape[FV2];
|
||
if (!A || !B || !C) return null;
|
||
var Max = Math.max(A.h20_1, B.h20_1, C.h20_1);
|
||
return isFinite(Max) ? Max : null;
|
||
}
|
||
var Body = Shape[FShapeBody];
|
||
var State = Body && Body[FBodyState];
|
||
var P = State && State[FBodyPosVec];
|
||
return (P && typeof P.h20_1 === 'number') ? P.h20_1 : null;
|
||
}
|
||
|
||
// 「栅栏类」= 静态几何中的**立面三角形**:
|
||
// · 必须是三角形(type 8)—— 非三角形静态体(type 1/4 等)是地面/地形代理,跳过会导致掉出地图
|
||
// · 最高点高于车体中心 1m(地面/缓坡不满足)
|
||
// · |法线 z| < 0.6(竖直立面)
|
||
function IsFenceLike(Shape) {
|
||
if (!IsStaticShape(Shape)) return false;
|
||
if (!IsTriangle(Shape)) return false;
|
||
var TankZ = LocalTankZ();
|
||
if (TankZ === null) return false;
|
||
var MaxZ = ShapeMaxZ(Shape);
|
||
if (MaxZ === null) return false;
|
||
if (MaxZ < TankZ + 100) return false;
|
||
var Nz = TriangleNormalZ(Shape);
|
||
return Nz !== null && Math.abs(Nz) < 0.6;
|
||
}
|
||
|
||
// ===== 诊断:识别「正在和什么物体重叠」=====
|
||
// 碰撞形状本身没有名字(地图三角形是合并的三角汤),所以用两级信息:
|
||
// ① 形状自身: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;
|
||
if (IsTriangle(Shape)) {
|
||
var A = Shape[FV0], B = Shape[FV1], C = Shape[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;
|
||
}
|
||
// 非三角形:取所在刚体的世界位置
|
||
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 };
|
||
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 Key = Cat + '/' + Type + '/' + (Nz === null ? '-' : Nz.toFixed(2));
|
||
if (FenceSampleKeys[Key]) return;
|
||
FenceSampleKeys[Key] = 1;
|
||
|
||
var Pt = ShapePoint(Shape);
|
||
var Near = FindNearNodeNames(Pt, 700);
|
||
console.log('[忽略][重叠物体] cat=' + Cat + ' type=' + Type + ' 三角=' + Tri +
|
||
' nz=' + (Nz === null ? '-' : Nz.toFixed(2)) +
|
||
' 位置=' + (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;
|
||
if (FenceShape && IsFenceLike(FenceShape)) {
|
||
Stat.skippedFence++;
|
||
var Key = FenceShape[FShapeCategory] + '/' + FenceShape[FShapeType];
|
||
Stat.byType[Key] = (Stat.byType[Key] || 0) + 1;
|
||
return true;
|
||
}
|
||
// 诊断:本车与任意非坦克形状配对但没跳过 → 记录样本(含被挡住的栅栏)
|
||
if (TankA && OwnerOf(ShapeA) === Local && !TankB) RecordFenceSample(ShapeB);
|
||
if (TankB && OwnerOf(ShapeB) === Local && !TankA) RecordFenceSample(ShapeA);
|
||
}
|
||
|
||
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('[忽略] 已接管碰撞配对过滤(坦克按阵营 / 陡峭静态三角形)');
|
||
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,
|
||
byType: Stat.byType, patched: Stat.patched
|
||
};
|
||
}
|
||
};
|
||
|
||
console.log('[3DTank] 忽略模块已加载(忽略坦克 / 忽略栅栏)');
|
||
})();
|