Files
TankIC/tools/hornet_rearview.js
T
TermiNexus 56a1356cce fix(tools): 补全新版符号替换(修复透视/雷达仍不可用)
- 双向校验误判:q100_1 / r11l / jyw / t101_1 在新版同名但含义不同(普通数值 / 别的方法),被当作未变而跳过

- 强制替换:q100_1 到 m10b_1(坦克管理)、r11l 到 j11y(坦克列表)、jyw 到 lz4(取实体)、t101_1 到 p10c_1

- 覆盖 hook_gamemode.js(文件名不匹配 hornet_ 被漏掉)

- 物理组件字段 gzh_1 改为 rzf_1(新版 gzh_1 已是 TerminatorCC 字段,非物理)

- 移除已不存在的 xyu(GameMode),改用 gm.lz4()(等价:取 GameMode 实体)

- 重新生成 hornet_bundle.js(10 模块 / 249229 字节)并同步 unpacked
2026-10-17 15:49:50 +08:00

681 lines
26 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后视镜 - 原生渲染方案 =====
// 在浏览器控制台运行(需先运行 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.lz4().jz1_1;
console.log('[后视] 场景对象:', scene);
// hC 渲染器实例(jr.lye_1)
var renderer = scene.gyw_1;
console.log('[后视] 渲染器:', renderer);
// Ff 绘制器实例(hC.r74_1)
ff = renderer.d75_1;
console.log('[后视] Ff绘制器:', ff);
// 场景根节点(jr.kyf_1)
rootNode = scene.fyx_1;
console.log('[后视] 根节点:', rootNode);
// 主相机 Ha 实例(jr.nyf_1)
mainCamera = scene.iyx_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('[后视] 相机u35_1:', mainCamera.u35_1);
console.log('[后视] 相机r35_1:', mainCamera.r35_1);
console.log('[后视] 相机b37_1:', mainCamera.b37_1);
if (!mainCamera.u35_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, 'rearview');
var uiCtx = uiCanvas.getContext('2d');
// ===== UI 绘制函数 =====
function DrawText(text, x, y, color, font, align, baseline) {
uiCtx.font = font || '12px monospace';
uiCtx.textAlign = align || 'left';
uiCtx.textBaseline = baseline || 'middle';
uiCtx.fillStyle = color || 'rgba(255,255,255,1)';
uiCtx.fillText(text, x, y);
}
function DrawTextOutlined(text, x, y, color, font, align, baseline) {
uiCtx.font = font || '12px monospace';
uiCtx.textAlign = align || 'left';
uiCtx.textBaseline = baseline || 'middle';
uiCtx.strokeStyle = 'rgba(0,0,0,0.85)';
uiCtx.lineWidth = 3;
uiCtx.strokeText(text, x, y);
uiCtx.fillStyle = color || 'rgba(255,255,255,1)';
uiCtx.fillText(text, x, y);
}
// ===== FBO 创建(后视镜渲染目标)=====
var rw = Config.width, rh = Config.height;
var rearFbo = null, rearTex = null, rearDepthRb = null;
var pixels = new Uint8Array(rw * rh * 4);
var imageData = uiCtx.createImageData(rw, rh);
// PBO 双缓冲异步回读(避免 readPixels 同步 GPU 停顿)
var pbos = [null, null];
var pboIndex = 0;
var pboReady = false; // 第一帧没有前帧数据,跳过回读
var pboSupported = false;
// Uint32 视图用于批量设置 alpha=255(修复树叶等半透明边缘)
var pixels32 = new Uint32Array(pixels.buffer);
if (gl && !initError) {
try {
rearFbo = gl.createFramebuffer();
rearTex = gl.createTexture();
rearDepthRb = gl.createRenderbuffer();
// 颜色纹理
gl.bindTexture(gl.TEXTURE_2D, rearTex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, rw, rh, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
// 深度缓冲
gl.bindRenderbuffer(gl.RENDERBUFFER, rearDepthRb);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, rw, rh);
// 组装 FBO
gl.bindFramebuffer(gl.FRAMEBUFFER, rearFbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, rearTex, 0);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, rearDepthRb);
var fboStatus = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
console.log('[后视] FBO状态:', fboStatus, '(完整=' + gl.FRAMEBUFFER_COMPLETE + ')');
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.bindTexture(gl.TEXTURE_2D, null);
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
if (fboStatus !== gl.FRAMEBUFFER_COMPLETE) {
throw new Error('FBO不完整: ' + fboStatus);
}
// 创建 PBO 双缓冲(WebGL2 PIXEL_PACK_BUFFER)
try {
var byteSize = rw * rh * 4;
for (var pi = 0; pi < 2; pi++) {
pbos[pi] = gl.createBuffer();
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbos[pi]);
gl.bufferData(gl.PIXEL_PACK_BUFFER, byteSize, gl.STREAM_READ);
}
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
pboSupported = true;
console.log('[后视] PBO 双缓冲已创建(异步回读)');
} catch(pboErr) {
console.warn('[后视] PBO 不支持,回退到同步 readPixels:', pboErr.message);
pboSupported = false;
}
} catch(e) {
initError = e;
console.error('[后视] FBO创建失败:', e.message);
}
}
// ===== Hook gl.bindFramebuffer(后视渲染时重定向到我们的 FBO)=====
var rearviewRendering = false;
var mainFbo = null;
var origBindFramebuffer = null;
var origH3c = null, origG3c = null;
if (gl && !initError) {
origBindFramebuffer = gl.bindFramebuffer.bind(gl);
gl.bindFramebuffer = function(target, fbo) {
// 记录主 FBO(非后视渲染时的绑定)
if (!rearviewRendering && target === gl.FRAMEBUFFER) {
mainFbo = fbo;
}
// 后视渲染时重定向到我们的 FBO
if (rearviewRendering && target === gl.FRAMEBUFFER && fbo !== rearFbo) {
return origBindFramebuffer(target, rearFbo);
}
return origBindFramebuffer(target, fbo);
};
console.log('[后视] gl.bindFramebuffer 已 Hook');
// Hook BS.h3c/g3c:后视渲染时返回 FBO 尺寸(修复 screenSize/aspect ratio)
// y3s 会调用 BS.h3c()/g3c() 获取 drawingBufferWidth/Height 设置相机 l36_1/m36_1
// 不 Hook 的话 aspect ratio 会用游戏 canvas 尺寸(如1920x1080),导致画面比例错误
origH3c = ff.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.u35_1.f20_1;
var savedYaw = mainCamera.u35_1.g20_1;
var savedRoll = mainCamera.u35_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.u36_1;
var savedM36 = mainCamera.v36_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.u35_1.f20_1 = rearPitch;
mainCamera.u35_1.g20_1 = Math.PI;
mainCamera.u35_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.u36_1 = rw;
mainCamera.v36_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.b3o_1 控制 z39_1(阴影),c3o_1 控制 c3a_1/d3a_1(后处理),z3n_1 控制 f3a_1
// 禁用后 y3s 只执行主场景渲染 z3k,跳过所有 FBO 切换的 pass
var savedX3n = ff.y38_1.e3o_1;
var savedY3n = ff.y38_1.f3o_1;
var savedZ3n = ff.y38_1.k3o_1;
ff.y38_1.e3o_1 = false;
ff.y38_1.f3o_1 = false;
ff.y38_1.k3o_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.y38_1.e3o_1 = savedX3n;
ff.y38_1.f3o_1 = savedY3n;
ff.y38_1.k3o_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.u35_1.f20_1 = savedPitch;
mainCamera.u35_1.g20_1 = savedYaw;
mainCamera.u35_1.h20_1 = savedRoll;
mainCamera.r35_1.f20_1 = savedX;
mainCamera.r35_1.g20_1 = savedY;
mainCamera.r35_1.h20_1 = savedZ;
mainCamera.u36_1 = savedL36;
mainCamera.v36_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.m10b_1.j11y();
var count = tankList.e1();
debugInfo.tankCount = count;
var visibleTanks = 0;
for (var i = 0; i < count; i++) {
var tank = tankList.g1(i);
if (!tank || tank === playerTank) continue;
if (!IsTankAlive(tank)) continue;
var pos = GetTankPosition(tank);
if (!pos) continue;
// 使用视图矩阵投影(与 3D 渲染一致)
// o37 变换前矩阵行:right(d[0,4,8]), forward(d[1,5,9]), up(d[2,6,10])
// Ua 变换后行:right, up, -forward
// 故变换后相机空间:camX=右,camY=上(up·rel),camZ=深度(-forward·rel,负=前方)
// 后视相机 up 朝下:世界"上"rel.z>0 → camY<0;渲染+FBO readback 使世界"上"显示在 canvas 顶部
// marker 用 + 号匹配:camY<0(上方) → +负 → screen.y<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;
// 换图自刷新:gameMode 实例变化时整体重启,重新获取场景/相机/GL hook
var NewGm = window.__gameMode__;
if (NewGm && NewGm !== gm) {
console.log('[后视] 检测到新战斗,正在重启后视镜...');
Stop();
_start();
return;
}
requestAnimationFrame(Loop);
// 旧实例已失效、新实例尚未捕获:跳过本帧
if (!window.__gameMode__) return;
RenderRearview();
}
// ===== 停止函数 =====
function Stop() {
running = false;
console.log('[后视] 停止后视镜');
// 恢复 gl.bindFramebuffer
if (origBindFramebuffer && gl) {
gl.bindFramebuffer = origBindFramebuffer;
console.log('[后视] gl.bindFramebuffer 已恢复');
}
// 恢复 BS.h3c/g3c
if (origH3c && ff && ff.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] 后视镜模块已加载');
})();