Files
TankIC/tools/hornet_radar.js
T

741 lines
28 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雷达 - 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;
// ===== 获取坦克方向(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透视):可见坦克 = ACTIVE(3) 或 SEMI_ACTIVE(2);其余画红 ×
var tankState = GetTankState(tank);
if (tankState !== 2 && tankState !== 3) {
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] 雷达模块已加载');
})();