Files
TankIC/3dtank_bundle.js
T
TermiNexus 90c377ad79 feat(3dtank): 本地镜像与 hornet 工具链迁到新版 bundle main.6469b966.js
旧 main.b7114b43.js 已被新构建替换(10,011,039 字节 / 1335 模块),本地镜像整体重切到新版并清掉旧产物;hornet 工具链按 tools/map_symbols.js 生成的新旧符号映射改名后重新打包(3dtank_bundle.js 与 unpacked/hornet_bundle.js 同步)。tools/symbol_map.json(8.9MB 映射产物)与切割解包目录不进库,可用 map_symbols.js / split.js 重新生成。
2026-09-17 19:59:40 +08:00

5497 lines
209 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 3DTank 工具链 Bundle (自动生成,请勿手动编辑)
// 构建: python build_bundle.py
// 用法: 全选粘贴到 3DTank 游戏页面控制台
// 操作: Alt+T 打开控制面板 | 左上角 HUD 显示状态
// ============================================================
// ===== hook_gamemode.js =====
// ===== GameMode Hook 脚本 v2(快速获取版)=====
// 改进:
// 1. 每次 yht 调用都更新引用(解决新战斗中旧引用失效问题)
// 2. ght.prototype 方法每次调用都更新
// 3. 定时器主动验证引用有效性,失效则重置
// 4. trySetGameMode 验证坦克列表,避免捕获未初始化实例
//
// 原理:
// module_87072.$_$.ac = yht 函数(接收场景,返回 ght 实例)
// module_87072.$_$.zb = ght 类(GameMode 组件类)
// module_87072.$_$.bc = kht 函数(接收场景,返回 GameMode entity)
// yht(scene).xzq_1 = Tht 实例(坦克列表管理器)
// Tht.z11b() = a12c_1(坦克列表,Lx 实例)
(function() {
console.log('===== GameMode Hook v2 开始 =====');
// 1. 检查 webpack 内部对象
if (!window.__wp_require__) {
console.error('[错误] window.__wp_require__ 不存在');
return;
}
// 2. 获取 module_87072 的导出
var mod = window.__wp_require__(87072);
if (!mod || !mod.$_$) {
console.error('[错误] module_87072 未加载或无 $_$ 导出');
return;
}
var ghtClass = mod.$_$.zb;
var yhtFunc = mod.$_$.ac;
console.log('[1] ght 类:', ghtClass ? ghtClass.name : '未找到');
console.log('[1] yht 函数:', typeof yhtFunc === 'function' ? '已找到' : '未找到');
if (!ghtClass) {
console.error('[错误] ght 类未导出');
return;
}
// ===== 辅助:设置 gameMode(无条件设置,坦克列表单独处理)=====
function trySetGameMode(gm, source) {
if (!gm) return false;
var updated = window.__gameMode__ !== gm;
window.__gameMode__ = gm;
// 尝试获取坦克列表(失败也不影响 gameMode 设置)
var tankCount = -1;
try {
var list = gm.q100_1.r11l();
tankCount = list.e1();
window.__tankList__ = list;
} catch(e) {
// xzq_1 或 z11b 暂时不可用,不影响 gameMode 设置
}
if (updated) {
console.log('%c[Hook] ght 已更新 (via ' + source + ') 坦克数:' + tankCount, 'color:#0f0;font-weight:bold');
}
return true;
}
// ===== 3. Hook yht 函数 — 每次调用都更新 =====
if (typeof yhtFunc === 'function' && !window.__yht_hooked__) {
window.__yht_hooked__ = true;
var origYht = yhtFunc;
mod.$_$.ac = function(t) {
var result = origYht.call(this, t);
if (result) trySetGameMode(result, 'yht');
return result;
};
console.log('[2] yht Hook 已安装(每次调用都更新)');
} else if (window.__yht_hooked__) {
console.log('[2] yht Hook 已存在');
}
// ===== 4. Hook ght.prototype 方法 — 每次调用都更新 =====
if (!window.__ght_proto_hooked__) {
window.__ght_proto_hooked__ = true;
var proto = ghtClass.prototype;
var methodCount = 0;
Object.getOwnPropertyNames(proto).forEach(function(name) {
if (name === 'constructor') return;
var orig = Object.getOwnPropertyDescriptor(proto, name);
if (!orig || typeof orig.value !== 'function') return;
var origFn = orig.value;
methodCount++;
proto[name] = function() {
trySetGameMode(this, 'ght.' + name);
return origFn.apply(this, arguments);
};
});
console.log('[3] ght.prototype 方法 Hook 已安装(' + methodCount + ' 个方法)');
} else {
console.log('[3] ght.prototype Hook 已存在');
}
// ===== 5. 定时器主动验证引用有效性 =====
if (!window.__ght_validator__) {
window.__ght_validator__ = true;
var validateCount = 0;
setInterval(function() {
validateCount++;
if (window.__gameMode__) {
// 验证当前引用是否有效
try {
var list = window.__gameMode__.q100_1.r11l();
window.__tankList__ = list;
} catch(e) {
// 引用失效(新战斗/退出战斗),重置等待重新捕获
console.log('%c[验证] ght 引用失效,重置等待重新捕获', 'color:#f80;font-weight:bold');
window.__gameMode__ = null;
window.__tankList__ = null;
}
} else {
// 未捕获,前5次每秒提示
if (validateCount <= 5) {
console.log('[验证] 等待 ght 捕获... (' + validateCount + ')');
}
}
}, 500);
console.log('[4] 定时验证器已启动(每500ms)');
} else {
console.log('[4] 定时验证器已存在');
}
// ===== 6. 状态显示 =====
if (window.__gameMode__) {
console.log('[5] 已有 ght 实例:', window.__gameMode__);
if (window.__tankList__) {
try {
console.log(' 当前坦克数量:', window.__tankList__.e1());
} catch(e) {}
}
} else {
console.log('[5] 等待 ght 实例捕获...');
console.log(' 提示: hook 已安装,战斗中触发事件会自动捕获');
}
// ===== 7. 辅助函数 =====
window.__listTanks__ = function() {
if (!window.__gameMode__) {
console.log('ght 实例未捕获,请等待');
return;
}
var list = window.__gameMode__.q100_1.r11l();
var count = list.e1();
console.log('=== 坦克列表 (' + count + " 个) ===");
for (var i = 0; i < count; i++) {
var tank = list.g1(i);
console.log('[' + i + ']', tank);
}
return list;
};
window.__probeTank__ = function(idx) {
if (!window.__gameMode__) {
console.log('ght 实例未捕获');
return;
}
var list = window.__gameMode__.q100_1.r11l();
var tank = list.g1(idx || 0);
console.log('坦克[' + (idx || 0) + ']:', tank);
console.log('字段:', Object.keys(tank));
Object.keys(tank).forEach(function(k) {
var v = tank[k];
var type = typeof v;
if (type === 'number' || type === 'string' || type === 'boolean') {
console.log(' ' + k + ' = ' + v + ' (' + type + ')');
} else if (v === null) {
console.log(' ' + k + ' = null');
} else if (type === 'object') {
console.log(' ' + k + ' = [object] (' + (v.constructor ? v.constructor.name : typeof v) + ')');
} else if (type === 'function') {
console.log(' ' + k + ' = [function]');
}
});
return tank;
};
// 主动刷新/检查 ght 实例
window.__refreshGameMode__ = function() {
if (window.__gameMode__) {
try {
var list = window.__gameMode__.q100_1.r11l();
console.log('%c[刷新] ght 引用有效,坦克数:' + list.e1(), 'color:#0f0;font-weight:bold');
return window.__gameMode__;
} catch(e) {
console.log('%c[刷新] 旧引用失效,已重置', 'color:#f80;font-weight:bold');
window.__gameMode__ = null;
window.__tankList__ = null;
}
}
console.log('[刷新] 等待 hook 捕获...(战斗中触发事件即可)');
return null;
};
console.log('\n===== Hook v2 安装完成 =====');
console.log('辅助函数:');
console.log(' __listTanks() - 列出所有坦克');
console.log(' __probeTank(idx) - 探查指定坦克的字段');
console.log(' __refreshGameMode__() - 刷新/检查 ght 实例');
console.log(' window.__gameMode__ - ght 实例(自动更新)');
console.log(' window.__tankList__ - 坦克列表(自动更新)');
})();
// ===== hornet_lib.js =====
// ===== 3DTank公共库 - 工具文件共享辅助函数 =====
// 在浏览器控制台运行(需先运行 hook_gamemode.js)
// 功能:提供坦克查询、队伍判断、相机信息、3D投影等公共函数
// 用法:各工具文件通过 var lib = window.__hornetLib__ 访问
(function() {
console.log('===== 3DTank公共库加载 =====');
if (!window.__gameMode__) {
console.warn('[公共库] window.__gameMode__ 暂未捕获,工具启动时将失败。hook_gamemode.js 已安装 Hook,战斗中会自动捕获');
}
// ===== 缓存队伍判断函数(ZCt/m2Fn) =====
var ZCt = null, m2Fn = null;
// 加载队伍判断函数(从 module_87072 导出)
// ZCt (mod.$_$.kd): 从坦克对象获取队伍ID
// m2 (mod.$_$.rk): 判断两个队伍ID是否相同且非中立 = t.equals(n) && !t.equals(E$())
function LoadTeamFunctions() {
if (ZCt && m2Fn) return { ZCt: ZCt, m2Fn: m2Fn };
try {
var mod87072 = window.__wp_require__(87072);
if (mod87072 && mod87072.$_$) {
ZCt = mod87072.$_$.kd;
m2Fn = mod87072.$_$.rk;
console.log('[公共库] 队伍判断函数已加载: ZCt=' + (typeof ZCt) + ', m2=' + (typeof m2Fn));
}
} catch(e) {
console.warn('[公共库] 加载队伍判断函数失败:', e.message);
}
return { ZCt: ZCt, m2Fn: m2Fn };
}
// ===== 从 entity 获取包含指定字段的组件 =====
// 遍历 entity.wyj_1.r2a_1(Map<组件标识, 组件列表>),value 是列表类(s_),g1(0) 获取实际组件
// 注意:用 !== undefined 检查字段存在性,避免 falsy 值(0/""/null/false)漏判
function FindComponentByField(entity, fieldName) {
if (!entity || !entity.pyt_1 || !entity.pyt_1.s2a_1) return null;
var result = null;
entity.pyt_1.s2a_1.forEach(function(valueList, key) {
if (result) return;
try {
var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList;
if (comp && comp[fieldName] !== undefined) result = comp;
} catch(e) {}
});
return result;
}
// ===== 获取坦克位置(G$t 物理组件的 pz7_1 = O$向量) =====
// 返回 {x, y, z},x/y 水平面,z 高度
function GetTankPosition(tank) {
var physicsComp = FindComponentByField(tank, 'gzh_1');
if (physicsComp && physicsComp.gzh_1) {
return {
x: physicsComp.gzh_1.f20_1,
y: physicsComp.gzh_1.g20_1,
z: physicsComp.gzh_1.h20_1
};
}
return null;
}
// ===== 获取坦克状态值(Yjt 组件的 vzr_1.k3_1) =====
// 状态枚举:0=DEAD, 1=DEAD_PHANTOM, 2=SEMI_ACTIVE, 3=ACTIVE, 4=DISABLED
// Yjt 特征:有 azs_1(状态机)和 vzr_1(状态枚举)字段
function GetTankState(tank) {
if (!tank || !tank.pyt_1 || !tank.pyt_1.s2a_1) return -1;
var tankComp = null;
tank.pyt_1.s2a_1.forEach(function(valueList, key) {
if (tankComp) return;
try {
var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList;
if (comp && comp.t101_1 && comp.o101_1 !== undefined) tankComp = comp;
} catch(e) {}
});
if (tankComp && tankComp.o101_1) return tankComp.o101_1.k3_1;
return -1;
}
// 只有 ACTIVE(3) 才是战斗中的坦克
function IsTankAlive(tank) { return GetTankState(tank) === 3; }
// ===== 获取坦克名称(yot 用户信息组件) =====
// s120_1 = userName, u120_1 = clanTag(可为null), t120_1 = userRank
function GetTankName(tank) {
var userComp = FindComponentByField(tank, 'k12a_1');
if (!userComp) return null;
var name = userComp.k12a_1 || '';
var clanTag = userComp.m12a_1 || '';
return clanTag ? (clanTag + ' ' + name) : name;
}
// ===== 获取坦克血量(Vkt 血量组件) =====
// f122_1 = 最大血量(max), g122_1 = 当前血量(current, 死亡时=0)
function GetTankHealth(tank) {
var healthComp = FindComponentByField(tank, 'y12b_1');
if (!healthComp) return null;
var max = healthComp.x12b_1 || 0;
var cur = healthComp.y12b_1 || 0;
if (max <= 0) return null;
return { current: cur, max: max, percent: cur / max };
}
// ===== 获取玩家坦克 =====
// 优先 ezr_1(当前观察坦克),备用 fzr_1(本地控制坦克),再备用坦克列表第一个
// checkPosition: 是否检查位置有效性(默认true;测试脚本传false以在死亡状态控制)
function GetPlayerTank(gm, checkPosition) {
if (checkPosition === undefined) checkPosition = true;
var t = gm.x100_1;
if (t && (!checkPosition || GetTankPosition(t))) return t;
t = gm.y100_1;
if (t && (!checkPosition || GetTankPosition(t))) return t;
try {
var tl = gm.q100_1.r11l();
if (tl.e1() > 0) {
t = tl.g1(0);
if (t && (!checkPosition || GetTankPosition(t))) return t;
}
} catch(e) {}
return null;
}
// ===== 判断是否友方 =====
// 策略1(优先):ZCt 获取双方队伍ID,m2 比较(同队且非中立)
// 策略2(备用):比较 gm.gzr_1(本地队伍ID)与坦克队伍ID
function IsFriendly(tank, playerTank, gm) {
if (!tank || !playerTank) return false;
if (ZCt && m2Fn) {
try {
var tankTeam = ZCt(tank);
var playerTeam = ZCt(playerTank);
if (tankTeam && playerTeam) return m2Fn(tankTeam, playerTeam);
} catch(e) {}
}
try {
if (gm && gm.z100_1 && ZCt) {
var team = ZCt(tank);
if (team && gm.z100_1) {
return team.equals(gm.z100_1) && m2Fn ? m2Fn(team, gm.z100_1) : team.equals(gm.z100_1);
}
}
} catch(e) {}
return false;
}
// ===== 获取相机信息($r/gq 相机组件) =====
// 遍历 GameMode entity 和所有坦克 entity 查找活跃相机组件
// 策略1:查找 $r/gq 相机组件(有 kym_1 位置 和 lym_1 方向)
// 策略2:查找 k7 相机控制器(有 qz9_1 和 gz9_1),从 gz9_1 获取相机组件
// 返回 {yaw, pitch, fov, pos:{x,y,z}}
// 注意:lym_1.h20_1 是 roll(绕Z),lym_1.f20_1 是 pitch(绕X)
function GetCameraInfo(gm) {
var entities = [];
try {
var gameScene = gm.jyw().iyt_1;
var gmEntity = gameScene.xyu("GameMode");
if (gmEntity) entities.push(gmEntity);
} catch(e) {}
try {
var tankList = gm.q100_1.r11l();
var tCount = tankList.e1();
for (var i = 0; i < tCount; i++) {
var t = tankList.g1(i);
if (t) entities.push(t);
}
} catch(e) {}
for (var i = 0; i < entities.length; i++) {
var entity = entities[i];
if (!entity || !entity.pyt_1 || !entity.pyt_1.s2a_1) continue;
var found = null;
entity.pyt_1.s2a_1.forEach(function(valueList, key) {
if (found) return;
try {
var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList;
if (!comp) return;
// 策略1:$r/gq 相机组件
if (comp.cyw_1 && comp.dyw_1 &&
comp.cyw_1.f20_1 !== undefined && comp.dyw_1.f20_1 !== undefined) {
if (Math.abs(comp.dyw_1.h20_1) > 0.001 || Math.abs(comp.dyw_1.f20_1) > 0.001) {
found = comp;
return;
}
}
// 策略2:k7 相机控制器
if (comp.jzj_1 && comp.zzi_1) {
var camComp = comp.zzi_1;
if (camComp && camComp.cyw_1 && camComp.dyw_1 &&
(Math.abs(camComp.dyw_1.h20_1) > 0.001 || Math.abs(camComp.dyw_1.f20_1) > 0.001)) {
found = camComp;
return;
}
}
} catch(e) {}
});
if (found) {
return {
yaw: found.dyw_1.h20_1,
pitch: found.dyw_1.f20_1,
fov: found.byw_1 || 1.0471976,
pos: {
x: found.cyw_1.f20_1,
y: found.cyw_1.g20_1,
z: found.cyw_1.h20_1
}
};
}
}
return null;
}
// ===== 获取全局碰撞检测器(uMt 组件,挂在 GameMode entity 上) =====
// g16x(startVec, dirVec, maxDist, resultObj, filterObj) = 全局射线检测
function GetUMtComponent(gm) {
try {
var gmEntity = gm.jyw();
if (!gmEntity || !gmEntity.pyt_1 || !gmEntity.pyt_1.s2a_1) return null;
var uMt = null;
gmEntity.pyt_1.s2a_1.forEach(function(valueList, key) {
if (uMt) return;
try {
var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList;
if (comp && typeof comp.m179 === 'function') uMt = comp;
} catch(e) {}
});
return uMt;
} catch(e) {
return null;
}
}
// ===== 构造 mock O$ 向量(用于射线检测接口) =====
function MkVec(x, y, z) {
return {
f20_1: x || 0, g20_1: y || 0, h20_1: z || 0,
k26: function(t) { this.f20_1 = t.f20_1; this.g20_1 = t.g20_1; this.h20_1 = t.h20_1; },
n23: function(x, y, z) { this.f20_1 = x; this.g20_1 = y; this.h20_1 = z; return this; }
};
}
// ===== 射线检测:返回命中距离(-1=未命中/异常) =====
// 从 fromPos 沿 (dirX,dirY,dirZ) 方向,最大距离 maxDist
function RaycastDistance(gm, fromPos, dirX, dirY, dirZ, maxDist) {
var uMt = GetUMtComponent(gm);
if (!uMt) return -1;
try {
var startVec = MkVec(fromPos.x, fromPos.y, fromPos.z);
var dirVec = MkVec(dirX, dirY, dirZ);
var result = {
y178_1: MkVec(0, 0, 0),
z178_1: MkVec(0, 0, 0),
a179_1: maxDist,
c179_1: null,
b179_1: null,
d179: function(t, n, i, r, e) {
this.y178_1.k26(t);
this.z178_1.k26(n);
this.a179_1 = i;
this.c179_1 = r;
this.b179_1 = e;
},
d3: function() { this.b179_1 = null; }
};
var filter = {
o1co_1: { h1: function() { return false; }, d3: function() {} },
w179: function() { return true; },
d3: function() { this.o1co_1.d3(); },
p1co: function() {}
};
var hit = uMt.m179(startVec, dirVec, maxDist, result, filter);
return (hit && result.a179_1 < maxDist) ? result.a179_1 : -1;
} catch(e) {
return -1;
}
}
// ===== 3D 到 2D 投影(用于 UI 标记) =====
// 标准透视投影:forward/right/up 三轴正交基
function Project3D(worldPos, camPos, yaw, pitch, fov, screenW, screenH) {
var cp = Math.cos(pitch);
var sp = Math.sin(pitch);
var cy = Math.cos(yaw);
var sy = Math.sin(yaw);
var forward = { x: cp * (-sy), y: cp * cy, z: sp };
var right = { x: cy, y: sy, z: 0 };
var up = {
x: right.y * forward.z - right.z * forward.y,
y: right.z * forward.x - right.x * forward.z,
z: right.x * forward.y - right.y * forward.x
};
var relX = worldPos.x - camPos.x;
var relY = worldPos.y - camPos.y;
var relZ = worldPos.z - camPos.z;
var camX = relX * right.x + relY * right.y + relZ * right.z;
var camY = relX * up.x + relY * up.y + relZ * up.z;
var camZ = relX * forward.x + relY * forward.y + relZ * forward.z;
if (camZ <= 0.1) return null;
var focalLength = screenH / (2 * Math.tan(fov / 2));
var screenX = screenW / 2 + (camX / camZ) * focalLength;
var screenY = screenH / 2 - (camY / camZ) * focalLength;
return { x: screenX, y: screenY, depth: camZ };
}
// ===== 用世界变换矩阵变换顶点(列主序4x4矩阵) =====
function TransformVertex(matrix, x, y, z) {
return {
x: matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12],
y: matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13],
z: matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14]
};
}
// ===== 让元素可拖动(左键拖动,改变 left/top) =====
// 适用于固定定位的小窗口(雷达、后视镜等)
// 实现要点:Pointer Events + setPointerCapture,保证 pointerup/pointercancel 必定回到元素,
// 并在 window 捕获阶段兜底,避免游戏画布抢事件后"松手仍粘着鼠标"。
function MakeDraggable(elem) {
if (!elem) return null;
elem.style.cursor = 'move';
var Active = false;
var PointerId = null;
var GrabX = 0;
var GrabY = 0;
function MoveTo(ClientX, ClientY) {
var NextX = Math.max(0, Math.min(window.innerWidth - 50, ClientX - GrabX));
var NextY = Math.max(0, Math.min(window.innerHeight - 30, ClientY - GrabY));
elem.style.left = NextX + 'px';
elem.style.top = NextY + 'px';
}
function Begin(ClientX, ClientY) {
var Rect = elem.getBoundingClientRect();
// 移除 right 定位,改用 left
elem.style.right = 'auto';
elem.style.transform = 'none';
elem.style.left = Rect.left + 'px';
elem.style.top = Rect.top + 'px';
GrabX = ClientX - Rect.left;
GrabY = ClientY - Rect.top;
Active = true;
elem.style.cursor = 'grabbing';
}
function End() {
if (!Active) return;
Active = false;
if (PointerId !== null && elem.releasePointerCapture) {
try { elem.releasePointerCapture(PointerId); } catch (Err) {}
}
PointerId = null;
elem.style.cursor = 'move';
}
if (window.PointerEvent) {
elem.addEventListener('pointerdown', function (e) {
if (e.button !== 0) return;
Begin(e.clientX, e.clientY);
PointerId = e.pointerId;
try { elem.setPointerCapture(e.pointerId); } catch (Err) {}
e.preventDefault();
});
elem.addEventListener('pointermove', function (e) {
if (!Active || (PointerId !== null && e.pointerId !== PointerId)) return;
if (e.buttons === 0) { End(); return; }
MoveTo(e.clientX, e.clientY);
});
elem.addEventListener('pointerup', function () { End(); });
elem.addEventListener('pointercancel', function () { End(); });
elem.addEventListener('lostpointercapture', function () { End(); });
window.addEventListener('pointerup', function () { End(); }, true);
} else {
elem.addEventListener('mousedown', function (e) {
if (e.button !== 0) return;
Begin(e.clientX, e.clientY);
e.preventDefault();
});
document.addEventListener('mousemove', function (e) {
if (!Active) return;
if (e.buttons === 0) { End(); return; }
MoveTo(e.clientX, e.clientY);
}, true);
document.addEventListener('mouseup', function () { End(); }, true);
}
window.addEventListener('blur', End);
window.addEventListener('mouseleave', End);
return { End: End, IsActive: function () { return Active; } };
}
// ===== 导出公共函数到 window.__hornetLib__ =====
window.__hornetLib__ = {
LoadTeamFunctions: LoadTeamFunctions,
FindComponentByField: FindComponentByField,
GetTankPosition: GetTankPosition,
GetTankState: GetTankState,
IsTankAlive: IsTankAlive,
GetTankName: GetTankName,
GetTankHealth: GetTankHealth,
GetPlayerTank: GetPlayerTank,
IsFriendly: IsFriendly,
GetCameraInfo: GetCameraInfo,
GetUMtComponent: GetUMtComponent,
MkVec: MkVec,
RaycastDistance: RaycastDistance,
Project3D: Project3D,
TransformVertex: TransformVertex,
MakeDraggable: MakeDraggable
};
console.log('[公共库] 已加载,可用函数:', Object.keys(window.__hornetLib__).join(', '));
})();
// ===== hornet_radar.js =====
// ===== 3DTank雷达 - 2D俯视图雷达 =====
// 在浏览器控制台运行(需先运行 hook_gamemode.js)
// 功能:在屏幕右上角显示2D雷达,显示所有坦克的相对位置
(function() {
function _start() {
console.log('===== 3DTank雷达启动 =====');
if (!window.__gameMode__) {
console.error('请先运行 hook_gamemode.js');
return;
}
// ===== 清理旧雷达 =====
if (window.__hornetRadar__) {
window.__hornetRadar__.stop();
console.log('已停止旧雷达');
}
var gm = window.__gameMode__;
// ===== 引用公共库(hornet_lib.js) =====
var lib = window.__hornetLib__;
if (!lib) {
console.error('[雷达] 请先运行 hornet_lib.js');
return;
}
var teamFns = lib.LoadTeamFunctions();
var ZCt = teamFns.ZCt, m2Fn = teamFns.m2Fn;
// 组件查找:使用公共库(已修复 falsy 值漏判 bug)
var FindComponentByField = lib.FindComponentByField;
// 坦克位置:使用公共库
var GetTankPosition = lib.GetTankPosition;
// ===== 获取坦克方向(Kkt 组件的 dzc_1) =====
function getTankDirection(tank) {
var hullComp = FindComponentByField(tank, 'vzl_1');
if (hullComp && hullComp.wzl_1) {
return {
x: hullComp.wzl_1.f20_1,
y: hullComp.wzl_1.g20_1,
z: hullComp.wzl_1.h20_1
};
}
return null;
}
// ===== 获取相机方向(gq/$r 组件的 lym_1.h20_1 = 偏航角) =====
// 相机组件可能在 GameMode entity、玩家坦克或任何坦克上
// 策略1:查找 $r/gq 相机组件(有 kym_1 和 lym_1 字段)
// 策略2:查找 k7 相机控制器(有 qz9_1 和 gz9_1 字段),从 gz9_1 获取相机组件
function getCameraHeading() {
var entities = [];
// 1. GameMode entity
try {
var gameScene = gm.jyw().iyt_1;
var gmEntity = gameScene.xyu("GameMode");
if (gmEntity) entities.push(gmEntity);
} catch(e) {}
// 2. 所有坦克列表中的 entity
try {
var tankList = gm.q100_1.r11l();
var tCount = tankList.e1();
for (var i = 0; i < tCount; i++) {
var t = tankList.g1(i);
if (t) entities.push(t);
}
} catch(e) {}
// 在每个 entity 上查找相机组件
for (var i = 0; i < entities.length; i++) {
var entity = entities[i];
if (!entity || !entity.pyt_1 || !entity.pyt_1.s2a_1) continue;
var found = null;
entity.pyt_1.s2a_1.forEach(function(valueList, key) {
if (found) return;
try {
var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList;
if (!comp) return;
// 策略1:查找 $r/gq 相机组件(有 kym_1 和 lym_1 字段)
if (comp.cyw_1 && comp.dyw_1 &&
comp.cyw_1.f20_1 !== undefined && comp.dyw_1.f20_1 !== undefined) {
// 检查是否是活跃的相机(lym_1 不全为0)
if (Math.abs(comp.dyw_1.h20_1) > 0.001 || Math.abs(comp.dyw_1.f20_1) > 0.001) {
found = comp;
return;
}
}
// 策略2:查找 k7 相机控制器(有 qz9_1 和 gz9_1 字段)
if (comp.jzj_1 && comp.zzi_1) {
var camComp = comp.zzi_1;
if (camComp && camComp.cyw_1 && camComp.dyw_1 &&
(Math.abs(camComp.dyw_1.h20_1) > 0.001 || Math.abs(camComp.dyw_1.f20_1) > 0.001)) {
found = camComp;
return;
}
}
} catch(e) {}
});
if (found) {
// lym_1.h20_1 = -atan2(t, n) = 偏航角
// lym_1.f20_1 = atan2(i, e) = 俯仰角
return {
yaw: -found.dyw_1.h20_1, // 偏航角(转为正向)
pitch: found.dyw_1.f20_1, // 俯仰角
pos: {
x: found.cyw_1.f20_1,
y: found.cyw_1.g20_1,
z: found.cyw_1.h20_1
}
};
}
}
return null;
}
// 玩家坦克:使用公共库
function GetPlayerTank() { return lib.GetPlayerTank(gm); }
// 友方判断:使用公共库
function IsFriendly(tank, playerTank) { return lib.IsFriendly(tank, playerTank, gm); }
// ===== 获取全局碰撞检测器(uMt 组件,挂在 GameMode entity 上) =====
var UMtComponent = null;
function getUMtComponent() {
if (UMtComponent) return UMtComponent;
try {
var gmEntity = gm.jyw();
if (!gmEntity || !gmEntity.pyt_1 || !gmEntity.pyt_1.s2a_1) return null;
gmEntity.pyt_1.s2a_1.forEach(function(valueList, key) {
if (UMtComponent) return;
try {
var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList;
if (comp && typeof comp.m179 === 'function') UMtComponent = comp;
} catch(e) {}
});
if (UMtComponent) console.log('[雷达] uMt 碰撞检测器已获取');
else UMtComponent = false;
} catch(e) {
console.warn('[雷达] 获取 uMt 失败:', e.message);
}
return UMtComponent || null;
}
// mock O$ 向量:使用公共库
var MkVec = lib.MkVec;
// ===== 射线检测:从 from 点沿 dir 方向,最大距离 maxDist,返回命中距离(-1=未命中) =====
function raycastDistance(fromPos, dirX, dirY, dirZ, maxDist) {
var uMt = getUMtComponent();
if (!uMt) return -1;
try {
var startVec = MkVec(fromPos.x, fromPos.y, fromPos.z);
var dirVec = MkVec(dirX, dirY, dirZ);
var result = {
y178_1: MkVec(0,0,0), z178_1: MkVec(0,0,0),
a179_1: maxDist, c179_1: null, b179_1: null,
d179: function(t, n, i, r, e) {
this.y178_1.k26(t); this.z178_1.k26(n);
this.a179_1 = i; this.c179_1 = r; this.b179_1 = e;
},
d3: function() { this.b179_1 = null; }
};
var filter = {
o1co_1: { h1: function() { return false; }, d3: function() {} },
w179: function() { return true; },
d3: function() {}, p1co: function() {}
};
var hit = uMt.m179(startVec, dirVec, maxDist, result, filter);
return (hit && result.a179_1 < maxDist) ? result.a179_1 : -1;
} catch(e) {
return -1;
}
}
// ===== 地形高度采样(分帧,双缓冲) =====
// 双缓冲:重采样时写 newHeights,旧 heights 继续显示,完成后原子切换
// 解决闪烁:重采样期间不清空 heights,ready 保持 true
var Terrain = {
gridSize: 72, // 72x72 网格
range: 30000, // 采样范围(与雷达范围一致)
// 当前显示的缓冲区
heights: null,
originX: 0, originY: 0,
minHeight: 99999, maxHeight: -99999,
// 重采样时的新缓冲区(null=未在重采样)
newHeights: null,
newOriginX: 0, newOriginY: 0,
newMinHeight: 99999, newMaxHeight: -99999,
sampling: false,
sampleIndex: 0,
lastSampleX: 99999, lastSampleY: 99999,
samplesPerFrame: 60,
rayStartZ: 5000,
rayMaxDist: 10000,
ready: false
};
// 首次初始化(heights 为 null 时调用)
function initTerrain(playerPos) {
Terrain.heights = new Float32Array(Terrain.gridSize * Terrain.gridSize);
for (var i = 0; i < Terrain.heights.length; i++) Terrain.heights[i] = NaN;
Terrain.originX = playerPos.x;
Terrain.originY = playerPos.y;
Terrain.minHeight = 99999;
Terrain.maxHeight = -99999;
Terrain.sampleIndex = 0;
Terrain.sampling = true;
Terrain.ready = false;
}
// 开始重采样(双缓冲:不清空 heights,保持 ready 状态)
function startResample(playerPos) {
Terrain.newHeights = new Float32Array(Terrain.gridSize * Terrain.gridSize);
for (var i = 0; i < Terrain.newHeights.length; i++) Terrain.newHeights[i] = NaN;
Terrain.newOriginX = playerPos.x;
Terrain.newOriginY = playerPos.y;
Terrain.newMinHeight = 99999;
Terrain.newMaxHeight = -99999;
Terrain.sampleIndex = 0;
Terrain.sampling = true;
// 不动 heights/originX/originY/min/max/ready —— 旧数据继续显示,避免闪烁
}
function updateTerrainSampling(playerPos) {
// 首次初始化
if (!Terrain.heights) {
initTerrain(playerPos);
Terrain.lastSampleX = playerPos.x;
Terrain.lastSampleY = playerPos.y;
} else if (!Terrain.sampling) {
// 玩家移动超过 range/3 重新采样(双缓冲,不阻塞显示)
var dx = playerPos.x - Terrain.lastSampleX;
var dy = playerPos.y - Terrain.lastSampleY;
if (dx*dx + dy*dy > (Terrain.range/3)*(Terrain.range/3)) {
Terrain.lastSampleX = playerPos.x;
Terrain.lastSampleY = playerPos.y;
startResample(playerPos);
}
}
if (!Terrain.sampling) return;
var uMt = getUMtComponent();
if (!uMt) { Terrain.sampling = false; return; }
var halfRange = Terrain.range / 2;
var cellSize = Terrain.range / (Terrain.gridSize - 1);
var sampled = 0;
// 目标缓冲区:重采样时写 newHeights,首次时写 heights
var target = Terrain.newHeights || Terrain.heights;
var originX = Terrain.newHeights ? Terrain.newOriginX : Terrain.originX;
var originY = Terrain.newHeights ? Terrain.newOriginY : Terrain.originY;
var minH = Terrain.newHeights ? Terrain.newMinHeight : Terrain.minHeight;
var maxH = Terrain.newHeights ? Terrain.newMaxHeight : Terrain.maxHeight;
while (Terrain.sampleIndex < Terrain.gridSize * Terrain.gridSize && sampled < Terrain.samplesPerFrame) {
var gx = Terrain.sampleIndex % Terrain.gridSize;
var gy = Math.floor(Terrain.sampleIndex / Terrain.gridSize);
var wx = originX - halfRange + gx * cellSize;
var wy = originY - halfRange + gy * cellSize;
// 向下射线检测地面高度
var dist = raycastDistance(
{ x: wx, y: wy, z: Terrain.rayStartZ },
0, 0, -1, Terrain.rayMaxDist
);
var height = dist > 0 ? (Terrain.rayStartZ - dist) : NaN;
target[Terrain.sampleIndex] = height;
if (!isNaN(height)) {
if (height < minH) minH = height;
if (height > maxH) maxH = height;
}
Terrain.sampleIndex++;
sampled++;
}
// 回写 min/max(基本类型,需手动赋值)
if (Terrain.newHeights) {
Terrain.newMinHeight = minH;
Terrain.newMaxHeight = maxH;
} else {
Terrain.minHeight = minH;
Terrain.maxHeight = maxH;
}
if (Terrain.sampleIndex >= Terrain.gridSize * Terrain.gridSize) {
// 采样完成:原子切换
if (Terrain.newHeights) {
Terrain.heights = Terrain.newHeights;
Terrain.originX = Terrain.newOriginX;
Terrain.originY = Terrain.newOriginY;
Terrain.minHeight = Terrain.newMinHeight;
Terrain.maxHeight = Terrain.newMaxHeight;
Terrain.newHeights = null;
console.log('[雷达] 地形重采样完成, 高度范围:', Terrain.minHeight.toFixed(0), '~', Terrain.maxHeight.toFixed(0));
} else {
console.log('[雷达] 地形采样完成, 高度范围:', Terrain.minHeight.toFixed(0), '~', Terrain.maxHeight.toFixed(0));
}
Terrain.sampling = false;
Terrain.ready = true;
}
}
var _terrainCanvas = null;
function drawTerrain(ctx, playerPos, playerAngle) {
if (!Terrain.ready || !Terrain.heights) return;
if (!_terrainCanvas) {
_terrainCanvas = document.createElement('canvas');
_terrainCanvas.width = radius * 2;
_terrainCanvas.height = radius * 2;
}
var tCtx = _terrainCanvas.getContext('2d');
var w = radius * 2;
var imgData = tCtx.createImageData(w, w);
var data = imgData.data;
var cosA = Math.cos(playerAngle);
var sinA = Math.sin(playerAngle);
var halfRange = Terrain.range / 2;
var cellSize = Terrain.range / (Terrain.gridSize - 1);
var heightRange = Terrain.maxHeight - Terrain.minHeight;
if (heightRange < 1) heightRange = 1;
var originX = Terrain.originX - halfRange;
var originY = Terrain.originY - halfRange;
var scale = Terrain.range / (radius * 2); // 雷达像素→世界距离
// 超出网格范围/未采样点的默认暗色(避免空洞)
var DEF_R = 15, DEF_G = 20, DEF_B = 35, DEF_A = 180;
// 遍历每个雷达像素,反算世界坐标,双线性插值查询高度图
// 海拔配色:蓝(低)→绿(中)→紫(高)
for (var py = 0; py < w; py++) {
for (var px = 0; px < w; px++) {
var ox = px - radius; // 相对中心x
var oy = radius - py; // 相对中心y(反转)
// 逆旋转(雷达→世界)
var rdx = ox * cosA + oy * sinA;
var rdy = -ox * sinA + oy * cosA;
var wx = playerPos.x + rdx * scale;
var wy = playerPos.y + rdy * scale;
var idx = (py * w + px) * 4;
// 双线性插值查询高度图
var gxf = (wx - originX) / cellSize;
var gyf = (wy - originY) / cellSize;
var gx0 = Math.floor(gxf);
var gy0 = Math.floor(gyf);
// 超出网格范围:填充默认暗色(不跳过,解决高度图不全)
if (gx0 < 0 || gx0 >= Terrain.gridSize || gy0 < 0 || gy0 >= Terrain.gridSize) {
data[idx] = DEF_R; data[idx + 1] = DEF_G; data[idx + 2] = DEF_B; data[idx + 3] = DEF_A;
continue;
}
var gx1 = Math.min(gx0 + 1, Terrain.gridSize - 1);
var gy1 = Math.min(gy0 + 1, Terrain.gridSize - 1);
var fx = gxf - gx0;
var fy = gyf - gy0;
// 四个角的高度
var h00 = Terrain.heights[gy0 * Terrain.gridSize + gx0];
var h10 = Terrain.heights[gy0 * Terrain.gridSize + gx1];
var h01 = Terrain.heights[gy1 * Terrain.gridSize + gx0];
var h11 = Terrain.heights[gy1 * Terrain.gridSize + gx1];
// 未采样到的点:填充默认暗色
if (isNaN(h00) || isNaN(h10) || isNaN(h01) || isNaN(h11)) {
data[idx] = DEF_R; data[idx + 1] = DEF_G; data[idx + 2] = DEF_B; data[idx + 3] = DEF_A;
continue;
}
// 双线性插值
var h0 = h00 * (1 - fx) + h10 * fx; // y=gy0 行 x 方向插值
var h1 = h01 * (1 - fx) + h11 * fx; // y=gy1 行 x 方向插值
var h = h0 * (1 - fy) + h1 * fy; // y 方向插值
var norm = (h - Terrain.minHeight) / heightRange;
// 蓝(50,100,230) → 绿(80,210,90) → 紫(190,80,220)
var r, g, b;
if (norm < 0.5) {
var t = norm * 2; // 0~1
r = Math.floor(50 + (80 - 50) * t);
g = Math.floor(100 + (210 - 100) * t);
b = Math.floor(230 + (90 - 230) * t);
} else {
var t = (norm - 0.5) * 2; // 0~1
r = Math.floor(80 + (190 - 80) * t);
g = Math.floor(210 + (80 - 210) * t);
b = Math.floor(90 + (220 - 90) * t);
}
data[idx] = r;
data[idx + 1] = g;
data[idx + 2] = b;
data[idx + 3] = 200;
}
}
tCtx.putImageData(imgData, 0, 0);
ctx.save();
ctx.globalAlpha = 0.6;
ctx.drawImage(_terrainCanvas, cx - radius, cy - radius);
ctx.restore();
}
// ===== 创建Canvas =====
var canvas = document.createElement('canvas');
canvas.id = 'hornetRadarCanvas';
canvas.width = 320;
canvas.height = 320;
canvas.style.cssText = 'position:fixed;top:10px;right:10px;z-index:99999;pointer-events:auto;cursor:move;border:2px solid rgba(255,255,255,0.3);border-radius:5px;background:rgba(0,0,0,0.3);box-shadow:0 0 10px rgba(0,0,0,0.3);';
document.body.appendChild(canvas);
if (lib.MakeDraggable) lib.MakeDraggable(canvas);
var ctx = canvas.getContext('2d');
var cx = canvas.width / 2;
var cy = canvas.height / 2;
var radius = 150;
var radarRange = 30000; // 雷达范围(世界单位),可通过 Alt+/Alt- 调整
var RADAR_MIN = 3000, RADAR_MAX = 60000, RADAR_STEP = 1.5;
// 从 localStorage 恢复范围
try {
var __savedRR = localStorage.getItem('hornet_radar_range');
if (__savedRR) {
var __rr = parseFloat(__savedRR);
if (!isNaN(__rr) && __rr >= RADAR_MIN && __rr <= RADAR_MAX) radarRange = __rr;
}
} catch(e) {}
// 透明度
var RadarOpacity = 1.0;
try {
var __savedRO = localStorage.getItem('hornet_radar_opacity');
if (__savedRO) {
var __ro = parseFloat(__savedRO);
if (!isNaN(__ro) && __ro >= 0.2 && __ro <= 1) {
RadarOpacity = __ro;
canvas.style.opacity = String(__ro);
}
}
} catch(e) {}
// ===== Alt+/Alt- 调整雷达范围(Alt+ 放大看局部,Alt- 缩小看全场) =====
function adjustRange(zoomIn) {
var newRange = zoomIn
? Math.max(RADAR_MIN, radarRange / RADAR_STEP)
: Math.min(RADAR_MAX, radarRange * RADAR_STEP);
if (newRange === radarRange) return;
radarRange = newRange;
try { localStorage.setItem('hornet_radar_range', String(radarRange)); } catch(e) {}
Terrain.range = newRange;
// 强制下一帧 updateTerrainSampling 触发重采样(双缓冲:保持旧数据可见)
Terrain.lastSampleX = 99999;
Terrain.lastSampleY = 99999;
// 不设 ready=false —— 旧 heights 继续显示,避免闪烁
// 若正在采样则中止,用新 range 重新开始
Terrain.newHeights = null;
Terrain.sampling = false;
console.log('[雷达] 范围' + (zoomIn ? '缩小(放大局部)' : '扩大(看全场)') + ': ' + (radarRange / 100).toFixed(0) + 'm');
}
var _keyHandler = function(e) {
if (!e.altKey) return;
var key = e.key.toLowerCase();
if (key === '+' || key === '=' || e.code === 'NumpadAdd' || e.code === 'Equal') {
e.preventDefault();
adjustRange(true);
} else if (key === '-' || key === '_' || e.code === 'NumpadSubtract' || e.code === 'Minus') {
e.preventDefault();
adjustRange(false);
}
};
window.addEventListener('keydown', _keyHandler);
// ===== 雷达状态 =====
var radar = {
canvas: canvas,
ctx: ctx,
running: true,
frameCount: 0,
tankCount: 0,
playerFound: false,
lastPlayerPos: null, // 上一帧玩家位置(用于计算朝向)
playerHeading: 0, // 玩家朝向角度(弧度)
stop: function() {
this.running = false;
if (canvas.parentNode) canvas.parentNode.removeChild(canvas);
window.removeEventListener('keydown', _keyHandler);
console.log('雷达已停止');
},
setRange: function(r) {
var n = parseFloat(r);
if (!isNaN(n) && n >= RADAR_MIN && n <= RADAR_MAX) {
radarRange = n;
try { localStorage.setItem('hornet_radar_range', String(n)); } catch(e) {}
Terrain.range = n;
Terrain.lastSampleX = 99999; Terrain.lastSampleY = 99999;
Terrain.newHeights = null; Terrain.sampling = false;
console.log('[雷达] 范围设为: ' + (n/100).toFixed(0) + 'm');
} else {
console.warn('[雷达] 范围应在 ' + (RADAR_MIN/100) + '~' + (RADAR_MAX/100) + 'm');
}
},
getRange: function() { return radarRange; },
setOpacity: function(o) {
var n = parseFloat(o);
if (!isNaN(n) && n >= 0.2 && n <= 1) {
RadarOpacity = n;
canvas.style.opacity = String(n);
try { localStorage.setItem('hornet_radar_opacity', String(n)); } catch(e) {}
} else {
console.warn('[雷达] 透明度应在 0.2~1');
}
},
getOpacity: function() { return RadarOpacity; }
};
window.__hornetRadar__ = radar;
// ===== 渲染循环 =====
function render() {
if (!radar.running) return;
radar.frameCount++;
try {
var tankList = gm.q100_1.r11l();
var count = tankList.e1();
radar.tankCount = count;
// 获取玩家坦克(带备用方案)
var playerTank = GetPlayerTank();
radar.playerFound = (playerTank !== null && playerTank !== undefined);
// 自由视角激活时,用虚拟相机位置
var fl = window.__hornetFreeLook__;
var flActive = fl && typeof fl.IsActive === 'function' && fl.IsActive();
// 获取玩家位置和方向
var playerPos = flActive ? fl.GetPosition() : (playerTank ? GetTankPosition(playerTank) : null);
var playerDir = flActive ? fl.GetDirection() : (playerTank ? getTankDirection(playerTank) : null);
// 地形高度采样(分帧)
if (playerPos) updateTerrainSampling(playerPos);
// 清空Canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 画雷达背景(方形)
ctx.fillStyle = 'rgba(0, 20, 0, 0.4)';
ctx.fillRect(cx - radius, cy - radius, radius * 2, radius * 2);
ctx.strokeStyle = 'rgba(100, 255, 100, 0.5)';
ctx.lineWidth = 2;
ctx.strokeRect(cx - radius, cy - radius, radius * 2, radius * 2);
// 画十字线
ctx.beginPath();
ctx.moveTo(cx - radius, cy);
ctx.lineTo(cx + radius, cy);
ctx.moveTo(cx, cy - radius);
ctx.lineTo(cx, cy + radius);
ctx.strokeStyle = 'rgba(100, 255, 100, 0.2)';
ctx.lineWidth = 1;
ctx.stroke();
// 画距离网格线(方形)
for (var r = radius / 3; r < radius; r += radius / 3) {
ctx.beginPath();
ctx.moveTo(cx - radius, cy - r);
ctx.lineTo(cx + radius, cy - r);
ctx.moveTo(cx - radius, cy + r);
ctx.lineTo(cx + radius, cy + r);
ctx.moveTo(cx - r, cy - radius);
ctx.lineTo(cx - r, cy + radius);
ctx.moveTo(cx + r, cy - radius);
ctx.lineTo(cx + r, cy + radius);
ctx.strokeStyle = 'rgba(100, 255, 100, 0.15)';
ctx.stroke();
}
// 获取相机朝向(偏航角)
var camHeading = getCameraHeading();
var playerAngle = radar.playerHeading;
// 自由视角激活时,直接用虚拟相机 yaw(取反:雷达旋转方向与相机yaw相反)
if (flActive && playerDir) {
playerAngle = -playerDir.yaw;
radar.playerHeading = playerAngle;
} else if (camHeading && camHeading.yaw !== undefined) {
// 使用相机偏航角作为雷达朝向
playerAngle = camHeading.yaw;
radar.playerHeading = playerAngle;
} else if (playerPos && radar.lastPlayerPos) {
// 备用方案:通过玩家位置变化计算朝向
var ddx = playerPos.x - radar.lastPlayerPos.x;
var ddy = playerPos.y - radar.lastPlayerPos.y;
var moveDist = Math.sqrt(ddx * ddx + ddy * ddy);
if (moveDist > 0.5) {
playerAngle = Math.atan2(ddx, ddy);
radar.playerHeading = playerAngle;
}
}
if (playerPos) radar.lastPlayerPos = playerPos;
radar._camHeading = camHeading; // 保存用于调试
// 绘制地形高度图
if (playerPos) drawTerrain(ctx, playerPos, playerAngle);
// 绘制所有坦克
var friendlyCount = 0, enemyCount = 0;
for (var i = 0; i < count; i++) {
var tank = tankList.g1(i);
if (!tank) continue;
// 跳过玩家自己
if (playerTank && tank === playerTank) continue;
var pos = GetTankPosition(tank);
if (!pos || !playerPos) continue;
// 计算相对位置(x和y是水平面)
var dx = pos.x - playerPos.x;
var dy = pos.y - playerPos.y;
// 旋转到相机朝向(相机朝上=北)
var cosA = Math.cos(playerAngle);
var sinA = Math.sin(playerAngle);
var rx = dx * cosA - dy * sinA;
var ry = dx * sinA + dy * cosA;
// 缩放到雷达范围
var radarX = cx + (rx / radarRange) * radius;
var radarY = cy - (ry / radarRange) * radius; // 注意y轴反转(屏幕y向下)
// 限制在雷达方形内
if (radarX < cx - radius) radarX = cx - radius;
if (radarX > cx + radius) radarX = cx + radius;
if (radarY < cy - radius) radarY = cy - radius;
if (radarY > cy + radius) radarY = cy + radius;
// 判断友方/敌方
var friendly = IsFriendly(tank, playerTank);
var color = friendly ? '#4488ff' : '#ff4444';
if (friendly) friendlyCount++; else enemyCount++;
// 画坦克点
ctx.beginPath();
ctx.arc(radarX, radarY, 5, 0, 2 * Math.PI);
ctx.fillStyle = color;
ctx.fill();
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 1;
ctx.stroke();
}
// 画玩家(中心,三角形朝上)
ctx.beginPath();
ctx.moveTo(cx, cy - 7);
ctx.lineTo(cx - 5, cy + 5);
ctx.lineTo(cx + 5, cy + 5);
ctx.closePath();
ctx.fillStyle = '#00ff00';
ctx.fill();
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 1;
ctx.stroke();
// 画北向指示
ctx.fillStyle = 'rgba(100, 255, 100, 0.8)';
ctx.font = '10px Arial';
ctx.textAlign = 'center';
ctx.fillText('N', cx, cy - radius + 12);
// 信息文本
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
ctx.font = '9px Arial';
ctx.textAlign = 'left';
ctx.fillText('Tanks: ' + count + ' (B:' + friendlyCount + ' R:' + enemyCount + ')', 5, 12);
if (radar.playerFound) {
ctx.fillText('Player: ' + (playerTank ? playerTank.hyt_1 : '?'), 5, 24);
} else {
ctx.fillText('Player: NOT FOUND', 5, 24);
}
ctx.fillText('Cam: ' + (radar._camHeading ? 'OK' : 'NULL'), 5, 36);
ctx.fillText('Angle: ' + (radar.playerHeading * 180 / Math.PI).toFixed(0), 5, 48);
ctx.fillText('Range: ' + (radarRange / 100).toFixed(0) + 'm', 5, 60);
ctx.fillText('Frame: ' + radar.frameCount, 5, canvas.height - 16);
ctx.fillText('Alt+/-: zoom', 5, canvas.height - 5);
} catch(e) {
ctx.fillStyle = 'rgba(255, 0, 0, 0.8)';
ctx.font = '10px Arial';
ctx.textAlign = 'left';
ctx.fillText('Error: ' + e.message, 5, 15);
}
requestAnimationFrame(render);
}
// 启动渲染
requestAnimationFrame(render);
console.log('雷达已启动');
console.log('提示: window.__hornetRadar__.stop() 停止雷达');
console.log('提示: window.__hornetRadar__.canvas 获取Canvas元素');
}
var Tool = {
Start: function() {
if (window.__hornetRadar__ && window.__hornetRadar__.running) return;
_start();
},
Stop: function() {
if (window.__hornetRadar__) window.__hornetRadar__.stop();
},
Status: function() {
return (window.__hornetRadar__ && window.__hornetRadar__.running) ? '运行中' : '已停止';
},
GetRunning: function() {
return !!(window.__hornetRadar__ && window.__hornetRadar__.running);
},
SetRange: function(r) { if (window.__hornetRadar__) window.__hornetRadar__.setRange(r); },
GetRange: function() { return window.__hornetRadar__ ? window.__hornetRadar__.getRange() : 30000; },
SetOpacity: function(o) { if (window.__hornetRadar__) window.__hornetRadar__.setOpacity(o); },
GetOpacity: function() { return window.__hornetRadar__ ? window.__hornetRadar__.getOpacity() : 1.0; }
};
window.__hornetTools__ = window.__hornetTools__ || {};
window.__hornetTools__.Radar = Tool;
console.log('[3DTank] 雷达模块已加载');
})();
// ===== hornet_3d_wireframe.js =====
// ===== 3DTank3D透视 - 线框/描边双模式 =====
// 在浏览器控制台运行(需先运行 hook_gamemode.js)
// 功能:在全屏叠加层上绘制敌方坦克的3D模型
// 模式:
// wireframe - 完整线框(绘制所有边)
// outline - 屏幕空间轮廓描边(只画外轮廓,合并所有mesh节点)
// 切换:window.__hornet3DWireframe__.setMode('outline')
(function() {
function _start() {
console.log('===== 3DTank3D透视启动 =====');
if (!window.__gameMode__) {
console.error('请先运行 hook_gamemode.js');
return;
}
// ===== 清理旧透视 =====
if (window.__hornet3DWireframe__) {
window.__hornet3DWireframe__.stop();
console.log('已停止旧透视');
}
if (window.__hornet3DOutline__) {
window.__hornet3DOutline__.stop();
console.log('已停止旧描边透视');
}
var gm = window.__gameMode__;
// ===== 当前模式 =====
var Mode = 'outline'; // 'wireframe' | 'outline'
try {
var __savedMode = localStorage.getItem('hornet_wireframe_mode');
if (__savedMode === 'wireframe' || __savedMode === 'outline') Mode = __savedMode;
} catch(e) {}
// ===== 线宽设置 =====
var LineWidth = 2;
try {
var __savedLW = localStorage.getItem('hornet_wireframe_linewidth');
if (__savedLW) {
var __n = parseFloat(__savedLW);
if (!isNaN(__n) && __n >= 0.5 && __n <= 8) LineWidth = __n;
}
} catch(e) {}
// ===== 渲染模式 =====
// 'team' - 队伍模式:所有坦克按队伍渲染(蓝=友方,红=敌方)
// 'whitelist' - 白名单:只有名单中的坦克渲染(按队伍颜色),其他不渲染
// 'blacklist' - 黑名单:名单中的坦克不渲染,其他按队伍颜色渲染
var HighlightMode = 'team';
try {
var __savedHM = localStorage.getItem('hornet_wireframe_highlight_mode');
if (__savedHM && ['team','whitelist','blacklist'].indexOf(__savedHM) >= 0) {
HighlightMode = __savedHM;
}
} catch(e) {}
// ===== 名单 =====
var HighlightList = [];
try {
var __savedHL = localStorage.getItem('hornet_highlight_list');
if (__savedHL) HighlightList = JSON.parse(__savedHL);
} catch(e) {}
function SaveHighlightList() {
try { localStorage.setItem('hornet_highlight_list', JSON.stringify(HighlightList)); } catch(e) {}
}
function IsInList(name) {
if (!name) return false;
for (var i = 0; i < HighlightList.length; i++) {
if (HighlightList[i] === name) return true;
}
return false;
}
// 判断坦克是否应渲染(根据 HighlightMode)
function ShouldRender(name) {
switch (HighlightMode) {
case 'team': return true; // 全部渲染
case 'whitelist': return IsInList(name); // 名单内渲染
case 'blacklist': return !IsInList(name); // 名单外渲染
default: return true;
}
}
// ===== 引用公共库(hornet_lib.js) =====
var lib = window.__hornetLib__;
if (!lib) {
console.error('[透视] 请先运行 hornet_lib.js');
return;
}
var teamFns = lib.LoadTeamFunctions();
var ZCt = teamFns.ZCt, m2Fn = teamFns.m2Fn;
// 公共函数别名(调用处不变)
var FindComponentByField = lib.FindComponentByField;
var GetTankPosition = lib.GetTankPosition;
var GetTankState = lib.GetTankState;
var IsTankAlive = lib.IsTankAlive;
var GetTankName = lib.GetTankName;
var GetTankHealth = lib.GetTankHealth;
function GetPlayerTank() { return lib.GetPlayerTank(gm); }
function IsFriendly(tank, playerTank) { return lib.IsFriendly(tank, playerTank, gm); }
function GetCameraInfo() {
// 自由视角激活时,用虚拟相机参数(位置/方向跟随鼠标)
var fl = window.__hornetFreeLook__;
if (fl && typeof fl.IsActive === 'function' && fl.IsActive()) {
var pos = fl.GetPosition();
var dir = fl.GetDirection();
if (pos && dir) {
return { pos: pos, yaw: dir.yaw, pitch: dir.pitch, fov: 1.0471976 };
}
}
return lib.GetCameraInfo(gm);
}
var Project3D = lib.Project3D;
var TransformVertex = lib.TransformVertex;
// ===== 获取 KMt / TDt 渲染组件 =====
function GetKMtComponent(entity) {
var comp = FindComponentByField(entity, 'u14l_1');
if (comp) return comp;
comp = FindComponentByField(entity, 't14l_1');
if (comp) return comp;
comp = FindComponentByField(entity, 'p14l_1');
return comp;
}
function GetTDtComponent(entity) {
var comp = FindComponentByField(entity, 'n11c_1');
if (comp) return comp;
comp = FindComponentByField(entity, 'q112_1');
return comp;
}
// ===== 从GPU读取顶点位置数据(缓冲区[0]) =====
function ReadVertexPositionsFromTp(tp) {
if (!tp || !tp.t4w_1) return null;
var tl = tp.t4w_1.g1(0);
if (!tl || !tl.d3x_1) return null;
var vq = tl.d3x_1;
var bs = vq.s3h_1;
if (!bs || !bs.b74_1) return null;
var gl = bs.b74_1;
var webglBuf = vq.r3x_1 && vq.r3x_1.z73_1;
if (!webglBuf) return null;
gl.bindBuffer(gl.ARRAY_BUFFER, webglBuf);
var bufSize = gl.getBufferParameter(gl.ARRAY_BUFFER, gl.BUFFER_SIZE);
if (bufSize <= 0) return null;
var f32 = new Float32Array(bufSize / 4);
gl.getBufferSubData(gl.ARRAY_BUFFER, 0, f32);
return f32;
}
// ===== 从GPU读取索引数据 =====
function ReadIndicesFromTp(tp) {
if (!tp.y4w_1 || typeof tp.y4w_1.e1 !== 'function') return null;
var meshCount = tp.y4w_1.e1();
if (meshCount === 0) return null;
var allIndices = [];
for (var m = 0; m < meshCount; m++) {
var il = tp.y4w_1.g1(m);
if (!il || !il.q3p_1) continue;
var fq = il.q3p_1;
var bs = fq.s3h_1;
if (!bs || !bs.b74_1) continue;
var gl = bs.b74_1;
var webglBuf = fq.j3x_1 && fq.j3x_1.z73_1;
if (!webglBuf) continue;
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, webglBuf);
var bufSize = gl.getBufferParameter(gl.ELEMENT_ARRAY_BUFFER, gl.BUFFER_SIZE);
if (bufSize <= 0) continue;
var indices = new Uint16Array(bufSize / 2);
gl.getBufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, indices);
allIndices.push(indices);
}
return allIndices;
}
// ===== GPU 数据缓存(按 Tp 资源引用,避免重复 GPU 读取) =====
var GpuDataCache = new Map();
var GpuCacheFrameCount = 0;
var GpuCacheClearInterval = 600;
var StrideDiagPrinted = new WeakSet();
function GetGpuData(tp) {
if (!tp) return null;
if (GpuDataCache.has(tp)) return GpuDataCache.get(tp);
var positions = ReadVertexPositionsFromTp(tp);
var indices = ReadIndicesFromTp(tp);
if (positions && indices && indices.length > 0) {
// 计算最大索引值,推断顶点 stride
var maxIdx = 0;
for (var i = 0; i < indices.length; i++) {
var idxArr = indices[i];
for (var j = 0; j < idxArr.length; j++) {
if (idxArr[j] > maxIdx) maxIdx = idxArr[j];
}
}
var stride = 3;
var vc3 = Math.floor(positions.length / 3);
if (maxIdx >= vc3) {
// stride=3 顶点数不够,尝试常见 interleaved 布局
var candidates = [8, 11, 5, 7, 14];
for (var c = 0; c < candidates.length; c++) {
var vc = Math.floor(positions.length / candidates[c]);
if (maxIdx < vc) { stride = candidates[c]; break; }
}
}
var data = { positions: positions, indices: indices, stride: stride };
GpuDataCache.set(tp, data);
if (!StrideDiagPrinted.has(tp)) {
StrideDiagPrinted.add(tp);
console.log('[透视][GPU数据] stride=' + stride + ', 缓冲区=' + positions.length + ' float, 最大索引=' + maxIdx + ', 顶点数=' + Math.floor(positions.length / stride));
}
return data;
}
return null;
}
// ===== 幽灵节点检测(三重检测:z34_1 + 世界矩阵 + 位置) =====
var NodeMatrixHistory = new WeakMap();
var TankPrevPos = new Map();
var GhostThreshold = 30;
var GhostCooldown = 120;
function IsNodeGhost(bpNode, tankIsMoving) {
if (!bpNode || !bpNode.n35_1) return false;
// 方案1:z34_1 字段检测(主方案)
var z34Val = bpNode.g35_1;
if (z34Val !== undefined && z34Val < 0.9) return true;
// 方案2:世界矩阵更新检测(后备方案)
var curMatrix = bpNode.n35_1;
var hist = NodeMatrixHistory.get(bpNode);
if (!hist) {
hist = { m: new Array(16), unchanged: 0, cooldown: 0 };
for (var i = 0; i < 16; i++) hist.m[i] = curMatrix[i];
NodeMatrixHistory.set(bpNode, hist);
return false;
}
var same = true;
for (var i = 0; i < 16; i++) {
if (Math.abs(hist.m[i] - curMatrix[i]) > 0.01) { same = false; break; }
}
if (same) hist.unchanged++;
else {
hist.unchanged = 0;
for (var i = 0; i < 16; i++) hist.m[i] = curMatrix[i];
}
if (hist.cooldown > 0) { hist.cooldown--; return true; }
if (tankIsMoving && hist.unchanged > GhostThreshold) {
hist.cooldown = GhostCooldown;
return true;
}
return false;
}
// ===== 递归收集mesh节点(每帧重新获取 Bp 节点引用) =====
function CollectMeshNodes(bpNode, nodes, depth, group) {
if (!bpNode || depth > 5) return;
var name = bpNode.e35_1 || '';
if (bpNode.t3m_1) {
var gpuData = GetGpuData(bpNode.t3m_1);
if (gpuData) {
nodes.push({
name: name, node: bpNode, tp: bpNode.t3m_1,
positions: gpuData.positions, indices: gpuData.indices, stride: gpuData.stride, group: group
});
}
}
if (bpNode.q4x_1 && typeof bpNode.q4x_1.e1 === 'function') {
try {
var lodCount = bpNode.q4x_1.e1();
for (var lod = 0; lod < lodCount && lod < 1; lod++) {
var lodLevel = bpNode.q4x_1.g1(lod);
if (!lodLevel) continue;
var lodTp = lodLevel.t3m_1 || lodLevel.v3m_1;
if (!lodTp) continue;
var lodGpuData = GetGpuData(lodTp);
if (lodGpuData) {
nodes.push({
name: name + '_lod' + lod, node: bpNode, tp: lodTp,
positions: lodGpuData.positions, indices: lodGpuData.indices, stride: lodGpuData.stride, group: group
});
}
}
} catch(e) {}
}
if (bpNode.p35_1 && typeof bpNode.p35_1.e1 === 'function') {
var childCount = bpNode.p35_1.e1();
for (var c = 0; c < childCount; c++) {
try {
var child = bpNode.p35_1.g1(c);
if (child) CollectMeshNodes(child, nodes, depth + 1, group);
} catch(e) {}
}
}
}
// ===== 提取坦克的mesh数据 =====
function GetTankMeshData(tank) {
var nodes = [];
var kmt = GetKMtComponent(tank);
if (kmt && kmt.t14l_1 && kmt.t14l_1.s17b_1) {
var skin = kmt.t14l_1;
try {
var partCount = skin.s17b_1.e1();
for (var p = 0; p < partCount; p++) {
var part = skin.s17b_1.g1(p);
if (!part || !part.v17b_1) continue;
var nodeCount = part.v17b_1.e1();
for (var n = 0; n < nodeCount; n++) {
var bpNode = part.v17b_1.g1(n);
if (!bpNode) continue;
CollectMeshNodes(bpNode, nodes, 0, 'hull');
}
}
} catch(e) {}
}
var tdt = GetTDtComponent(tank);
if (tdt && tdt.n11c_1 && tdt.n11c_1.m17a_1) {
try { CollectMeshNodes(tdt.n11c_1.m17a_1, nodes, 0, 'turret'); } catch(e) {}
}
return nodes;
}
// ===== 检查节点是否有效(幽灵检测 + 位置检查) =====
function IsNodeValid(bpNode, pos, tankIsMoving, diagStats) {
var worldMatrix = bpNode.n35_1;
if (!worldMatrix) return null;
if (diagStats) diagStats.totalNodes++;
// 幽灵检测1:z34_1 字段
var z34Val = bpNode.g35_1;
if (z34Val !== undefined && z34Val < 0.9) {
if (diagStats) diagStats.ghostNodes++;
return null;
}
// 幽灵检测2:世界矩阵更新
if (IsNodeGhost(bpNode, tankIsMoving)) {
if (diagStats) diagStats.ghostNodes++;
return null;
}
// 位置检查
var nodeDx = worldMatrix[12] - pos.x;
var nodeDy = worldMatrix[13] - pos.y;
var nodeDz = worldMatrix[14] - pos.z;
if (nodeDx*nodeDx + nodeDy*nodeDy + nodeDz*nodeDz > 4000000) {
if (diagStats) diagStats.filteredNodes++;
return null;
}
return worldMatrix;
}
// ===== 线框模式:绘制单个节点的所有边 =====
function DrawWireframeNode(ctx, meshData, worldMatrix, pos, camInfo, screenW, screenH) {
var positions = meshData.positions;
var indicesArr = meshData.indices;
var stride = meshData.stride || 3;
var vertexCount = Math.floor(positions.length / stride);
var projected = new Array(vertexCount);
for (var v = 0; v < vertexCount; v++) {
var wp = TransformVertex(worldMatrix, positions[v*stride], positions[v*stride+1], positions[v*stride+2]);
// 顶点级合理性检查
var ddx = wp.x - pos.x, ddy = wp.y - pos.y, ddz = wp.z - pos.z;
if (ddx*ddx + ddy*ddy + ddz*ddz > 6250000) {
projected[v] = null;
continue;
}
projected[v] = Project3D(wp, camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH);
}
var drawnEdges = new Set();
ctx.beginPath();
for (var mi = 0; mi < indicesArr.length; mi++) {
var indices = indicesArr[mi];
for (var idx = 0; idx < indices.length; idx += 3) {
var i0 = indices[idx], i1 = indices[idx+1], i2 = indices[idx+2];
if (!projected[i0] || !projected[i1] || !projected[i2]) continue;
var edges = [
[Math.min(i0, i1), Math.max(i0, i1)],
[Math.min(i1, i2), Math.max(i1, i2)],
[Math.min(i2, i0), Math.max(i2, i0)]
];
for (var e = 0; e < 3; e++) {
var key = edges[e][0] + '_' + edges[e][1];
if (drawnEdges.has(key)) continue;
drawnEdges.add(key);
ctx.moveTo(projected[edges[e][0]].x, projected[edges[e][0]].y);
ctx.lineTo(projected[edges[e][1]].x, projected[edges[e][1]].y);
}
}
}
ctx.stroke();
}
// ===== 描边模式:屏幕空间光栅化轮廓提取 =====
// 算法(真正的"屏幕几何"方法):
// 1. 把所有面向相机的三角形光栅化(扫描线填充)到像素掩码
// 2. 车身和炮塔在屏幕上重叠时,填充区域自动合并
// 3. 找出填充区域的边界像素(填充像素的4邻居中有未填充像素)
// 4. 只画边界像素 = 屏幕空间外轮廓
var PixelMask = null;
var PixelMaskW = 0;
var PixelMaskH = 0;
function GetPixelMask(w, h) {
if (!PixelMask || PixelMaskW !== w || PixelMaskH !== h) {
PixelMask = new Uint8Array(w * h);
PixelMaskW = w;
PixelMaskH = h;
} else {
PixelMask.fill(0);
}
return PixelMask;
}
// 扫描线光栅化三角形
function RasterizeTriangle(pixels, w, h, x0, y0, x1, y1, x2, y2) {
// 按 y 排序三个顶点
if (y0 > y1) { var t = y0; y0 = y1; y1 = t; t = x0; x0 = x1; x1 = t; }
if (y1 > y2) { var t = y1; y1 = y2; y2 = t; t = x1; x1 = x2; x2 = t; }
if (y0 > y1) { var t = y0; y0 = y1; y1 = t; t = x0; x0 = x1; x1 = t; }
var minY = Math.max(0, Math.ceil(y0));
var maxY = Math.min(h - 1, Math.floor(y2));
if (minY > maxY) return;
for (var y = minY; y <= maxY; y++) {
var xa, xb;
if (y < y1) {
// 上半部分:y0 -> y1 和 y0 -> y2
xa = (y1 === y0) ? x0 : x0 + (x1 - x0) * (y - y0) / (y1 - y0);
xb = (y2 === y0) ? x0 : x0 + (x2 - x0) * (y - y0) / (y2 - y0);
} else {
// 下半部分:y1 -> y2 和 y0 -> y2
xa = (y2 === y1) ? x1 : x1 + (x2 - x1) * (y - y1) / (y2 - y1);
xb = (y2 === y0) ? x0 : x0 + (x2 - x0) * (y - y0) / (y2 - y0);
}
if (xa > xb) { var t = xa; xa = xb; xb = t; }
var minXA = Math.max(0, Math.ceil(xa - 0.5));
var maxXA = Math.min(w - 1, Math.floor(xb + 0.5));
var rowOffset = y * w;
for (var x = minXA; x <= maxXA; x++) {
pixels[rowOffset + x] = 1;
}
}
}
function DrawTankOutline(ctx, meshNodes, pos, tankIsMoving, camInfo, screenW, screenH, diagStats, color) {
var pixels = GetPixelMask(screenW, screenH);
// 记录坦克的屏幕边界框(用于后续边界检测优化)
var minX = screenW, minY = screenH, maxX = 0, maxY = 0;
for (var ni = 0; ni < meshNodes.length; ni++) {
var meshData = meshNodes[ni];
var bpNode = meshData.node;
var worldMatrix = IsNodeValid(bpNode, pos, tankIsMoving, diagStats);
if (!worldMatrix) continue;
var positions = meshData.positions;
var indicesArr = meshData.indices;
var stride = meshData.stride || 3;
// 变换顶点到世界空间并投影到屏幕
var vertexCount = Math.floor(positions.length / stride);
var worldVerts = new Array(vertexCount);
var screenVerts = new Array(vertexCount);
for (var v = 0; v < vertexCount; v++) {
worldVerts[v] = TransformVertex(worldMatrix, positions[v*stride], positions[v*stride+1], positions[v*stride+2]);
screenVerts[v] = Project3D(worldVerts[v], camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH);
if (screenVerts[v]) {
if (screenVerts[v].x < minX) minX = screenVerts[v].x;
if (screenVerts[v].y < minY) minY = screenVerts[v].y;
if (screenVerts[v].x > maxX) maxX = screenVerts[v].x;
if (screenVerts[v].y > maxY) maxY = screenVerts[v].y;
}
}
// 遍历三角形,光栅化面向相机的
for (var mi = 0; mi < indicesArr.length; mi++) {
var indices = indicesArr[mi];
for (var i = 0; i < indices.length; i += 3) {
var a = indices[i], b = indices[i+1], c = indices[i+2];
var va = worldVerts[a], vb = worldVerts[b], vc = worldVerts[c];
var sa = screenVerts[a], sb = screenVerts[b], sc = screenVerts[c];
if (!sa || !sb || !sc) continue;
// 计算法线判断是否面向相机
var e1x = vb.x - va.x, e1y = vb.y - va.y, e1z = vb.z - va.z;
var e2x = vc.x - va.x, e2y = vc.y - va.y, e2z = vc.z - va.z;
var nx = e1y * e2z - e1z * e2y;
var ny = e1z * e2x - e1x * e2z;
var nz = e1x * e2y - e1y * e2x;
var cx = (va.x + vb.x + vc.x) / 3;
var cy = (va.y + vb.y + vc.y) / 3;
var cz = (va.z + vb.z + vc.z) / 3;
var viewX = camInfo.pos.x - cx;
var viewY = camInfo.pos.y - cy;
var viewZ = camInfo.pos.z - cz;
var dot = nx * viewX + ny * viewY + nz * viewZ;
if (dot <= 0) continue; // 背向相机,跳过
// 光栅化三角形到像素掩码
RasterizeTriangle(pixels, screenW, screenH, sa.x, sa.y, sb.x, sb.y, sc.x, sc.y);
}
}
}
// 找出边界像素并绘制(只在坦克边界框内遍历)
var bx0 = Math.max(0, Math.floor(minX) - 1);
var by0 = Math.max(0, Math.floor(minY) - 1);
var bx1 = Math.min(screenW - 1, Math.ceil(maxX) + 1);
var by1 = Math.min(screenH - 1, Math.ceil(maxY) + 1);
ctx.fillStyle = color;
var outlineCount = 0;
for (var y = by0; y <= by1; y++) {
var rowOffset = y * screenW;
for (var x = bx0; x <= bx1; x++) {
if (pixels[rowOffset + x] === 0) continue;
// 检查4个邻居是否有未填充像素
var isBoundary = false;
if (x === 0 || pixels[rowOffset + x - 1] === 0) isBoundary = true;
else if (x === screenW - 1 || pixels[rowOffset + x + 1] === 0) isBoundary = true;
else if (y === 0 || pixels[rowOffset - screenW + x] === 0) isBoundary = true;
else if (y === screenH - 1 || pixels[rowOffset + screenW + x] === 0) isBoundary = true;
if (isBoundary) {
ctx.fillRect(x, y, 3, 3);
outlineCount++;
}
}
}
// 清除像素掩码(供下一个坦克使用)
for (var y2 = by0; y2 <= by1; y2++) {
var rowOff = y2 * screenW;
for (var x2 = bx0; x2 <= bx1; x2++) {
pixels[rowOff + x2] = 0;
}
}
if (diagStats) diagStats.outlineEdges += outlineCount;
return outlineCount;
}
// ===== 创建全屏Canvas =====
var canvas = document.createElement('canvas');
canvas.id = 'hornet3DWireframeCanvas';
canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:99998;pointer-events:none;';
document.body.appendChild(canvas);
var ctx = canvas.getContext('2d');
function ResizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
ResizeCanvas();
window.addEventListener('resize', ResizeCanvas);
// ===== 诊断统计 =====
var DiagStats = { totalNodes: 0, filteredNodes: 0, renderedNodes: 0, ghostNodes: 0, outlineEdges: 0 };
// ===== ESP状态 =====
var esp = {
canvas: canvas,
ctx: ctx,
running: true,
frameCount: 0,
mode: Mode,
setMode: function(m) {
if (m === 'wireframe' || m === 'outline') {
Mode = m;
this.mode = m;
try { localStorage.setItem('hornet_wireframe_mode', m); } catch(e) {}
console.log('[透视] 模式切换为: ' + m);
} else {
console.warn('[透视] 未知模式: ' + m + ',支持: wireframe | outline');
}
},
getMode: function() { return Mode; },
setLineWidth: function(w) {
var n = parseFloat(w);
if (!isNaN(n) && n >= 0.5 && n <= 8) {
LineWidth = n;
try { localStorage.setItem('hornet_wireframe_linewidth', String(n)); } catch(e) {}
console.log('[透视] 线宽设置为: ' + n);
} else {
console.warn('[透视] 线宽应在 0.5 ~ 8 之间,当前: ' + w);
}
},
getLineWidth: function() { return LineWidth; },
setHighlightMode: function(mode) {
var valid = ['team','whitelist','blacklist'];
if (valid.indexOf(mode) >= 0) {
HighlightMode = mode;
try { localStorage.setItem('hornet_wireframe_highlight_mode', mode); } catch(e) {}
console.log('[透视] 渲染模式切换为: ' + mode);
} else {
console.warn('[透视] 未知渲染模式: ' + mode + ',支持: ' + valid.join(', '));
}
},
getHighlightMode: function() { return HighlightMode; },
setHighlightList: function(names) {
HighlightList = Array.isArray(names) ? names.slice() : [];
SaveHighlightList();
console.log('[透视] 高亮名单已更新:', HighlightList.length + '人');
},
addHighlight: function(name) {
if (name && HighlightList.indexOf(name) < 0) {
HighlightList.push(name);
SaveHighlightList();
console.log('[透视] 已添加高亮:', name);
}
},
removeHighlight: function(name) {
var idx = HighlightList.indexOf(name);
if (idx >= 0) {
HighlightList.splice(idx, 1);
SaveHighlightList();
console.log('[透视] 已移除高亮:', name);
}
},
getHighlightList: function() {
return HighlightList.slice();
},
clearHighlight: function() {
HighlightList = [];
SaveHighlightList();
console.log('[透视] 高亮名单已清空');
},
stop: function() {
this.running = false;
window.removeEventListener('resize', ResizeCanvas);
if (canvas.parentNode) canvas.parentNode.removeChild(canvas);
console.log('透视已停止');
}
};
window.__hornet3DWireframe__ = esp;
// ===== 渲染循环 =====
function Render() {
if (!esp.running) return;
esp.frameCount++;
// 定期清空 GPU 数据缓存
GpuCacheFrameCount++;
if (GpuCacheFrameCount >= GpuCacheClearInterval) {
GpuDataCache.clear();
GpuCacheFrameCount = 0;
}
DiagStats = { totalNodes: 0, filteredNodes: 0, renderedNodes: 0, ghostNodes: 0, outlineEdges: 0 };
try {
var screenW = canvas.width;
var screenH = canvas.height;
ctx.clearRect(0, 0, screenW, screenH);
var camInfo = GetCameraInfo();
if (!camInfo) {
requestAnimationFrame(Render);
return;
}
var playerTank = GetPlayerTank();
if (!playerTank) {
requestAnimationFrame(Render);
return;
}
// 相机位置为0时使用玩家位置
if (Math.abs(camInfo.pos.x) < 0.1 && Math.abs(camInfo.pos.y) < 0.1 && Math.abs(camInfo.pos.z) < 0.1) {
var pPos = GetTankPosition(playerTank);
if (pPos) camInfo.pos = { x: pPos.x, y: pPos.y, z: pPos.z + 100 };
}
var tankList = gm.q100_1.r11l();
var count = tankList.e1();
var drawnCount = 0;
for (var i = 0; i < count; i++) {
var tank = tankList.g1(i);
if (!tank || tank === playerTank) continue;
var physicsComp = FindComponentByField(tank, 'gzh_1');
if (!physicsComp || !physicsComp.gzh_1) continue;
var tankState = GetTankState(tank);
if (tankState !== 3) continue;
var pos = {
x: physicsComp.gzh_1.f20_1,
y: physicsComp.gzh_1.g20_1,
z: physicsComp.gzh_1.h20_1
};
// 坦克移动检测
var tankId = tank.hyt_1;
var prevPos = TankPrevPos.get(tankId);
var tankIsMoving = false;
if (prevPos) {
var pdx = pos.x - prevPos.x, pdy = pos.y - prevPos.y, pdz = pos.z - prevPos.z;
tankIsMoving = (pdx*pdx + pdy*pdy + pdz*pdz) > 100;
}
TankPrevPos.set(tankId, { x: pos.x, y: pos.y, z: pos.z });
// 距离检查(50000 = 500米)
var dx = pos.x - camInfo.pos.x;
var dy = pos.y - camInfo.pos.y;
var dz = pos.z - camInfo.pos.z;
var distance = Math.sqrt(dx*dx + dy*dy + dz*dz);
if (distance > 50000) continue;
var friendly = IsFriendly(tank, playerTank);
var tankName = GetTankName(tank) || ('ID:' + tank.hyt_1);
// 根据 HighlightMode 判断是否渲染该坦克
if (!ShouldRender(tankName)) continue;
// 颜色按队伍:友方=蓝,敌方=红
var color = friendly ? 'rgba(68, 136, 255, 0.6)' : 'rgba(255, 68, 68, 0.6)';
var tankColor = friendly ? 'rgba(68, 136, 255, 0.9)' : 'rgba(255, 68, 68, 0.9)';
// 先画中心点、距离和血量(独立于3D模型渲染)
var centerProj = Project3D(pos, camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH);
if (centerProj) {
// 血量信息
var health = GetTankHealth(tank);
var barW = 60, barH = 6;
var barX = centerProj.x - barW / 2;
var barY = centerProj.y - barH - 4;
if (health) {
// 背景
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
ctx.fillRect(barX - 1, barY - 1, barW + 2, barH + 2);
// 血量百分比颜色:绿>60%, 黄>30%, 红
var hpColor = health.percent > 0.6 ? '#44ff44' :
health.percent > 0.3 ? '#ffaa00' : '#ff4444';
ctx.fillStyle = hpColor;
ctx.fillRect(barX, barY, barW * health.percent, barH);
// 边框
ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
ctx.lineWidth = 1;
ctx.strokeRect(barX, barY, barW, barH);
// 名称(血量条上方)
ctx.fillStyle = tankColor;
ctx.font = 'bold 11px Arial';
ctx.textAlign = 'center';
ctx.fillText(tankName, centerProj.x, barY - 14);
// 距离(血量条上方第二行)
ctx.font = '10px Arial';
ctx.fillText(Math.round(distance / 100) + 'm', centerProj.x, barY - 3);
// 血量数值(血量条下方)
ctx.fillStyle = hpColor;
ctx.font = '10px Arial';
ctx.fillText(Math.round(health.current) + '/' + Math.round(health.max), centerProj.x, barY + barH + 10);
} else {
ctx.fillStyle = tankColor;
ctx.font = 'bold 11px Arial';
ctx.textAlign = 'center';
ctx.fillText(tankName, centerProj.x, centerProj.y - 18);
ctx.font = '10px Arial';
ctx.fillText(Math.round(distance / 100) + 'm', centerProj.x, centerProj.y - 6);
}
// 中心点
ctx.fillStyle = tankColor;
ctx.beginPath();
ctx.arc(centerProj.x, centerProj.y, 2, 0, 2 * Math.PI);
ctx.fill();
}
// 3D模型渲染(仅在mesh数据可用时)
var meshNodes = GetTankMeshData(tank);
if (!meshNodes || meshNodes.length === 0) {
drawnCount++;
continue;
}
// 检查是否有炮塔
var hasTurret = false;
for (var ti = 0; ti < meshNodes.length; ti++) {
if (meshNodes[ti].group === 'turret') { hasTurret = true; break; }
}
if (!hasTurret) {
drawnCount++;
continue;
}
ctx.strokeStyle = color;
ctx.lineWidth = LineWidth;
if (Mode === 'wireframe') {
for (var ni = 0; ni < meshNodes.length; ni++) {
var meshData = meshNodes[ni];
var bpNode = meshData.node;
var worldMatrix = IsNodeValid(bpNode, pos, tankIsMoving, DiagStats);
if (!worldMatrix) continue;
DrawWireframeNode(ctx, meshData, worldMatrix, pos, camInfo, screenW, screenH);
DiagStats.renderedNodes++;
}
} else {
DrawTankOutline(ctx, meshNodes, pos, tankIsMoving, camInfo, screenW, screenH, DiagStats, color);
}
drawnCount++;
}
// 玩家血量显示(左上角)
var playerHealth = GetTankHealth(playerTank);
if (playerHealth) {
var pBarW = 200, pBarH = 12;
var pBarX = 10, pBarY = 10;
// 背景
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
ctx.fillRect(pBarX - 1, pBarY - 1, pBarW + 2, pBarH + 2);
// 血量颜色
var pHpColor = playerHealth.percent > 0.6 ? '#44ff44' :
playerHealth.percent > 0.3 ? '#ffaa00' : '#ff4444';
ctx.fillStyle = pHpColor;
ctx.fillRect(pBarX, pBarY, pBarW * playerHealth.percent, pBarH);
// 边框
ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
ctx.lineWidth = 1;
ctx.strokeRect(pBarX, pBarY, pBarW, pBarH);
// 文字
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 11px Arial';
ctx.textAlign = 'center';
ctx.fillText('HP: ' + Math.round(playerHealth.current) + ' / ' + Math.round(playerHealth.max) +
' (' + Math.round(playerHealth.percent * 100) + '%)',
pBarX + pBarW / 2, pBarY + pBarH - 1);
}
// 屏幕信息
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.font = '10px Arial';
ctx.textAlign = 'left';
var infoText = '[' + Mode.toUpperCase() + '] | ' + drawnCount + ' tanks';
if (Mode === 'outline') {
infoText += ' | Edges: ' + DiagStats.outlineEdges;
} else {
infoText += ' | Nodes: ' + DiagStats.renderedNodes + '/' + DiagStats.totalNodes;
}
infoText += ' | Ghost: ' + DiagStats.ghostNodes + ' | Frame: ' + esp.frameCount;
ctx.fillText(infoText, 5, screenH - 5);
} catch(e) {
console.error('[透视] 错误:', e.message, e.stack ? e.stack.split('\n')[1] : '');
}
requestAnimationFrame(Render);
}
// 启动渲染
requestAnimationFrame(Render);
console.log('3D透视已启动(双模式: wireframe | outline)');
console.log('当前模式: ' + Mode);
console.log('提示: window.__hornet3DWireframe__.setMode("outline") 切换到描边模式');
console.log('提示: window.__hornet3DWireframe__.setMode("wireframe") 切换到线框模式');
console.log('提示: window.__hornet3DWireframe__.addHighlight("玩家名") 添加高亮(金色标记)');
console.log('提示: window.__hornet3DWireframe__.stop() 停止透视');
}
var Tool = {
Start: function() {
if (window.__hornet3DWireframe__ && window.__hornet3DWireframe__.running) return;
_start();
},
Stop: function() {
if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.stop();
},
Status: function() {
return (window.__hornet3DWireframe__ && window.__hornet3DWireframe__.running) ? ('运行中(' + window.__hornet3DWireframe__.mode + ')') : '已停止';
},
GetRunning: function() {
return !!(window.__hornet3DWireframe__ && window.__hornet3DWireframe__.running);
},
SetMode: function(m) {
if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.setMode(m);
},
GetMode: function() {
return window.__hornet3DWireframe__ ? window.__hornet3DWireframe__.getMode() : null;
},
SetLineWidth: function(w) {
if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.setLineWidth(w);
},
GetLineWidth: function() {
return window.__hornet3DWireframe__ ? window.__hornet3DWireframe__.getLineWidth() : 2;
},
SetHighlightMode: function(mode) {
if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.setHighlightMode(mode);
},
GetHighlightMode: function() {
return window.__hornet3DWireframe__ ? window.__hornet3DWireframe__.getHighlightMode() : 'team';
},
SetHighlightList: function(names) {
if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.setHighlightList(names);
},
AddHighlight: function(name) {
if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.addHighlight(name);
},
RemoveHighlight: function(name) {
if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.removeHighlight(name);
},
GetHighlightList: function() {
return window.__hornet3DWireframe__ ? window.__hornet3DWireframe__.getHighlightList() : [];
},
ClearHighlight: function() {
if (window.__hornet3DWireframe__) window.__hornet3DWireframe__.clearHighlight();
}
};
window.__hornetTools__ = window.__hornetTools__ || {};
window.__hornetTools__.Wireframe = Tool;
console.log('[3DTank] 3D透视模块已加载');
})();
// ===== hornet_rearview.js =====
// ===== 3DTank后视镜 - 原生渲染方案 =====
// 在浏览器控制台运行(需先运行 hook_gamemode.js)
// 功能:复用游戏原生渲染器(Ff.y3s)+ FBO 重定向,在小 canvas 中渲染玩家背后的 3D 场景
// 原理:Hook gl.bindFramebuffer 重定向到自定义 FBO,用后视相机参数调用游戏渲染器,readback 显示
(function() {
function _start() {
console.log('===== 3DTank后视镜(原生渲染版)启动 =====');
if (!window.__gameMode__) {
console.error('请先运行 hook_gamemode.js');
return;
}
if (window.__hornetRearview__) {
window.__hornetRearview__.Stop();
console.log('已停止旧后视镜');
}
var gm = window.__gameMode__;
// ===== 引用公共库(hornet_lib.js) =====
var lib = window.__hornetLib__;
if (!lib) {
console.error('[后视] 请先运行 hornet_lib.js');
return;
}
var teamFns = lib.LoadTeamFunctions();
var ZCt = teamFns.ZCt, m2Fn = teamFns.m2Fn;
// 公共函数别名(调用处不变)
var FindComponentByField = lib.FindComponentByField;
var GetTankPosition = lib.GetTankPosition;
var GetTankState = lib.GetTankState;
var IsTankAlive = lib.IsTankAlive;
var GetTankName = lib.GetTankName;
var GetTankHealth = lib.GetTankHealth;
function GetPlayerTank() { return lib.GetPlayerTank(gm); }
function IsFriendly(tank, playerTank) { return lib.IsFriendly(tank, playerTank, gm); }
function GetCameraInfo() { return lib.GetCameraInfo(gm); }
// ===== 配置 =====
var Config = {
width: 400, height: 240, fov: 1.2,
maxRange: 15000, pitchOffset: -0.05,
cameraHeight: 250, near: 50, far: 30000,
refreshInterval: 0 // 每帧渲染(0=每帧,用 PBO 异步回读避免 GPU 停顿)
};
// 从 localStorage 恢复 FOV 和透明度
try {
var __savedFov = localStorage.getItem('hornet_rearview_fov');
if (__savedFov) {
var __fv = parseFloat(__savedFov);
if (!isNaN(__fv) && __fv >= 0.5 && __fv <= 2.5) Config.fov = __fv;
}
} catch(e) {}
var RearviewOpacity = 1.0;
try {
var __savedROp = localStorage.getItem('hornet_rearview_opacity');
if (__savedROp) {
var __rop = parseFloat(__savedROp);
if (!isNaN(__rop) && __rop >= 0.2 && __rop <= 1) RearviewOpacity = __rop;
}
} catch(e) {}
// ===== UI 层禁用(防止技能条等 UI 混入后视 FBO)=====
// ff.x38_1 是渲染层队列数组,索引对应层:
// 20=Hud, 21=GuiBack, 22=GuiMain, 23=GuiFront, 24=DebugGui
// 替换为空假对象,使 z3k 填充和渲染都跳过这些层
var uiLayerIndices = [20, 21, 22, 23, 24];
var fakeQueue = {
e1: function() { return 0; },
g1: function(i) { return null; },
w2a: function(x) {},
d3: function() {},
ii: function(x) {}
};
// ===== 获取游戏渲染对象 =====
var scene = null, ff = null, rootNode = null, mainCamera = null, gl = null;
var initError = null;
try {
scene = gm.jyw().iyt_1;
console.log('[后视] 场景对象:', scene);
// hC 渲染器实例(jr.lye_1)
var renderer = scene.fyo_1;
console.log('[后视] 渲染器:', renderer);
// Ff 绘制器实例(hC.r74_1)
ff = renderer.d75_1;
console.log('[后视] Ff绘制器:', ff);
// 场景根节点(jr.kyf_1)
rootNode = scene.eyp_1;
console.log('[后视] 根节点:', rootNode);
// 主相机 Ha 实例(jr.nyf_1)
mainCamera = scene.hyp_1;
console.log('[后视] 主相机:', mainCamera);
// 游戏 gl context(Ff.p39_1.p73_1 = BS.p73_1)
gl = ff.w39_1.b74_1;
console.log('[后视] GL上下文:', gl);
if (!ff || !rootNode || !mainCamera || !gl) {
throw new Error('关键字段缺失');
}
// 探测相机字段
console.log('[后视] 相机s35_1:', mainCamera.s35_1);
console.log('[后视] 相机r35_1:', mainCamera.r35_1);
console.log('[后视] 相机b37_1:', mainCamera.b37_1);
if (!mainCamera.s35_1 || !mainCamera.r35_1) {
throw new Error('相机字段缺失');
}
} catch(e) {
initError = e;
console.error('[后视] 初始化失败:', e.message, e.stack);
}
// ===== 创建 UI canvas(显示后视画面 + UI 标记)=====
var uiCanvas = document.createElement('canvas');
uiCanvas.width = Config.width;
uiCanvas.height = Config.height;
uiCanvas.style.cssText = 'position:fixed;top:10px;right:10px;width:' + Config.width + 'px;height:' + Config.height + 'px;z-index:99999;border:2px solid rgba(120,200,255,0.6);border-radius:6px;pointer-events:auto;cursor:move;box-shadow:0 0 12px rgba(0,0,0,0.6);opacity:' + RearviewOpacity + ';';
document.body.appendChild(uiCanvas);
if (lib.MakeDraggable) lib.MakeDraggable(uiCanvas);
var uiCtx = uiCanvas.getContext('2d');
// ===== UI 绘制函数 =====
function DrawText(text, x, y, color, font, align, baseline) {
uiCtx.font = font || '12px monospace';
uiCtx.textAlign = align || 'left';
uiCtx.textBaseline = baseline || 'middle';
uiCtx.fillStyle = color || 'rgba(255,255,255,1)';
uiCtx.fillText(text, x, y);
}
function DrawTextOutlined(text, x, y, color, font, align, baseline) {
uiCtx.font = font || '12px monospace';
uiCtx.textAlign = align || 'left';
uiCtx.textBaseline = baseline || 'middle';
uiCtx.strokeStyle = 'rgba(0,0,0,0.85)';
uiCtx.lineWidth = 3;
uiCtx.strokeText(text, x, y);
uiCtx.fillStyle = color || 'rgba(255,255,255,1)';
uiCtx.fillText(text, x, y);
}
// ===== FBO 创建(后视镜渲染目标)=====
var rw = Config.width, rh = Config.height;
var rearFbo = null, rearTex = null, rearDepthRb = null;
var pixels = new Uint8Array(rw * rh * 4);
var imageData = uiCtx.createImageData(rw, rh);
// PBO 双缓冲异步回读(避免 readPixels 同步 GPU 停顿)
var pbos = [null, null];
var pboIndex = 0;
var pboReady = false; // 第一帧没有前帧数据,跳过回读
var pboSupported = false;
// Uint32 视图用于批量设置 alpha=255(修复树叶等半透明边缘)
var pixels32 = new Uint32Array(pixels.buffer);
if (gl && !initError) {
try {
rearFbo = gl.createFramebuffer();
rearTex = gl.createTexture();
rearDepthRb = gl.createRenderbuffer();
// 颜色纹理
gl.bindTexture(gl.TEXTURE_2D, rearTex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, rw, rh, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
// 深度缓冲
gl.bindRenderbuffer(gl.RENDERBUFFER, rearDepthRb);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, rw, rh);
// 组装 FBO
gl.bindFramebuffer(gl.FRAMEBUFFER, rearFbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, rearTex, 0);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, rearDepthRb);
var fboStatus = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
console.log('[后视] FBO状态:', fboStatus, '(完整=' + gl.FRAMEBUFFER_COMPLETE + ')');
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.bindTexture(gl.TEXTURE_2D, null);
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
if (fboStatus !== gl.FRAMEBUFFER_COMPLETE) {
throw new Error('FBO不完整: ' + fboStatus);
}
// 创建 PBO 双缓冲(WebGL2 PIXEL_PACK_BUFFER)
try {
var byteSize = rw * rh * 4;
for (var pi = 0; pi < 2; pi++) {
pbos[pi] = gl.createBuffer();
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbos[pi]);
gl.bufferData(gl.PIXEL_PACK_BUFFER, byteSize, gl.STREAM_READ);
}
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
pboSupported = true;
console.log('[后视] PBO 双缓冲已创建(异步回读)');
} catch(pboErr) {
console.warn('[后视] PBO 不支持,回退到同步 readPixels:', pboErr.message);
pboSupported = false;
}
} catch(e) {
initError = e;
console.error('[后视] FBO创建失败:', e.message);
}
}
// ===== Hook gl.bindFramebuffer(后视渲染时重定向到我们的 FBO)=====
var rearviewRendering = false;
var mainFbo = null;
var origBindFramebuffer = null;
var origH3c = null, origG3c = null;
if (gl && !initError) {
origBindFramebuffer = gl.bindFramebuffer.bind(gl);
gl.bindFramebuffer = function(target, fbo) {
// 记录主 FBO(非后视渲染时的绑定)
if (!rearviewRendering && target === gl.FRAMEBUFFER) {
mainFbo = fbo;
}
// 后视渲染时重定向到我们的 FBO
if (rearviewRendering && target === gl.FRAMEBUFFER && fbo !== rearFbo) {
return origBindFramebuffer(target, rearFbo);
}
return origBindFramebuffer(target, fbo);
};
console.log('[后视] gl.bindFramebuffer 已 Hook');
// Hook BS.h3c/g3c:后视渲染时返回 FBO 尺寸(修复 screenSize/aspect ratio)
// y3s 会调用 BS.h3c()/g3c() 获取 drawingBufferWidth/Height 设置相机 l36_1/m36_1
// 不 Hook 的话 aspect ratio 会用游戏 canvas 尺寸(如1920x1080),导致画面比例错误
origH3c = ff.w39_1.o3c.bind(ff.w39_1);
origG3c = ff.w39_1.n3c.bind(ff.w39_1);
ff.w39_1.o3c = function() { return rearviewRendering ? rw : origH3c(); };
ff.w39_1.n3c = function() { return rearviewRendering ? rh : origG3c(); };
console.log('[后视] BS.o3c/n3c 已 Hook');
}
// ===== 渲染后视镜 =====
var frameCount = 0;
var running = true;
var debugInfo = {
frame: 0, tankCount: 0, renderedTanks: 0, lastError: null
};
function RenderRearview() {
if (!running || initError || !ff || !mainCamera || !rootNode || !gl || !rearFbo) return;
frameCount++;
debugInfo.frame = frameCount;
// 频率控制
if (frameCount % (Config.refreshInterval + 1) !== 0) return;
var playerTank = GetPlayerTank();
if (!playerTank) {
uiCtx.clearRect(0, 0, rw, rh);
DrawText('未找到玩家坦克', rw/2, rh/2, 'rgba(255,200,80,1)', '13px monospace', 'center');
return;
}
var playerPos = GetTankPosition(playerTank);
if (!playerPos) {
uiCtx.clearRect(0, 0, rw, rh);
DrawText('无位置数据', rw/2, rh/2, 'rgba(255,200,80,1)', '13px monospace', 'center');
return;
}
var camInfo = GetCameraInfo();
if (!camInfo) {
uiCtx.clearRect(0, 0, rw, rh);
DrawText('无相机数据', rw/2, rh/2, 'rgba(255,200,80,1)', '13px monospace', 'center');
return;
}
// 后视方向修复:camInfo.yaw = $r.lym_1.h20_1 是 roll(不是 yaw!)
// o37: forward.y = d[5] = cos(pitch)*cos(roll) + sin(pitch)*sin(yaw)*sin(roll)
// yaw=π 时 sin(yaw)=0,故后视 forward.y = cos(rearPitch)*cos(roll)
// roll = camInfo.yaw+π → cos(camInfo.yaw+π) = -cos(camInfo.yaw)
// 故后视 forward.y = -cos(rearPitch)*cos(camInfo.yaw),主视为 +cos(pitch)*cos(camInfo.yaw)
// → 后视水平方向 = -主视水平方向(朝后看 ✓,与 rearPitch 符号无关)
// o37: forward.z = d[9] = sin(pitch)*cos(yaw)
// 后视 yaw=π: forward.z = -sin(rearPitch)
// 要后视与主视同垂直方向(低头时都朝下看地形):rearPitch = -camInfo.pitch
// → 后视 forward.z = -sin(-camInfo.pitch) = sin(camInfo.pitch) = 主视 forward.z ✓
// up 向量:rearPitch=-camInfo.pitch 时 up.z<0(朝下),FBO 画面颠倒
// GL readPixels(自底向上) + putImageData(自顶向下) = Y 翻转,使显示画面正立 ✓
var rearPitch = -camInfo.pitch;
// 后视相机位置:玩家位置 + 高度偏移
var camPosX = playerPos.x;
var camPosY = playerPos.y;
var camPosZ = playerPos.z + Config.cameraHeight;
// ===== 保存主相机参数 =====
var savedPitch = mainCamera.s35_1.f20_1;
var savedYaw = mainCamera.s35_1.g20_1;
var savedRoll = mainCamera.s35_1.h20_1;
var savedX = mainCamera.r35_1.f20_1;
var savedY = mainCamera.r35_1.g20_1;
var savedZ = mainCamera.r35_1.h20_1;
var savedL36 = mainCamera.s36_1;
var savedM36 = mainCamera.t36_1;
var savedS36 = mainCamera.z36_1;
var savedQ36 = mainCamera.x36_1;
var savedR36 = mainCamera.y36_1;
// 临时暂停自由视角的 h37 hook,避免覆盖后视镜相机参数
var fl = window.__hornetFreeLook__;
if (fl && typeof fl.SetSuspended === 'function') fl.SetSuspended(true);
try {
// ===== 设置后视相机参数 =====
// g=π 使水平方向反向(朝后看),h=yaw+π 保持水平视角正确
// f=-pitch 使垂直方向与主视一致(低头看后方地形)
// up 朝下 → FBO 画面颠倒 → GL readback Y 翻转修正
mainCamera.s35_1.f20_1 = rearPitch;
mainCamera.s35_1.g20_1 = Math.PI;
mainCamera.s35_1.h20_1 = camInfo.yaw + Math.PI;
mainCamera.r35_1.f20_1 = camPosX;
mainCamera.r35_1.g20_1 = camPosY;
mainCamera.r35_1.h20_1 = camPosZ;
mainCamera.s36_1 = rw;
mainCamera.t36_1 = rh;
mainCamera.z36_1 = Config.fov;
mainCamera.x36_1 = Config.near;
mainCamera.y36_1 = Config.far;
// 更新相机矩阵(viewMatrix, projectionMatrix, viewProjectionMatrix)
mainCamera.o37();
// 保存后视视图矩阵副本(用于 UI 标记投影,finally 会恢复相机所以需提前保存)
var rearViewMatrix = new Float32Array(mainCamera.b37_1);
// ===== 保存并禁用阴影/后处理(避免它们切换 FBO 覆盖 rearFbo)=====
// p38_1.x3n_1 控制 z39_1(阴影),y3n_1 控制 c3a_1/d3a_1(后处理),z3n_1 控制 f3a_1
// 禁用后 y3s 只执行主场景渲染 z3k,跳过所有 FBO 切换的 pass
var savedX3n = ff.w38_1.e3o_1;
var savedY3n = ff.w38_1.f3o_1;
var savedZ3n = ff.w38_1.g3o_1;
ff.w38_1.e3o_1 = false;
ff.w38_1.f3o_1 = false;
ff.w38_1.g3o_1 = false;
// ===== 禁用 UI 渲染层(防止技能条等 UI 混入后视 FBO)=====
// 保存 UI 层队列,替换为空假对象,y3s 后恢复
var savedUiQueues = {};
for (var ui = 0; ui < uiLayerIndices.length; ui++) {
var li = uiLayerIndices[ui];
savedUiQueues[li] = ff.e39_1[li];
ff.e39_1[li] = fakeQueue;
}
// ===== 渲染到 FBO =====
rearviewRendering = true;
// 清除 FBO
origBindFramebuffer(gl.FRAMEBUFFER, rearFbo);
gl.viewport(0, 0, rw, rh);
gl.clearColor(0.15, 0.18, 0.22, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 调用游戏原生渲染器(已禁用阴影/后处理/UI层,只渲染 3D 场景)
ff.f3t(rootNode, mainCamera);
rearviewRendering = false;
// ===== Readback FBO 到 canvas(PBO 异步回读,避免 GPU 管线停顿)=====
origBindFramebuffer(gl.FRAMEBUFFER, rearFbo);
if (pboSupported) {
// PBO 异步回读:当前帧发起异步 readPixels 到 PBO[current],读取上一帧的 PBO[previous] 数据
var curPbo = pboIndex;
var prevPbo = 1 - pboIndex;
// 发起当前帧的异步 readPixels(写入 PBO,不阻塞 GPU)
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbos[curPbo]);
gl.readPixels(0, 0, rw, rh, gl.RGBA, gl.UNSIGNED_BYTE, 0);
// 读取上一帧的 PBO 数据(此时 GPU 应已完成)
if (pboReady) {
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbos[prevPbo]);
gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, pixels);
}
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
pboIndex = prevPbo;
pboReady = true;
} else {
// 回退:同步 readPixels
gl.readPixels(0, 0, rw, rh, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
}
// 显示画面(g=π 渲染已自带 Y 翻转,直接复制)
// 修复半透明边缘:强制 alpha=255(树叶等 alpha 混合会导致 canvas 透明)
for (var ai = 0; ai < pixels32.length; ai++) {
pixels32[ai] = pixels32[ai] | 0xFF000000; // little-endian: 高字节=alpha
}
imageData.data.set(pixels);
uiCtx.putImageData(imageData, 0, 0);
// 恢复主 FBO
origBindFramebuffer(gl.FRAMEBUFFER, mainFbo);
} catch(e) {
debugInfo.lastError = e.message;
console.error('[后视] 渲染失败:', e.message, e.stack);
uiCtx.clearRect(0, 0, rw, rh);
DrawText('渲染错误: ' + e.message, rw/2, rh/2, 'rgba(255,100,100,1)', '11px monospace', 'center');
} finally {
// ===== 确保 rearviewRendering 已重置 =====
rearviewRendering = false;
// ===== 恢复阴影/后处理(异常时也要恢复)=====
try {
if (typeof savedX3n !== 'undefined') {
ff.w38_1.e3o_1 = savedX3n;
ff.w38_1.f3o_1 = savedY3n;
ff.w38_1.g3o_1 = savedZ3n;
}
} catch(e) {
console.error('[后视] 恢复阴影/后处理失败:', e.message);
}
// ===== 恢复 UI 渲染层(异常时也要恢复,否则游戏 UI 永久失效)=====
try {
if (savedUiQueues) {
for (var ui2 = 0; ui2 < uiLayerIndices.length; ui2++) {
var li2 = uiLayerIndices[ui2];
ff.e39_1[li2] = savedUiQueues[li2];
}
}
} catch(e) {
console.error('[后视] 恢复 UI 层失败:', e.message);
}
// ===== 恢复主相机参数 =====
try {
mainCamera.s35_1.f20_1 = savedPitch;
mainCamera.s35_1.g20_1 = savedYaw;
mainCamera.s35_1.h20_1 = savedRoll;
mainCamera.r35_1.f20_1 = savedX;
mainCamera.r35_1.g20_1 = savedY;
mainCamera.r35_1.h20_1 = savedZ;
mainCamera.s36_1 = savedL36;
mainCamera.t36_1 = savedM36;
mainCamera.z36_1 = savedS36;
mainCamera.x36_1 = savedQ36;
mainCamera.y36_1 = savedR36;
mainCamera.o37();
} catch(e) {
console.error('[后视] 恢复相机失败:', e.message);
}
// 恢复自由视角的 h37 hook
if (fl && typeof fl.SetSuspended === 'function') fl.SetSuspended(false);
}
// ===== 绘制 UI 标记(坦克位置标记)=====
if (rearViewMatrix) {
DrawUIMarkers(playerTank, playerPos, camInfo, rearViewMatrix);
}
}
// ===== UI 标记绘制 =====
function DrawUIMarkers(playerTank, playerPos, camInfo, viewMatrix) {
var tankList = gm.q100_1.r11l();
var count = tankList.e1();
debugInfo.tankCount = count;
var visibleTanks = 0;
for (var i = 0; i < count; i++) {
var tank = tankList.g1(i);
if (!tank || tank === playerTank) continue;
if (!IsTankAlive(tank)) continue;
var pos = GetTankPosition(tank);
if (!pos) continue;
// 使用视图矩阵投影(与 3D 渲染一致)
// o37 变换前矩阵行:right(d[0,4,8]), forward(d[1,5,9]), up(d[2,6,10])
// Ua 变换后行:right, up, -forward
// 故变换后相机空间:camX=右,camY=上(up·rel),camZ=深度(-forward·rel,负=前方)
// 后视相机 up 朝下:世界"上"rel.z>0 → camY<0;渲染+FBO readback 使世界"上"显示在 canvas 顶部
// marker 用 + 号匹配:camY<0(上方) → +负 → screen.y<rh/2 → canvas 顶部
var wx = pos.x, wy = pos.y, wz = pos.z;
var m = viewMatrix;
var camX = m[0]*wx + m[4]*wy + m[8]*wz + m[12];
var camY = m[1]*wx + m[5]*wy + m[9]*wz + m[13]; // 上:up·rel
var camZ = m[2]*wx + m[6]*wy + m[10]*wz + m[14]; // 深度:-forward·rel,负=前方
if (camZ >= -0.1) continue; // 剔除相机后方(深度非负)
var depth = -camZ; // 正值=距离
var focalLength = rh / (2 * Math.tan(Config.fov / 2));
var screen = {
x: rw / 2 + (camX / depth) * focalLength,
y: rh / 2 + (camY / depth) * focalLength, // +号匹配 GL readback Y 翻转后的画面
depth: depth
};
if (screen.x < 0 || screen.x > rw || screen.y < 0 || screen.y > rh) continue;
if (screen.depth > Config.maxRange) continue;
visibleTanks++;
var friendly = IsFriendly(tank, playerTank);
var color = friendly ? 'rgba(80,180,255,1)' : 'rgba(255,80,60,1)';
var bgColor = friendly ? 'rgba(40,90,140,0.7)' : 'rgba(140,40,30,0.7)';
// 方框标记
uiCtx.strokeStyle = color;
uiCtx.lineWidth = 1.5;
uiCtx.strokeRect(screen.x - 6, screen.y - 6, 12, 12);
// 名称
var name = GetTankName(tank);
if (name) {
DrawTextOutlined(name, screen.x, screen.y - 12, color, '10px monospace', 'center');
}
// 血量条
var health = GetTankHealth(tank);
if (health) {
var barW = 24, barH = 3;
var barX = screen.x - barW / 2, barY = screen.y + 8;
uiCtx.fillStyle = 'rgba(0,0,0,0.6)';
uiCtx.fillRect(barX, barY, barW, barH);
var hpColor = health.percent > 0.6 ? 'rgba(80,255,80,1)' :
health.percent > 0.3 ? 'rgba(255,200,60,1)' : 'rgba(255,80,60,1)';
uiCtx.fillStyle = hpColor;
uiCtx.fillRect(barX, barY, barW * health.percent, barH);
}
}
debugInfo.renderedTanks = visibleTanks;
// 边框 + 标题
uiCtx.strokeStyle = 'rgba(120,200,255,0.4)';
uiCtx.lineWidth = 1;
uiCtx.strokeRect(0, 0, rw, rh);
DrawTextOutlined('后视镜', 8, 12, 'rgba(120,200,255,0.9)', '11px monospace', 'left');
DrawTextOutlined(visibleTanks + '/' + count, rw - 8, 12, 'rgba(180,180,180,0.9)', '11px monospace', 'right');
if (debugInfo.lastError) {
DrawTextOutlined('ERR: ' + debugInfo.lastError.substring(0, 30), 8, rh - 8, 'rgba(255,100,100,0.9)', '10px monospace', 'left');
}
}
// ===== 主循环 =====
function Loop() {
if (!running) return;
requestAnimationFrame(Loop);
RenderRearview();
}
// ===== 停止函数 =====
function Stop() {
running = false;
console.log('[后视] 停止后视镜');
// 恢复 gl.bindFramebuffer
if (origBindFramebuffer && gl) {
gl.bindFramebuffer = origBindFramebuffer;
console.log('[后视] gl.bindFramebuffer 已恢复');
}
// 恢复 BS.h3c/g3c
if (origH3c && ff && ff.w39_1) {
ff.w39_1.o3c = origH3c;
ff.w39_1.n3c = origG3c;
console.log('[后视] BS.o3c/n3c 已恢复');
}
// 清理 FBO
if (gl && rearFbo) gl.deleteFramebuffer(rearFbo);
if (gl && rearTex) gl.deleteTexture(rearTex);
if (gl && rearDepthRb) gl.deleteRenderbuffer(rearDepthRb);
// 清理 PBO
if (gl && pbos[0]) gl.deleteBuffer(pbos[0]);
if (gl && pbos[1]) gl.deleteBuffer(pbos[1]);
// 移除 canvas
if (uiCanvas && uiCanvas.parentNode) {
uiCanvas.parentNode.removeChild(uiCanvas);
}
window.__hornetRearview__ = null;
}
// ===== 启动 =====
window.__hornetRearview__ = {
Stop: Stop, Config: Config,
setFov: function(f) {
var n = parseFloat(f);
if (!isNaN(n) && n >= 0.5 && n <= 2.5) {
Config.fov = n;
try { localStorage.setItem('hornet_rearview_fov', String(n)); } catch(e) {}
console.log('[后视] FOV 设为: ' + n);
} else {
console.warn('[后视] FOV 应在 0.5~2.5');
}
},
getFov: function() { return Config.fov; },
setOpacity: function(o) {
var n = parseFloat(o);
if (!isNaN(n) && n >= 0.2 && n <= 1) {
RearviewOpacity = n;
uiCanvas.style.opacity = String(n);
try { localStorage.setItem('hornet_rearview_opacity', String(n)); } catch(e) {}
} else {
console.warn('[后视] 透明度应在 0.2~1');
}
},
getOpacity: function() { return RearviewOpacity; }
};
if (initError) {
uiCtx.clearRect(0, 0, rw, rh);
DrawText('初始化失败:', rw/2, rh/2 - 10, 'rgba(255,100,100,1)', '13px monospace', 'center');
DrawText(initError.message, rw/2, rh/2 + 10, 'rgba(255,200,80,1)', '11px monospace', 'center');
console.error('[后视] 初始化失败,请检查字段访问');
return;
}
console.log('[后视] 原生渲染后视镜已启动');
console.log('[后视] 配置: ' + rw + 'x' + rh + ', FOV=' + Config.fov + ', 间隔=' + Config.refreshInterval);
Loop();
}
var Tool = {
Start: function() {
if (window.__hornetRearview__) return;
_start();
},
Stop: function() {
if (window.__hornetRearview__) window.__hornetRearview__.Stop();
},
Status: function() {
return window.__hornetRearview__ ? '运行中' : '已停止';
},
GetRunning: function() {
return !!window.__hornetRearview__;
},
SetFov: function(f) { if (window.__hornetRearview__) window.__hornetRearview__.setFov(f); },
GetFov: function() { return window.__hornetRearview__ ? window.__hornetRearview__.getFov() : 1.2; },
SetOpacity: function(o) { if (window.__hornetRearview__) window.__hornetRearview__.setOpacity(o); },
GetOpacity: function() { return window.__hornetRearview__ ? window.__hornetRearview__.getOpacity() : 1.0; }
};
window.__hornetTools__ = window.__hornetTools__ || {};
window.__hornetTools__.Rearview = Tool;
console.log('[3DTank] 后视镜模块已加载');
})();
// ===== hornet_aim_warning.js =====
// ===== 3DTank瞄准警告 - 敌方坦克瞄准线预判 =====
// 在浏览器控制台运行(需先运行 hook_gamemode.js)
// 功能:当敌方坦克炮塔朝向玩家时,立刻显示HUD警告并标记方向
// 独立轻量级脚本,不依赖3D透视渲染
(function() {
function _start() {
console.log('===== 3DTank瞄准警告启动 =====');
if (!window.__gameMode__) {
console.error('请先运行 hook_gamemode.js');
return;
}
// ===== 清理旧实例 =====
if (window.__hornetAimWarning__) {
window.__hornetAimWarning__.Stop();
console.log('已停止旧瞄准警告');
}
var gm = window.__gameMode__;
// ===== 设置参数(localStorage 持久化)=====
// Sensitivity: 灵敏度系数,>1 更灵敏(阈值更大,更易触发),<1 更迟钝
var Sensitivity = 1.0;
try {
var __savedSens = localStorage.getItem('hornet_aim_sensitivity');
if (__savedSens) {
var __s = parseFloat(__savedSens);
if (!isNaN(__s) && __s >= 0.3 && __s <= 3.0) Sensitivity = __s;
}
} catch(e) {}
// SoundEnabled: 警告声音开关
var SoundEnabled = false;
try {
var __savedSE = localStorage.getItem('hornet_aim_sound');
if (__savedSE === 'true') SoundEnabled = true;
} catch(e) {}
var _audioCtx = null;
var _lastBeepTime = 0;
function PlayBeep() {
if (!SoundEnabled) return;
var now = Date.now();
if (now - _lastBeepTime < 300) return; // 最多每300ms响一次
_lastBeepTime = now;
try {
if (!_audioCtx) _audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var osc = _audioCtx.createOscillator();
var gain = _audioCtx.createGain();
osc.frequency.value = 880;
osc.type = 'square';
gain.gain.value = 0.08;
osc.connect(gain);
gain.connect(_audioCtx.destination);
osc.start();
gain.gain.exponentialRampToValueAtTime(0.001, _audioCtx.currentTime + 0.15);
osc.stop(_audioCtx.currentTime + 0.15);
} catch(e) {}
}
// ===== 引用公共库(hornet_lib.js) =====
var lib = window.__hornetLib__;
if (!lib) {
console.error('[瞄准] 请先运行 hornet_lib.js');
return;
}
var teamFns = lib.LoadTeamFunctions();
var ZCt = teamFns.ZCt, m2Fn = teamFns.m2Fn;
// 公共函数别名(调用处不变)
var FindComponentByField = lib.FindComponentByField;
var GetTankPosition = lib.GetTankPosition;
var GetTankState = lib.GetTankState;
var IsTankAlive = lib.IsTankAlive;
// ===== 获取 TDt 武器皮肤组件(炮塔) =====
function GetTDtComponent(entity) {
var comp = FindComponentByField(entity, 'n11c_1');
if (comp) return comp;
comp = FindComponentByField(entity, 'q112_1');
return comp;
}
// 坦克名称/血量:使用公共库
var GetTankName = lib.GetTankName;
var GetTankHealth = lib.GetTankHealth;
// ===== 获取炮塔瞄准角度 =====
// 从炮塔 Bp 节点世界矩阵(g35_1)提取水平面前向方向
// 返回 { angle: 炮塔朝向与"敌方→玩家"方向的夹角, distance: 距离, turretAngle: 炮塔朝向角度, turretDir: {x,y} }
function GetTurretAimAngle(tank, enemyPos, playerPos) {
var tdt = GetTDtComponent(tank);
if (!tdt || !tdt.n11c_1 || !tdt.n11c_1.m17a_1) return null;
var bpNode = tdt.n11c_1.m17a_1;
var m = bpNode.n35_1;
if (!m) return null;
// 敌方→玩家方向(x,y 水平面)
var dx = playerPos.x - enemyPos.x;
var dy = playerPos.y - enemyPos.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 100) return null; // 太近不判断
var tx = dx / dist, ty = dy / dist;
// 尝试 X 轴(m0,m1) 和 Y 轴(m4,m5),取最小夹角
var minAngle = Math.PI;
var turretAngle = 0;
var turretDir = { x: 1, y: 0 };
var axes = [
{ x: m[0], y: m[1] },
{ x: m[4], y: m[5] }
];
for (var ai = 0; ai < axes.length; ai++) {
var ax = axes[ai].x, ay = axes[ai].y;
var len = Math.sqrt(ax * ax + ay * ay);
if (len < 0.001) continue;
var nx = ax / len, ny = ay / len;
var dot = nx * tx + ny * ty;
var ang = Math.acos(Math.max(-1, Math.min(1, dot)));
if (ang < minAngle) {
minAngle = ang;
turretAngle = Math.atan2(ny, nx);
turretDir = { x: nx, y: ny };
}
}
return { angle: minAngle, distance: dist, turretAngle: turretAngle, turretDir: turretDir };
}
// ===== 全局碰撞检测器(uMt 组件,挂在 GameMode entity 上) =====
var UMtComponent = null;
function GetUMtComponent() {
if (UMtComponent) return UMtComponent;
try {
var gmEntity = gm.jyw();
if (!gmEntity || !gmEntity.pyt_1 || !gmEntity.pyt_1.s2a_1) return null;
gmEntity.pyt_1.s2a_1.forEach(function(valueList, key) {
if (UMtComponent) return;
try {
var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList;
if (comp && typeof comp.m179 === 'function') UMtComponent = comp;
} catch(e) {}
});
if (UMtComponent) console.log('[瞄准] uMt 碰撞检测器已获取');
else UMtComponent = false;
} catch(e) {
console.warn('[瞄准] 获取 uMt 组件失败:', e.message);
}
return UMtComponent || null;
}
// mock O$ 向量:使用公共库
var MkVec = lib.MkVec;
// ===== 射线检测视线遮挡(缓存:每10帧更新) =====
var RaycastCache = new Map();
var RaycastFailCount = 0;
function IsLineOfSightBlocked(enemyPos, playerPos, tankId, frame) {
var cached = RaycastCache.get(tankId);
if (cached && (frame - cached.frame) < 10) return cached.blocked;
var uMt = GetUMtComponent();
if (!uMt) return false;
var blocked = false;
try {
var dx = playerPos.x - enemyPos.x;
var dy = playerPos.y - enemyPos.y;
var dz = playerPos.z - enemyPos.z;
var dist = Math.sqrt(dx*dx + dy*dy + dz*dz);
if (dist < 200) {
RaycastCache.set(tankId, { frame: frame, blocked: false });
return false;
}
var ndx = dx / dist, ndy = dy / dist, ndz = dz / dist;
var startVec = MkVec(enemyPos.x, enemyPos.y, enemyPos.z + 200);
var dirVec = MkVec(ndx, ndy, ndz);
var maxDist = dist - 300;
var result = {
y178_1: MkVec(0,0,0),
z178_1: MkVec(0,0,0),
a179_1: maxDist,
c179_1: null,
b179_1: null,
d179: function(t, n, i, r, e) {
this.y178_1.k26(t);
this.z178_1.k26(n);
this.a179_1 = i;
this.c179_1 = r;
this.b179_1 = e;
},
d3: function() { this.b179_1 = null; }
};
var filter = {
o1co_1: { h1: function() { return false; }, d3: function() {} },
w179: function() { return true; },
d3: function() { this.o1co_1.d3(); },
p1co: function() {}
};
var hit = uMt.m179(startVec, dirVec, maxDist, result, filter);
blocked = !!hit && result.a179_1 < (maxDist - 50);
RaycastFailCount = 0;
} catch(e) {
RaycastFailCount++;
if (RaycastFailCount <= 3) console.warn('[瞄准] 射线检测异常:', e.message);
blocked = false;
}
RaycastCache.set(tankId, { frame: frame, blocked: blocked });
return blocked;
}
// 玩家坦克/友方/相机/投影:使用公共库
function GetPlayerTank() { return lib.GetPlayerTank(gm); }
function IsFriendly(tank, playerTank) { return lib.IsFriendly(tank, playerTank, gm); }
function GetCameraInfo() { return lib.GetCameraInfo(gm); }
var Project3D = lib.Project3D;
// ===== 创建 Canvas =====
var canvas = document.createElement('canvas');
canvas.id = 'hornetAimWarningCanvas';
canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:99998;pointer-events:none;';
document.body.appendChild(canvas);
var ctx = canvas.getContext('2d');
function ResizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
ResizeCanvas();
window.addEventListener('resize', ResizeCanvas);
// ===== 警告闪烁状态 =====
var WarningPulse = 0; // 闪烁动画相位
// ===== 死亡检测状态 =====
var PlayerPrevHealth = -1; // 上一帧血量(-1=未初始化)
var DeathInfo = null; // 死亡信息 { killerName, distance, expected, time, threats }
var DeathDisplayTime = 360; // 死亡信息显示帧数(约6秒)
var DeathStats = { total: 0, expected: 0, unexpected: 0 }; // 死亡统计
// ===== 主循环 =====
var running = true;
var frameCount = 0;
function Loop() {
if (!running) return;
frameCount++;
requestAnimationFrame(Loop);
var screenW = canvas.width;
var screenH = canvas.height;
ctx.clearRect(0, 0, screenW, screenH);
var playerTank = GetPlayerTank();
if (!playerTank) return;
var playerPos = GetTankPosition(playerTank);
if (!playerPos) return;
var camInfo = GetCameraInfo();
if (!camInfo) return;
// 相机位置为0时使用玩家位置
if (Math.abs(camInfo.pos.x) < 0.1 && Math.abs(camInfo.pos.y) < 0.1 && Math.abs(camInfo.pos.z) < 0.1) {
camInfo.pos = { x: playerPos.x, y: playerPos.y, z: playerPos.z + 100 };
}
var tankList = gm.q100_1.r11l();
var count = tankList.e1();
var threats = [];
// 遍历所有敌方坦克,检测炮塔朝向
for (var i = 0; i < count; i++) {
var tank = tankList.g1(i);
if (!tank || tank === playerTank) continue;
if (!IsTankAlive(tank)) continue;
var pos = GetTankPosition(tank);
if (!pos) continue;
// 只检测敌方
if (IsFriendly(tank, playerTank)) continue;
var aimInfo = GetTurretAimAngle(tank, pos, playerPos);
if (!aimInfo) continue;
// 瞄准阈值随距离变化:近距离~11度(0.20rad),远距离~3度
// 公式:0.20 / (1 + distance/8000),乘以灵敏度系数
var aimThreshold = 0.20 / (1 + aimInfo.distance / 8000) * Sensitivity;
if (aimInfo.angle >= aimThreshold) continue;
// 墙体阻隔判断
var blocked = IsLineOfSightBlocked(pos, playerPos, tank.hyt_1, frameCount);
if (blocked) continue;
// 记录威胁
var proj = Project3D(pos, camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH);
var tankName = GetTankName(tank) || ('ID:' + tank.hyt_1);
threats.push({
pos: pos,
distance: aimInfo.distance,
angle: aimInfo.angle,
turretDir: aimInfo.turretDir,
screenProj: proj,
tankId: tank.hyt_1,
tankName: tankName
});
}
// ===== 死亡检测 =====
var playerHealth = GetTankHealth(playerTank);
var playerCurHealth = playerHealth ? playerHealth.current : 0;
// 检测血量从 >0 变为 0 = 死亡
if (PlayerPrevHealth > 0 && playerCurHealth <= 0) {
// 死亡时,从 threats 列表找最近的瞄准者作为可能的击杀者
var killer = null;
var minDist = Infinity;
for (var ki = 0; ki < threats.length; ki++) {
if (threats[ki].distance < minDist) {
minDist = threats[ki].distance;
killer = threats[ki];
}
}
DeathInfo = {
killerName: killer ? killer.tankName : null,
killerDistance: killer ? killer.distance : 0,
expected: threats.length > 0, // 有瞄准者 = 意料之内
threatCount: threats.length,
time: frameCount
};
DeathStats.total++;
if (DeathInfo.expected) DeathStats.expected++;
else DeathStats.unexpected++;
console.log('[瞄准] 玩家死亡! ' + (killer ? '被 ' + killer.tankName + ' 击杀' : '被偷击杀') +
' | 意料之' + (threats.length > 0 ? '内' : '外') +
' | 瞄准者: ' + threats.length +
' | 统计: ' + DeathStats.total + '死 (内' + DeathStats.expected + '/外' + DeathStats.unexpected + ')');
}
// 复活检测:血量恢复 >0
if (PlayerPrevHealth <= 0 && playerCurHealth > 0) {
DeathInfo = null;
console.log('[瞄准] 玩家复活,重置死亡信息');
}
PlayerPrevHealth = playerCurHealth;
// ===== 绘制瞄准线(每个威胁的弹道轨迹) =====
if (threats.length > 0) {
PlayBeep();
ctx.strokeStyle = 'rgba(255, 40, 40, 0.5)';
ctx.lineWidth = 2;
for (var ti = 0; ti < threats.length; ti++) {
var tr = threats[ti];
var startPos = { x: tr.pos.x, y: tr.pos.y, z: tr.pos.z + 200 }; // 炮塔高度
var endPos = { x: playerPos.x, y: playerPos.y, z: playerPos.z };
var segs = 20;
var prevScreen = null;
ctx.beginPath();
for (var si = 0; si <= segs; si++) {
var st = si / segs;
var swp = {
x: startPos.x + (endPos.x - startPos.x) * st,
y: startPos.y + (endPos.y - startPos.y) * st,
z: startPos.z + (endPos.z - startPos.z) * st
};
var sp = Project3D(swp, camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH);
if (sp) {
if (prevScreen) ctx.lineTo(sp.x, sp.y);
else ctx.moveTo(sp.x, sp.y);
prevScreen = sp;
} else {
prevScreen = null;
}
}
ctx.stroke();
}
}
// ===== 每个威胁的方向标记 =====
for (var thi = 0; thi < threats.length; thi++) {
var threat = threats[thi];
var proj = threat.screenProj;
if (proj && proj.x >= 0 && proj.x <= screenW && proj.y >= 0 && proj.y <= screenH) {
// ===== 威胁在屏幕内:在威胁位置画红色警告三角形 =====
var mx = proj.x, my = proj.y - 35;
ctx.strokeStyle = 'rgba(255, 0, 0, 1)';
ctx.lineWidth = 2.5;
ctx.fillStyle = 'rgba(255, 0, 0, 0.4)';
ctx.beginPath();
ctx.moveTo(mx, my - 10);
ctx.lineTo(mx - 9, my + 6);
ctx.lineTo(mx + 9, my + 6);
ctx.closePath();
ctx.fill();
ctx.stroke();
// 感叹号
ctx.fillStyle = 'rgba(255, 255, 255, 1)';
ctx.font = 'bold 13px Arial';
ctx.textAlign = 'center';
ctx.fillText('!', mx, my + 4);
// 距离标签
ctx.fillStyle = 'rgba(255, 200, 200, 0.95)';
ctx.font = 'bold 11px Arial';
ctx.fillText(Math.round(threat.distance / 100) + 'm', mx, my + 22);
// 坦克名称
ctx.fillStyle = 'rgba(255, 240, 240, 0.95)';
ctx.font = 'bold 11px Arial';
ctx.fillText(threat.tankName, mx, my + 36);
} else {
// ===== 威胁不在屏幕内:屏幕边缘红色箭头指示方向 =====
var tdx = threat.pos.x - playerPos.x;
var tdy = threat.pos.y - playerPos.y;
var tlen = Math.sqrt(tdx * tdx + tdy * tdy);
if (tlen > 0.001) {
var ndx = tdx / tlen, ndy = tdy / tlen;
// 计算相对相机偏航角的方向
var fwdX = -Math.sin(camInfo.yaw), fwdY = Math.cos(camInfo.yaw);
var rgtX = Math.cos(camInfo.yaw), rgtY = Math.sin(camInfo.yaw);
var screenRight = ndx * rgtX + ndy * rgtY;
var screenFwd = ndx * fwdX + ndy * fwdY;
var sAng = Math.atan2(screenRight, screenFwd);
var edgeR = Math.min(screenW, screenH) * 0.42;
var ex = screenW / 2 + Math.sin(sAng) * edgeR;
var ey = screenH / 2 - Math.cos(sAng) * edgeR;
// 限制在屏幕边缘内
ex = Math.max(30, Math.min(screenW - 30, ex));
ey = Math.max(30, Math.min(screenH - 30, ey));
// 画箭头(指向威胁方向)
ctx.fillStyle = 'rgba(255, 0, 0, 0.95)';
ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(ex + Math.sin(sAng) * 14, ey - Math.cos(sAng) * 14);
ctx.lineTo(ex + Math.sin(sAng + 2.5) * 9, ey - Math.cos(sAng + 2.5) * 9);
ctx.lineTo(ex + Math.sin(sAng - 2.5) * 9, ey - Math.cos(sAng - 2.5) * 9);
ctx.closePath();
ctx.fill();
ctx.stroke();
// 距离标签
ctx.fillStyle = 'rgba(255, 255, 255, 0.95)';
ctx.font = 'bold 11px Arial';
ctx.textAlign = 'center';
ctx.fillText(Math.round(threat.distance / 100) + 'm', ex, ey + 22);
// 坦克名称
ctx.fillStyle = 'rgba(255, 240, 240, 0.95)';
ctx.font = 'bold 10px Arial';
ctx.fillText(threat.tankName, ex, ey + 36);
}
}
}
// ===== 左侧中央常显警告框(有威胁时) =====
if (threats.length > 0) {
WarningPulse += 0.15;
var pulseAlpha = 0.55 + Math.sin(WarningPulse) * 0.2; // 脉动透明度
// 动态高度:标题区 + 威胁列表
var maxListed = 4; // 最多列出4个威胁名称
var listedCount = Math.min(threats.length, maxListed);
var boxW = 240;
var boxH = 70 + listedCount * 18 + (threats.length > maxListed ? 16 : 0);
var boxX = 20;
var boxY = screenH / 2 - boxH / 2;
// 警告框背景(脉动)
ctx.fillStyle = 'rgba(80, 0, 0, ' + pulseAlpha + ')';
ctx.fillRect(boxX, boxY, boxW, boxH);
ctx.strokeStyle = 'rgba(255, 0, 0, 1)';
ctx.lineWidth = 2.5;
ctx.strokeRect(boxX, boxY, boxW, boxH);
// 警告标题
ctx.fillStyle = 'rgba(255, 60, 60, 1)';
ctx.font = 'bold 26px Arial';
ctx.textAlign = 'center';
ctx.fillText('⚠ 被瞄准', boxX + boxW / 2, boxY + 32);
ctx.font = 'bold 13px Arial';
ctx.fillStyle = 'rgba(255, 220, 220, 0.95)';
ctx.fillText(threats.length + ' 个威胁锁定你', boxX + boxW / 2, boxY + 54);
// 威胁坦克名称列表
ctx.textAlign = 'left';
ctx.font = 'bold 12px Arial';
for (var li = 0; li < listedCount; li++) {
var t = threats[li];
var lineY = boxY + 72 + li * 18;
// 名称 + 距离
ctx.fillStyle = 'rgba(255, 240, 240, 0.95)';
ctx.fillText('• ' + t.tankName, boxX + 12, lineY);
ctx.fillStyle = 'rgba(255, 180, 180, 0.85)';
ctx.textAlign = 'right';
ctx.fillText(Math.round(t.distance / 100) + 'm', boxX + boxW - 12, lineY);
ctx.textAlign = 'left';
}
// 超出部分
if (threats.length > maxListed) {
ctx.fillStyle = 'rgba(255, 200, 200, 0.7)';
ctx.font = 'italic 11px Arial';
ctx.textAlign = 'center';
ctx.fillText('等 ' + (threats.length - maxListed) + ' 个...', boxX + boxW / 2, boxY + 72 + listedCount * 18 + 12);
}
}
// ===== 死亡信息显示(屏幕中央,持续6秒或直到复活) =====
if (DeathInfo && (frameCount - DeathInfo.time) < DeathDisplayTime) {
var dBoxW = 400, dBoxH = 120;
var dBoxX = screenW / 2 - dBoxW / 2;
var dBoxY = screenH / 2 - dBoxH / 2 - 60; // 偏上一点
// 半透明背景
ctx.fillStyle = 'rgba(0, 0, 0, 0.75)';
ctx.fillRect(dBoxX, dBoxY, dBoxW, dBoxH);
// 边框(意料之内=红色,意料之外=紫色)
var borderColor = DeathInfo.expected ? 'rgba(255, 60, 60, 1)' : 'rgba(200, 80, 255, 1)';
ctx.strokeStyle = borderColor;
ctx.lineWidth = 3;
ctx.strokeRect(dBoxX, dBoxY, dBoxW, dBoxH);
// 标题
ctx.textAlign = 'center';
ctx.fillStyle = 'rgba(255, 80, 80, 1)';
ctx.font = 'bold 26px Arial';
ctx.fillText('你被击杀', dBoxX + dBoxW / 2, dBoxY + 36);
// 击杀者信息
ctx.font = 'bold 18px Arial';
if (DeathInfo.killerName) {
ctx.fillStyle = 'rgba(255, 220, 220, 1)';
ctx.fillText('击杀者: ' + DeathInfo.killerName, dBoxX + dBoxW / 2, dBoxY + 66);
ctx.font = '12px Arial';
ctx.fillStyle = 'rgba(255, 180, 180, 0.8)';
ctx.fillText('距离 ' + Math.round(DeathInfo.killerDistance / 100) + 'm', dBoxX + dBoxW / 2, dBoxY + 86);
} else {
ctx.fillStyle = 'rgba(220, 180, 255, 1)';
ctx.fillText('未知来源(偷袭)', dBoxX + dBoxW / 2, dBoxY + 66);
}
// 意料之内/外标签
ctx.font = 'bold 16px Arial';
if (DeathInfo.expected) {
ctx.fillStyle = 'rgba(255, 100, 100, 1)';
ctx.fillText('⚠ 意料之内(已在瞄准名单中)', dBoxX + dBoxW / 2, dBoxY + 108);
} else {
ctx.fillStyle = 'rgba(200, 100, 255, 1)';
ctx.fillText('✦ 意料之外(偷袭!)', dBoxX + dBoxW / 2, dBoxY + 108);
}
}
// ===== 顶部状态栏(始终显示) =====
ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
ctx.font = '11px Arial';
ctx.textAlign = 'left';
var statusText = '[瞄准警告] 监控中 | 威胁: ' + threats.length +
' | 死亡: ' + DeathStats.total + '(内' + DeathStats.expected + '/外' + DeathStats.unexpected + ')' +
' | Frame: ' + frameCount;
ctx.fillText(statusText, 5, 18);
}
// ===== 启动 =====
Loop();
console.log('[瞄准] 警告系统已启动,监控敌方坦克炮塔朝向');
// ===== 暴露控制接口 =====
window.__hornetAimWarning__ = {
running: true,
Stop: function() {
this.running = false;
running = false;
window.removeEventListener('resize', ResizeCanvas);
if (canvas && canvas.parentNode) canvas.parentNode.removeChild(canvas);
console.log('[瞄准] 警告系统已停止');
},
GetDeathStats: function() {
return DeathStats;
},
ResetDeathStats: function() {
DeathStats = { total: 0, expected: 0, unexpected: 0 };
console.log('[瞄准] 死亡统计已重置');
},
setSensitivity: function(s) {
var n = parseFloat(s);
if (!isNaN(n) && n >= 0.3 && n <= 3.0) {
Sensitivity = n;
try { localStorage.setItem('hornet_aim_sensitivity', String(n)); } catch(e) {}
console.log('[瞄准] 灵敏度设为: ' + n);
} else {
console.warn('[瞄准] 灵敏度应在 0.3~3.0');
}
},
getSensitivity: function() { return Sensitivity; },
setSound: function(on) {
SoundEnabled = !!on;
try { localStorage.setItem('hornet_aim_sound', SoundEnabled ? 'true' : 'false'); } catch(e) {}
console.log('[瞄准] 警告声音: ' + (SoundEnabled ? '开' : '关'));
},
getSound: function() { return SoundEnabled; }
};
}
var Tool = {
Start: function() {
if (window.__hornetAimWarning__ && window.__hornetAimWarning__.running) return;
_start();
},
Stop: function() {
if (window.__hornetAimWarning__) window.__hornetAimWarning__.Stop();
},
Status: function() {
return (window.__hornetAimWarning__ && window.__hornetAimWarning__.running) ? '运行中' : '已停止';
},
GetRunning: function() {
return !!(window.__hornetAimWarning__ && window.__hornetAimWarning__.running);
},
SetSensitivity: function(s) { if (window.__hornetAimWarning__) window.__hornetAimWarning__.setSensitivity(s); },
GetSensitivity: function() { return window.__hornetAimWarning__ ? window.__hornetAimWarning__.getSensitivity() : 1.0; },
SetSound: function(on) { if (window.__hornetAimWarning__) window.__hornetAimWarning__.setSound(on); },
GetSound: function() { return window.__hornetAimWarning__ ? window.__hornetAimWarning__.getSound() : false; }
};
window.__hornetTools__ = window.__hornetTools__ || {};
window.__hornetTools__.AimWarning = Tool;
console.log('[3DTank] 瞄准警告模块已加载');
})();
// ===== hornet_freelook.js =====
// ===== 3DTank自由视角 =====
// 在浏览器控制台运行(需先运行 hook_gamemode.js 和 hornet_lib.js)
// 功能:解锁主相机,WASD移动/鼠标右键旋转/QE升降/Shift加速/滚轮调速
// 原理:Hook mainCamera.h37,在相机矩阵更新前覆盖l35_1/k35_1参数
(function() {
function _start() {
console.log('===== 3DTank自由视角启动 =====');
if (!window.__gameMode__) {
console.error('请先运行 hook_gamemode.js');
return;
}
if (window.__hornetFreeLook__) {
window.__hornetFreeLook__.Stop();
console.log('已停止旧自由视角');
}
var gm = window.__gameMode__;
// ===== 引用公共库 =====
var lib = window.__hornetLib__;
if (!lib) {
console.error('[自由视角] 请先运行 hornet_lib.js');
return;
}
function GetPlayerTank() { return lib.GetPlayerTank(gm); }
function GetTankPosition(tank) { return lib.GetTankPosition(tank); }
// ===== 获取游戏渲染对象 =====
var scene = null, mainCamera = null, ff = null;
var initError = null;
try {
scene = gm.jyw().iyt_1;
mainCamera = scene.hyp_1;
ff = scene.fyo_1.d75_1;
if (!mainCamera || !mainCamera.s35_1 || !mainCamera.r35_1 || !mainCamera.o37) {
throw new Error('相机字段缺失');
}
console.log('[自由视角] 主相机:', mainCamera.constructor.name,
'l35=(' + mainCamera.s35_1.f20_1.toFixed(2) + ',' + mainCamera.s35_1.g20_1.toFixed(2) + ',' + mainCamera.s35_1.h20_1.toFixed(2) + ')',
'k35=(' + mainCamera.r35_1.f20_1.toFixed(0) + ',' + mainCamera.r35_1.g20_1.toFixed(0) + ',' + mainCamera.r35_1.h20_1.toFixed(0) + ')');
} catch(e) {
initError = e;
console.error('[自由视角] 初始化失败:', e.message);
}
if (initError) return;
// ===== 自由视角状态 =====
var freeLookActive = false;
var running = true;
// 自由视角参数(从当前相机参数初始化)
var freeYaw = 0, freePitch = 0, freeRoll = 0;
var freePosX = 0, freePosY = 0, freePosZ = 0;
var moveSpeed = 1200; // 基础移动速度(世界单位/秒)
var speedMultiplier = 1; // 滚轮调速倍数(0.2~5.0)
// 按键状态
var keys = {};
var mouseSensitivity = 0.005; // 鼠标灵敏度
// ===== Hook mainCamera.h37 =====
var hadOwnH37 = mainCamera.hasOwnProperty('o37');
var origH37 = mainCamera.o37;
function applyFreeLookParams() {
mainCamera.s35_1.f20_1 = freePitch; // pitch (f)
mainCamera.s35_1.g20_1 = freeRoll; // roll (g) 保持初始值
mainCamera.s35_1.h20_1 = freeYaw; // yaw (h) 鼠标左右控制
mainCamera.r35_1.f20_1 = freePosX;
mainCamera.r35_1.g20_1 = freePosY;
mainCamera.r35_1.h20_1 = freePosZ;
}
// 缓存从 viewMatrix 提取的前方向/右方向(h37 更新后设置)
// o37 行向量布局(列主序存储):
// 行0 = (d[0], d[4], d[8]) = right 向量
// 行1 = (d[1], d[5], d[9]) = up 向量
// 行2 = (d[2], d[6], d[10]) = -forward 向量
var cachedFwdX = 0, cachedFwdY = 0, cachedRightX = 0, cachedRightY = 0;
var cachedInitialized = false;
var suspended = false; // 后视镜等模块渲染时临时暂停 hook
mainCamera.o37 = function() {
if (freeLookActive && !suspended) {
applyFreeLookParams();
origH37.call(this);
// 从更新后的 viewMatrix (u36_1) 提取前方向和右方向(水平分量 x,y)
if (this.b37_1) {
cachedFwdX = -this.b37_1[2]; // forward.x
cachedFwdY = -this.b37_1[6]; // forward.y
cachedRightX = this.b37_1[0]; // right.x
cachedRightY = this.b37_1[4]; // right.y
cachedInitialized = true;
}
} else {
return origH37.call(this);
}
};
console.log('[自由视角] mainCamera.o37 已 Hook');
// ===== 开启/关闭自由视角 =====
function enableFreeLook() {
// 从当前相机参数初始化(平滑过渡)
// l35_1: f=pitch, g=roll, h=yaw
freePitch = mainCamera.s35_1.f20_1;
freeRoll = mainCamera.s35_1.g20_1; // roll 保持初始值
freeYaw = mainCamera.s35_1.h20_1; // yaw 鼠标控制
freePosX = mainCamera.r35_1.f20_1;
freePosY = mainCamera.r35_1.g20_1;
freePosZ = mainCamera.r35_1.h20_1;
freeLookActive = true;
console.log('[自由视角] 已开启 — WASD移动 | 鼠标旋转 | 空格上升/Shift下降 | 滚轮调速 | Alt+F关闭');
updateHud();
}
function disableFreeLook() {
freeLookActive = false;
keys = {}; // 清空按键状态,防止卡死
console.log('[自由视角] 已关闭,恢复游戏相机控制');
updateHud();
}
function toggleFreeLook() {
if (freeLookActive) disableFreeLook();
else enableFreeLook();
}
// ===== 键盘事件(capture阶段拦截,防止游戏接收WASD) =====
// 用 e.code(物理键位)匹配,比 e.key 更稳定,不受 Shift 修饰影响
var keyMap = {
'KeyW': 'w', 'KeyA': 'a', 'KeyS': 's', 'KeyD': 'd',
'KeyQ': 'q', 'KeyE': 'e',
'Space': ' ',
'ShiftLeft': 'shift', 'ShiftRight': 'shift'
};
function onKeyDown(e) {
// 焦点在输入框/选择框时不拦截(允许面板输入)
var ae = document.activeElement;
if (ae && (ae.tagName === 'INPUT' || ae.tagName === 'SELECT' || ae.tagName === 'TEXTAREA')) return;
// Alt+F 切换(全局,无论是否开启)
if (e.altKey && (e.key === 'f' || e.key === 'F')) {
e.preventDefault();
e.stopPropagation();
toggleFreeLook();
return;
}
if (!freeLookActive) return;
var key = keyMap[e.code] || e.key.toLowerCase();
if (keyMap[e.code]) {
keys[key] = true;
e.preventDefault();
e.stopPropagation();
}
}
function onKeyUp(e) {
// 焦点在输入框/选择框时不拦截
var ae = document.activeElement;
if (ae && (ae.tagName === 'INPUT' || ae.tagName === 'SELECT' || ae.tagName === 'TEXTAREA')) return;
var key = keyMap[e.code] || e.key.toLowerCase();
if (keyMap[e.code]) {
keys[key] = false; // 总是清理按键状态,防止卡死
// 只在自由视角激活时拦截事件,暂停时放行给游戏
if (freeLookActive) {
e.preventDefault();
e.stopPropagation();
}
}
}
// ===== 鼠标事件(自由视角激活时鼠标移动直接旋转,不按键) =====
function onMouseMove(e) {
if (!freeLookActive) return;
// 鼠标左移→视角左转(yaw减),鼠标上移→视角上抬(pitch增)
freeYaw -= e.movementX * mouseSensitivity;
freePitch -= e.movementY * mouseSensitivity;
// 限制俯仰角到±80°,避免接近90°时的万向锁翻转
var limit = 80 * Math.PI / 180;
if (freePitch > limit) freePitch = limit;
if (freePitch < -limit) freePitch = -limit;
}
function onWheel(e) {
if (!freeLookActive) return;
e.preventDefault();
e.stopPropagation();
// 滚轮上=加速,下=减速
if (e.deltaY < 0) speedMultiplier = Math.min(5.0, speedMultiplier + 0.2);
else speedMultiplier = Math.max(0.2, speedMultiplier - 0.2);
updateHud();
}
// capture 阶段拦截键盘
window.addEventListener('keydown', onKeyDown, true);
window.addEventListener('keyup', onKeyUp, true);
document.addEventListener('mousemove', onMouseMove);
window.addEventListener('wheel', onWheel, true);
// ===== HUD 显示 =====
var hud = document.createElement('div');
hud.id = 'hornetFreeLookHud';
hud.style.cssText = 'position:fixed;bottom:10px;left:10px;z-index:99999;' +
'background:rgba(0,0,0,0.7);color:#8f8;padding:6px 10px;border-radius:4px;' +
'font:12px monospace;pointer-events:none;border:1px solid rgba(120,255,120,0.5);' +
'display:none;user-select:none;';
document.body.appendChild(hud);
function updateHud() {
if (!freeLookActive) { hud.style.display = 'none'; return; }
hud.style.display = 'block';
hud.innerHTML =
'[自由视角 ON] 速度:' + (moveSpeed * speedMultiplier).toFixed(0) + '/秒<br>' +
'位置:(' + freePosX.toFixed(0) + ',' + freePosY.toFixed(0) + ',' + freePosZ.toFixed(0) + ')<br>' +
'Yaw:' + freeYaw.toFixed(2) + ' Pitch:' + freePitch.toFixed(2) + '<br>' +
'WASD移动 鼠标旋转 空格上升/Shift下降 滚轮调速 Alt+F关闭';
}
// ===== 主循环:处理按键 → 更新位置 =====
var rafId = null;
var lastTime = performance.now();
function loop(now) {
if (!running) return;
var dt = (now - lastTime) / 1000;
lastTime = now;
if (dt > 0.1) dt = 0.1; // 防止卡顿后大跳
if (freeLookActive) {
var speed = moveSpeed * speedMultiplier * dt; // 每秒 moveSpeed 单位
// 相机前方向/右方向:优先用 viewMatrix 提取的真实方向(跟随鼠标朝向)
var fwdX, fwdY, rightX, rightY;
if (cachedInitialized) {
// 从 viewMatrix 提取的水平方向分量(列主序)
fwdX = cachedFwdX;
fwdY = cachedFwdY;
rightX = cachedRightX;
rightY = cachedRightY;
// 归一化水平方向(pitch 不为0时向量长度<1,保持移动速度一致)
var fwdLen = Math.sqrt(fwdX * fwdX + fwdY * fwdY);
if (fwdLen > 1e-6) { fwdX /= fwdLen; fwdY /= fwdLen; }
var rightLen = Math.sqrt(rightX * rightX + rightY * rightY);
if (rightLen > 1e-6) { rightX /= rightLen; rightY /= rightLen; }
} else {
// fallback: sin/cos(viewMatrix 尚未初始化时)
fwdX = -Math.sin(freeYaw);
fwdY = -Math.cos(freeYaw);
rightX = Math.cos(freeYaw);
rightY = -Math.sin(freeYaw);
}
var moved = false;
// W/S 前后(水平移动,可与其他键组合)
if (keys['w']) { freePosX += fwdX * speed; freePosY += fwdY * speed; moved = true; }
if (keys['s']) { freePosX -= fwdX * speed; freePosY -= fwdY * speed; moved = true; }
// A/D 左右(水平移动,可与其他键组合)
if (keys['a']) { freePosX -= rightX * speed; freePosY -= rightY * speed; moved = true; }
if (keys['d']) { freePosX += rightX * speed; freePosY += rightY * speed; moved = true; }
// 空格上升 / Shift 下降(绝对 z 轴,可与 WASD 组合)
if (keys[' ']) { freePosZ += speed; moved = true; }
if (keys['shift']) { freePosZ -= speed; moved = true; }
// Q/E 升降(备用)
if (keys['q']) { freePosZ -= speed; moved = true; }
if (keys['e']) { freePosZ += speed; moved = true; }
if (moved) updateHud();
}
rafId = requestAnimationFrame(loop);
}
rafId = requestAnimationFrame(loop);
// ===== 对外接口 =====
var self = {
Stop: function() {
running = false;
if (rafId) cancelAnimationFrame(rafId);
disableFreeLook();
// 恢复 h37
if (hadOwnH37) mainCamera.o37 = origH37;
else delete mainCamera.o37;
// 移除监听
window.removeEventListener('keydown', onKeyDown, true);
window.removeEventListener('keyup', onKeyUp, true);
document.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('wheel', onWheel, true);
if (hud.parentNode) hud.parentNode.removeChild(hud);
window.__hornetFreeLook__ = null;
console.log('[自由视角] 已停止');
},
Status: function() {
// 返回字符串供面板 HUD 显示(其他工具也是字符串)
if (!running) return '未运行';
return freeLookActive ? '已开启' : '已暂停';
},
GetRunning: function() { return freeLookActive; },
IsActive: function() { return freeLookActive; },
Toggle: function() { toggleFreeLook(); },
Enable: function() { if (!freeLookActive) enableFreeLook(); },
Disable: function() { if (freeLookActive) disableFreeLook(); },
// 供其他模块(雷达/3D透视)获取虚拟相机参数
GetPosition: function() { return freeLookActive ? { x: freePosX, y: freePosY, z: freePosZ } : null; },
GetDirection: function() { return freeLookActive ? { yaw: freeYaw, pitch: freePitch } : null; },
// 供后视镜等模块渲染时临时暂停 h37 hook
SetSuspended: function(s) { suspended = s; }
};
window.__hornetFreeLook__ = self;
window.__hornetTools__ = window.__hornetTools__ || {};
window.__hornetTools__.FreeLook = self;
// 启动时自动激活自由视角
enableFreeLook();
console.log('[自由视角] 就绪 — 已激活,Alt+F 切换激活/暂停');
}
function _stop() {
if (window.__hornetFreeLook__) window.__hornetFreeLook__.Stop();
}
function _status() {
return window.__hornetFreeLook__ ? window.__hornetFreeLook__.Status() : '未运行';
}
function _getRunning() {
return !!(window.__hornetFreeLook__ && window.__hornetFreeLook__.GetRunning());
}
window.__hornetTools__ = window.__hornetTools__ || {};
window.__hornetTools__.FreeLook = {
Start: _start,
Stop: _stop,
Status: _status,
GetRunning: _getRunning
};
})();
// ===== hornet_warsandbox.js =====
// ===== 3DTank 战争沙盘 - 真实3D俯瞰渲染 =====
// 在浏览器控制台运行(需先运行 hook_gamemode.js 和 hornet_lib.js)
// 功能:80%x90% 可拖拽缩放的全景俯瞰图,使用游戏原生渲染器渲染 3D 场景
// 原理:Hook gl.bindFramebuffer 重定向到自定义 FBO,用俯瞰相机参数调用游戏渲染器
// 操作:拖拽标题移动窗口 | 拖拽地图平移 | 滚轮缩放 | 双击居中 | Alt+M 开关
(function() {
function _start() {
console.log('===== 3DTank 战争沙盘(3D俯瞰版)启动 =====');
if (!window.__gameMode__) {
console.error('请先运行 hook_gamemode.js');
return;
}
if (window.__hornetWarSandbox__) {
window.__hornetWarSandbox__.Stop();
console.log('已停止旧战争沙盘');
}
var gm = window.__gameMode__;
var lib = window.__hornetLib__;
if (!lib) {
console.error('[沙盘] 请先运行 hornet_lib.js');
return;
}
var teamFns = lib.LoadTeamFunctions();
var FindComponentByField = lib.FindComponentByField;
var GetTankPosition = lib.GetTankPosition;
var GetTankState = lib.GetTankState;
var IsTankAlive = lib.IsTankAlive;
var GetTankName = lib.GetTankName;
var GetTankHealth = lib.GetTankHealth;
function GetPlayerTank() { return lib.GetPlayerTank(gm); }
function IsFriendly(tank, playerTank) { return lib.IsFriendly(tank, playerTank, gm); }
function getTankDirection(tank) {
// 从车体皮肤组件(KMt)的Bp节点世界矩阵提取前向方向
// dzc_1 是局部空间向量且可能未设置,不可靠
var hullComp = FindComponentByField(tank, 't14l_1') ||
FindComponentByField(tank, 'u14l_1') ||
FindComponentByField(tank, 'p14l_1');
if (!hullComp || !hullComp.t14l_1) return null;
var skin = hullComp.t14l_1;
if (!skin || !skin.s17b_1) return null;
try {
var partCount = skin.s17b_1.e1();
for (var p = 0; p < partCount; p++) {
var part = skin.s17b_1.g1(p);
if (!part || !part.v17b_1) continue;
var nodeCount = part.v17b_1.e1();
for (var n = 0; n < nodeCount; n++) {
var bpNode = part.v17b_1.g1(n);
if (!bpNode || !bpNode.n35_1) continue;
// 跳过幽灵节点(z34_1 < 0.9)
if (bpNode.g35_1 !== undefined && bpNode.g35_1 < 0.9) continue;
var m = bpNode.n35_1;
// 列主序世界矩阵:Col0=(m[0],m[1],m[2]) Col1=(m[4],m[5],m[6]) Col2=(m[8],m[9],m[10])
// 游戏坐标:x,y 水平面; z(h20_1)=高度。找"上"列(|z|最大),其余为水平方向
var az0 = Math.abs(m[2]), az1 = Math.abs(m[6]), az2 = Math.abs(m[10]);
// 尝试 Col1(Y前向约定,Z-up游戏常见)作为前向,若 Col1 是"上"则用 Col2
var fx, fy;
if (az1 < 0.7) {
// Col1 不是上方向,用作前向
fx = m[4]; fy = m[5];
} else if (az2 < 0.7) {
// Col1 是上,用 Col2 作前向
fx = m[8]; fy = m[9];
} else {
// Col0 不是上,用作前向
fx = m[0]; fy = m[1];
}
var len = Math.sqrt(fx*fx + fy*fy);
if (len > 0.01) {
return { x: fx/len, y: fy/len, z: 0 };
}
}
}
} catch(e) {}
return null;
}
// ===== 获取游戏渲染对象 =====
var scene = null, ff = null, rootNode = null, mainCamera = null, gl = null;
var initError = null;
try {
scene = gm.jyw().iyt_1;
ff = scene.fyo_1.d75_1;
rootNode = scene.eyp_1;
mainCamera = scene.hyp_1;
gl = ff.w39_1.b74_1;
if (!ff || !rootNode || !mainCamera || !gl) throw new Error('关键字段缺失');
} catch(e) {
initError = e;
console.error('[沙盘] 初始化失败:', e.message);
}
if (initError) return;
// ===== FBO 配置(固定内部分辨率,性能优先)=====
var FBO_W = 960, FBO_H = 720;
var Config = {
fov: 1.0, // 俯瞰 FOV
near: 100, far: 80000,
minHeight: 3000, // 最小相机高度(放大)
maxHeight: 60000, // 最大相机高度(缩小)
defaultHeight: 20000
};
// 设置参数(localStorage 持久化)
var ShowGrid = true;
try {
if (localStorage.getItem('hornet_ws_showgrid') === 'false') ShowGrid = false;
} catch(e) {}
var MarkerSize = 1.0;
try {
var __savedMS = localStorage.getItem('hornet_ws_marker_size');
if (__savedMS) {
var __ms = parseFloat(__savedMS);
if (!isNaN(__ms) && __ms >= 0.5 && __ms <= 3.0) MarkerSize = __ms;
}
} catch(e) {}
// ===== 创建 UI =====
var container = document.createElement('div');
container.id = 'hornetWarSandbox';
var winW = Math.floor(window.innerWidth * 0.8);
var winH = Math.floor(window.innerHeight * 0.9);
container.style.cssText =
'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:100002;' +
'width:' + winW + 'px;height:' + winH + 'px;' +
'background:rgba(8,12,20,0.82);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);' +
'border:1px solid rgba(125,249,255,0.25);border-radius:12px;' +
'box-shadow:0 12px 48px rgba(0,0,0,0.7);display:flex;flex-direction:column;overflow:hidden;' +
'user-select:none;-webkit-user-select:none;';
document.body.appendChild(container);
var titleBar = document.createElement('div');
titleBar.style.cssText =
'display:flex;justify-content:space-between;align-items:center;padding:10px 16px;' +
'background:rgba(125,249,255,0.06);border-bottom:1px solid rgba(125,249,255,0.15);cursor:move;' +
'font:14px Consolas,monospace;color:#7df9ff;flex-shrink:0;';
titleBar.innerHTML = '<span style="font-weight:bold;text-shadow:0 0 4px rgba(125,249,255,0.4);">战争沙盘</span>';
container.appendChild(titleBar);
var ctrlBox = document.createElement('div');
ctrlBox.style.cssText = 'display:flex;gap:10px;align-items:center;';
titleBar.appendChild(ctrlBox);
var infoLabel = document.createElement('span');
infoLabel.style.cssText = 'font-size:11px;color:#5a7a8a;';
ctrlBox.appendChild(infoLabel);
var closeBtn = document.createElement('button');
closeBtn.textContent = 'X';
closeBtn.style.cssText =
'padding:2px 8px;border:1px solid rgba(255,100,100,0.4);border-radius:4px;' +
'background:rgba(200,50,50,0.3);color:#ff8080;cursor:pointer;font:12px Consolas;';
closeBtn.onclick = function() { _stop(); };
ctrlBox.appendChild(closeBtn);
var canvas = document.createElement('canvas');
canvas.style.cssText = 'flex:1;cursor:grab;display:block;';
container.appendChild(canvas);
var ctx = canvas.getContext('2d');
function resizeCanvas() {
var rect = canvas.getBoundingClientRect();
canvas.width = Math.floor(rect.width);
canvas.height = Math.floor(rect.height);
}
resizeCanvas();
// ===== FBO 创建 =====
var sandboxFbo = null, sandboxTex = null, sandboxDepthRb = null;
var pixels = new Uint8Array(FBO_W * FBO_H * 4);
var pixels32 = new Uint32Array(pixels.buffer);
var imageData = ctx.createImageData(FBO_W, FBO_H);
var tempCanvas = document.createElement('canvas');
tempCanvas.width = FBO_W; tempCanvas.height = FBO_H;
var tempCtx = tempCanvas.getContext('2d');
var pbos = [null, null], pboIndex = 0, pboReady = false, pboSupported = false;
try {
sandboxFbo = gl.createFramebuffer();
sandboxTex = gl.createTexture();
sandboxDepthRb = gl.createRenderbuffer();
gl.bindTexture(gl.TEXTURE_2D, sandboxTex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, FBO_W, FBO_H, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.bindRenderbuffer(gl.RENDERBUFFER, sandboxDepthRb);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, FBO_W, FBO_H);
gl.bindFramebuffer(gl.FRAMEBUFFER, sandboxFbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, sandboxTex, 0);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, sandboxDepthRb);
var status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.bindTexture(gl.TEXTURE_2D, null);
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
if (status !== gl.FRAMEBUFFER_COMPLETE) throw new Error('FBO不完整: ' + status);
// PBO
try {
var byteSize = FBO_W * FBO_H * 4;
for (var i = 0; i < 2; i++) {
pbos[i] = gl.createBuffer();
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbos[i]);
gl.bufferData(gl.PIXEL_PACK_BUFFER, byteSize, gl.STREAM_READ);
}
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
pboSupported = true;
} catch(e) { pboSupported = false; }
console.log('[沙盘] FBO ' + FBO_W + 'x' + FBO_H + ' 已创建' + (pboSupported ? ' (PBO)' : ' (同步)'));
} catch(e) {
initError = e;
console.error('[沙盘] FBO创建失败:', e.message);
}
if (initError) return;
// ===== Hook gl.bindFramebuffer + BS.h3c/g3c =====
var sandboxRendering = false;
var mainFbo = null;
var origBindFramebuffer = gl.bindFramebuffer.bind(gl);
gl.bindFramebuffer = function(target, fbo) {
if (!sandboxRendering && target === gl.FRAMEBUFFER) mainFbo = fbo;
if (sandboxRendering && target === gl.FRAMEBUFFER && fbo !== sandboxFbo) {
return origBindFramebuffer(target, sandboxFbo);
}
return origBindFramebuffer(target, fbo);
};
var origH3c = ff.w39_1.o3c.bind(ff.w39_1);
var origG3c = ff.w39_1.n3c.bind(ff.w39_1);
ff.w39_1.o3c = function() { return sandboxRendering ? FBO_W : origH3c(); };
ff.w39_1.n3c = function() { return sandboxRendering ? FBO_H : origG3c(); };
// UI 层禁用
var uiLayerIndices = [20, 21, 22, 23, 24];
var fakeQueue = {
e1: function() { return 0; }, g1: function(i) { return null; },
w2a: function(x) {}, d3: function() {}, ii: function(x) {}
};
// ===== 视图状态 =====
var viewCenterX = 0, viewCenterY = 0;
var camHeight = Config.defaultHeight;
var viewYaw = 0; // 地图旋转(0=北朝上)
var initialized = false;
// ===== 指针拖动绑定(Pointer Events + setPointerCapture,避免松手后粘手)=====
// HandleEl: 触发拖动的元素;OnStart/OnMove/OnEnd 分别处理开始/移动/结束
function BindPointerDrag(HandleEl, OnStart, OnMove, OnEnd) {
var Active = false;
var PointerId = null;
function Finish() {
if (!Active) return;
Active = false;
if (PointerId !== null && HandleEl.releasePointerCapture) {
try { HandleEl.releasePointerCapture(PointerId); } catch (Err) {}
}
PointerId = null;
OnEnd();
}
if (window.PointerEvent) {
HandleEl.addEventListener('pointerdown', function(e) {
if (e.button !== 0) return;
if (OnStart(e) === false) return;
Active = true;
PointerId = e.pointerId;
try { HandleEl.setPointerCapture(e.pointerId); } catch (Err) {}
e.preventDefault();
e.stopPropagation();
}, true);
HandleEl.addEventListener('pointermove', function(e) {
if (!Active || (PointerId !== null && e.pointerId !== PointerId)) return;
if (e.buttons === 0) { Finish(); return; } // 兜底:按键已松开
OnMove(e);
e.preventDefault();
}, true);
HandleEl.addEventListener('pointerup', Finish, true);
HandleEl.addEventListener('pointercancel', Finish, true);
HandleEl.addEventListener('lostpointercapture', Finish, true);
window.addEventListener('pointerup', Finish, true);
} else {
HandleEl.addEventListener('mousedown', function(e) {
if (e.button !== 0) return;
if (OnStart(e) === false) return;
Active = true;
e.preventDefault();
}, true);
document.addEventListener('mousemove', function(e) {
if (!Active) return;
if (e.buttons === 0) { Finish(); return; }
OnMove(e);
}, true);
document.addEventListener('mouseup', Finish, true);
}
window.addEventListener('blur', Finish);
window.addEventListener('mouseleave', Finish);
return { End: Finish, IsActive: function() { return Active; } };
}
// ===== 窗口拖动 =====
var dsx = 0, dsy = 0, dwx = 0, dwy = 0;
BindPointerDrag(titleBar, function(e) {
if (e.target.tagName === 'BUTTON') return false;
dsx = e.clientX; dsy = e.clientY;
var rect = container.getBoundingClientRect();
dwx = rect.left; dwy = rect.top;
container.style.transform = 'none';
container.style.left = dwx + 'px'; container.style.top = dwy + 'px';
}, function(e) {
var nx = Math.max(0, Math.min(window.innerWidth - 100, dwx + e.clientX - dsx));
var ny = Math.max(0, Math.min(window.innerHeight - 40, dwy + e.clientY - dsy));
container.style.left = nx + 'px'; container.style.top = ny + 'px';
}, function() {});
// ===== 地图平移 =====
var psx = 0, psy = 0, pcx = 0, pcy = 0;
BindPointerDrag(canvas, function(e) {
psx = e.clientX; psy = e.clientY;
pcx = viewCenterX; pcy = viewCenterY;
canvas.style.cursor = 'grabbing';
}, function(e) {
// 俯瞰视图:屏幕像素→世界单位,X/Y 方向缩放不同(FBO 拉伸到 canvas)
var ext = getViewExtents();
var dx = (e.clientX - psx) * ext.wppX;
var dy = (e.clientY - psy) * ext.wppY;
viewCenterX = pcx - dx;
viewCenterY = pcy + dy; // y 反转
}, function() {
canvas.style.cursor = 'grab';
});
// 滚轮缩放(改变相机高度)
canvas.addEventListener('wheel', function(e) {
e.preventDefault();
var factor = e.deltaY < 0 ? 0.85 : 1.15;
var newHeight = camHeight * factor;
if (newHeight < Config.minHeight || newHeight > Config.maxHeight) return;
camHeight = newHeight;
}, { passive: false });
// 双击居中
canvas.addEventListener('dblclick', function() {
var player = GetPlayerTank();
if (player) {
var pos = GetTankPosition(player);
if (pos) { viewCenterX = pos.x; viewCenterY = pos.y; }
} else {
var fl = window.__hornetFreeLook__;
if (fl && fl.IsActive && fl.IsActive()) {
var p = fl.GetPosition();
if (p) { viewCenterX = p.x; viewCenterY = p.y; }
}
}
});
// ===== 世界→屏幕投影(俯瞰视图,用于 2D 叠加层)=====
// 注意:FBO(960x720) 被拉伸到 canvas(cw x ch),若两者比例不同则 X/Y 缩放不同
// 相机以 FBO_W/FBO_H 为 aspect 渲染,可见世界范围:
// visH = 2*camHeight*tan(fov/2) (世界单位,纵向)
// visW = visH * (FBO_W/FBO_H) (世界单位,横向)
// FBO 拉伸到 canvas 后:
// worldPerPixelX = visW / cw
// worldPerPixelY = visH / ch
function getViewExtents() {
var ch = canvas.height, cw = canvas.width;
var visH = 2 * camHeight * Math.tan(Config.fov / 2);
var visW = visH * (FBO_W / FBO_H);
return {
visW: visW, visH: visH,
wppX: visW / cw, // X方向 世界单位/像素
wppY: visH / ch // Y方向 世界单位/像素
};
}
function worldToScreen(wx, wy) {
var ext = getViewExtents();
var cw = canvas.width, ch = canvas.height;
return {
x: cw / 2 + (wx - viewCenterX) / ext.wppX,
y: ch / 2 - (wy - viewCenterY) / ext.wppY
};
}
// ===== 2D 叠加层绘制 =====
function drawOverlays() {
var cw = canvas.width, ch = canvas.height;
var playerTank = GetPlayerTank();
// 网格(X/Y 方向缩放不同,用 ext 计算)
if (ShowGrid) {
ctx.strokeStyle = 'rgba(125,249,255,0.08)';
ctx.lineWidth = 1;
var steps = [500, 1000, 2000, 5000, 10000, 20000, 50000];
var step = 5000;
var ext = getViewExtents();
// 用较小的 wpp 选择网格间距(保证密度合适)
var minWpp = Math.min(ext.wppX, ext.wppY);
for (var i = 0; i < steps.length; i++) {
if (steps[i] / minWpp > 60) { step = steps[i]; break; }
}
var left = viewCenterX - (cw/2) * ext.wppX;
var right = viewCenterX + (cw/2) * ext.wppX;
var top = viewCenterY + (ch/2) * ext.wppY;
var bottom = viewCenterY - (ch/2) * ext.wppY;
var startX = Math.floor(left/step)*step;
var startY = Math.floor(bottom/step)*step;
ctx.beginPath();
for (var wx = startX; wx <= right; wx += step) {
var p = worldToScreen(wx, 0);
ctx.moveTo(p.x, 0); ctx.lineTo(p.x, ch);
}
for (var wy = startY; wy <= top; wy += step) {
var p = worldToScreen(0, wy);
ctx.moveTo(0, p.y); ctx.lineTo(cw, p.y);
}
ctx.stroke();
// 距离标签
ctx.fillStyle = 'rgba(125,249,255,0.35)';
ctx.font = '10px Consolas';
ctx.textAlign = 'left'; ctx.textBaseline = 'top';
for (var wx2 = startX; wx2 <= right; wx2 += step) {
var p = worldToScreen(wx2, 0);
ctx.fillText((wx2/100).toFixed(0)+'m', p.x+2, 2);
}
}
// 坦克标记
var tanks = [];
try {
var list = gm.q100_1.r11l();
var n = list.e1();
for (var i = 0; i < n; i++) { var t = list.g1(i); if (t) tanks.push(t); }
} catch(e) {}
var margin = 50;
tanks.forEach(function(tank) {
var pos = GetTankPosition(tank);
if (!pos) return;
var p = worldToScreen(pos.x, pos.y);
if (p.x < -margin || p.x > cw+margin || p.y < -margin || p.y > ch+margin) return;
var isPlayer = (tank === playerTank);
var friendly = IsFriendly(tank, playerTank);
var alive = IsTankAlive(tank);
var state = GetTankState(tank);
if (state !== 3 && !isPlayer) return;
var color, glow;
if (isPlayer) { color = '#ffdd44'; glow = 'rgba(255,221,68,0.7)'; }
else if (friendly) { color = '#44ff88'; glow = 'rgba(68,255,136,0.5)'; }
else { color = '#ff5566'; glow = 'rgba(255,85,102,0.5)'; }
var radius = (isPlayer ? 8 : 6) * MarkerSize;
if (!alive) { radius = 4 * MarkerSize; color = '#666'; glow = 'none'; }
// 方向指示(真实坦克朝向)
var dir = getTankDirection(tank);
if (dir && alive) {
var dirLen = Math.sqrt(dir.x*dir.x + dir.y*dir.y);
if (dirLen > 0.01) {
var dx = dir.x / dirLen, dy = dir.y / dirLen;
var arrowLen = 22;
ctx.strokeStyle = color;
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.moveTo(p.x, p.y);
ctx.lineTo(p.x + dx * arrowLen, p.y - dy * arrowLen);
ctx.stroke();
// 箭头头
var ax = p.x + dx * arrowLen, ay = p.y - dy * arrowLen;
var ang = Math.atan2(-dy, dx);
ctx.beginPath();
ctx.moveTo(ax, ay);
ctx.lineTo(ax - 7*Math.cos(ang-0.4), ay - 7*Math.sin(ang-0.4));
ctx.moveTo(ax, ay);
ctx.lineTo(ax - 7*Math.cos(ang+0.4), ay - 7*Math.sin(ang+0.4));
ctx.stroke();
}
}
// 光晕标记
if (alive) {
ctx.save();
ctx.shadowColor = glow;
ctx.shadowBlur = 12;
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(p.x, p.y, radius, 0, Math.PI*2);
ctx.fill();
ctx.restore();
} else {
ctx.fillStyle = color;
ctx.fillRect(p.x-3, p.y-3, 6, 6);
}
// 玩家外圈
if (isPlayer && alive) {
ctx.strokeStyle = 'rgba(255,221,68,0.6)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(p.x, p.y, radius+6, 0, Math.PI*2);
ctx.stroke();
}
// 名称
var name = GetTankName(tank);
if (name) {
ctx.font = 'bold 11px Consolas';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.strokeStyle = 'rgba(0,0,0,0.85)';
ctx.lineWidth = 3;
ctx.strokeText(name, p.x, p.y - radius - 5);
ctx.fillStyle = alive ? '#fff' : '#666';
ctx.fillText(name, p.x, p.y - radius - 5);
}
// 血量条
if (alive) {
var health = GetTankHealth(tank);
if (health) {
var pct = health.cur / health.max;
if (pct > 1) pct = 1;
var barW = 34, barH = 5;
var bx = p.x - barW/2, by = p.y + radius + 4;
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(bx, by, barW, barH);
var hpColor = pct > 0.6 ? '#2ecc71' : (pct > 0.3 ? '#f5a623' : '#e05555');
ctx.fillStyle = hpColor;
ctx.fillRect(bx, by, barW * pct, barH);
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
ctx.lineWidth = 0.5;
ctx.strokeRect(bx, by, barW, barH);
}
}
});
// 罗盘
var ccx = cw - 50, ccy = 50, cr = 28;
ctx.save();
ctx.strokeStyle = 'rgba(125,249,255,0.4)';
ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.arc(ccx, ccy, cr, 0, Math.PI*2); ctx.stroke();
ctx.fillStyle = 'rgba(8,12,20,0.6)';
ctx.beginPath(); ctx.arc(ccx, ccy, cr-1, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#ff5566';
ctx.beginPath();
ctx.moveTo(ccx, ccy - cr + 4);
ctx.lineTo(ccx - 5, ccy);
ctx.lineTo(ccx + 5, ccy);
ctx.closePath(); ctx.fill();
ctx.fillStyle = 'rgba(200,210,220,0.6)';
ctx.beginPath();
ctx.moveTo(ccx, ccy + cr - 4);
ctx.lineTo(ccx - 5, ccy);
ctx.lineTo(ccx + 5, ccy);
ctx.closePath(); ctx.fill();
ctx.fillStyle = '#7df9ff';
ctx.font = 'bold 12px Consolas';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('N', ccx, ccy - cr - 8);
ctx.restore();
// 中心十字准星
ctx.strokeStyle = 'rgba(125,249,255,0.2)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(cw/2 - 15, ch/2); ctx.lineTo(cw/2 + 15, ch/2);
ctx.moveTo(cw/2, ch/2 - 15); ctx.lineTo(cw/2, ch/2 + 15);
ctx.stroke();
}
// ===== 主渲染循环 =====
var running = true;
var rafId = null;
var frameCount = 0;
function render() {
if (!running) return;
frameCount++;
var cw = canvas.width, ch = canvas.height;
// 初始化视图中心
if (!initialized) {
var player = GetPlayerTank();
if (player) {
var pos = GetTankPosition(player);
if (pos) { viewCenterX = pos.x; viewCenterY = pos.y; initialized = true; }
}
}
// 暂停 FreeLook
var fl = window.__hornetFreeLook__;
if (fl && typeof fl.SetSuspended === 'function') fl.SetSuspended(true);
try {
// 保存主相机参数
var sPitch = mainCamera.s35_1.f20_1;
var sRoll = mainCamera.s35_1.g20_1;
var sYaw = mainCamera.s35_1.h20_1;
var sX = mainCamera.r35_1.f20_1;
var sY = mainCamera.r35_1.g20_1;
var sZ = mainCamera.r35_1.h20_1;
var sL36 = mainCamera.s36_1, sM36 = mainCamera.t36_1;
var sS36 = mainCamera.z36_1, sQ36 = mainCamera.x36_1, sR36 = mainCamera.y36_1;
// 设置俯瞰相机参数
// pitch=-π/2 朝下看,roll=0 不翻转,yaw=0 北朝上
mainCamera.s35_1.f20_1 = -Math.PI / 2;
mainCamera.s35_1.g20_1 = 0;
mainCamera.s35_1.h20_1 = 0;
mainCamera.r35_1.f20_1 = viewCenterX;
mainCamera.r35_1.g20_1 = viewCenterY;
mainCamera.r35_1.h20_1 = camHeight;
mainCamera.s36_1 = FBO_W;
mainCamera.t36_1 = FBO_H;
mainCamera.z36_1 = Config.fov;
mainCamera.x36_1 = Config.near;
mainCamera.y36_1 = Config.far;
mainCamera.o37();
// 禁用阴影/后处理
var savedX3n = ff.w38_1.e3o_1;
var savedY3n = ff.w38_1.f3o_1;
var savedZ3n = ff.w38_1.g3o_1;
ff.w38_1.e3o_1 = false;
ff.w38_1.f3o_1 = false;
ff.w38_1.g3o_1 = false;
// 禁用 UI 层
var savedUiQueues = {};
for (var ui = 0; ui < uiLayerIndices.length; ui++) {
var li = uiLayerIndices[ui];
savedUiQueues[li] = ff.e39_1[li];
ff.e39_1[li] = fakeQueue;
}
// 渲染到 FBO
sandboxRendering = true;
origBindFramebuffer(gl.FRAMEBUFFER, sandboxFbo);
gl.viewport(0, 0, FBO_W, FBO_H);
gl.clearColor(0.05, 0.08, 0.12, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
ff.f3t(rootNode, mainCamera);
sandboxRendering = false;
// Readback
origBindFramebuffer(gl.FRAMEBUFFER, sandboxFbo);
if (pboSupported) {
var curPbo = pboIndex, prevPbo = 1 - pboIndex;
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbos[curPbo]);
gl.readPixels(0, 0, FBO_W, FBO_H, gl.RGBA, gl.UNSIGNED_BYTE, 0);
if (pboReady) {
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbos[prevPbo]);
gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, pixels);
}
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
pboIndex = prevPbo;
pboReady = true;
} else {
gl.readPixels(0, 0, FBO_W, FBO_H, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
}
// 强制 alpha=255
for (var ai = 0; ai < pixels32.length; ai++) pixels32[ai] = pixels32[ai] | 0xFF000000;
imageData.data.set(pixels);
// Y 翻转显示(roll=0 时 GL readback 产生上下颠倒图像,需翻转)
tempCtx.putImageData(imageData, 0, 0);
ctx.clearRect(0, 0, cw, ch);
ctx.save();
ctx.imageSmoothingEnabled = true;
ctx.scale(1, -1);
ctx.drawImage(tempCanvas, 0, -ch, cw, ch);
ctx.restore();
// 恢复主 FBO
origBindFramebuffer(gl.FRAMEBUFFER, mainFbo);
// 恢复渲染状态
ff.w38_1.e3o_1 = savedX3n;
ff.w38_1.f3o_1 = savedY3n;
ff.w38_1.g3o_1 = savedZ3n;
for (var ui2 = 0; ui2 < uiLayerIndices.length; ui2++) {
ff.e39_1[uiLayerIndices[ui2]] = savedUiQueues[uiLayerIndices[ui2]];
}
// 恢复主相机
mainCamera.s35_1.f20_1 = sPitch;
mainCamera.s35_1.g20_1 = sRoll;
mainCamera.s35_1.h20_1 = sYaw;
mainCamera.r35_1.f20_1 = sX;
mainCamera.r35_1.g20_1 = sY;
mainCamera.r35_1.h20_1 = sZ;
mainCamera.s36_1 = sL36; mainCamera.t36_1 = sM36;
mainCamera.z36_1 = sS36; mainCamera.x36_1 = sQ36; mainCamera.y36_1 = sR36;
mainCamera.o37();
// 绘制 2D 叠加层(坦克标记/网格/罗盘)
drawOverlays();
} catch(e) {
console.error('[沙盘] 渲染失败:', e.message);
sandboxRendering = false;
} finally {
// 确保 FreeLook 恢复
if (fl && typeof fl.SetSuspended === 'function') fl.SetSuspended(false);
}
// 信息更新
if (frameCount % 30 === 0) {
var ext2 = getViewExtents();
var viewRange = cw * ext2.wppX; // 横向可视范围
infoLabel.textContent = '高度:' + (camHeight/100).toFixed(0) + 'm | 范围:' +
(viewRange/100).toFixed(0) + 'm | 拖拽平移 滚轮缩放 双击居中';
}
rafId = requestAnimationFrame(render);
}
window.addEventListener('resize', resizeCanvas);
rafId = requestAnimationFrame(render);
console.log('[沙盘] 战争沙盘已就绪 — 拖拽地图平移 | 滚轮缩放 | 双击居中玩家 | Alt+M 或 X 关闭');
// ===== 对外接口 =====
var self = {
Stop: function() {
running = false;
if (rafId) cancelAnimationFrame(rafId);
// 恢复 hook
gl.bindFramebuffer = origBindFramebuffer;
ff.w39_1.o3c = origH3c;
ff.w39_1.n3c = origG3c;
window.removeEventListener('resize', resizeCanvas);
if (container.parentNode) container.parentNode.removeChild(container);
window.__hornetWarSandbox__ = null;
console.log('[沙盘] 战争沙盘已关闭');
},
Status: function() { return running ? '已开启' : '未运行'; },
GetRunning: function() { return running; },
SetShowGrid: function(on) {
ShowGrid = !!on;
try { localStorage.setItem('hornet_ws_showgrid', ShowGrid ? 'true' : 'false'); } catch(e) {}
console.log('[沙盘] 网格: ' + (ShowGrid ? '显示' : '隐藏'));
},
GetShowGrid: function() { return ShowGrid; },
SetMarkerSize: function(s) {
var n = parseFloat(s);
if (!isNaN(n) && n >= 0.5 && n <= 3.0) {
MarkerSize = n;
try { localStorage.setItem('hornet_ws_marker_size', String(n)); } catch(e) {}
console.log('[沙盘] 标记大小: ' + n);
} else {
console.warn('[沙盘] 标记大小应在 0.5~3.0');
}
},
GetMarkerSize: function() { return MarkerSize; }
};
window.__hornetWarSandbox__ = self;
window.__hornetTools__ = window.__hornetTools__ || {};
window.__hornetTools__.WarSandbox = self;
}
function _stop() {
if (window.__hornetWarSandbox__) window.__hornetWarSandbox__.Stop();
}
function _status() {
return window.__hornetWarSandbox__ ? window.__hornetWarSandbox__.Status() : '未运行';
}
function _getRunning() {
return !!(window.__hornetWarSandbox__ && window.__hornetWarSandbox__.GetRunning());
}
window.__hornetTools__ = window.__hornetTools__ || {};
window.__hornetTools__.WarSandbox = {
Start: _start,
Stop: _stop,
Status: _status,
GetRunning: _getRunning,
SetShowGrid: function(on) { if (window.__hornetWarSandbox__) window.__hornetWarSandbox__.SetShowGrid(on); },
GetShowGrid: function() { return window.__hornetWarSandbox__ ? window.__hornetWarSandbox__.GetShowGrid() : true; },
SetMarkerSize: function(s) { if (window.__hornetWarSandbox__) window.__hornetWarSandbox__.SetMarkerSize(s); },
GetMarkerSize: function() { return window.__hornetWarSandbox__ ? window.__hornetWarSandbox__.GetMarkerSize() : 1.0; }
};
})();
// ===== hornet_panel.js =====
// ===== 3DTank 控制面板 =====
// 功能:Alt+T 切换面板显示,控制各工具开关
// 左上角 HUD 实时显示 GHT 捕获状态和各工具运行状态
// HUD 和面板支持鼠标拖动
// 依赖:hook_gamemode.js + hornet_lib.js + 各 hornet_*.js 工具模块
(function() {
console.log('[3DTank] 控制面板加载');
// ===== 清理旧面板 =====
if (window.__hornetPanel__) {
window.__hornetPanel__.Destroy();
console.log('已清理旧控制面板');
}
// ===== 通用拖动功能 =====
// elem: 要拖动的元素,handle: 触发拖动的元素(默认=elem)
// ignoreTags: 点击这些标签时不触发拖动(如 BUTTON)
// 实现要点:Pointer Events + setPointerCapture 把指针"锁"在 handle 上,
// pointerup/pointercancel 必定回到 handle;并在 window 捕获阶段加兜底,
// 避免面板内 stopPropagation 或游戏画布抢事件导致"松手后仍粘着鼠标"。
// 返回:{ End, IsActive },供隐藏面板时强制结束拖动
function MakeDraggable(elem, handle, ignoreTags) {
handle = handle || elem;
ignoreTags = ignoreTags || ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA'];
handle.style.cursor = 'move';
var Active = false;
var PointerId = null;
var GrabX = 0;
var GrabY = 0;
function MoveTo(ClientX, ClientY) {
var MaxX = window.innerWidth - 40;
var MaxY = window.innerHeight - 24;
var NextX = Math.max(0, Math.min(MaxX, ClientX - GrabX));
var NextY = Math.max(0, Math.min(MaxY, ClientY - GrabY));
elem.style.left = NextX + 'px';
elem.style.top = NextY + 'px';
}
function Begin(ClientX, ClientY) {
var Rect = elem.getBoundingClientRect();
// 移除 transform 居中,改用绝对定位,避免拖动起点跳变
elem.style.transform = 'none';
elem.style.left = Rect.left + 'px';
elem.style.top = Rect.top + 'px';
GrabX = ClientX - Rect.left;
GrabY = ClientY - Rect.top;
Active = true;
handle.style.cursor = 'grabbing';
}
function End() {
if (!Active) return;
Active = false;
if (PointerId !== null && handle.releasePointerCapture) {
try { handle.releasePointerCapture(PointerId); } catch (Err) {}
}
PointerId = null;
handle.style.cursor = 'move';
}
function CanStart(Target) {
return !Target || ignoreTags.indexOf(Target.tagName) < 0;
}
if (window.PointerEvent) {
// 捕获阶段监听:先于元素自身的 stopPropagation 执行
handle.addEventListener('pointerdown', function (e) {
if (e.button !== 0) return;
if (!CanStart(e.target)) return;
Begin(e.clientX, e.clientY);
PointerId = e.pointerId;
try { handle.setPointerCapture(e.pointerId); } catch (Err) {}
e.preventDefault();
e.stopPropagation();
}, true);
handle.addEventListener('pointermove', function (e) {
if (!Active || (PointerId !== null && e.pointerId !== PointerId)) return;
// 兜底:按键已经松开(例如在窗口外松手)→ 结束拖动
if (e.buttons === 0) { End(); return; }
MoveTo(e.clientX, e.clientY);
e.preventDefault();
}, true);
handle.addEventListener('pointerup', function () { End(); }, true);
handle.addEventListener('pointercancel', function () { End(); }, true);
handle.addEventListener('lostpointercapture', function () { End(); }, true);
window.addEventListener('pointerup', function () { End(); }, true);
} else {
// 旧浏览器回退:mouse 事件 + document 捕获阶段监听
handle.addEventListener('mousedown', function (e) {
if (e.button !== 0) return;
if (!CanStart(e.target)) return;
Begin(e.clientX, e.clientY);
e.preventDefault();
}, true);
document.addEventListener('mousemove', function (e) {
if (!Active) return;
if (e.buttons === 0) { End(); return; }
MoveTo(e.clientX, e.clientY);
}, true);
document.addEventListener('mouseup', function () { End(); }, true);
}
// 公共兜底:窗口失焦 / 页面切后台 / 鼠标离开窗口
window.addEventListener('blur', End);
window.addEventListener('mouseleave', End);
document.addEventListener('visibilitychange', function () { if (document.hidden) End(); });
return { End: End, IsActive: function () { return Active; } };
}
// ===== 工具列表(动态从 __hornetTools__ 获取)=====
function GetToolList() {
var tools = window.__hornetTools__ || {};
return [
{ key: 'Radar', label: '雷达', obj: tools.Radar },
{ key: 'Wireframe', label: '3D透视', obj: tools.Wireframe, hasMode: true },
{ key: 'Rearview', label: '后视镜', obj: tools.Rearview },
{ key: 'AimWarning', label: '瞄准警告', obj: tools.AimWarning },
{ key: 'FreeLook', label: '自由视角', obj: tools.FreeLook },
{ key: 'WarSandbox', label: '战争沙盘', obj: tools.WarSandbox }
];
}
// ===== HUD(左上角状态显示,可拖动)=====
var hud = document.createElement('div');
hud.id = '3dtank-hud';
hud.style.cssText =
'position:fixed;top:10px;left:10px;z-index:100000;pointer-events:auto;cursor:move;' +
'font:11px Consolas,monospace;color:#7df9ff;background:rgba(5,10,18,0.55);' +
'backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);' +
'padding:8px 12px;border-radius:6px;border:1px solid rgba(125,249,255,0.25);' +
'white-space:pre;line-height:1.6;text-shadow:0 0 3px rgba(125,249,255,0.4);' +
'user-select:none;-webkit-user-select:none;box-shadow:0 2px 12px rgba(0,0,0,0.4);';
document.body.appendChild(hud);
var HudDrag = MakeDraggable(hud);
// ===== 面板(可拖动,Alt+T 切换)=====
var panel = document.createElement('div');
panel.id = '3dtank-panel';
// 初始居中:用 left/top 50% + transform,拖动后移除 transform
panel.style.cssText =
'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:100001;' +
'background:rgba(12,16,24,0.72);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);' +
'border:1px solid rgba(125,249,255,0.2);border-radius:12px;' +
'padding:18px 22px;color:#e8eef5;font:13px Consolas,monospace;min-width:340px;' +
'box-shadow:0 8px 32px rgba(0,0,0,0.6),0 0 1px rgba(125,249,255,0.3),inset 0 1px 0 rgba(255,255,255,0.05);' +
'display:none;user-select:none;-webkit-user-select:none;';
// 标题(可拖动手柄)
var title = document.createElement('div');
title.textContent = '3DTank 控制面板';
title.style.cssText =
'font-size:16px;font-weight:bold;color:#7df9ff;margin-bottom:4px;' +
'text-shadow:0 0 6px rgba(125,249,255,0.4);' +
'cursor:move;padding-bottom:8px;border-bottom:1px solid rgba(125,249,255,0.15);' +
'letter-spacing:0.5px;';
panel.appendChild(title);
var subtitle = document.createElement('div');
subtitle.textContent = 'Alt+T 切换显示/隐藏 | 拖动标题移动面板';
subtitle.style.cssText = 'font-size:11px;color:#5a7a8a;margin:6px 0 14px;';
panel.appendChild(subtitle);
// 工具按钮容器
var btnContainer = document.createElement('div');
panel.appendChild(btnContainer);
// ===== 3D透视设置子面板(可折叠)=====
var wfSettings = document.createElement('div');
wfSettings.style.cssText = 'margin-top:12px;padding-top:10px;border-top:1px solid rgba(125,249,255,0.08);';
var wfHeader = document.createElement('div');
wfHeader.style.cssText = 'display:flex;justify-content:space-between;align-items:center;cursor:pointer;color:#7df9ff;font-weight:bold;font-size:13px;';
wfHeader.innerHTML = '<span>3D透视设置</span><span id="wfToggle" style="font-size:11px;color:#5a7a8a;">▼</span>';
wfSettings.appendChild(wfHeader);
var wfBody = document.createElement('div');
wfBody.style.cssText = 'margin-top:8px;display:block;';
wfSettings.appendChild(wfBody);
var wfCollapsed = false;
wfHeader.onclick = function() {
wfCollapsed = !wfCollapsed;
wfBody.style.display = wfCollapsed ? 'none' : 'block';
document.getElementById('wfToggle').textContent = wfCollapsed ? '▶' : '▼';
};
// 行布局辅助
function MakeRow(labelText) {
var row = document.createElement('div');
row.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:6px 0;';
var lab = document.createElement('span');
lab.style.cssText = 'color:#b8c8d8;font-size:12px;';
lab.textContent = labelText;
row.appendChild(lab);
var ctrl = document.createElement('div');
ctrl.style.cssText = 'display:flex;gap:6px;align-items:center;';
row.appendChild(ctrl);
wfBody.appendChild(row);
return { row: row, ctrl: ctrl };
}
// 线宽调节
var lwRow = MakeRow('线宽');
var lwSlider = document.createElement('input');
lwSlider.type = 'range';
lwSlider.min = '0.5'; lwSlider.max = '8'; lwSlider.step = '0.5';
lwSlider.style.cssText = 'width:120px;accent-color:#7df9ff;';
var lwVal = document.createElement('span');
lwVal.style.cssText = 'color:#7df9ff;font-size:11px;min-width:28px;text-align:right;';
lwRow.ctrl.appendChild(lwSlider);
lwRow.ctrl.appendChild(lwVal);
lwSlider.oninput = function() {
var v = parseFloat(lwSlider.value);
lwVal.textContent = v.toFixed(1);
var tools = window.__hornetTools__ || {};
if (tools.Wireframe) tools.Wireframe.SetLineWidth(v);
};
// 渲染模式选择
var hmRow = MakeRow('渲染模式');
var hmSelect = document.createElement('select');
hmSelect.style.cssText =
'padding:3px 6px;border:1px solid rgba(125,249,255,0.25);border-radius:4px;' +
'background:rgba(20,30,40,0.6);color:#e8eef5;font:11px Consolas;cursor:pointer;';
var hmOptions = [
{ v: 'team', t: '队伍模式(蓝友/红敌)' },
{ v: 'whitelist', t: '白名单(仅名单内渲染)' },
{ v: 'blacklist', t: '黑名单(名单内不渲染)' }
];
hmOptions.forEach(function(o) {
var opt = document.createElement('option');
opt.value = o.v; opt.textContent = o.t;
hmSelect.appendChild(opt);
});
hmRow.ctrl.appendChild(hmSelect);
hmSelect.onchange = function() {
var tools = window.__hornetTools__ || {};
if (tools.Wireframe) tools.Wireframe.SetHighlightMode(hmSelect.value);
};
// 名单管理
var hlRow = MakeRow('名单');
var hlInput = document.createElement('input');
hlInput.type = 'text';
hlInput.placeholder = '玩家名';
hlInput.style.cssText =
'width:120px;padding:3px 6px;border:1px solid rgba(125,249,255,0.25);border-radius:4px;' +
'background:rgba(20,30,40,0.6);color:#e8eef5;font:11px Consolas;';
var hlAddBtn = document.createElement('button');
hlAddBtn.textContent = '+';
hlAddBtn.style.cssText =
'padding:3px 8px;border:1px solid rgba(46,204,113,0.4);border-radius:4px;' +
'background:rgba(46,204,113,0.2);color:#2ecc71;cursor:pointer;font:bold 12px Consolas;';
hlRow.ctrl.appendChild(hlInput);
hlRow.ctrl.appendChild(hlAddBtn);
var hlList = document.createElement('div');
hlList.style.cssText = 'margin-top:6px;max-height:120px;overflow-y:auto;font-size:11px;';
wfBody.appendChild(hlList);
function RefreshHlList() {
var tools = window.__hornetTools__ || {};
var arr = tools.Wireframe ? tools.Wireframe.GetHighlightList() : [];
hlList.innerHTML = '';
if (arr.length === 0) {
var empty = document.createElement('div');
empty.style.cssText = 'color:#5a7a8a;padding:4px 0;font-style:italic;';
empty.textContent = '(空)';
hlList.appendChild(empty);
return;
}
arr.forEach(function(name) {
var item = document.createElement('div');
item.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:3px 4px;border-bottom:1px solid rgba(125,249,255,0.05);';
var ns = document.createElement('span');
ns.style.cssText = 'color:#b8c8d8;';
ns.textContent = name;
item.appendChild(ns);
var delBtn = document.createElement('button');
delBtn.textContent = '×';
delBtn.style.cssText =
'padding:1px 6px;border:1px solid rgba(224,85,85,0.4);border-radius:3px;' +
'background:rgba(224,85,85,0.2);color:#e05555;cursor:pointer;font:bold 11px Consolas;';
delBtn.onclick = function() {
if (tools.Wireframe) tools.Wireframe.RemoveHighlight(name);
RefreshHlList();
};
item.appendChild(delBtn);
hlList.appendChild(item);
});
}
function AddFromInput() {
var name = hlInput.value.trim();
if (!name) return;
var tools = window.__hornetTools__ || {};
if (tools.Wireframe) tools.Wireframe.AddHighlight(name);
hlInput.value = '';
RefreshHlList();
}
hlAddBtn.onclick = AddFromInput;
hlInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') { e.preventDefault(); AddFromInput(); }
});
var hlClearRow = document.createElement('div');
hlClearRow.style.cssText = 'margin-top:4px;text-align:right;';
var hlClearBtn = document.createElement('button');
hlClearBtn.textContent = '清空名单';
hlClearBtn.style.cssText =
'padding:3px 8px;border:1px solid rgba(224,85,85,0.4);border-radius:4px;' +
'background:rgba(224,85,85,0.15);color:#e05555;cursor:pointer;font:11px Consolas;';
hlClearBtn.onclick = function() {
var tools = window.__hornetTools__ || {};
if (tools.Wireframe) tools.Wireframe.ClearHighlight();
RefreshHlList();
};
hlClearRow.appendChild(hlClearBtn);
wfBody.appendChild(hlClearRow);
panel.appendChild(wfSettings);
// ===== 通用可折叠设置子面板辅助函数 =====
function MakeSettingsSection(titleText) {
var sec = document.createElement('div');
sec.style.cssText = 'margin-top:10px;padding-top:8px;border-top:1px solid rgba(125,249,255,0.08);';
var hdr = document.createElement('div');
hdr.style.cssText = 'display:flex;justify-content:space-between;align-items:center;cursor:pointer;color:#7df9ff;font-weight:bold;font-size:12px;';
var toggleChar = document.createElement('span');
toggleChar.style.cssText = 'font-size:10px;color:#5a7a8a;';
toggleChar.textContent = '▶';
hdr.innerHTML = '<span>' + titleText + '</span>';
hdr.appendChild(toggleChar);
sec.appendChild(hdr);
var body = document.createElement('div');
body.style.cssText = 'margin-top:6px;display:none;';
sec.appendChild(body);
hdr.onclick = function() {
var shown = body.style.display !== 'none';
body.style.display = shown ? 'none' : 'block';
toggleChar.textContent = shown ? '▶' : '▼';
};
panel.appendChild(sec);
function row(labelText) {
var r = document.createElement('div');
r.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:5px 0;';
var lab = document.createElement('span');
lab.style.cssText = 'color:#b8c8d8;font-size:11px;';
lab.textContent = labelText;
r.appendChild(lab);
var ctrl = document.createElement('div');
ctrl.style.cssText = 'display:flex;gap:6px;align-items:center;';
r.appendChild(ctrl);
body.appendChild(r);
return { row: r, ctrl: ctrl };
}
function slider(labelText, min, max, step, fmt, onInput) {
var r = row(labelText);
var s = document.createElement('input');
s.type = 'range'; s.min = String(min); s.max = String(max); s.step = String(step);
s.style.cssText = 'width:110px;accent-color:#7df9ff;';
var v = document.createElement('span');
v.style.cssText = 'color:#7df9ff;font-size:10px;min-width:32px;text-align:right;';
s.oninput = function() {
var val = parseFloat(s.value);
v.textContent = fmt(val);
onInput(val);
};
r.ctrl.appendChild(s); r.ctrl.appendChild(v);
return {
set: function(val) { s.value = val; v.textContent = fmt(parseFloat(val)); },
get: function() { return parseFloat(s.value); }
};
}
function checkbox(labelText, onChange) {
var r = row(labelText);
var c = document.createElement('input');
c.type = 'checkbox';
c.style.cssText = 'accent-color:#7df9ff;width:16px;height:16px;cursor:pointer;';
c.onchange = function() { onChange(c.checked); };
r.ctrl.appendChild(c);
return {
set: function(on) { c.checked = !!on; },
get: function() { return c.checked; }
};
}
return { sec: sec, body: body, row: row, slider: slider, checkbox: checkbox };
}
// ===== 雷达设置 =====
var radarSec = MakeSettingsSection('雷达设置');
var radarRangeCtl = radarSec.slider('范围(m)', 30, 600, 10, function(v){ return v.toFixed(0); }, function(v) {
var tools = window.__hornetTools__ || {};
if (tools.Radar) tools.Radar.SetRange(v * 100);
});
var radarOpacityCtl = radarSec.slider('透明度', 0.2, 1.0, 0.05, function(v){ return v.toFixed(2); }, function(v) {
var tools = window.__hornetTools__ || {};
if (tools.Radar) tools.Radar.SetOpacity(v);
});
// ===== 后视镜设置 =====
var rvSec = MakeSettingsSection('后视镜设置');
var rvFovCtl = rvSec.slider('FOV', 0.5, 2.5, 0.05, function(v){ return v.toFixed(2); }, function(v) {
var tools = window.__hornetTools__ || {};
if (tools.Rearview) tools.Rearview.SetFov(v);
});
var rvOpacityCtl = rvSec.slider('透明度', 0.2, 1.0, 0.05, function(v){ return v.toFixed(2); }, function(v) {
var tools = window.__hornetTools__ || {};
if (tools.Rearview) tools.Rearview.SetOpacity(v);
});
// ===== 瞄准警告设置 =====
var awSec = MakeSettingsSection('瞄准警告设置');
var awSensCtl = awSec.slider('灵敏度', 0.3, 3.0, 0.1, function(v){ return v.toFixed(1); }, function(v) {
var tools = window.__hornetTools__ || {};
if (tools.AimWarning) tools.AimWarning.SetSensitivity(v);
});
var awSoundCtl = awSec.checkbox('警告声音', function(on) {
var tools = window.__hornetTools__ || {};
if (tools.AimWarning) tools.AimWarning.SetSound(on);
});
// ===== 战争沙盘设置 =====
var wsSec = MakeSettingsSection('战争沙盘设置');
var wsGridCtl = wsSec.checkbox('显示网格', function(on) {
var tools = window.__hornetTools__ || {};
if (tools.WarSandbox) tools.WarSandbox.SetShowGrid(on);
});
var wsMarkerCtl = wsSec.slider('标记大小', 0.5, 3.0, 0.1, function(v){ return v.toFixed(1); }, function(v) {
var tools = window.__hornetTools__ || {};
if (tools.WarSandbox) tools.WarSandbox.SetMarkerSize(v);
});
// 从工具读取当前设置(打开面板时同步)
function SyncWfSettings() {
var tools = window.__hornetTools__ || {};
if (tools.Wireframe) {
var lw = tools.Wireframe.GetLineWidth();
lwSlider.value = lw; lwVal.textContent = lw.toFixed(1);
var hm = tools.Wireframe.GetHighlightMode();
hmSelect.value = hm;
}
RefreshHlList();
// 同步其他工具设置
if (tools.Radar) {
radarRangeCtl.set(tools.Radar.GetRange() / 100);
radarOpacityCtl.set(tools.Radar.GetOpacity());
}
if (tools.Rearview) {
rvFovCtl.set(tools.Rearview.GetFov());
rvOpacityCtl.set(tools.Rearview.GetOpacity());
}
if (tools.AimWarning) {
awSensCtl.set(tools.AimWarning.GetSensitivity());
awSoundCtl.set(tools.AimWarning.GetSound());
}
if (tools.WarSandbox) {
wsGridCtl.set(tools.WarSandbox.GetShowGrid());
wsMarkerCtl.set(tools.WarSandbox.GetMarkerSize());
}
}
// 底部状态
var footer = document.createElement('div');
footer.style.cssText = 'margin-top:14px;padding-top:10px;border-top:1px solid rgba(125,249,255,0.08);font-size:11px;color:#5a7a8a;text-align:center;';
footer.textContent = '点击按钮切换 开/关';
panel.appendChild(footer);
// 热键显示区域(UpdateHotkeyDisplay 在热键系统定义后调用)
var hotkeySection = document.createElement('div');
hotkeySection.style.cssText = 'margin-top:12px;padding-top:10px;border-top:1px solid rgba(125,249,255,0.08);font-size:11px;';
function UpdateHotkeyDisplay() {
if (!HotkeyLabels || !Hotkeys) return;
var html = '<div style="color:#7a9aaa;margin-bottom:4px;">热键(控制台可改: __hornetPanel__.SetHotkey)</div>';
Object.keys(HotkeyLabels).forEach(function(k) {
html += '<div style="display:flex;justify-content:space-between;padding:2px 0;">' +
'<span style="color:#5a7a8a;">' + HotkeyLabels[k] + '</span>' +
'<span style="color:#7df9ff;font-weight:bold;">' + Hotkeys[k] + '</span></div>';
});
hotkeySection.innerHTML = html;
}
panel.appendChild(hotkeySection);
document.body.appendChild(panel);
// 面板通过标题栏拖动
var PanelDrag = MakeDraggable(panel, title);
// ===== 阻止面板内事件冒泡到 window(防止游戏/工具拦截键盘鼠标)=====
// 鼠标事件:面板上所有鼠标事件都阻止冒泡(防止游戏转视角/开炮等)
['mousedown','mouseup','mousemove','wheel','click','dblclick','contextmenu'].forEach(function(evtName) {
panel.addEventListener(evtName, function(e) { e.stopPropagation(); });
});
// 键盘事件:只在输入元素上阻止冒泡(保留面板上其他位置的热键功能)
['keydown','keypress','keyup'].forEach(function(evtName) {
panel.addEventListener(evtName, function(e) {
var tag = e.target.tagName;
if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA') {
e.stopPropagation();
}
});
});
// HUD 也阻止鼠标事件冒泡(防止拖动HUD时游戏响应)
['mousedown','mouseup','mousemove','wheel','click','contextmenu'].forEach(function(evtName) {
hud.addEventListener(evtName, function(e) { e.stopPropagation(); });
});
// ===== 构建工具按钮(含模式切换)=====
var btnMap = {}; // key -> { row, btn, label, tool, modeBtns }
function BuildButtons() {
btnContainer.innerHTML = '';
btnMap = {};
var list = GetToolList();
list.forEach(function(t) {
var row = document.createElement('div');
row.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid rgba(125,249,255,0.06);';
var label = document.createElement('span');
label.textContent = t.label;
label.style.cssText = 'color:#b8c8d8;font-size:13px;';
row.appendChild(label);
// 右侧按钮容器
var btnBox = document.createElement('div');
btnBox.style.cssText = 'display:flex;gap:4px;align-items:center;';
var modeBtns = null;
// Wireframe 模式切换按钮
if (t.hasMode && t.obj && t.obj.SetMode) {
modeBtns = {};
var modes = [
{ mode: 'wireframe', text: '线框' },
{ mode: 'outline', text: '描边' }
];
modes.forEach(function(mb) {
var b = document.createElement('button');
b.textContent = mb.text;
b.style.cssText =
'padding:4px 8px;border:1px solid rgba(125,249,255,0.25);border-radius:4px;' +
'font:11px Consolas,monospace;cursor:pointer;color:#e8eef5;min-width:44px;' +
'background:rgba(20,30,40,0.6);transition:all 0.2s;';
b.onclick = function() {
if (t.obj) {
t.obj.SetMode(mb.mode);
UpdateButtons();
}
};
btnBox.appendChild(b);
modeBtns[mb.mode] = b;
});
}
var btn = document.createElement('button');
btn.style.cssText =
'padding:6px 16px;border:none;border-radius:6px;font:bold 12px Consolas,monospace;' +
'cursor:pointer;min-width:72px;color:#fff;transition:all 0.2s;' +
'box-shadow:0 2px 8px rgba(0,0,0,0.3);';
btn.onclick = function() {
if (!t.obj) {
console.warn('[3DTank] 工具 ' + t.key + ' 未加载');
return;
}
if (t.obj.GetRunning()) {
t.obj.Stop();
} else {
t.obj.Start();
}
UpdateButtons();
};
btnBox.appendChild(btn);
row.appendChild(btnBox);
btnContainer.appendChild(row);
btnMap[t.key] = { btn: btn, label: label, tool: t.obj, modeBtns: modeBtns };
});
}
BuildButtons();
// ===== 更新按钮状态 =====
function UpdateButtons() {
Object.keys(btnMap).forEach(function(key) {
var info = btnMap[key];
if (!info.tool) {
info.btn.textContent = '未加载';
info.btn.style.background = '#555';
info.btn.disabled = true;
return;
}
var running = info.tool.GetRunning();
info.btn.textContent = running ? '关闭' : '开启';
info.btn.style.background = running
? 'linear-gradient(135deg,#e05555,#c0333d)'
: 'linear-gradient(135deg,#2ecc71,#27ae60)';
info.btn.disabled = false;
// 更新模式按钮高亮
if (info.modeBtns) {
var curMode = null;
try { curMode = window.__hornet3DWireframe__ && window.__hornet3DWireframe__.mode; } catch(e) {}
Object.keys(info.modeBtns).forEach(function(mk) {
var mb = info.modeBtns[mk];
if (mk === curMode && running) {
mb.style.background = 'linear-gradient(135deg,#2ecc71,#27ae60)';
mb.style.opacity = '1';
mb.style.color = '#fff';
} else {
mb.style.background = 'rgba(20,30,40,0.6)';
mb.style.opacity = '0.6';
mb.style.color = '#8899aa';
}
});
}
});
}
// ===== 更新 HUD =====
function UpdateHud() {
requestAnimationFrame(UpdateHud);
var ght = window.__gameMode__;
var ghtStatus = ght ? '已捕获' : '未捕获';
var ghtColor = ght ? '#7df9ff' : '#f5a623';
var lines = [];
lines.push('3DTank 控制台 [拖动移动]');
lines.push('GHT: ' + ghtStatus);
var list = GetToolList();
list.forEach(function(t) {
var status = t.obj ? t.obj.Status() : '未加载';
var dot = t.obj && t.obj.GetRunning() ? '●' : '○';
lines.push(dot + ' ' + t.label + ': ' + status);
});
hud.textContent = lines.join('\n');
hud.style.color = ghtColor;
}
UpdateHud();
// ===== 热键系统(localStorage 持久化)=====
var DefaultHotkeys = {
panel: 'Alt+T',
radar: 'Alt+R',
wireframe: 'Alt+W',
rearview: 'Alt+V',
aimWarning: 'Alt+A',
freeLook: 'Alt+F',
warSandbox: 'Alt+M'
};
var HotkeyLabels = {
panel: '面板',
radar: '雷达',
wireframe: '3D透视',
rearview: '后视镜',
aimWarning: '瞄准警告',
freeLook: '自由视角',
warSandbox: '战争沙盘'
};
var Hotkeys = {};
try {
var saved = localStorage.getItem('3dtank_hotkeys');
var parsed = saved ? JSON.parse(saved) : {};
Object.keys(DefaultHotkeys).forEach(function(k) {
Hotkeys[k] = parsed[k] || DefaultHotkeys[k];
});
} catch(e) {
Object.keys(DefaultHotkeys).forEach(function(k) { Hotkeys[k] = DefaultHotkeys[k]; });
}
function SaveHotkeys() {
try { localStorage.setItem('3dtank_hotkeys', JSON.stringify(Hotkeys)); } catch(e) {}
}
function EventToHotkey(e) {
var parts = [];
if (e.altKey) parts.push('Alt');
if (e.ctrlKey) parts.push('Ctrl');
if (e.shiftKey) parts.push('Shift');
var key = e.key;
if (key.length === 1) key = key.toUpperCase();
if (key === ' ') key = 'Space';
parts.push(key);
return parts.join('+');
}
function HidePanel() {
panel.style.display = 'none';
PanelDrag.End(); // 防止隐藏时拖动状态残留(粘手)
}
function TogglePanel() {
if (panel.style.display === 'block') {
HidePanel();
} else {
panel.style.display = 'block';
UpdateButtons();
SyncWfSettings();
}
}
function OnHotkey(e) {
if (e.key === 'Escape' && panel.style.display === 'block') {
HidePanel();
return;
}
var hk = EventToHotkey(e);
if (hk === Hotkeys.panel) {
e.preventDefault();
TogglePanel();
return;
}
var toolMap = { radar: 'Radar', wireframe: 'Wireframe', rearview: 'Rearview', aimWarning: 'AimWarning', freeLook: 'FreeLook', warSandbox: 'WarSandbox' };
Object.keys(toolMap).forEach(function(k) {
if (hk === Hotkeys[k]) {
e.preventDefault();
var tools = window.__hornetTools__ || {};
var tool = tools[toolMap[k]];
if (tool) {
if (tool.GetRunning()) tool.Stop();
else tool.Start();
UpdateButtons();
}
}
});
}
window.addEventListener('keydown', OnHotkey);
// 热键系统定义完成,更新显示
UpdateHotkeyDisplay();
// ===== 暴露接口 =====
window.__hornetPanel__ = {
Toggle: TogglePanel,
Show: function() { panel.style.display = 'block'; UpdateButtons(); SyncWfSettings(); },
Hide: HidePanel,
UpdateButtons: UpdateButtons,
RebuildButtons: BuildButtons,
GetHotkeys: function() { var r = {}; Object.keys(Hotkeys).forEach(function(k) { r[k] = Hotkeys[k]; }); return r; },
SetHotkey: function(name, key) {
if (DefaultHotkeys[name]) {
Hotkeys[name] = key;
SaveHotkeys();
UpdateHotkeyDisplay();
console.log('[3DTank] 热键 ' + name + ' 已设为 ' + key);
} else {
console.warn('[3DTank] 未知热键名: ' + name + ',可用: ' + Object.keys(DefaultHotkeys).join(', '));
}
},
ResetHotkeys: function() {
Object.keys(DefaultHotkeys).forEach(function(k) { Hotkeys[k] = DefaultHotkeys[k]; });
SaveHotkeys();
UpdateHotkeyDisplay();
console.log('[3DTank] 热键已重置为默认值');
},
Destroy: function() {
HudDrag.End();
PanelDrag.End();
if (hud.parentNode) hud.parentNode.removeChild(hud);
if (panel.parentNode) panel.parentNode.removeChild(panel);
window.removeEventListener('keydown', OnHotkey);
}
};
console.log('[3DTank] 控制面板已就绪,按 Alt+T 打开面板,HUD/面板/窗口可拖动,热键已加载');
})();