// ===== 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] 雷达模块已加载'); })();