hornet_lib / hook_gamemode / radar / 透视 / 后视镜 / 瞄准警告 / 自由视角 / 战争沙盘 全部改为 Fld/Mth 常量取值;schema 补齐渲染器、网格、射线、武器、皮肤槽位等 88 项字段与别名;SelfCheck 改为只校验核心字段,避免别名误报。
668 lines
26 KiB
JavaScript
668 lines
26 KiB
JavaScript
// ===== 3DTank瞄准警告 - 敌方坦克瞄准线预判 =====
|
||
// 在浏览器控制台运行(需先运行 hook_gamemode.js)
|
||
// 功能:当敌方坦克炮塔朝向玩家时,立刻显示HUD警告并标记方向
|
||
// 独立轻量级脚本,不依赖3D透视渲染
|
||
|
||
(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 FEntityId = Fld('Entity.Id', 'iz1_1');
|
||
var FEntityRoot = Fld('Entity.Root', 'qz1_1');
|
||
var FEntityComponentMap = Fld('Entity.ComponentMap', 's2a_1');
|
||
var MGameModeScene = Mth('GameMode.Scene', 'lz4');
|
||
var MCollisionRaycast = Mth('CollisionWorld.Raycast', 'a17p');
|
||
var FWeaponComponent = Fld('Weapon.Component', 'n11c_1');
|
||
var FWeaponComponentAlt = Fld('Weapon.ComponentAlt', 'q112_1');
|
||
var FWeaponTurretRoot = Fld('Weapon.TurretRoot', 'm17a_1');
|
||
var FNodeMatrix = Fld('Node.MatrixOld', 'n35_1');
|
||
var FRayHitPoint = Fld('Ray.HitPoint', 'y178_1');
|
||
var FRayHitNormal = Fld('Ray.HitNormal', 'z178_1');
|
||
var FRayDistance = Fld('Ray.Distance', 'a179_1');
|
||
var FRayHitShape = Fld('Ray.HitShape', 'q17o_1');
|
||
var FRayHitBody = Fld('Ray.HitBody', 'b179_1');
|
||
var FRayFilterQuery = Fld('RayFilter.Query', 'l1da_1');
|
||
|
||
function _start() {
|
||
console.log('===== 3DTank瞄准警告启动 =====');
|
||
|
||
// 允许在未进入战局 / ght 尚未捕获时启动:先出状态行,威胁判定等进入战局再算
|
||
if (window.__hornetAimWarning__) {
|
||
window.__hornetAimWarning__.Stop();
|
||
console.log('已停止旧瞄准警告');
|
||
}
|
||
|
||
var gm = window.__gameMode__ || null;
|
||
|
||
// ===== 设置参数(localStorage 持久化)=====
|
||
// Sensitivity: 灵敏度系数,>1 更灵敏(阈值更大,更易触发),<1 更迟钝
|
||
var Sensitivity = 1.0;
|
||
try {
|
||
var __savedSens = localStorage.getItem('hornet_aim_sensitivity');
|
||
if (__savedSens) {
|
||
var __s = parseFloat(__savedSens);
|
||
if (!isNaN(__s) && __s >= 0.3 && __s <= 3.0) Sensitivity = __s;
|
||
}
|
||
} catch(e) {}
|
||
// SoundEnabled: 警告声音开关
|
||
var SoundEnabled = false;
|
||
try {
|
||
var __savedSE = localStorage.getItem('hornet_aim_sound');
|
||
if (__savedSE === 'true') SoundEnabled = true;
|
||
} catch(e) {}
|
||
var _audioCtx = null;
|
||
var _lastBeepTime = 0;
|
||
function PlayBeep() {
|
||
if (!SoundEnabled) return;
|
||
var now = Date.now();
|
||
if (now - _lastBeepTime < 300) return; // 最多每300ms响一次
|
||
_lastBeepTime = now;
|
||
try {
|
||
if (!_audioCtx) _audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||
var osc = _audioCtx.createOscillator();
|
||
var gain = _audioCtx.createGain();
|
||
osc.frequency.value = 880;
|
||
osc.type = 'square';
|
||
gain.gain.value = 0.08;
|
||
osc.connect(gain);
|
||
gain.connect(_audioCtx.destination);
|
||
osc.start();
|
||
gain.gain.exponentialRampToValueAtTime(0.001, _audioCtx.currentTime + 0.15);
|
||
osc.stop(_audioCtx.currentTime + 0.15);
|
||
} catch(e) {}
|
||
}
|
||
|
||
// ===== 引用公共库(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;
|
||
|
||
// 公共函数别名(调用处不变)
|
||
var FindComponentByField = lib.FindComponentByField;
|
||
var GetTankPosition = lib.GetTankPosition;
|
||
var GetTankState = lib.GetTankState;
|
||
var IsTankAlive = lib.IsTankAlive;
|
||
|
||
// ===== 获取 TDt 武器皮肤组件(炮塔) =====
|
||
function GetTDtComponent(entity) {
|
||
var comp = FindComponentByField(entity, FWeaponComponent);
|
||
if (comp) return comp;
|
||
comp = FindComponentByField(entity, FWeaponComponentAlt);
|
||
return comp;
|
||
}
|
||
|
||
// 坦克名称/血量:使用公共库
|
||
var GetTankName = lib.GetTankName;
|
||
var GetTankHealth = lib.GetTankHealth;
|
||
|
||
// ===== 获取炮塔瞄准角度 =====
|
||
// 从炮塔 Bp 节点世界矩阵(n35_1)提取水平面前向方向
|
||
// 返回 { angle: 炮塔朝向与"敌方→玩家"方向的夹角, distance: 距离, turretAngle: 炮塔朝向角度, turretDir: {x,y} }
|
||
function GetTurretAimAngle(tank, enemyPos, playerPos) {
|
||
var tdt = GetTDtComponent(tank);
|
||
if (!tdt || !tdt[FWeaponComponent] || !tdt[FWeaponComponent][FWeaponTurretRoot]) return null;
|
||
var bpNode = tdt[FWeaponComponent][FWeaponTurretRoot];
|
||
var m = bpNode[FNodeMatrix];
|
||
if (!m) return null;
|
||
|
||
// 敌方→玩家方向(x,y 水平面)
|
||
var dx = playerPos.x - enemyPos.x;
|
||
var dy = playerPos.y - enemyPos.y;
|
||
var dist = Math.sqrt(dx * dx + dy * dy);
|
||
if (dist < 100) return null; // 太近不判断
|
||
var tx = dx / dist, ty = dy / dist;
|
||
|
||
// 尝试 X 轴(m0,m1) 和 Y 轴(m4,m5),取最小夹角
|
||
var minAngle = Math.PI;
|
||
var turretAngle = 0;
|
||
var turretDir = { x: 1, y: 0 };
|
||
var axes = [
|
||
{ x: m[0], y: m[1] },
|
||
{ x: m[4], y: m[5] }
|
||
];
|
||
for (var ai = 0; ai < axes.length; ai++) {
|
||
var ax = axes[ai].x, ay = axes[ai].y;
|
||
var len = Math.sqrt(ax * ax + ay * ay);
|
||
if (len < 0.001) continue;
|
||
var nx = ax / len, ny = ay / len;
|
||
var dot = nx * tx + ny * ty;
|
||
var ang = Math.acos(Math.max(-1, Math.min(1, dot)));
|
||
if (ang < minAngle) {
|
||
minAngle = ang;
|
||
turretAngle = Math.atan2(ny, nx);
|
||
turretDir = { x: nx, y: ny };
|
||
}
|
||
}
|
||
|
||
return { angle: minAngle, distance: dist, turretAngle: turretAngle, turretDir: turretDir };
|
||
}
|
||
|
||
// ===== 全局碰撞检测器(uMt 组件,挂在 GameMode entity 上) =====
|
||
var UMtComponent = null;
|
||
function GetUMtComponent() {
|
||
if (UMtComponent) return UMtComponent;
|
||
try {
|
||
var gmEntity = gm[MGameModeScene]();
|
||
if (!gmEntity || !gmEntity[FEntityRoot] || !gmEntity[FEntityRoot][FEntityComponentMap]) return null;
|
||
gmEntity[FEntityRoot][FEntityComponentMap].forEach(function(valueList, key) {
|
||
if (UMtComponent) return;
|
||
try {
|
||
var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList;
|
||
if (comp && typeof comp[MCollisionRaycast] === '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;
|
||
|
||
// ===== 射线检测视线遮挡(缓存:每10帧更新) =====
|
||
var RaycastCache = new Map();
|
||
var RaycastFailCount = 0;
|
||
|
||
function IsLineOfSightBlocked(enemyPos, playerPos, tankId, frame) {
|
||
var cached = RaycastCache.get(tankId);
|
||
if (cached && (frame - cached.frame) < 10) return cached.blocked;
|
||
|
||
var uMt = GetUMtComponent();
|
||
if (!uMt) return false;
|
||
|
||
var blocked = false;
|
||
try {
|
||
var dx = playerPos.x - enemyPos.x;
|
||
var dy = playerPos.y - enemyPos.y;
|
||
var dz = playerPos.z - enemyPos.z;
|
||
var dist = Math.sqrt(dx*dx + dy*dy + dz*dz);
|
||
if (dist < 200) {
|
||
RaycastCache.set(tankId, { frame: frame, blocked: false });
|
||
return false;
|
||
}
|
||
var ndx = dx / dist, ndy = dy / dist, ndz = dz / dist;
|
||
|
||
var startVec = MkVec(enemyPos.x, enemyPos.y, enemyPos.z + 200);
|
||
var dirVec = MkVec(ndx, ndy, ndz);
|
||
var maxDist = dist - 300;
|
||
|
||
var result = {
|
||
[FRayHitPoint]: MkVec(0,0,0),
|
||
[FRayHitNormal]: MkVec(0,0,0),
|
||
[FRayDistance]: maxDist,
|
||
[FRayHitShape]: null,
|
||
[FRayHitBody]: null,
|
||
r17o: function(t, n, i, r, e) {
|
||
this[FRayHitPoint].k26(t);
|
||
this[FRayHitNormal].k26(n);
|
||
this[FRayDistance] = i;
|
||
this[FRayHitShape] = r;
|
||
this[FRayHitBody] = e;
|
||
},
|
||
d3: function() { this[FRayHitBody] = null; }
|
||
};
|
||
|
||
var filter = {
|
||
[FRayFilterQuery]: { h1: function() { return false; }, d3: function() {} },
|
||
w179: function() { return true; },
|
||
d3: function() { this[FRayFilterQuery].d3(); },
|
||
m1da: function() {}
|
||
};
|
||
|
||
var hit = uMt[MCollisionRaycast](startVec, dirVec, maxDist, result, filter);
|
||
blocked = !!hit && result[FRayDistance] < (maxDist - 50);
|
||
RaycastFailCount = 0;
|
||
} catch(e) {
|
||
RaycastFailCount++;
|
||
if (RaycastFailCount <= 3) console.warn('[瞄准] 射线检测异常:', e.message);
|
||
blocked = false;
|
||
}
|
||
|
||
RaycastCache.set(tankId, { frame: frame, blocked: blocked });
|
||
return blocked;
|
||
}
|
||
|
||
// 玩家坦克/友方/相机/投影:使用公共库
|
||
function GetPlayerTank() { return lib.GetPlayerTank(gm); }
|
||
function IsFriendly(tank, playerTank) { return lib.IsFriendly(tank, playerTank, gm); }
|
||
function GetCameraInfo() { return lib.GetCameraInfo(gm); }
|
||
var Project3D = lib.Project3D;
|
||
|
||
// ===== 创建 Canvas =====
|
||
var canvas = document.createElement('canvas');
|
||
canvas.id = 'hornetAimWarningCanvas';
|
||
canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:99998;pointer-events:none;';
|
||
document.body.appendChild(canvas);
|
||
var ctx = canvas.getContext('2d');
|
||
|
||
function ResizeCanvas() {
|
||
canvas.width = window.innerWidth;
|
||
canvas.height = window.innerHeight;
|
||
}
|
||
ResizeCanvas();
|
||
window.addEventListener('resize', ResizeCanvas);
|
||
|
||
// ===== 警告闪烁状态 =====
|
||
var WarningPulse = 0; // 闪烁动画相位
|
||
|
||
// ===== 死亡检测状态 =====
|
||
var PlayerPrevHealth = -1; // 上一帧血量(-1=未初始化)
|
||
var DeathInfo = null; // 死亡信息 { killerName, distance, expected, time, threats }
|
||
var DeathDisplayTime = 360; // 死亡信息显示帧数(约6秒)
|
||
var DeathStats = { total: 0, expected: 0, unexpected: 0 }; // 死亡统计
|
||
|
||
// ===== 主循环 =====
|
||
var running = true;
|
||
var frameCount = 0;
|
||
|
||
function Loop() {
|
||
if (!running) return;
|
||
frameCount++;
|
||
requestAnimationFrame(Loop);
|
||
|
||
// 换图自刷新:gameMode 实例变化时重绑并清空该地图的缓存
|
||
var NewGm = window.__gameMode__;
|
||
if (NewGm && NewGm !== gm) {
|
||
gm = NewGm;
|
||
UMtComponent = null; // 碰撞检测器需从新场景重新获取
|
||
RaycastCache.clear(); // 旧地图的视线遮挡缓存失效
|
||
DeathInfo = null; // 清掉旧地图的死亡提示
|
||
console.log('[瞄准] 检测到新战斗,已重置缓存');
|
||
}
|
||
// ght 尚未捕获 / 不在战局:清屏并显示等待提示
|
||
if (!window.__gameMode__) {
|
||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||
ctx.fillStyle = 'rgba(255, 220, 120, 0.85)';
|
||
ctx.font = '11px Arial';
|
||
ctx.textAlign = 'left';
|
||
ctx.fillText('[瞄准警告] 等待战局…(ght 未捕获)', 5, 18);
|
||
return;
|
||
}
|
||
|
||
var screenW = canvas.width;
|
||
var screenH = canvas.height;
|
||
ctx.clearRect(0, 0, screenW, screenH);
|
||
|
||
var playerTank = GetPlayerTank();
|
||
if (!playerTank) return;
|
||
|
||
var playerPos = GetTankPosition(playerTank);
|
||
if (!playerPos) return;
|
||
|
||
var camInfo = GetCameraInfo();
|
||
if (!camInfo) return;
|
||
|
||
// 相机位置为0时使用玩家位置
|
||
if (Math.abs(camInfo.pos.x) < 0.1 && Math.abs(camInfo.pos.y) < 0.1 && Math.abs(camInfo.pos.z) < 0.1) {
|
||
camInfo.pos = { x: playerPos.x, y: playerPos.y, z: playerPos.z + 100 };
|
||
}
|
||
|
||
var tankList = gm.m10b_1.j11y();
|
||
var count = tankList.e1();
|
||
var threats = [];
|
||
|
||
// 遍历所有敌方坦克,检测炮塔朝向
|
||
for (var i = 0; i < count; i++) {
|
||
var tank = tankList.g1(i);
|
||
if (!tank || tank === playerTank) continue;
|
||
if (!IsTankAlive(tank)) continue;
|
||
|
||
var pos = GetTankPosition(tank);
|
||
if (!pos) continue;
|
||
|
||
// 只检测敌方
|
||
if (IsFriendly(tank, playerTank)) continue;
|
||
|
||
var aimInfo = GetTurretAimAngle(tank, pos, playerPos);
|
||
if (!aimInfo) continue;
|
||
|
||
// 瞄准阈值随距离变化:近距离~11度(0.20rad),远距离~3度
|
||
// 公式:0.20 / (1 + distance/8000),乘以灵敏度系数
|
||
var aimThreshold = 0.20 / (1 + aimInfo.distance / 8000) * Sensitivity;
|
||
if (aimInfo.angle >= aimThreshold) continue;
|
||
|
||
// 墙体阻隔判断
|
||
var blocked = IsLineOfSightBlocked(pos, playerPos, tank[FEntityId], frameCount);
|
||
if (blocked) continue;
|
||
|
||
// 记录威胁
|
||
var proj = Project3D(pos, camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH);
|
||
var tankName = GetTankName(tank) || ('ID:' + tank[FEntityId]);
|
||
threats.push({
|
||
pos: pos,
|
||
distance: aimInfo.distance,
|
||
angle: aimInfo.angle,
|
||
turretDir: aimInfo.turretDir,
|
||
screenProj: proj,
|
||
tankId: tank[FEntityId],
|
||
tankName: tankName
|
||
});
|
||
}
|
||
|
||
// ===== 死亡检测 =====
|
||
var playerHealth = GetTankHealth(playerTank);
|
||
var playerCurHealth = playerHealth ? playerHealth.current : 0;
|
||
// 检测血量从 >0 变为 0 = 死亡
|
||
if (PlayerPrevHealth > 0 && playerCurHealth <= 0) {
|
||
// 死亡时,从 threats 列表找最近的瞄准者作为可能的击杀者
|
||
var killer = null;
|
||
var minDist = Infinity;
|
||
for (var ki = 0; ki < threats.length; ki++) {
|
||
if (threats[ki].distance < minDist) {
|
||
minDist = threats[ki].distance;
|
||
killer = threats[ki];
|
||
}
|
||
}
|
||
DeathInfo = {
|
||
killerName: killer ? killer.tankName : null,
|
||
killerDistance: killer ? killer.distance : 0,
|
||
expected: threats.length > 0, // 有瞄准者 = 意料之内
|
||
threatCount: threats.length,
|
||
time: frameCount
|
||
};
|
||
DeathStats.total++;
|
||
if (DeathInfo.expected) DeathStats.expected++;
|
||
else DeathStats.unexpected++;
|
||
console.log('[瞄准] 玩家死亡! ' + (killer ? '被 ' + killer.tankName + ' 击杀' : '被偷击杀') +
|
||
' | 意料之' + (threats.length > 0 ? '内' : '外') +
|
||
' | 瞄准者: ' + threats.length +
|
||
' | 统计: ' + DeathStats.total + '死 (内' + DeathStats.expected + '/外' + DeathStats.unexpected + ')');
|
||
}
|
||
// 复活检测:血量恢复 >0
|
||
if (PlayerPrevHealth <= 0 && playerCurHealth > 0) {
|
||
DeathInfo = null;
|
||
console.log('[瞄准] 玩家复活,重置死亡信息');
|
||
}
|
||
PlayerPrevHealth = playerCurHealth;
|
||
|
||
// ===== 绘制瞄准线(每个威胁的弹道轨迹) =====
|
||
if (threats.length > 0) {
|
||
PlayBeep();
|
||
ctx.strokeStyle = 'rgba(255, 40, 40, 0.5)';
|
||
ctx.lineWidth = 2;
|
||
for (var ti = 0; ti < threats.length; ti++) {
|
||
var tr = threats[ti];
|
||
var startPos = { x: tr.pos.x, y: tr.pos.y, z: tr.pos.z + 200 }; // 炮塔高度
|
||
var endPos = { x: playerPos.x, y: playerPos.y, z: playerPos.z };
|
||
var segs = 20;
|
||
var prevScreen = null;
|
||
ctx.beginPath();
|
||
for (var si = 0; si <= segs; si++) {
|
||
var st = si / segs;
|
||
var swp = {
|
||
x: startPos.x + (endPos.x - startPos.x) * st,
|
||
y: startPos.y + (endPos.y - startPos.y) * st,
|
||
z: startPos.z + (endPos.z - startPos.z) * st
|
||
};
|
||
var sp = Project3D(swp, camInfo.pos, camInfo.yaw, camInfo.pitch, camInfo.fov, screenW, screenH);
|
||
if (sp) {
|
||
if (prevScreen) ctx.lineTo(sp.x, sp.y);
|
||
else ctx.moveTo(sp.x, sp.y);
|
||
prevScreen = sp;
|
||
} else {
|
||
prevScreen = null;
|
||
}
|
||
}
|
||
ctx.stroke();
|
||
}
|
||
}
|
||
|
||
// ===== 每个威胁的方向标记 =====
|
||
for (var thi = 0; thi < threats.length; thi++) {
|
||
var threat = threats[thi];
|
||
var proj = threat.screenProj;
|
||
|
||
if (proj && proj.x >= 0 && proj.x <= screenW && proj.y >= 0 && proj.y <= screenH) {
|
||
// ===== 威胁在屏幕内:在威胁位置画红色警告三角形 =====
|
||
var mx = proj.x, my = proj.y - 35;
|
||
ctx.strokeStyle = 'rgba(255, 0, 0, 1)';
|
||
ctx.lineWidth = 2.5;
|
||
ctx.fillStyle = 'rgba(255, 0, 0, 0.4)';
|
||
ctx.beginPath();
|
||
ctx.moveTo(mx, my - 10);
|
||
ctx.lineTo(mx - 9, my + 6);
|
||
ctx.lineTo(mx + 9, my + 6);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
ctx.stroke();
|
||
// 感叹号
|
||
ctx.fillStyle = 'rgba(255, 255, 255, 1)';
|
||
ctx.font = 'bold 13px Arial';
|
||
ctx.textAlign = 'center';
|
||
ctx.fillText('!', mx, my + 4);
|
||
// 距离标签
|
||
ctx.fillStyle = 'rgba(255, 200, 200, 0.95)';
|
||
ctx.font = 'bold 11px Arial';
|
||
ctx.fillText(Math.round(threat.distance / 100) + 'm', mx, my + 22);
|
||
// 坦克名称
|
||
ctx.fillStyle = 'rgba(255, 240, 240, 0.95)';
|
||
ctx.font = 'bold 11px Arial';
|
||
ctx.fillText(threat.tankName, mx, my + 36);
|
||
} else {
|
||
// ===== 威胁不在屏幕内:屏幕边缘红色箭头指示方向 =====
|
||
var tdx = threat.pos.x - playerPos.x;
|
||
var tdy = threat.pos.y - playerPos.y;
|
||
var tlen = Math.sqrt(tdx * tdx + tdy * tdy);
|
||
if (tlen > 0.001) {
|
||
var ndx = tdx / tlen, ndy = tdy / tlen;
|
||
// 计算相对相机偏航角的方向
|
||
var fwdX = -Math.sin(camInfo.yaw), fwdY = Math.cos(camInfo.yaw);
|
||
var rgtX = Math.cos(camInfo.yaw), rgtY = Math.sin(camInfo.yaw);
|
||
var screenRight = ndx * rgtX + ndy * rgtY;
|
||
var screenFwd = ndx * fwdX + ndy * fwdY;
|
||
var sAng = Math.atan2(screenRight, screenFwd);
|
||
var edgeR = Math.min(screenW, screenH) * 0.42;
|
||
var ex = screenW / 2 + Math.sin(sAng) * edgeR;
|
||
var ey = screenH / 2 - Math.cos(sAng) * edgeR;
|
||
// 限制在屏幕边缘内
|
||
ex = Math.max(30, Math.min(screenW - 30, ex));
|
||
ey = Math.max(30, Math.min(screenH - 30, ey));
|
||
|
||
// 画箭头(指向威胁方向)
|
||
ctx.fillStyle = 'rgba(255, 0, 0, 0.95)';
|
||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
|
||
ctx.lineWidth = 1.5;
|
||
ctx.beginPath();
|
||
ctx.moveTo(ex + Math.sin(sAng) * 14, ey - Math.cos(sAng) * 14);
|
||
ctx.lineTo(ex + Math.sin(sAng + 2.5) * 9, ey - Math.cos(sAng + 2.5) * 9);
|
||
ctx.lineTo(ex + Math.sin(sAng - 2.5) * 9, ey - Math.cos(sAng - 2.5) * 9);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
ctx.stroke();
|
||
|
||
// 距离标签
|
||
ctx.fillStyle = 'rgba(255, 255, 255, 0.95)';
|
||
ctx.font = 'bold 11px Arial';
|
||
ctx.textAlign = 'center';
|
||
ctx.fillText(Math.round(threat.distance / 100) + 'm', ex, ey + 22);
|
||
// 坦克名称
|
||
ctx.fillStyle = 'rgba(255, 240, 240, 0.95)';
|
||
ctx.font = 'bold 10px Arial';
|
||
ctx.fillText(threat.tankName, ex, ey + 36);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ===== 左侧中央常显警告框(有威胁时) =====
|
||
if (threats.length > 0) {
|
||
WarningPulse += 0.15;
|
||
var pulseAlpha = 0.55 + Math.sin(WarningPulse) * 0.2; // 脉动透明度
|
||
|
||
// 动态高度:标题区 + 威胁列表
|
||
var maxListed = 4; // 最多列出4个威胁名称
|
||
var listedCount = Math.min(threats.length, maxListed);
|
||
var boxW = 240;
|
||
var boxH = 70 + listedCount * 18 + (threats.length > maxListed ? 16 : 0);
|
||
var boxX = 20;
|
||
var boxY = screenH / 2 - boxH / 2;
|
||
|
||
// 警告框背景(脉动)
|
||
ctx.fillStyle = 'rgba(80, 0, 0, ' + pulseAlpha + ')';
|
||
ctx.fillRect(boxX, boxY, boxW, boxH);
|
||
ctx.strokeStyle = 'rgba(255, 0, 0, 1)';
|
||
ctx.lineWidth = 2.5;
|
||
ctx.strokeRect(boxX, boxY, boxW, boxH);
|
||
|
||
// 警告标题
|
||
ctx.fillStyle = 'rgba(255, 60, 60, 1)';
|
||
ctx.font = 'bold 26px Arial';
|
||
ctx.textAlign = 'center';
|
||
ctx.fillText('⚠ 被瞄准', boxX + boxW / 2, boxY + 32);
|
||
ctx.font = 'bold 13px Arial';
|
||
ctx.fillStyle = 'rgba(255, 220, 220, 0.95)';
|
||
ctx.fillText(threats.length + ' 个威胁锁定你', boxX + boxW / 2, boxY + 54);
|
||
|
||
// 威胁坦克名称列表
|
||
ctx.textAlign = 'left';
|
||
ctx.font = 'bold 12px Arial';
|
||
for (var li = 0; li < listedCount; li++) {
|
||
var t = threats[li];
|
||
var lineY = boxY + 72 + li * 18;
|
||
// 名称 + 距离
|
||
ctx.fillStyle = 'rgba(255, 240, 240, 0.95)';
|
||
ctx.fillText('• ' + t.tankName, boxX + 12, lineY);
|
||
ctx.fillStyle = 'rgba(255, 180, 180, 0.85)';
|
||
ctx.textAlign = 'right';
|
||
ctx.fillText(Math.round(t.distance / 100) + 'm', boxX + boxW - 12, lineY);
|
||
ctx.textAlign = 'left';
|
||
}
|
||
// 超出部分
|
||
if (threats.length > maxListed) {
|
||
ctx.fillStyle = 'rgba(255, 200, 200, 0.7)';
|
||
ctx.font = 'italic 11px Arial';
|
||
ctx.textAlign = 'center';
|
||
ctx.fillText('等 ' + (threats.length - maxListed) + ' 个...', boxX + boxW / 2, boxY + 72 + listedCount * 18 + 12);
|
||
}
|
||
}
|
||
|
||
// ===== 死亡信息显示(屏幕中央,持续6秒或直到复活) =====
|
||
if (DeathInfo && (frameCount - DeathInfo.time) < DeathDisplayTime) {
|
||
var dBoxW = 400, dBoxH = 120;
|
||
var dBoxX = screenW / 2 - dBoxW / 2;
|
||
var dBoxY = screenH / 2 - dBoxH / 2 - 60; // 偏上一点
|
||
|
||
// 半透明背景
|
||
ctx.fillStyle = 'rgba(0, 0, 0, 0.75)';
|
||
ctx.fillRect(dBoxX, dBoxY, dBoxW, dBoxH);
|
||
// 边框(意料之内=红色,意料之外=紫色)
|
||
var borderColor = DeathInfo.expected ? 'rgba(255, 60, 60, 1)' : 'rgba(200, 80, 255, 1)';
|
||
ctx.strokeStyle = borderColor;
|
||
ctx.lineWidth = 3;
|
||
ctx.strokeRect(dBoxX, dBoxY, dBoxW, dBoxH);
|
||
|
||
// 标题
|
||
ctx.textAlign = 'center';
|
||
ctx.fillStyle = 'rgba(255, 80, 80, 1)';
|
||
ctx.font = 'bold 26px Arial';
|
||
ctx.fillText('你被击杀', dBoxX + dBoxW / 2, dBoxY + 36);
|
||
|
||
// 击杀者信息
|
||
ctx.font = 'bold 18px Arial';
|
||
if (DeathInfo.killerName) {
|
||
ctx.fillStyle = 'rgba(255, 220, 220, 1)';
|
||
ctx.fillText('击杀者: ' + DeathInfo.killerName, dBoxX + dBoxW / 2, dBoxY + 66);
|
||
ctx.font = '12px Arial';
|
||
ctx.fillStyle = 'rgba(255, 180, 180, 0.8)';
|
||
ctx.fillText('距离 ' + Math.round(DeathInfo.killerDistance / 100) + 'm', dBoxX + dBoxW / 2, dBoxY + 86);
|
||
} else {
|
||
ctx.fillStyle = 'rgba(220, 180, 255, 1)';
|
||
ctx.fillText('未知来源(偷袭)', dBoxX + dBoxW / 2, dBoxY + 66);
|
||
}
|
||
|
||
// 意料之内/外标签
|
||
ctx.font = 'bold 16px Arial';
|
||
if (DeathInfo.expected) {
|
||
ctx.fillStyle = 'rgba(255, 100, 100, 1)';
|
||
ctx.fillText('⚠ 意料之内(已在瞄准名单中)', dBoxX + dBoxW / 2, dBoxY + 108);
|
||
} else {
|
||
ctx.fillStyle = 'rgba(200, 100, 255, 1)';
|
||
ctx.fillText('✦ 意料之外(偷袭!)', dBoxX + dBoxW / 2, dBoxY + 108);
|
||
}
|
||
}
|
||
|
||
// ===== 顶部状态栏(始终显示) =====
|
||
ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
|
||
ctx.font = '11px Arial';
|
||
ctx.textAlign = 'left';
|
||
var statusText = '[瞄准警告] 监控中 | 威胁: ' + threats.length +
|
||
' | 死亡: ' + DeathStats.total + '(内' + DeathStats.expected + '/外' + DeathStats.unexpected + ')' +
|
||
' | Frame: ' + frameCount;
|
||
ctx.fillText(statusText, 5, 18);
|
||
}
|
||
|
||
// ===== 启动 =====
|
||
Loop();
|
||
console.log('[瞄准] 警告系统已启动,监控敌方坦克炮塔朝向');
|
||
|
||
// ===== 暴露控制接口 =====
|
||
window.__hornetAimWarning__ = {
|
||
running: true,
|
||
Stop: function() {
|
||
this.running = false;
|
||
running = false;
|
||
window.removeEventListener('resize', ResizeCanvas);
|
||
if (canvas && canvas.parentNode) canvas.parentNode.removeChild(canvas);
|
||
console.log('[瞄准] 警告系统已停止');
|
||
},
|
||
GetDeathStats: function() {
|
||
return DeathStats;
|
||
},
|
||
ResetDeathStats: function() {
|
||
DeathStats = { total: 0, expected: 0, unexpected: 0 };
|
||
console.log('[瞄准] 死亡统计已重置');
|
||
},
|
||
setSensitivity: function(s) {
|
||
var n = parseFloat(s);
|
||
if (!isNaN(n) && n >= 0.3 && n <= 3.0) {
|
||
Sensitivity = n;
|
||
try { localStorage.setItem('hornet_aim_sensitivity', String(n)); } catch(e) {}
|
||
console.log('[瞄准] 灵敏度设为: ' + n);
|
||
} else {
|
||
console.warn('[瞄准] 灵敏度应在 0.3~3.0');
|
||
}
|
||
},
|
||
getSensitivity: function() { return Sensitivity; },
|
||
setSound: function(on) {
|
||
SoundEnabled = !!on;
|
||
try { localStorage.setItem('hornet_aim_sound', SoundEnabled ? 'true' : 'false'); } catch(e) {}
|
||
console.log('[瞄准] 警告声音: ' + (SoundEnabled ? '开' : '关'));
|
||
},
|
||
getSound: function() { return SoundEnabled; }
|
||
};
|
||
}
|
||
|
||
var Tool = {
|
||
Start: function() {
|
||
if (window.__hornetAimWarning__ && window.__hornetAimWarning__.running) return;
|
||
_start();
|
||
},
|
||
Stop: function() {
|
||
if (window.__hornetAimWarning__) window.__hornetAimWarning__.Stop();
|
||
},
|
||
Status: function() {
|
||
return (window.__hornetAimWarning__ && window.__hornetAimWarning__.running) ? '运行中' : '已停止';
|
||
},
|
||
GetRunning: function() {
|
||
return !!(window.__hornetAimWarning__ && window.__hornetAimWarning__.running);
|
||
},
|
||
SetSensitivity: function(s) { if (window.__hornetAimWarning__) window.__hornetAimWarning__.setSensitivity(s); },
|
||
GetSensitivity: function() { return window.__hornetAimWarning__ ? window.__hornetAimWarning__.getSensitivity() : 1.0; },
|
||
SetSound: function(on) { if (window.__hornetAimWarning__) window.__hornetAimWarning__.setSound(on); },
|
||
GetSound: function() { return window.__hornetAimWarning__ ? window.__hornetAimWarning__.getSound() : false; }
|
||
};
|
||
|
||
window.__hornetTools__ = window.__hornetTools__ || {};
|
||
window.__hornetTools__.AimWarning = Tool;
|
||
console.log('[3DTank] 瞄准警告模块已加载');
|
||
})();
|