744 lines
28 KiB
JavaScript
744 lines
28 KiB
JavaScript
// ===== 3DTank雷达 - 2D俯视图雷达 =====
|
||
// 在浏览器控制台运行(需先运行 hook_gamemode.js)
|
||
// 功能:在屏幕右上角显示2D雷达,显示所有坦克的相对位置
|
||
|
||
(function() {
|
||
function _start() {
|
||
console.log('===== 3DTank雷达启动 =====');
|
||
|
||
// 允许在未进入战局 / ght 尚未捕获时启动:先出雷达框,内容等进入战局再出
|
||
if (window.__hornetRadar__) {
|
||
window.__hornetRadar__.stop();
|
||
console.log('已停止旧雷达');
|
||
}
|
||
|
||
var gm = window.__gameMode__ || null;
|
||
|
||
// ===== 引用公共库(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;
|
||
// 人机标记:ScanBots 定期扫描(同队内装备分数相同且 < 8000)→ IsBot 查询
|
||
var ScanBots = lib.ScanBots, IsBot = lib.IsBot;
|
||
|
||
// 组件查找:使用公共库(已修复 falsy 值漏判 bug)
|
||
var FindComponentByField = lib.FindComponentByField;
|
||
|
||
// 坦克位置:使用公共库
|
||
var GetTankPosition = lib.GetTankPosition;
|
||
|
||
// 坦克状态:使用公共库(0=DEAD,1=DEAD_PHANTOM,2=SEMI_ACTIVE,3=ACTIVE,4=DISABLED)
|
||
var GetTankState = lib.GetTankState;
|
||
var GetTankHealth = lib.GetTankHealth;
|
||
var IsTankAlive = lib.IsTankAlive;
|
||
|
||
// ===== 获取坦克方向(Kkt 组件的 dzc_1) =====
|
||
function getTankDirection(tank) {
|
||
var hullComp = FindComponentByField(tank, 'qzv_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;
|
||
}
|
||
|
||
// ===== 获取相机朝向(新版:直接用场景主相机 scene.iyx_1 的视图矩阵算水平朝向) =====
|
||
// 雷达约定:forward = (sin(angle), cos(angle)),x/y 为水平面
|
||
function getCameraHeading() {
|
||
var ci = null;
|
||
try { ci = lib.GetCameraInfo(gm); } catch (e) { ci = null; }
|
||
if (!ci) return null;
|
||
return { yaw: ci.heading, pitch: ci.pitch, pos: ci.pos };
|
||
}
|
||
|
||
// 玩家坦克:使用公共库
|
||
function GetPlayerTank() { return lib.GetPlayerTank(gm); }
|
||
|
||
// 友方判断:使用公共库
|
||
function IsFriendly(tank, playerTank) { return lib.IsFriendly(tank, playerTank, gm); }
|
||
|
||
// 画死亡标记(物理体仍存在但已阵亡):红色 ×
|
||
function DrawDeadCross(x, y) {
|
||
var s = 5;
|
||
ctx.strokeStyle = '#ff4444';
|
||
ctx.lineWidth = 2;
|
||
ctx.beginPath();
|
||
ctx.moveTo(x - s, y - s);
|
||
ctx.lineTo(x + s, y + s);
|
||
ctx.moveTo(x + s, y - s);
|
||
ctx.lineTo(x - s, y + s);
|
||
ctx.stroke();
|
||
}
|
||
|
||
// ===== 获取全局碰撞检测器(uMt 组件,挂在 GameMode entity 上) =====
|
||
var UMtComponent = null;
|
||
function getUMtComponent() {
|
||
if (UMtComponent) return UMtComponent;
|
||
try {
|
||
var gmEntity = gm.lz4();
|
||
if (!gmEntity || !gmEntity.qz1_1 || !gmEntity.qz1_1.s2a_1) return null;
|
||
gmEntity.qz1_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.a17p === '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, q17o_1: null, b179_1: null,
|
||
r17o: function(t, n, i, r, e) {
|
||
this.y178_1.k26(t); this.z178_1.k26(n);
|
||
this.a179_1 = i; this.q17o_1 = r; this.b179_1 = e;
|
||
},
|
||
d3: function() { this.b179_1 = null; }
|
||
};
|
||
var filter = {
|
||
l1da_1: { h1: function() { return false; }, d3: function() {} },
|
||
w179: function() { return true; },
|
||
d3: function() {}, m1da: function() {}
|
||
};
|
||
var hit = uMt.a17p(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, 'radar');
|
||
|
||
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++;
|
||
|
||
// ===== 新战斗/新地图自刷新:gameMode 实例变化时重置缓存并重新采样地形 =====
|
||
var NewGm = window.__gameMode__;
|
||
if (NewGm && NewGm !== gm) {
|
||
gm = NewGm;
|
||
UMtComponent = null; // 碰撞检测器需从新场景重新获取
|
||
radar.lastPlayerPos = null;
|
||
radar.playerHeading = 0;
|
||
Terrain.heights = null; // 丢弃旧地图高度图,触发重新采样
|
||
Terrain.newHeights = null;
|
||
Terrain.sampling = false;
|
||
Terrain.ready = false;
|
||
Terrain.lastSampleX = 99999;
|
||
Terrain.lastSampleY = 99999;
|
||
radar.tankCount = 0;
|
||
console.log('[雷达] 检测到新战斗/新地图,已重置缓存并重新采样地形');
|
||
}
|
||
// ght 尚未捕获 / 不在战局:仍画静态雷达框,内容留空(等进入战局自动出)
|
||
var Ready = !!gm;
|
||
|
||
// 进战局后打印一次自诊断(位置/相机链路),便于新版字段平移时快速定位
|
||
if (Ready && !radar._diagPrinted) {
|
||
radar._diagPrinted = true;
|
||
try { console.log('[雷达] 自诊断:', lib.SelfDiagnose()); } catch (e) {}
|
||
}
|
||
|
||
try {
|
||
var tankList = null, count = 0;
|
||
if (Ready) {
|
||
tankList = gm.m10b_1.j11y();
|
||
count = tankList.e1();
|
||
}
|
||
radar.tankCount = count;
|
||
|
||
// 人机扫描:装备分数不会每帧变,节流到约每 0.3 秒一次
|
||
if (Ready && ScanBots && (Date.now() - (radar._lastBotScan || 0) > 300)) {
|
||
radar._lastBotScan = Date.now();
|
||
ScanBots();
|
||
}
|
||
|
||
// 获取玩家坦克(带备用方案)
|
||
var playerTank = Ready ? GetPlayerTank() : null;
|
||
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 = Ready ? getCameraHeading() : null;
|
||
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, deadCount = 0, botCount = 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;
|
||
|
||
// 死亡判断(同 3D透视):直接调用引擎的 s14j();血量归零也算死亡(状态偶有滞后)
|
||
var tankState = GetTankState(tank);
|
||
var hp = GetTankHealth(tank);
|
||
if (!IsTankAlive(tank) || (hp && hp.current <= 0)) {
|
||
DrawDeadCross(radarX, radarY);
|
||
deadCount++;
|
||
continue;
|
||
}
|
||
|
||
// 判断友方/敌方
|
||
var friendly = IsFriendly(tank, playerTank);
|
||
var color = friendly ? '#4488ff' : '#ff4444';
|
||
if (friendly) friendlyCount++; else enemyCount++;
|
||
|
||
// 画坦克点:人机用「灰方块 + 黄框」,真人用「实心圆 + 白边」
|
||
var bot = (typeof IsBot === 'function') && IsBot(tank);
|
||
if (bot) botCount++;
|
||
if (bot) {
|
||
ctx.beginPath();
|
||
ctx.rect(radarX - 5, radarY - 5, 10, 10);
|
||
ctx.fillStyle = 'rgba(170,170,170,0.55)';
|
||
ctx.fill();
|
||
ctx.strokeStyle = '#ffd24a';
|
||
ctx.lineWidth = 1.5;
|
||
ctx.stroke();
|
||
} else {
|
||
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);
|
||
|
||
// 等待战局提示(ght 尚未捕获 / 不在战局)
|
||
if (!Ready) {
|
||
ctx.fillStyle = 'rgba(255, 220, 120, 0.95)';
|
||
ctx.font = 'bold 11px Arial';
|
||
ctx.textAlign = 'center';
|
||
ctx.fillText('等待战局…', cx, cy + 4);
|
||
}
|
||
|
||
// 信息文本
|
||
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
|
||
ctx.font = '9px Arial';
|
||
ctx.textAlign = 'left';
|
||
ctx.fillText('Tanks: ' + count + ' (B:' + friendlyCount + ' R:' + enemyCount + ' X:' + deadCount + ' Bot:' + botCount + ')', 5, 12);
|
||
if (radar.playerFound) {
|
||
ctx.fillText('Player: ' + (playerTank ? playerTank.iz1_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] 雷达模块已加载');
|
||
})();
|