// ===== 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.lz4().jz1_1;
mainCamera = scene.iyx_1;
ff = scene.gyw_1.d75_1;
if (!mainCamera || !mainCamera.u35_1 || !mainCamera.r35_1 || !mainCamera.o37) {
throw new Error('相机字段缺失');
}
console.log('[自由视角] 主相机:', mainCamera.constructor.name,
'l35=(' + mainCamera.u35_1.f20_1.toFixed(2) + ',' + mainCamera.u35_1.g20_1.toFixed(2) + ',' + mainCamera.u35_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.u35_1.f20_1 = freePitch; // pitch (f)
mainCamera.u35_1.g20_1 = freeRoll; // roll (g) 保持初始值
mainCamera.u35_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.u35_1.f20_1;
freeRoll = mainCamera.u35_1.g20_1; // roll 保持初始值
freeYaw = mainCamera.u35_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) + '/秒
' +
'位置:(' + freePosX.toFixed(0) + ',' + freePosY.toFixed(0) + ',' + freePosZ.toFixed(0) + ')
' +
'Yaw:' + freeYaw.toFixed(2) + ' Pitch:' + freePitch.toFixed(2) + '
' +
'WASD移动 鼠标旋转 空格上升/Shift下降 滚轮调速 Alt+F关闭';
}
// ===== 主循环:处理按键 → 更新位置 =====
var rafId = null;
var lastTime = performance.now();
function loop(now) {
if (!running) return;
// 换图自刷新:gameMode 实例变化时整体重启,重新 Hook 新相机的 o37
var NewGm = window.__gameMode__;
if (NewGm && NewGm !== gm) {
console.log('[自由视角] 检测到新战斗,正在重启以绑定新相机...');
self.Stop();
_start();
return;
}
// 旧实例已失效、新实例尚未捕获:跳过本帧
if (!window.__gameMode__) { rafId = requestAnimationFrame(loop); return; }
var dt = (now - lastTime) / 1000;
lastTime = now;
if (dt > 0.1) dt = 0.1; // 防止卡顿后大跳
if (freeLookActive) {
var speed = moveSpeed * speedMultiplier * dt; // 每秒 moveSpeed 单位
// 相机前方向/右方向:优先用 viewMatrix 提取的真实方向(跟随鼠标朝向)
var fwdX, fwdY, rightX, rightY;
if (cachedInitialized) {
// 从 viewMatrix 提取的水平方向分量(列主序)
fwdX = cachedFwdX;
fwdY = cachedFwdY;
rightX = cachedRightX;
rightY = cachedRightY;
// 归一化水平方向(pitch 不为0时向量长度<1,保持移动速度一致)
var fwdLen = Math.sqrt(fwdX * fwdX + fwdY * fwdY);
if (fwdLen > 1e-6) { fwdX /= fwdLen; fwdY /= fwdLen; }
var rightLen = Math.sqrt(rightX * rightX + rightY * rightY);
if (rightLen > 1e-6) { rightX /= rightLen; rightY /= rightLen; }
} else {
// fallback: sin/cos(viewMatrix 尚未初始化时)
fwdX = -Math.sin(freeYaw);
fwdY = -Math.cos(freeYaw);
rightX = Math.cos(freeYaw);
rightY = -Math.sin(freeYaw);
}
var moved = false;
// W/S 前后(水平移动,可与其他键组合)
if (keys['w']) { freePosX += fwdX * speed; freePosY += fwdY * speed; moved = true; }
if (keys['s']) { freePosX -= fwdX * speed; freePosY -= fwdY * speed; moved = true; }
// A/D 左右(水平移动,可与其他键组合)
if (keys['a']) { freePosX -= rightX * speed; freePosY -= rightY * speed; moved = true; }
if (keys['d']) { freePosX += rightX * speed; freePosY += rightY * speed; moved = true; }
// 空格上升 / Shift 下降(绝对 z 轴,可与 WASD 组合)
if (keys[' ']) { freePosZ += speed; moved = true; }
if (keys['shift']) { freePosZ -= speed; moved = true; }
// Q/E 升降(备用)
if (keys['q']) { freePosZ -= speed; moved = true; }
if (keys['e']) { freePosZ += speed; moved = true; }
if (moved) updateHud();
}
rafId = requestAnimationFrame(loop);
}
rafId = requestAnimationFrame(loop);
// ===== 对外接口 =====
var self = {
Stop: function() {
running = false;
if (rafId) cancelAnimationFrame(rafId);
disableFreeLook();
// 恢复 h37
if (hadOwnH37) mainCamera.o37 = origH37;
else delete mainCamera.o37;
// 移除监听
window.removeEventListener('keydown', onKeyDown, true);
window.removeEventListener('keyup', onKeyUp, true);
document.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('wheel', onWheel, true);
if (hud.parentNode) hud.parentNode.removeChild(hud);
window.__hornetFreeLook__ = null;
console.log('[自由视角] 已停止');
},
Status: function() {
// 返回字符串供面板 HUD 显示(其他工具也是字符串)
if (!running) return '未运行';
return freeLookActive ? '已开启' : '已暂停';
},
GetRunning: function() { return freeLookActive; },
IsActive: function() { return freeLookActive; },
Toggle: function() { toggleFreeLook(); },
Enable: function() { if (!freeLookActive) enableFreeLook(); },
Disable: function() { if (freeLookActive) disableFreeLook(); },
// 供其他模块(雷达/3D透视)获取虚拟相机参数
GetPosition: function() { return freeLookActive ? { x: freePosX, y: freePosY, z: freePosZ } : null; },
GetDirection: function() { return freeLookActive ? { yaw: freeYaw, pitch: freePitch } : null; },
// 供后视镜等模块渲染时临时暂停 h37 hook
SetSuspended: function(s) { suspended = s; }
};
window.__hornetFreeLook__ = self;
window.__hornetTools__ = window.__hornetTools__ || {};
window.__hornetTools__.FreeLook = self;
// 启动时自动激活自由视角
enableFreeLook();
console.log('[自由视角] 就绪 — 已激活,Alt+F 切换激活/暂停');
}
function _stop() {
if (window.__hornetFreeLook__) window.__hornetFreeLook__.Stop();
}
function _status() {
return window.__hornetFreeLook__ ? window.__hornetFreeLook__.Status() : '未运行';
}
function _getRunning() {
return !!(window.__hornetFreeLook__ && window.__hornetFreeLook__.GetRunning());
}
window.__hornetTools__ = window.__hornetTools__ || {};
window.__hornetTools__.FreeLook = {
Start: _start,
Stop: _stop,
Status: _status,
GetRunning: _getRunning
};
})();