Files
TankIC/tools/hornet_warsandbox.js
T
TermiNexus 66a74df6e3 refactor(schema): 全部工具改为经字段抽象层取值
hornet_lib / hook_gamemode / radar / 透视 / 后视镜 / 瞄准警告 / 自由视角 / 战争沙盘 全部改为 Fld/Mth 常量取值;schema 补齐渲染器、网格、射线、武器、皮肤槽位等 88 项字段与别名;SelfCheck 改为只校验核心字段,避免别名误报。
2026-10-20 22:04:07 +08:00

896 lines
36 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() {
// ===== 字段抽象层(语义名 → 匿名字段,总表在 hornet_schema.js)=====
var Schema = window.__hornetSchema__ || null;
function Fld(Key, Fallback) { var N = Schema ? Schema.FieldName(Key) : null; return N || Fallback; }
function Mth(Key, Fallback) { var N = Schema ? Schema.MethodName(Key) : null; return N || Fallback; }
// 向量
var FVecX = Fld('Vec.X', 'f20_1');
var FVecY = Fld('Vec.Y', 'g20_1');
var FVecZ = Fld('Vec.Z', 'h20_1');
// 战局 / 场景
var MScene = Mth('GameMode.Scene', 'lz4');
var FCameraSceneRef = Fld('Camera.SceneRef', 'jz1_1');
var FTankManager = Fld('GameMode.TankManager', 'm10b_1');
var MTankList = Mth('GameMode.TankList', 'j11y');
// 渲染器
var FRenderer = Fld('Renderer.Object', 'd75_1');
var FRenderDevice = Fld('Renderer.Device', 'w39_1');
var FRenderGl = Fld('Renderer.Gl', 'b74_1');
var FRenderFlags = Fld('Renderer.Flags', 'y38_1');
var FRenderShadow = Fld('Renderer.Shadow', 'e3o_1');
var FRenderPostFx = Fld('Renderer.PostFx', 'f3o_1');
var FRenderFlagAux = Fld('Renderer.FlagAux', 'k3o_1');
var FRenderQueueList = Fld('Renderer.QueueList', 'e39_1');
// 相机
var FCameraRotation = Fld('Camera.Rotation', 'u35_1');
var FNodeChildren = Fld('Node.Children', 'r35_1'); // 本文件里该字段是相机位置向量(与节点子列表同名复用)
var FCameraRenderWidth = Fld('Camera.RenderWidth', 'u36_1');
var FCameraRenderHeight = Fld('Camera.RenderHeight', 'v36_1');
var FCameraFov = Fld('Camera.Fov', 'z36_1');
var FCameraNear = Fld('Camera.Near', 'x36_1');
var FCameraFar = Fld('Camera.Far', 'y36_1');
// 皮肤 / 节点
var FSkinSlotHull = Fld('Skin.SlotHull', 'w14w_1');
var FSkinSlotHullLegacy = Fld('Skin.SlotHullLegacy', 't14l_1');
var FSkinSlotHullAlt = Fld('Skin.SlotHullAlt', 'w14x_1');
var FSkinSlotHullOld = Fld('Skin.SlotHullOld', 'p14l_1');
var FSkinParts = Fld('Skin.Parts', 'g17r_1');
var FSkinPartNode = Fld('Skin.PartNode', 'j17r_1');
var FNodeMatrixOld = Fld('Node.MatrixOld', 'n35_1');
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, FSkinSlotHull) ||
FindComponentByField(tank, FSkinSlotHullLegacy) ||
FindComponentByField(tank, FSkinSlotHullAlt) ||
FindComponentByField(tank, FSkinSlotHullOld);
if (!hullComp) return null;
var skin = (hullComp[FSkinSlotHull] && hullComp[FSkinSlotHull][FSkinParts]) ? hullComp[FSkinSlotHull] :
(hullComp[FSkinSlotHullLegacy] && hullComp[FSkinSlotHullLegacy][FSkinParts]) ? hullComp[FSkinSlotHullLegacy] :
(hullComp[FSkinSlotHullOld] && hullComp[FSkinSlotHullOld][FSkinParts]) ? hullComp[FSkinSlotHullOld] : null;
if (!skin) return null;
try {
var partCount = skin[FSkinParts].e1();
for (var p = 0; p < partCount; p++) {
var part = skin[FSkinParts].g1(p);
if (!part || !part[FSkinPartNode]) continue;
var nodeCount = part[FSkinPartNode].e1();
for (var n = 0; n < nodeCount; n++) {
var bpNode = part[FSkinPartNode].g1(n);
if (!bpNode || !bpNode[FNodeMatrixOld]) continue;
// 不按 g35_1 过滤:它是引擎的细节/淡出权重(0~1),不是幽灵标志,
// 过滤会漏掉低细节节点、导致取不到朝向
var m = bpNode[FNodeMatrixOld];
// 列主序世界矩阵: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[MScene]()[FCameraSceneRef];
ff = scene.gyw_1[FRenderer];
rootNode = scene.fyx_1;
mainCamera = scene.iyx_1;
gl = ff[FRenderDevice][FRenderGl];
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[FRenderDevice].o3c.bind(ff[FRenderDevice]);
var origG3c = ff[FRenderDevice].n3c.bind(ff[FRenderDevice]);
ff[FRenderDevice].o3c = function() { return sandboxRendering ? FBO_W : origH3c(); };
ff[FRenderDevice].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[FTankManager][MTankList]();
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[FCameraRotation][FVecX];
var sRoll = mainCamera[FCameraRotation][FVecY];
var sYaw = mainCamera[FCameraRotation][FVecZ];
var sX = mainCamera[FNodeChildren][FVecX];
var sY = mainCamera[FNodeChildren][FVecY];
var sZ = mainCamera[FNodeChildren][FVecZ];
var sL36 = mainCamera[FCameraRenderWidth], sM36 = mainCamera[FCameraRenderHeight];
var sS36 = mainCamera[FCameraFov], sQ36 = mainCamera[FCameraNear], sR36 = mainCamera[FCameraFar];
// 设置俯瞰相机参数
// pitch=-π/2 朝下看,roll=0 不翻转,yaw=0 北朝上
mainCamera[FCameraRotation][FVecX] = -Math.PI / 2;
mainCamera[FCameraRotation][FVecY] = 0;
mainCamera[FCameraRotation][FVecZ] = 0;
mainCamera[FNodeChildren][FVecX] = viewCenterX;
mainCamera[FNodeChildren][FVecY] = viewCenterY;
mainCamera[FNodeChildren][FVecZ] = camHeight;
mainCamera[FCameraRenderWidth] = FBO_W;
mainCamera[FCameraRenderHeight] = FBO_H;
mainCamera[FCameraFov] = Config.fov;
mainCamera[FCameraNear] = Config.near;
mainCamera[FCameraFar] = Config.far;
mainCamera.o37();
// 禁用阴影/后处理
var savedX3n = ff[FRenderFlags][FRenderShadow];
var savedY3n = ff[FRenderFlags][FRenderPostFx];
var savedZ3n = ff[FRenderFlags][FRenderFlagAux];
ff[FRenderFlags][FRenderShadow] = false;
ff[FRenderFlags][FRenderPostFx] = false;
ff[FRenderFlags][FRenderFlagAux] = false;
// 禁用 UI 层
var savedUiQueues = {};
for (var ui = 0; ui < uiLayerIndices.length; ui++) {
var li = uiLayerIndices[ui];
savedUiQueues[li] = ff[FRenderQueueList][li];
ff[FRenderQueueList][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[FRenderFlags][FRenderShadow] = savedX3n;
ff[FRenderFlags][FRenderPostFx] = savedY3n;
ff[FRenderFlags][FRenderFlagAux] = savedZ3n;
for (var ui2 = 0; ui2 < uiLayerIndices.length; ui2++) {
ff[FRenderQueueList][uiLayerIndices[ui2]] = savedUiQueues[uiLayerIndices[ui2]];
}
// 恢复主相机
mainCamera[FCameraRotation][FVecX] = sPitch;
mainCamera[FCameraRotation][FVecY] = sRoll;
mainCamera[FCameraRotation][FVecZ] = sYaw;
mainCamera[FNodeChildren][FVecX] = sX;
mainCamera[FNodeChildren][FVecY] = sY;
mainCamera[FNodeChildren][FVecZ] = sZ;
mainCamera[FCameraRenderWidth] = sL36; mainCamera[FCameraRenderHeight] = sM36;
mainCamera[FCameraFov] = sS36; mainCamera[FCameraNear] = sQ36; mainCamera[FCameraFar] = 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[FRenderDevice].o3c = origH3c;
ff[FRenderDevice].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; }
};
})();