- hornet 工具链:无 ght 启动;透视冻结节点拖拽跟随、金箱标记、性能优化;雷达死亡红叉;鼠标稳定器;窗口布局持久化 - 本地镜像:切割与单包加载(pack_unpacked.js)+ SW 单包缓存 + F5 兜底刷新;入口更新为 main.9f4e7a89.js - 中继 wss_server.js:转发改 socket 反压的原样直发(零等待零缓存);落盘改单条有序异步队列(默认开启);新增反压看门狗(默认 60s)
246 lines
10 KiB
JavaScript
246 lines
10 KiB
JavaScript
// hornet_mouse.js — 鼠标输入稳定器
|
||
// 目标:缓解原版「指针锁定下鼠标乱晃 / 难控制」的问题(游戏自身行为,非本工具链引入)。
|
||
//
|
||
// 游戏侧机制(module_03569):
|
||
// · 锁定:Lh() 先 requestPointerLock({unadjustedMovement:true})(原始输入),**失败则静默回退**
|
||
// 成不带 unadjustedMovement 的普通锁定 —— 后者会带上 OS 鼠标加速,手感立刻变飘。
|
||
// · 视角:mousemove 里直接把 e.movementX / e.movementY 喂给输入管理器
|
||
// (fal_1.ia9(qh(), movementX, movementY)),没有任何尖峰/跳变过滤。
|
||
//
|
||
// 本模块做三件事,全部可在面板「鼠标稳定」里开关:
|
||
// ① 强制原始输入:接管 Element.prototype.requestPointerLock,始终带 unadjustedMovement,
|
||
// 并把「回退/失败」打到控制台(手感异常的第一嫌疑人)。
|
||
// ② 锁后/恢复跳变抑制:指针锁定刚建立、或事件流中断(切窗口/失焦)后恢复的头 DropMs 毫秒内
|
||
// 丢弃 mousemove —— 这一两下常带巨大 movementX/Y,会直接把视角甩飞。
|
||
// ③ 尖峰钳制:单事件 |movementX| / |movementY| 超过 MaxDelta(0 = 关闭)时截断,
|
||
// 抑制驱动/焦点抖动造成的瞬时尖峰。
|
||
//
|
||
// 状态:HUD 里显示「鼠标: RAW 48/s 尖峰320 钳0 丢1」,或
|
||
// console.log(window.__hornetTools__.Mouse.GetStatus())
|
||
(function () {
|
||
'use strict';
|
||
|
||
var lib = window.__hornetLib__;
|
||
if (!lib) {
|
||
console.error('[鼠标] 请先加载 hornet_lib.js');
|
||
return;
|
||
}
|
||
|
||
// ===== 配置(localStorage 持久化)=====
|
||
var CONFIG_KEY = 'hornet_mouse_cfg';
|
||
var Config = {
|
||
ForceRaw: true, // 强制原始输入(unadjustedMovement),关掉则完全不干预指针锁定
|
||
DropMs: 120, // 锁定建立 / 事件流恢复后的忽略时长(ms,0 = 不丢事件)
|
||
GapMs: 250, // 事件间隔超过它视为「恢复」,同样触发忽略窗口(ms)
|
||
MaxDelta: 0 // 单事件位移钳制阈值(px,0 = 不钳制)
|
||
};
|
||
try {
|
||
var Saved = JSON.parse(localStorage.getItem(CONFIG_KEY) || '{}');
|
||
if (typeof Saved.ForceRaw === 'boolean') Config.ForceRaw = Saved.ForceRaw;
|
||
if (typeof Saved.DropMs === 'number') Config.DropMs = Saved.DropMs;
|
||
if (typeof Saved.GapMs === 'number') Config.GapMs = Saved.GapMs;
|
||
if (typeof Saved.MaxDelta === 'number') Config.MaxDelta = Saved.MaxDelta;
|
||
} catch (e) {}
|
||
function SaveConfig() {
|
||
try { localStorage.setItem(CONFIG_KEY, JSON.stringify(Config)); } catch (e) {}
|
||
}
|
||
|
||
function Now() {
|
||
return (window.performance && window.performance.now) ? window.performance.now() : Date.now();
|
||
}
|
||
|
||
// ===== 运行状态与统计 =====
|
||
var Stat = {
|
||
running: false,
|
||
rawMode: '未知', // 原始输入 / 带加速 / 未知
|
||
locked: false,
|
||
dropUntil: 0, // 该时刻前的 mousemove 全部丢弃
|
||
dropped: 0,
|
||
clamped: 0,
|
||
events: 0,
|
||
maxAbs: 0, // 本统计窗口内最大单事件位移
|
||
lastEventAt: 0,
|
||
rate: 0,
|
||
rateEvents: 0,
|
||
rateWindow: 0,
|
||
lastAnomalyAt: 0
|
||
};
|
||
|
||
var OrigRequestPointerLock = null;
|
||
var Attached = false;
|
||
|
||
// ===== ① 指针锁定:强制原始输入 =====
|
||
function InstallLockPatch() {
|
||
if (OrigRequestPointerLock) return;
|
||
OrigRequestPointerLock = Element.prototype.requestPointerLock;
|
||
Element.prototype.requestPointerLock = function (Options) {
|
||
var Opt = Options;
|
||
if (Config.ForceRaw && !(Opt && Opt.unadjustedMovement)) {
|
||
Opt = { unadjustedMovement: true }; // 显式请求原始输入(不改调用方对象)
|
||
}
|
||
// 只有在「本来没锁」时才算一次真正的锁定(避免已锁定时的重复请求反复刷新忽略窗口,
|
||
// 那会让鼠标一直被丢弃而失去响应)
|
||
var WasLocked = !!document.pointerLockElement;
|
||
Stat.locked = true;
|
||
if (!WasLocked) {
|
||
Stat.lastEventAt = 0; // 重新计时,避免把旧的间隔当成「恢复」
|
||
Stat.dropUntil = Config.DropMs > 0 ? (Now() + Config.DropMs) : 0;
|
||
}
|
||
|
||
var P;
|
||
try {
|
||
P = OrigRequestPointerLock.call(this, Opt);
|
||
} catch (Err) {
|
||
console.error('[鼠标] requestPointerLock 调用异常:', Err && Err.message);
|
||
return null;
|
||
}
|
||
var WantRaw = !!(Opt && Opt.unadjustedMovement);
|
||
if (P && typeof P.then === 'function') {
|
||
P.then(
|
||
function () {
|
||
Stat.rawMode = WantRaw ? '原始输入' : '带加速';
|
||
console.log('[鼠标] 指针锁定成功,模式=' + Stat.rawMode +
|
||
(WantRaw ? '' : '(带 OS 加速,手感会飘;可在面板勾选「强制原始输入」)'));
|
||
},
|
||
function (Err) {
|
||
Stat.rawMode = '未知';
|
||
console.warn('[鼠标] 指针锁定失败: ' + (Err && Err.message));
|
||
},
|
||
);
|
||
} else {
|
||
Stat.rawMode = WantRaw ? '原始输入' : '带加速';
|
||
}
|
||
return P;
|
||
};
|
||
}
|
||
|
||
function UninstallLockPatch() {
|
||
if (!OrigRequestPointerLock) return;
|
||
Element.prototype.requestPointerLock = OrigRequestPointerLock;
|
||
OrigRequestPointerLock = null;
|
||
}
|
||
|
||
// ===== ②③ mousemove 拦截:丢弃跳变 + 钳制尖峰 =====
|
||
// 注册在 window 的捕获阶段:一定早于游戏挂在画布/document 上的处理器,丢弃/改写才有效
|
||
function OnMouseMoveCapture(e) {
|
||
Stat.events++;
|
||
var T = Now();
|
||
|
||
// 事件流「恢复」判定(切窗口/失焦回来时的第一下常是巨幅位移)
|
||
if (Config.GapMs > 0 && Stat.lastEventAt && (T - Stat.lastEventAt) > Config.GapMs && Config.DropMs > 0) {
|
||
Stat.dropUntil = T + Config.DropMs;
|
||
}
|
||
Stat.lastEventAt = T;
|
||
|
||
// ② 忽略窗口内:整条事件丢弃(stopImmediatePropagation 阻断游戏处理器)
|
||
if (Stat.dropUntil && T < Stat.dropUntil) {
|
||
Stat.dropped++;
|
||
e.stopImmediatePropagation();
|
||
return;
|
||
}
|
||
|
||
var Dx = e.movementX, Dy = e.movementY;
|
||
if (typeof Dx !== 'number' || typeof Dy !== 'number') return;
|
||
|
||
var Ax = Math.abs(Dx), Ay = Math.abs(Dy);
|
||
if (Ax > Stat.maxAbs) Stat.maxAbs = Ax;
|
||
if (Ay > Stat.maxAbs) Stat.maxAbs = Ay;
|
||
|
||
// ③ 尖峰钳制:改写事件上的 movementX/Y(own property 遮蔽原型 getter)
|
||
if (Config.MaxDelta > 0 && (Ax > Config.MaxDelta || Ay > Config.MaxDelta)) {
|
||
var Nx = Ax > Config.MaxDelta ? (Dx > 0 ? Config.MaxDelta : -Config.MaxDelta) : Dx;
|
||
var Ny = Ay > Config.MaxDelta ? (Dy > 0 ? Config.MaxDelta : -Config.MaxDelta) : Dy;
|
||
try {
|
||
Object.defineProperty(e, 'movementX', { value: Nx, configurable: true });
|
||
Object.defineProperty(e, 'movementY', { value: Ny, configurable: true });
|
||
Stat.clamped++;
|
||
} catch (Err) {
|
||
// 属性不可写则退化为丢弃该事件(同样能压住甩飞)
|
||
Stat.clamped++;
|
||
e.stopImmediatePropagation();
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 事件率统计(2s 窗口),尖峰异常时节流提示,帮助定位是「抖动」还是「跳变」
|
||
Stat.rateEvents++;
|
||
if (!Stat.rateWindow) Stat.rateWindow = T;
|
||
if (T - Stat.rateWindow >= 2000) {
|
||
Stat.rate = Math.round((Stat.rateEvents * 1000) / (T - Stat.rateWindow));
|
||
Stat.rateWindow = T;
|
||
Stat.rateEvents = 0;
|
||
var Bad = Stat.maxAbs > 400 && (T - Stat.lastAnomalyAt) > 3000;
|
||
if (Bad) {
|
||
Stat.lastAnomalyAt = T;
|
||
console.warn('[鼠标] 出现大位移尖峰: ' + Stat.maxAbs.toFixed(0) + 'px(事件率 ' + Stat.rate +
|
||
'/s,累计钳制 ' + Stat.clamped + ' 次)。若频繁甩飞,把「尖峰钳制」调成 250~400 试试');
|
||
}
|
||
Stat.maxAbs = 0; // 只统计窗口内最大值
|
||
}
|
||
}
|
||
|
||
// ===== 启动 / 停止 =====
|
||
function _start() {
|
||
if (Stat.running) return;
|
||
if (Config.ForceRaw) InstallLockPatch();
|
||
if (!Attached) {
|
||
window.addEventListener('mousemove', OnMouseMoveCapture, true);
|
||
Attached = true;
|
||
}
|
||
Stat.running = true;
|
||
console.log('[鼠标] 稳定器已启动 强制原始输入=' + Config.ForceRaw +
|
||
' 锁后忽略=' + Config.DropMs + 'ms 尖峰钳制=' + (Config.MaxDelta > 0 ? Config.MaxDelta + 'px' : '关'));
|
||
}
|
||
|
||
function _stop() {
|
||
if (!Stat.running) return;
|
||
UninstallLockPatch();
|
||
if (Attached) {
|
||
window.removeEventListener('mousemove', OnMouseMoveCapture, true);
|
||
Attached = false;
|
||
}
|
||
Stat.running = false;
|
||
Stat.dropped = 0; Stat.clamped = 0; Stat.events = 0; Stat.maxAbs = 0;
|
||
Stat.dropUntil = 0; Stat.lastEventAt = 0; Stat.rawMode = '未知';
|
||
console.log('[鼠标] 稳定器已停止');
|
||
}
|
||
|
||
var Api = {
|
||
config: Config,
|
||
_start: _start,
|
||
_stop: _stop,
|
||
setForceRaw: function (On) { Config.ForceRaw = !!On; SaveConfig(); if (Stat.running) { if (On) InstallLockPatch(); else UninstallLockPatch(); } },
|
||
setDropMs: function (Ms) { Config.DropMs = Math.max(0, Number(Ms) || 0); SaveConfig(); },
|
||
setMaxDelta: function (Px) { Config.MaxDelta = Math.max(0, Number(Px) || 0); SaveConfig(); },
|
||
getStatus: function () {
|
||
return {
|
||
running: Stat.running, rawMode: Stat.rawMode, rate: Stat.rate,
|
||
maxAbs: Math.round(Stat.maxAbs), clamped: Stat.clamped, dropped: Stat.dropped, events: Stat.events
|
||
};
|
||
}
|
||
};
|
||
window.__hornetMouse__ = Api;
|
||
|
||
var Tool = {
|
||
Start: function () { _start(); },
|
||
Stop: function () { _stop(); },
|
||
Status: function () {
|
||
if (!Stat.running) return '已停止';
|
||
return Stat.rawMode + ' ' + Stat.rate + '/s 尖峰' + Math.round(Stat.maxAbs) +
|
||
' 钳' + Stat.clamped + ' 丢' + Stat.dropped;
|
||
},
|
||
GetRunning: function () { return Stat.running; },
|
||
SetForceRaw: function (On) { Api.setForceRaw(On); },
|
||
GetForceRaw: function () { return Config.ForceRaw; },
|
||
SetDropMs: function (Ms) { Api.setDropMs(Ms); },
|
||
GetDropMs: function () { return Config.DropMs; },
|
||
SetMaxDelta: function (Px) { Api.setMaxDelta(Px); },
|
||
GetMaxDelta: function () { return Config.MaxDelta; },
|
||
GetStatus: function () { return Api.getStatus(); }
|
||
};
|
||
|
||
window.__hornetTools__ = window.__hornetTools__ || {};
|
||
window.__hornetTools__.Mouse = Tool;
|
||
console.log('[3DTank] 鼠标稳定模块已加载');
|
||
})();
|