- 根因:新版 main.7cfa4058 重拆后游戏符号整体平移,工具里写死的字段名(pyt_1/q100_1/r11l/oyz/jyw/cyw_1/dyw_1…)全部对不上 → 找不到组件/相机/场景 - 新增 tools/apply_symbol_map.js:按 map_symbols 映射批量替换,4 道安全规则:① 长名或含下划线 ② 排除 JS 关键字/内置名黑名单 ③ 游戏符号命名特征(含数字或 _数字 结尾) ④ 双向校验(旧名在新 bundle 仍存在→视为未变跳过;新名不存在→映射错误丢弃) - 单遍替换,避免链式污染(映射表新旧同名复用时会被二次替换) - 应用到 12 个 hornet_*.js 共 231 处(跳过 25764 个未变符号) - 重新生成 hornet_bundle.js(10 模块 / 249263 字节)并同步 unpacked
921 lines
36 KiB
JavaScript
921 lines
36 KiB
JavaScript
// ===== 3DTank 控制面板 =====
|
||
// 功能:Alt+T 切换面板显示,控制各工具开关
|
||
// 左上角 HUD 实时显示 GHT 捕获状态和各工具运行状态
|
||
// HUD 和面板支持鼠标拖动
|
||
// 依赖:hook_gamemode.js + hornet_lib.js + 各 hornet_*.js 工具模块
|
||
|
||
(function() {
|
||
console.log('[3DTank] 控制面板加载');
|
||
|
||
// ===== 清理旧面板 =====
|
||
if (window.__hornetPanel__) {
|
||
window.__hornetPanel__.Destroy();
|
||
console.log('已清理旧控制面板');
|
||
}
|
||
|
||
// 通用拖动功能
|
||
// elem: 要拖动的元素,handle: 触发拖动的元素(默认=elem)
|
||
// ignoreTags: 点击这些标签时不触发拖动(如 BUTTON)
|
||
// key: 可选,传入后把窗口位置存到 localStorage(hornet_layout_<key>)并在创建时还原
|
||
// 实现要点:Pointer Events + setPointerCapture 把指针"锁"在 handle 上,
|
||
// pointerup/pointercancel 必定回到 handle;并在 window 捕获阶段加兜底,
|
||
// 避免面板内 stopPropagation 或游戏画布抢事件导致"松手后仍粘着鼠标"。
|
||
// 返回:{ End, IsActive },供隐藏面板时强制结束拖动
|
||
function MakeDraggable(elem, handle, ignoreTags, key) {
|
||
handle = handle || elem;
|
||
ignoreTags = ignoreTags || ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA'];
|
||
handle.style.cursor = 'move';
|
||
|
||
var StorageKey = key ? ('hornet_layout_' + key) : null;
|
||
|
||
function ClampX(v) { return Math.max(0, Math.min(window.innerWidth - 40, v)); }
|
||
function ClampY(v) { return Math.max(0, Math.min(window.innerHeight - 24, v)); }
|
||
|
||
// 还原上次保存的位置
|
||
if (StorageKey) {
|
||
try {
|
||
var __saved = localStorage.getItem(StorageKey);
|
||
if (__saved) {
|
||
var __p = JSON.parse(__saved);
|
||
if (__p && typeof __p.left === 'number' && typeof __p.top === 'number') {
|
||
elem.style.transform = 'none';
|
||
elem.style.left = ClampX(__p.left) + 'px';
|
||
elem.style.top = ClampY(__p.top) + 'px';
|
||
}
|
||
}
|
||
} catch(e) {}
|
||
}
|
||
|
||
function SavePos() {
|
||
if (!StorageKey) return;
|
||
try {
|
||
var r = elem.getBoundingClientRect();
|
||
localStorage.setItem(StorageKey, JSON.stringify({ left: Math.round(r.left), top: Math.round(r.top) }));
|
||
} catch(e) {}
|
||
}
|
||
|
||
var Active = false;
|
||
var PointerId = null;
|
||
var GrabX = 0;
|
||
var GrabY = 0;
|
||
|
||
function MoveTo(ClientX, ClientY) {
|
||
elem.style.left = ClampX(ClientX - GrabX) + 'px';
|
||
elem.style.top = ClampY(ClientY - GrabY) + 'px';
|
||
}
|
||
|
||
function Begin(ClientX, ClientY) {
|
||
var Rect = elem.getBoundingClientRect();
|
||
// 移除 transform 居中,改用绝对定位,避免拖动起点跳变
|
||
elem.style.transform = 'none';
|
||
elem.style.left = Rect.left + 'px';
|
||
elem.style.top = Rect.top + 'px';
|
||
GrabX = ClientX - Rect.left;
|
||
GrabY = ClientY - Rect.top;
|
||
Active = true;
|
||
handle.style.cursor = 'grabbing';
|
||
}
|
||
|
||
function End() {
|
||
if (!Active) return;
|
||
Active = false;
|
||
if (PointerId !== null && handle.releasePointerCapture) {
|
||
try { handle.releasePointerCapture(PointerId); } catch (Err) {}
|
||
}
|
||
PointerId = null;
|
||
handle.style.cursor = 'move';
|
||
SavePos();
|
||
}
|
||
|
||
function CanStart(Target) {
|
||
return !Target || ignoreTags.indexOf(Target.tagName) < 0;
|
||
}
|
||
|
||
if (window.PointerEvent) {
|
||
// 捕获阶段监听:先于元素自身的 stopPropagation 执行
|
||
handle.addEventListener('pointerdown', function (e) {
|
||
if (e.button !== 0) return;
|
||
if (!CanStart(e.target)) return;
|
||
Begin(e.clientX, e.clientY);
|
||
PointerId = e.pointerId;
|
||
try { handle.setPointerCapture(e.pointerId); } catch (Err) {}
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
}, true);
|
||
|
||
handle.addEventListener('pointermove', function (e) {
|
||
if (!Active || (PointerId !== null && e.pointerId !== PointerId)) return;
|
||
// 兜底:按键已经松开(例如在窗口外松手)→ 结束拖动
|
||
if (e.buttons === 0) { End(); return; }
|
||
MoveTo(e.clientX, e.clientY);
|
||
e.preventDefault();
|
||
}, true);
|
||
|
||
handle.addEventListener('pointerup', function () { End(); }, true);
|
||
handle.addEventListener('pointercancel', function () { End(); }, true);
|
||
handle.addEventListener('lostpointercapture', function () { End(); }, true);
|
||
window.addEventListener('pointerup', function () { End(); }, true);
|
||
} else {
|
||
// 旧浏览器回退:mouse 事件 + document 捕获阶段监听
|
||
handle.addEventListener('mousedown', function (e) {
|
||
if (e.button !== 0) return;
|
||
if (!CanStart(e.target)) return;
|
||
Begin(e.clientX, e.clientY);
|
||
e.preventDefault();
|
||
}, true);
|
||
document.addEventListener('mousemove', function (e) {
|
||
if (!Active) return;
|
||
if (e.buttons === 0) { End(); return; }
|
||
MoveTo(e.clientX, e.clientY);
|
||
}, true);
|
||
document.addEventListener('mouseup', function () { End(); }, true);
|
||
}
|
||
|
||
// 公共兜底:窗口失焦 / 页面切后台 / 鼠标离开窗口
|
||
window.addEventListener('blur', End);
|
||
window.addEventListener('mouseleave', End);
|
||
document.addEventListener('visibilitychange', function () { if (document.hidden) End(); });
|
||
|
||
return { End: End, IsActive: function () { return Active; } };
|
||
}
|
||
|
||
// ===== 工具列表(动态从 __hornetTools__ 获取)=====
|
||
function GetToolList() {
|
||
var tools = window.__hornetTools__ || {};
|
||
return [
|
||
{ key: 'Radar', label: '雷达', obj: tools.Radar },
|
||
{ key: 'Wireframe', label: '3D透视', obj: tools.Wireframe, hasMode: true },
|
||
{ key: 'Rearview', label: '后视镜', obj: tools.Rearview },
|
||
{ key: 'AimWarning', label: '瞄准警告', obj: tools.AimWarning },
|
||
{ key: 'FreeLook', label: '自由视角', obj: tools.FreeLook },
|
||
{ key: 'WarSandbox', label: '战争沙盘', obj: tools.WarSandbox },
|
||
{ key: 'Mouse', label: '鼠标稳定', obj: tools.Mouse }
|
||
];
|
||
}
|
||
|
||
// ===== 工具启用状态(持久化 + 默认启用)=====
|
||
// 默认除「自由视角」外全部开启:自由视角会直接接管相机,默认开启过于突兀
|
||
var ToolKeys = ['Radar', 'Wireframe', 'Rearview', 'AimWarning', 'FreeLook', 'WarSandbox', 'Mouse'];
|
||
var ToolDefaults = { Radar: true, Wireframe: true, Rearview: true, AimWarning: true, FreeLook: false, WarSandbox: true, Mouse: true };
|
||
var ToolEnabled = {};
|
||
try {
|
||
var __savedEnabled = JSON.parse(localStorage.getItem('hornet_tool_enabled') || '{}');
|
||
ToolKeys.forEach(function(k) {
|
||
ToolEnabled[k] = (typeof __savedEnabled[k] === 'boolean') ? __savedEnabled[k] : ToolDefaults[k];
|
||
});
|
||
} catch(e) {
|
||
ToolKeys.forEach(function(k) { ToolEnabled[k] = ToolDefaults[k]; });
|
||
}
|
||
// 把各工具当前的运行状态写回持久化(开关时调用)
|
||
function SaveToolEnabled() {
|
||
try {
|
||
var tools = window.__hornetTools__ || {};
|
||
ToolKeys.forEach(function(k) {
|
||
var t = tools[k];
|
||
if (t && typeof t.GetRunning === 'function') ToolEnabled[k] = !!t.GetRunning();
|
||
});
|
||
localStorage.setItem('hornet_tool_enabled', JSON.stringify(ToolEnabled));
|
||
} catch(e) {}
|
||
}
|
||
|
||
// 需要 ght 才能初始化的工具(要 hook 相机 / GL)——等捕获后再启动,避免失败报错刷屏
|
||
var GameModeTools = { FreeLook: true, Rearview: true, WarSandbox: true };
|
||
|
||
// 自动按保存状态开启:覆盖层(雷达/透视/瞄准)无 ght 也能先开并出 UI;
|
||
// 每 30 帧检查一次,全部进入运行状态即停
|
||
var AutoEnableDone = false;
|
||
var AutoEnableFrame = 0;
|
||
function TryAutoEnable() {
|
||
if (AutoEnableDone) return;
|
||
AutoEnableFrame++;
|
||
if (AutoEnableFrame % 30 !== 0) return;
|
||
|
||
var HasGm = !!window.__gameMode__;
|
||
var tools = window.__hornetTools__ || {};
|
||
var pending = 0;
|
||
ToolKeys.forEach(function(k) {
|
||
if (!ToolEnabled[k]) return;
|
||
if (GameModeTools[k] && !HasGm) { pending++; return; } // 等 ght 捕获
|
||
var t = tools[k];
|
||
if (!t || typeof t.Start !== 'function') return;
|
||
if (typeof t.GetRunning === 'function' && t.GetRunning()) return;
|
||
pending++;
|
||
try { t.Start(); } catch(e) {}
|
||
});
|
||
if (pending === 0) {
|
||
AutoEnableDone = true;
|
||
UpdateButtons();
|
||
console.log('[3DTank] 已按保存状态自动开启工具(覆盖层无 ght 也可先开;后视镜/沙盘/自由视角等 ght 捕获后开)');
|
||
}
|
||
}
|
||
|
||
// ===== HUD(左上角状态显示)—— 已按需求移除:**不挂载到页面**,等于不再显示。
|
||
// 注意:保留 hud 对象与 UpdateHud 循环 —— 循环里还负责 TryAutoEnable()(按保存状态自动开启工具)。
|
||
var hud = document.createElement('div');
|
||
hud.id = '3dtank-hud';
|
||
// 原样式 + document.body.appendChild(hud) 已移除(那就是常驻左上角的「3DTank 控制台」小窗)
|
||
var HudDrag = MakeDraggable(hud, null, null, 'hud');
|
||
|
||
// ===== 面板(可拖动,Alt+T 切换)=====
|
||
var panel = document.createElement('div');
|
||
panel.id = '3dtank-panel';
|
||
// 初始居中:用 left/top 50% + transform,拖动后移除 transform
|
||
panel.style.cssText =
|
||
'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:100001;' +
|
||
'background:rgba(12,16,24,0.72);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);' +
|
||
'border:1px solid rgba(125,249,255,0.2);border-radius:12px;' +
|
||
'padding:18px 22px;color:#e8eef5;font:13px Consolas,monospace;min-width:340px;' +
|
||
'box-shadow:0 8px 32px rgba(0,0,0,0.6),0 0 1px rgba(125,249,255,0.3),inset 0 1px 0 rgba(255,255,255,0.05);' +
|
||
'display:none;user-select:none;-webkit-user-select:none;';
|
||
|
||
// 标题(可拖动手柄)
|
||
var title = document.createElement('div');
|
||
title.textContent = '3DTank 控制面板';
|
||
title.style.cssText =
|
||
'font-size:16px;font-weight:bold;color:#7df9ff;margin-bottom:4px;' +
|
||
'text-shadow:0 0 6px rgba(125,249,255,0.4);' +
|
||
'cursor:move;padding-bottom:8px;border-bottom:1px solid rgba(125,249,255,0.15);' +
|
||
'letter-spacing:0.5px;';
|
||
panel.appendChild(title);
|
||
|
||
var subtitle = document.createElement('div');
|
||
subtitle.textContent = 'Alt+T 切换显示/隐藏 | 拖动标题移动面板';
|
||
subtitle.style.cssText = 'font-size:11px;color:#5a7a8a;margin:6px 0 14px;';
|
||
panel.appendChild(subtitle);
|
||
|
||
// 工具按钮容器
|
||
var btnContainer = document.createElement('div');
|
||
panel.appendChild(btnContainer);
|
||
|
||
// ===== 3D透视设置子面板(可折叠)=====
|
||
var wfSettings = document.createElement('div');
|
||
wfSettings.style.cssText = 'margin-top:12px;padding-top:10px;border-top:1px solid rgba(125,249,255,0.08);';
|
||
|
||
var wfHeader = document.createElement('div');
|
||
wfHeader.style.cssText = 'display:flex;justify-content:space-between;align-items:center;cursor:pointer;color:#7df9ff;font-weight:bold;font-size:13px;';
|
||
wfHeader.innerHTML = '<span>3D透视设置</span><span id="wfToggle" style="font-size:11px;color:#5a7a8a;">▼</span>';
|
||
wfSettings.appendChild(wfHeader);
|
||
|
||
var wfBody = document.createElement('div');
|
||
wfBody.style.cssText = 'margin-top:8px;display:block;';
|
||
wfSettings.appendChild(wfBody);
|
||
var wfCollapsed = false;
|
||
wfHeader.onclick = function() {
|
||
wfCollapsed = !wfCollapsed;
|
||
wfBody.style.display = wfCollapsed ? 'none' : 'block';
|
||
document.getElementById('wfToggle').textContent = wfCollapsed ? '▶' : '▼';
|
||
};
|
||
|
||
// 行布局辅助
|
||
function MakeRow(labelText) {
|
||
var row = document.createElement('div');
|
||
row.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:6px 0;';
|
||
var lab = document.createElement('span');
|
||
lab.style.cssText = 'color:#b8c8d8;font-size:12px;';
|
||
lab.textContent = labelText;
|
||
row.appendChild(lab);
|
||
var ctrl = document.createElement('div');
|
||
ctrl.style.cssText = 'display:flex;gap:6px;align-items:center;';
|
||
row.appendChild(ctrl);
|
||
wfBody.appendChild(row);
|
||
return { row: row, ctrl: ctrl };
|
||
}
|
||
|
||
// 线宽调节
|
||
var lwRow = MakeRow('线宽');
|
||
var lwSlider = document.createElement('input');
|
||
lwSlider.type = 'range';
|
||
lwSlider.min = '0.5'; lwSlider.max = '8'; lwSlider.step = '0.5';
|
||
lwSlider.style.cssText = 'width:120px;accent-color:#7df9ff;';
|
||
var lwVal = document.createElement('span');
|
||
lwVal.style.cssText = 'color:#7df9ff;font-size:11px;min-width:28px;text-align:right;';
|
||
lwRow.ctrl.appendChild(lwSlider);
|
||
lwRow.ctrl.appendChild(lwVal);
|
||
lwSlider.oninput = function() {
|
||
var v = parseFloat(lwSlider.value);
|
||
lwVal.textContent = v.toFixed(1);
|
||
var tools = window.__hornetTools__ || {};
|
||
if (tools.Wireframe) tools.Wireframe.SetLineWidth(v);
|
||
};
|
||
|
||
// 渲染模式选择
|
||
var hmRow = MakeRow('渲染模式');
|
||
var hmSelect = document.createElement('select');
|
||
hmSelect.style.cssText =
|
||
'padding:3px 6px;border:1px solid rgba(125,249,255,0.25);border-radius:4px;' +
|
||
'background:rgba(20,30,40,0.6);color:#e8eef5;font:11px Consolas;cursor:pointer;';
|
||
var hmOptions = [
|
||
{ v: 'team', t: '队伍模式(蓝友/红敌)' },
|
||
{ v: 'whitelist', t: '白名单(仅名单内渲染)' },
|
||
{ v: 'blacklist', t: '黑名单(名单内不渲染)' }
|
||
];
|
||
hmOptions.forEach(function(o) {
|
||
var opt = document.createElement('option');
|
||
opt.value = o.v; opt.textContent = o.t;
|
||
hmSelect.appendChild(opt);
|
||
});
|
||
hmRow.ctrl.appendChild(hmSelect);
|
||
hmSelect.onchange = function() {
|
||
var tools = window.__hornetTools__ || {};
|
||
if (tools.Wireframe) tools.Wireframe.SetHighlightMode(hmSelect.value);
|
||
};
|
||
|
||
// 名单管理
|
||
var hlRow = MakeRow('名单');
|
||
var hlInput = document.createElement('input');
|
||
hlInput.type = 'text';
|
||
hlInput.placeholder = '玩家名';
|
||
hlInput.style.cssText =
|
||
'width:120px;padding:3px 6px;border:1px solid rgba(125,249,255,0.25);border-radius:4px;' +
|
||
'background:rgba(20,30,40,0.6);color:#e8eef5;font:11px Consolas;';
|
||
var hlAddBtn = document.createElement('button');
|
||
hlAddBtn.textContent = '+';
|
||
hlAddBtn.style.cssText =
|
||
'padding:3px 8px;border:1px solid rgba(46,204,113,0.4);border-radius:4px;' +
|
||
'background:rgba(46,204,113,0.2);color:#2ecc71;cursor:pointer;font:bold 12px Consolas;';
|
||
hlRow.ctrl.appendChild(hlInput);
|
||
hlRow.ctrl.appendChild(hlAddBtn);
|
||
|
||
var hlList = document.createElement('div');
|
||
hlList.style.cssText = 'margin-top:6px;max-height:120px;overflow-y:auto;font-size:11px;';
|
||
wfBody.appendChild(hlList);
|
||
|
||
function RefreshHlList() {
|
||
var tools = window.__hornetTools__ || {};
|
||
var arr = tools.Wireframe ? tools.Wireframe.GetHighlightList() : [];
|
||
hlList.innerHTML = '';
|
||
if (arr.length === 0) {
|
||
var empty = document.createElement('div');
|
||
empty.style.cssText = 'color:#5a7a8a;padding:4px 0;font-style:italic;';
|
||
empty.textContent = '(空)';
|
||
hlList.appendChild(empty);
|
||
return;
|
||
}
|
||
arr.forEach(function(name) {
|
||
var item = document.createElement('div');
|
||
item.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:3px 4px;border-bottom:1px solid rgba(125,249,255,0.05);';
|
||
var ns = document.createElement('span');
|
||
ns.style.cssText = 'color:#b8c8d8;';
|
||
ns.textContent = name;
|
||
item.appendChild(ns);
|
||
var delBtn = document.createElement('button');
|
||
delBtn.textContent = '×';
|
||
delBtn.style.cssText =
|
||
'padding:1px 6px;border:1px solid rgba(224,85,85,0.4);border-radius:3px;' +
|
||
'background:rgba(224,85,85,0.2);color:#e05555;cursor:pointer;font:bold 11px Consolas;';
|
||
delBtn.onclick = function() {
|
||
if (tools.Wireframe) tools.Wireframe.RemoveHighlight(name);
|
||
RefreshHlList();
|
||
};
|
||
item.appendChild(delBtn);
|
||
hlList.appendChild(item);
|
||
});
|
||
}
|
||
function AddFromInput() {
|
||
var name = hlInput.value.trim();
|
||
if (!name) return;
|
||
var tools = window.__hornetTools__ || {};
|
||
if (tools.Wireframe) tools.Wireframe.AddHighlight(name);
|
||
hlInput.value = '';
|
||
RefreshHlList();
|
||
}
|
||
hlAddBtn.onclick = AddFromInput;
|
||
hlInput.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Enter') { e.preventDefault(); AddFromInput(); }
|
||
});
|
||
|
||
var hlClearRow = document.createElement('div');
|
||
hlClearRow.style.cssText = 'margin-top:4px;text-align:right;';
|
||
var hlClearBtn = document.createElement('button');
|
||
hlClearBtn.textContent = '清空名单';
|
||
hlClearBtn.style.cssText =
|
||
'padding:3px 8px;border:1px solid rgba(224,85,85,0.4);border-radius:4px;' +
|
||
'background:rgba(224,85,85,0.15);color:#e05555;cursor:pointer;font:11px Consolas;';
|
||
hlClearBtn.onclick = function() {
|
||
var tools = window.__hornetTools__ || {};
|
||
if (tools.Wireframe) tools.Wireframe.ClearHighlight();
|
||
RefreshHlList();
|
||
};
|
||
hlClearRow.appendChild(hlClearBtn);
|
||
wfBody.appendChild(hlClearRow);
|
||
|
||
panel.appendChild(wfSettings);
|
||
|
||
// ===== 通用可折叠设置子面板辅助函数 =====
|
||
function MakeSettingsSection(titleText) {
|
||
var sec = document.createElement('div');
|
||
sec.style.cssText = 'margin-top:10px;padding-top:8px;border-top:1px solid rgba(125,249,255,0.08);';
|
||
var hdr = document.createElement('div');
|
||
hdr.style.cssText = 'display:flex;justify-content:space-between;align-items:center;cursor:pointer;color:#7df9ff;font-weight:bold;font-size:12px;';
|
||
var toggleChar = document.createElement('span');
|
||
toggleChar.style.cssText = 'font-size:10px;color:#5a7a8a;';
|
||
toggleChar.textContent = '▶';
|
||
hdr.innerHTML = '<span>' + titleText + '</span>';
|
||
hdr.appendChild(toggleChar);
|
||
sec.appendChild(hdr);
|
||
var body = document.createElement('div');
|
||
body.style.cssText = 'margin-top:6px;display:none;';
|
||
sec.appendChild(body);
|
||
hdr.onclick = function() {
|
||
var shown = body.style.display !== 'none';
|
||
body.style.display = shown ? 'none' : 'block';
|
||
toggleChar.textContent = shown ? '▶' : '▼';
|
||
};
|
||
panel.appendChild(sec);
|
||
function row(labelText) {
|
||
var r = document.createElement('div');
|
||
r.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:5px 0;';
|
||
var lab = document.createElement('span');
|
||
lab.style.cssText = 'color:#b8c8d8;font-size:11px;';
|
||
lab.textContent = labelText;
|
||
r.appendChild(lab);
|
||
var ctrl = document.createElement('div');
|
||
ctrl.style.cssText = 'display:flex;gap:6px;align-items:center;';
|
||
r.appendChild(ctrl);
|
||
body.appendChild(r);
|
||
return { row: r, ctrl: ctrl };
|
||
}
|
||
function slider(labelText, min, max, step, fmt, onInput) {
|
||
var r = row(labelText);
|
||
var s = document.createElement('input');
|
||
s.type = 'range'; s.min = String(min); s.max = String(max); s.step = String(step);
|
||
s.style.cssText = 'width:110px;accent-color:#7df9ff;';
|
||
var v = document.createElement('span');
|
||
v.style.cssText = 'color:#7df9ff;font-size:10px;min-width:32px;text-align:right;';
|
||
s.oninput = function() {
|
||
var val = parseFloat(s.value);
|
||
v.textContent = fmt(val);
|
||
onInput(val);
|
||
};
|
||
r.ctrl.appendChild(s); r.ctrl.appendChild(v);
|
||
return {
|
||
set: function(val) { s.value = val; v.textContent = fmt(parseFloat(val)); },
|
||
get: function() { return parseFloat(s.value); }
|
||
};
|
||
}
|
||
function checkbox(labelText, onChange) {
|
||
var r = row(labelText);
|
||
var c = document.createElement('input');
|
||
c.type = 'checkbox';
|
||
c.style.cssText = 'accent-color:#7df9ff;width:16px;height:16px;cursor:pointer;';
|
||
c.onchange = function() { onChange(c.checked); };
|
||
r.ctrl.appendChild(c);
|
||
return {
|
||
set: function(on) { c.checked = !!on; },
|
||
get: function() { return c.checked; }
|
||
};
|
||
}
|
||
return { sec: sec, body: body, row: row, slider: slider, checkbox: checkbox };
|
||
}
|
||
|
||
// ===== 雷达设置 =====
|
||
var radarSec = MakeSettingsSection('雷达设置');
|
||
var radarRangeCtl = radarSec.slider('范围(m)', 30, 600, 10, function(v){ return v.toFixed(0); }, function(v) {
|
||
var tools = window.__hornetTools__ || {};
|
||
if (tools.Radar) tools.Radar.SetRange(v * 100);
|
||
});
|
||
var radarOpacityCtl = radarSec.slider('透明度', 0.2, 1.0, 0.05, function(v){ return v.toFixed(2); }, function(v) {
|
||
var tools = window.__hornetTools__ || {};
|
||
if (tools.Radar) tools.Radar.SetOpacity(v);
|
||
});
|
||
|
||
// ===== 鼠标稳定设置 =====
|
||
// 强制原始输入:游戏锁定失败会静默回退成带 OS 加速的普通锁定,手感会飘
|
||
// 锁后忽略:锁定刚建立 / 失焦恢复后的头几毫秒位移常是巨幅跳变,直接丢弃
|
||
// 尖峰钳制:单事件位移超过阈值就截断(0 = 关闭)
|
||
var mouseSec = MakeSettingsSection('鼠标稳定');
|
||
var mouseRawCtl = mouseSec.checkbox('强制原始输入', function(on) {
|
||
var tools = window.__hornetTools__ || {};
|
||
if (tools.Mouse) tools.Mouse.SetForceRaw(on);
|
||
});
|
||
var mouseDropCtl = mouseSec.slider('锁后忽略(ms)', 0, 500, 20, function(v){ return v.toFixed(0); }, function(v) {
|
||
var tools = window.__hornetTools__ || {};
|
||
if (tools.Mouse) tools.Mouse.SetDropMs(v);
|
||
});
|
||
var mouseClampCtl = mouseSec.slider('尖峰钳制(px)', 0, 1000, 25, function(v){ return v.toFixed(0); }, function(v) {
|
||
var tools = window.__hornetTools__ || {};
|
||
if (tools.Mouse) tools.Mouse.SetMaxDelta(v);
|
||
});
|
||
(function () { // 回填鼠标模块里已保存的配置
|
||
var tools = window.__hornetTools__ || {};
|
||
if (!tools.Mouse) return;
|
||
mouseRawCtl.set(tools.Mouse.GetForceRaw());
|
||
mouseDropCtl.set(tools.Mouse.GetDropMs());
|
||
mouseClampCtl.set(tools.Mouse.GetMaxDelta());
|
||
})();
|
||
|
||
// ===== 后视镜设置 =====
|
||
var rvSec = MakeSettingsSection('后视镜设置');
|
||
var rvFovCtl = rvSec.slider('FOV', 0.5, 2.5, 0.05, function(v){ return v.toFixed(2); }, function(v) {
|
||
var tools = window.__hornetTools__ || {};
|
||
if (tools.Rearview) tools.Rearview.SetFov(v);
|
||
});
|
||
var rvOpacityCtl = rvSec.slider('透明度', 0.2, 1.0, 0.05, function(v){ return v.toFixed(2); }, function(v) {
|
||
var tools = window.__hornetTools__ || {};
|
||
if (tools.Rearview) tools.Rearview.SetOpacity(v);
|
||
});
|
||
|
||
// ===== 瞄准警告设置 =====
|
||
var awSec = MakeSettingsSection('瞄准警告设置');
|
||
var awSensCtl = awSec.slider('灵敏度', 0.3, 3.0, 0.1, function(v){ return v.toFixed(1); }, function(v) {
|
||
var tools = window.__hornetTools__ || {};
|
||
if (tools.AimWarning) tools.AimWarning.SetSensitivity(v);
|
||
});
|
||
var awSoundCtl = awSec.checkbox('警告声音', function(on) {
|
||
var tools = window.__hornetTools__ || {};
|
||
if (tools.AimWarning) tools.AimWarning.SetSound(on);
|
||
});
|
||
|
||
// ===== 战争沙盘设置 =====
|
||
var wsSec = MakeSettingsSection('战争沙盘设置');
|
||
var wsGridCtl = wsSec.checkbox('显示网格', function(on) {
|
||
var tools = window.__hornetTools__ || {};
|
||
if (tools.WarSandbox) tools.WarSandbox.SetShowGrid(on);
|
||
});
|
||
var wsMarkerCtl = wsSec.slider('标记大小', 0.5, 3.0, 0.1, function(v){ return v.toFixed(1); }, function(v) {
|
||
var tools = window.__hornetTools__ || {};
|
||
if (tools.WarSandbox) tools.WarSandbox.SetMarkerSize(v);
|
||
});
|
||
|
||
// ===== 电磁炮(RAILGUN / z4t)设置 =====
|
||
// 锁定/蓄力时长实时覆盖 window.__hornetLockSec__(游戏内 z4t.c18o() 优先读它),
|
||
// 拖动滑块**立即生效**,不需要重打包或刷新页面。
|
||
// 实测门槛:1.5s / 2.0s / 原版3~5s = 满伤;1.0s / 0.1s = 无伤害 ⇒ 门槛在 1.0~1.5s。
|
||
var rgSec = MakeSettingsSection('电磁炮设置');
|
||
var rgSecVal = 1.5;
|
||
function ApplyLock(v) { window.__hornetLockSec__ = (v > 0 ? v : undefined); }
|
||
function SaveLock(on, v) {
|
||
try {
|
||
localStorage.setItem('hornet_lock_on', on ? '1' : '0');
|
||
localStorage.setItem('hornet_lock_sec', String(v));
|
||
} catch (e) {}
|
||
}
|
||
var rgOnCtl = rgSec.checkbox('启用时长覆盖', function(on) {
|
||
ApplyLock(on ? rgSecVal : 0);
|
||
SaveLock(on, rgSecVal);
|
||
});
|
||
var rgLockCtl = rgSec.slider('锁定/蓄力(s)', 0.1, 5.0, 0.05, function(v) { return v.toFixed(2); }, function(v) {
|
||
rgSecVal = v;
|
||
if (rgOnCtl.get()) ApplyLock(v);
|
||
SaveLock(rgOnCtl.get(), v);
|
||
});
|
||
(function () { // 回填上次保存的配置;**默认开启覆盖、默认值 1.5s**(实测满伤的最小锁定/蓄力时长)
|
||
try {
|
||
var on = localStorage.getItem('hornet_lock_on') !== '0';
|
||
var v = parseFloat(localStorage.getItem('hornet_lock_sec'));
|
||
if (isFinite(v) && v > 0) rgSecVal = v;
|
||
rgLockCtl.set(rgSecVal);
|
||
rgOnCtl.set(on);
|
||
ApplyLock(on ? rgSecVal : 0);
|
||
} catch (e) {}
|
||
})();
|
||
|
||
// 从工具读取当前设置(打开面板时同步)
|
||
function SyncWfSettings() {
|
||
var tools = window.__hornetTools__ || {};
|
||
if (tools.Wireframe) {
|
||
var lw = tools.Wireframe.GetLineWidth();
|
||
lwSlider.value = lw; lwVal.textContent = lw.toFixed(1);
|
||
var hm = tools.Wireframe.GetHighlightMode();
|
||
hmSelect.value = hm;
|
||
}
|
||
RefreshHlList();
|
||
// 同步其他工具设置
|
||
if (tools.Radar) {
|
||
radarRangeCtl.set(tools.Radar.GetRange() / 100);
|
||
radarOpacityCtl.set(tools.Radar.GetOpacity());
|
||
}
|
||
if (tools.Rearview) {
|
||
rvFovCtl.set(tools.Rearview.GetFov());
|
||
rvOpacityCtl.set(tools.Rearview.GetOpacity());
|
||
}
|
||
if (tools.AimWarning) {
|
||
awSensCtl.set(tools.AimWarning.GetSensitivity());
|
||
awSoundCtl.set(tools.AimWarning.GetSound());
|
||
}
|
||
if (tools.WarSandbox) {
|
||
wsGridCtl.set(tools.WarSandbox.GetShowGrid());
|
||
wsMarkerCtl.set(tools.WarSandbox.GetMarkerSize());
|
||
}
|
||
}
|
||
|
||
// 底部状态
|
||
var footer = document.createElement('div');
|
||
footer.style.cssText = 'margin-top:14px;padding-top:10px;border-top:1px solid rgba(125,249,255,0.08);font-size:11px;color:#5a7a8a;text-align:center;';
|
||
footer.textContent = '点击按钮切换 开/关';
|
||
panel.appendChild(footer);
|
||
|
||
// 热键显示区域(UpdateHotkeyDisplay 在热键系统定义后调用)
|
||
var hotkeySection = document.createElement('div');
|
||
hotkeySection.style.cssText = 'margin-top:12px;padding-top:10px;border-top:1px solid rgba(125,249,255,0.08);font-size:11px;';
|
||
function UpdateHotkeyDisplay() {
|
||
if (!HotkeyLabels || !Hotkeys) return;
|
||
var html = '<div style="color:#7a9aaa;margin-bottom:4px;">热键(控制台可改: __hornetPanel__.SetHotkey)</div>';
|
||
Object.keys(HotkeyLabels).forEach(function(k) {
|
||
html += '<div style="display:flex;justify-content:space-between;padding:2px 0;">' +
|
||
'<span style="color:#5a7a8a;">' + HotkeyLabels[k] + '</span>' +
|
||
'<span style="color:#7df9ff;font-weight:bold;">' + Hotkeys[k] + '</span></div>';
|
||
});
|
||
hotkeySection.innerHTML = html;
|
||
}
|
||
panel.appendChild(hotkeySection);
|
||
|
||
document.body.appendChild(panel);
|
||
// 面板通过标题栏拖动
|
||
var PanelDrag = MakeDraggable(panel, title, null, 'panel');
|
||
|
||
// ===== 阻止面板内事件冒泡到 window(防止游戏/工具拦截键盘鼠标)=====
|
||
// 鼠标事件:面板上所有鼠标事件都阻止冒泡(防止游戏转视角/开炮等)
|
||
['mousedown','mouseup','mousemove','wheel','click','dblclick','contextmenu'].forEach(function(evtName) {
|
||
panel.addEventListener(evtName, function(e) { e.stopPropagation(); });
|
||
});
|
||
// 键盘事件:只在输入元素上阻止冒泡(保留面板上其他位置的热键功能)
|
||
['keydown','keypress','keyup'].forEach(function(evtName) {
|
||
panel.addEventListener(evtName, function(e) {
|
||
var tag = e.target.tagName;
|
||
if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA') {
|
||
e.stopPropagation();
|
||
}
|
||
});
|
||
});
|
||
// HUD 也阻止鼠标事件冒泡(防止拖动HUD时游戏响应)
|
||
['mousedown','mouseup','mousemove','wheel','click','contextmenu'].forEach(function(evtName) {
|
||
hud.addEventListener(evtName, function(e) { e.stopPropagation(); });
|
||
});
|
||
|
||
// ===== 构建工具按钮(含模式切换)=====
|
||
var btnMap = {}; // key -> { row, btn, label, tool, modeBtns }
|
||
function BuildButtons() {
|
||
btnContainer.innerHTML = '';
|
||
btnMap = {};
|
||
var list = GetToolList();
|
||
list.forEach(function(t) {
|
||
var row = document.createElement('div');
|
||
row.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid rgba(125,249,255,0.06);';
|
||
|
||
var label = document.createElement('span');
|
||
label.textContent = t.label;
|
||
label.style.cssText = 'color:#b8c8d8;font-size:13px;';
|
||
row.appendChild(label);
|
||
|
||
// 右侧按钮容器
|
||
var btnBox = document.createElement('div');
|
||
btnBox.style.cssText = 'display:flex;gap:4px;align-items:center;';
|
||
|
||
var modeBtns = null;
|
||
|
||
// Wireframe 模式切换按钮
|
||
if (t.hasMode && t.obj && t.obj.SetMode) {
|
||
modeBtns = {};
|
||
var modes = [
|
||
{ mode: 'wireframe', text: '线框' },
|
||
{ mode: 'outline', text: '描边' }
|
||
];
|
||
modes.forEach(function(mb) {
|
||
var b = document.createElement('button');
|
||
b.textContent = mb.text;
|
||
b.style.cssText =
|
||
'padding:4px 8px;border:1px solid rgba(125,249,255,0.25);border-radius:4px;' +
|
||
'font:11px Consolas,monospace;cursor:pointer;color:#e8eef5;min-width:44px;' +
|
||
'background:rgba(20,30,40,0.6);transition:all 0.2s;';
|
||
b.onclick = function() {
|
||
if (t.obj) {
|
||
t.obj.SetMode(mb.mode);
|
||
UpdateButtons();
|
||
}
|
||
};
|
||
btnBox.appendChild(b);
|
||
modeBtns[mb.mode] = b;
|
||
});
|
||
}
|
||
|
||
var btn = document.createElement('button');
|
||
btn.style.cssText =
|
||
'padding:6px 16px;border:none;border-radius:6px;font:bold 12px Consolas,monospace;' +
|
||
'cursor:pointer;min-width:72px;color:#fff;transition:all 0.2s;' +
|
||
'box-shadow:0 2px 8px rgba(0,0,0,0.3);';
|
||
btn.onclick = function() {
|
||
if (!t.obj) {
|
||
console.warn('[3DTank] 工具 ' + t.key + ' 未加载');
|
||
return;
|
||
}
|
||
if (t.obj.GetRunning()) {
|
||
t.obj.Stop();
|
||
} else {
|
||
t.obj.Start();
|
||
}
|
||
UpdateButtons();
|
||
SaveToolEnabled();
|
||
};
|
||
btnBox.appendChild(btn);
|
||
|
||
row.appendChild(btnBox);
|
||
btnContainer.appendChild(row);
|
||
btnMap[t.key] = { btn: btn, label: label, tool: t.obj, modeBtns: modeBtns };
|
||
});
|
||
}
|
||
BuildButtons();
|
||
|
||
// ===== 更新按钮状态 =====
|
||
function UpdateButtons() {
|
||
Object.keys(btnMap).forEach(function(key) {
|
||
var info = btnMap[key];
|
||
if (!info.tool) {
|
||
info.btn.textContent = '未加载';
|
||
info.btn.style.background = '#555';
|
||
info.btn.disabled = true;
|
||
return;
|
||
}
|
||
var running = info.tool.GetRunning();
|
||
info.btn.textContent = running ? '关闭' : '开启';
|
||
info.btn.style.background = running
|
||
? 'linear-gradient(135deg,#e05555,#c0333d)'
|
||
: 'linear-gradient(135deg,#2ecc71,#27ae60)';
|
||
info.btn.disabled = false;
|
||
|
||
// 更新模式按钮高亮
|
||
if (info.modeBtns) {
|
||
var curMode = null;
|
||
try { curMode = window.__hornet3DWireframe__ && window.__hornet3DWireframe__.mode; } catch(e) {}
|
||
Object.keys(info.modeBtns).forEach(function(mk) {
|
||
var mb = info.modeBtns[mk];
|
||
if (mk === curMode && running) {
|
||
mb.style.background = 'linear-gradient(135deg,#2ecc71,#27ae60)';
|
||
mb.style.opacity = '1';
|
||
mb.style.color = '#fff';
|
||
} else {
|
||
mb.style.background = 'rgba(20,30,40,0.6)';
|
||
mb.style.opacity = '0.6';
|
||
mb.style.color = '#8899aa';
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
// ===== 更新 HUD(HUD 本体已移除,这里主要保留 TryAutoEnable 与循环)=====
|
||
function UpdateHud() {
|
||
requestAnimationFrame(UpdateHud);
|
||
TryAutoEnable(); // gameMode 捕获后自动按保存状态开启工具
|
||
if (!hud.parentNode) return; // HUD 未挂载(已移除)→ 不必拼状态文本/写 DOM
|
||
var ght = window.__gameMode__;
|
||
var lines = [];
|
||
lines.push('3DTank 控制台 [拖动移动]');
|
||
lines.push('GHT: ' + (ght ? '已捕获' : '未捕获'));
|
||
var list = GetToolList();
|
||
list.forEach(function(t) {
|
||
var status = t.obj ? t.obj.Status() : '未加载';
|
||
var dot = t.obj && t.obj.GetRunning() ? '●' : '○';
|
||
lines.push(dot + ' ' + t.label + ': ' + status);
|
||
});
|
||
hud.textContent = lines.join('\n');
|
||
hud.style.color = ght ? '#7df9ff' : '#f5a623';
|
||
}
|
||
UpdateHud();
|
||
|
||
// ===== 热键系统(localStorage 持久化)=====
|
||
var DefaultHotkeys = {
|
||
panel: 'Alt+T',
|
||
radar: 'Alt+R',
|
||
wireframe: 'Alt+W',
|
||
rearview: 'Alt+V',
|
||
aimWarning: 'Alt+A',
|
||
freeLook: 'Alt+F',
|
||
warSandbox: 'Alt+M'
|
||
};
|
||
var HotkeyLabels = {
|
||
panel: '面板',
|
||
radar: '雷达',
|
||
wireframe: '3D透视',
|
||
rearview: '后视镜',
|
||
aimWarning: '瞄准警告',
|
||
freeLook: '自由视角',
|
||
warSandbox: '战争沙盘'
|
||
};
|
||
var Hotkeys = {};
|
||
try {
|
||
var saved = localStorage.getItem('3dtank_hotkeys');
|
||
var parsed = saved ? JSON.parse(saved) : {};
|
||
Object.keys(DefaultHotkeys).forEach(function(k) {
|
||
Hotkeys[k] = parsed[k] || DefaultHotkeys[k];
|
||
});
|
||
} catch(e) {
|
||
Object.keys(DefaultHotkeys).forEach(function(k) { Hotkeys[k] = DefaultHotkeys[k]; });
|
||
}
|
||
|
||
function SaveHotkeys() {
|
||
try { localStorage.setItem('3dtank_hotkeys', JSON.stringify(Hotkeys)); } catch(e) {}
|
||
}
|
||
|
||
function EventToHotkey(e) {
|
||
var parts = [];
|
||
if (e.altKey) parts.push('Alt');
|
||
if (e.ctrlKey) parts.push('Ctrl');
|
||
if (e.shiftKey) parts.push('Shift');
|
||
var key = e.key;
|
||
if (key.length === 1) key = key.toUpperCase();
|
||
if (key === ' ') key = 'Space';
|
||
parts.push(key);
|
||
return parts.join('+');
|
||
}
|
||
|
||
function HidePanel() {
|
||
panel.style.display = 'none';
|
||
PanelDrag.End(); // 防止隐藏时拖动状态残留(粘手)
|
||
}
|
||
|
||
// 释放鼠标指针锁定:游戏中鼠标被 Pointer Lock 捕获时指针隐藏、锁定在画布上,
|
||
// 面板点不到,需退出锁定让光标恢复可交互
|
||
function ReleasePointerLock() {
|
||
try {
|
||
if (document.exitPointerLock && document.pointerLockElement) {
|
||
document.exitPointerLock();
|
||
}
|
||
} catch(e) {}
|
||
}
|
||
|
||
function ShowPanel() {
|
||
panel.style.display = 'block';
|
||
ReleasePointerLock();
|
||
UpdateButtons();
|
||
SyncWfSettings();
|
||
}
|
||
|
||
function TogglePanel() {
|
||
if (panel.style.display === 'block') {
|
||
HidePanel();
|
||
} else {
|
||
ShowPanel();
|
||
}
|
||
}
|
||
|
||
function OnHotkey(e) {
|
||
if (e.key === 'Escape' && panel.style.display === 'block') {
|
||
HidePanel();
|
||
return;
|
||
}
|
||
var hk = EventToHotkey(e);
|
||
if (hk === Hotkeys.panel) {
|
||
e.preventDefault();
|
||
TogglePanel();
|
||
return;
|
||
}
|
||
var toolMap = { radar: 'Radar', wireframe: 'Wireframe', rearview: 'Rearview', aimWarning: 'AimWarning', freeLook: 'FreeLook', warSandbox: 'WarSandbox' };
|
||
Object.keys(toolMap).forEach(function(k) {
|
||
if (hk === Hotkeys[k]) {
|
||
e.preventDefault();
|
||
var tools = window.__hornetTools__ || {};
|
||
var tool = tools[toolMap[k]];
|
||
if (tool) {
|
||
if (tool.GetRunning()) tool.Stop();
|
||
else tool.Start();
|
||
UpdateButtons();
|
||
SaveToolEnabled();
|
||
}
|
||
}
|
||
});
|
||
}
|
||
// F5 刷新页面(游戏会吞掉 F5)——捕获阶段兜底,先于游戏处理
|
||
window.addEventListener('keydown', function(e) {
|
||
if (e.key === 'F5') { e.preventDefault(); e.stopImmediatePropagation(); location.reload(); }
|
||
}, true);
|
||
window.addEventListener('keydown', OnHotkey);
|
||
// 热键系统定义完成,更新显示
|
||
UpdateHotkeyDisplay();
|
||
|
||
// ===== 暴露接口 =====
|
||
window.__hornetPanel__ = {
|
||
Toggle: TogglePanel,
|
||
Show: ShowPanel,
|
||
Hide: HidePanel,
|
||
UpdateButtons: UpdateButtons,
|
||
RebuildButtons: BuildButtons,
|
||
GetHotkeys: function() { var r = {}; Object.keys(Hotkeys).forEach(function(k) { r[k] = Hotkeys[k]; }); return r; },
|
||
SetHotkey: function(name, key) {
|
||
if (DefaultHotkeys[name]) {
|
||
Hotkeys[name] = key;
|
||
SaveHotkeys();
|
||
UpdateHotkeyDisplay();
|
||
console.log('[3DTank] 热键 ' + name + ' 已设为 ' + key);
|
||
} else {
|
||
console.warn('[3DTank] 未知热键名: ' + name + ',可用: ' + Object.keys(DefaultHotkeys).join(', '));
|
||
}
|
||
},
|
||
ResetHotkeys: function() {
|
||
Object.keys(DefaultHotkeys).forEach(function(k) { Hotkeys[k] = DefaultHotkeys[k]; });
|
||
SaveHotkeys();
|
||
UpdateHotkeyDisplay();
|
||
console.log('[3DTank] 热键已重置为默认值');
|
||
},
|
||
Destroy: function() {
|
||
HudDrag.End();
|
||
PanelDrag.End();
|
||
if (hud.parentNode) hud.parentNode.removeChild(hud);
|
||
if (panel.parentNode) panel.parentNode.removeChild(panel);
|
||
window.removeEventListener('keydown', OnHotkey);
|
||
}
|
||
};
|
||
|
||
console.log('[3DTank] 控制面板已就绪,按 Alt+T 打开面板,HUD/面板/窗口可拖动,热键已加载');
|
||
})();
|