Files
TankIC/tools/hornet_warsandbox.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

852 lines
33 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 战争沙盘 - 真实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 是局部空间向量且可能未设置,不可靠
// 皮肤字段:新 build 是 w14w_1;旧 build 是 t14l_1(新版 t14l_1 已改成悬停车体状态 Qmt)
var hullComp = FindComponentByField(tank, 'w14w_1') ||
FindComponentByField(tank, 't14l_1') ||
FindComponentByField(tank, 'w14x_1') ||
FindComponentByField(tank, 'p14l_1');
if (!hullComp) return null;
var skin = (hullComp.w14w_1 && hullComp.w14w_1.g17r_1) ? hullComp.w14w_1 :
(hullComp.t14l_1 && hullComp.t14l_1.g17r_1) ? hullComp.t14l_1 :
(hullComp.p14l_1 && hullComp.p14l_1.g17r_1) ? hullComp.p14l_1 : null;
if (!skin) return null;
try {
var partCount = skin.g17r_1.e1();
for (var p = 0; p < partCount; p++) {
var part = skin.g17r_1.g1(p);
if (!part || !part.j17r_1) continue;
var nodeCount = part.j17r_1.e1();
for (var n = 0; n < nodeCount; n++) {
var bpNode = part.j17r_1.g1(n);
if (!bpNode || !bpNode.n35_1) continue;
// 不按 g35_1 过滤:它是引擎的细节/淡出权重(0~1),不是幽灵标志,
// 过滤会漏掉低细节节点、导致取不到朝向
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.lz4().jz1_1;
ff = scene.gyw_1.d75_1;
rootNode = scene.fyx_1;
mainCamera = scene.iyx_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);
// ===== 窗口布局持久化(位置 + 尺寸)=====
function SaveWinLayout() {
try {
var r = container.getBoundingClientRect();
localStorage.setItem('hornet_layout_warsandbox', JSON.stringify({
left: Math.round(r.left), top: Math.round(r.top),
width: Math.round(r.width), height: Math.round(r.height)
}));
} catch(e) {}
}
try {
var __wsSaved = localStorage.getItem('hornet_layout_warsandbox');
if (__wsSaved) {
var __wsP = JSON.parse(__wsSaved);
if (__wsP && typeof __wsP.left === 'number' && typeof __wsP.top === 'number') {
container.style.transform = 'none';
container.style.left = Math.max(0, __wsP.left) + 'px';
container.style.top = Math.max(0, __wsP.top) + 'px';
if (typeof __wsP.width === 'number') container.style.width = Math.max(320, __wsP.width) + 'px';
if (typeof __wsP.height === 'number') container.style.height = Math.max(240, __wsP.height) + 'px';
}
}
} catch(e) {}
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();
// ===== 右下角缩放手柄:允许自由缩放窗口 =====
var resizeGrip = document.createElement('div');
resizeGrip.style.cssText =
'position:absolute;right:0;bottom:0;width:18px;height:18px;cursor:nwse-resize;z-index:2;' +
'background:linear-gradient(135deg,transparent 45%,rgba(125,249,255,0.35) 45%);' +
'border-bottom-right-radius:12px;';
container.appendChild(resizeGrip);
var __rsx = 0, __rsy = 0, __rsw = 0, __rsh = 0;
BindPointerDrag(resizeGrip, function(e) {
var r = container.getBoundingClientRect();
__rsx = e.clientX; __rsy = e.clientY; __rsw = r.width; __rsh = r.height;
return true;
}, function(e) {
container.style.transform = 'none';
container.style.width = Math.max(320, __rsw + (e.clientX - __rsx)) + 'px';
container.style.height = Math.max(240, __rsh + (e.clientY - __rsy)) + 'px';
resizeCanvas();
}, function() {
resizeCanvas();
SaveWinLayout();
});
// ===== 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() { SaveWinLayout(); });
// ===== 地图平移 =====
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.m10b_1.j11y();
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;
// 换图自刷新:gameMode 实例变化时整体重启,重新获取场景/相机/GL hook
var NewGm = window.__gameMode__;
if (NewGm && NewGm !== gm) {
console.log('[沙盘] 检测到新战斗,正在重启战争沙盘...');
self.Stop();
_start();
return;
}
frameCount++;
// 旧实例已失效、新实例尚未捕获:跳过本帧
if (!window.__gameMode__) { rafId = requestAnimationFrame(render); return; }
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.u35_1.f20_1;
var sRoll = mainCamera.u35_1.g20_1;
var sYaw = mainCamera.u35_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.u36_1, sM36 = mainCamera.v36_1;
var sS36 = mainCamera.z36_1, sQ36 = mainCamera.x36_1, sR36 = mainCamera.y36_1;
// 设置俯瞰相机参数
// pitch=-π/2 朝下看,roll=0 不翻转,yaw=0 北朝上
mainCamera.u35_1.f20_1 = -Math.PI / 2;
mainCamera.u35_1.g20_1 = 0;
mainCamera.u35_1.h20_1 = 0;
mainCamera.r35_1.f20_1 = viewCenterX;
mainCamera.r35_1.g20_1 = viewCenterY;
mainCamera.r35_1.h20_1 = camHeight;
mainCamera.u36_1 = FBO_W;
mainCamera.v36_1 = FBO_H;
mainCamera.z36_1 = Config.fov;
mainCamera.x36_1 = Config.near;
mainCamera.y36_1 = Config.far;
mainCamera.o37();
// 禁用阴影/后处理
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 层
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.y38_1.e3o_1 = savedX3n;
ff.y38_1.f3o_1 = savedY3n;
ff.y38_1.k3o_1 = savedZ3n;
for (var ui2 = 0; ui2 < uiLayerIndices.length; ui2++) {
ff.e39_1[uiLayerIndices[ui2]] = savedUiQueues[uiLayerIndices[ui2]];
}
// 恢复主相机
mainCamera.u35_1.f20_1 = sPitch;
mainCamera.u35_1.g20_1 = sRoll;
mainCamera.u35_1.h20_1 = sYaw;
mainCamera.r35_1.f20_1 = sX;
mainCamera.r35_1.g20_1 = sY;
mainCamera.r35_1.h20_1 = sZ;
mainCamera.u36_1 = sL36; mainCamera.v36_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; }
};
})();