feat(rhythm): 鼓手模式重构——屏幕六区鼓组 + 无鼓伴奏合拍(用户裁定重构)

- 屏幕3列×2行六区鼓组(列=声像左中右/行=音高上亮下沉),六件程序合成打击乐,三重冗余蒙眼可辨
- 无鼓伴奏BGM上跟随领奏 call&response + 加花句;3曲90/105/120 BPM解锁制,BPM实测89.99/105.00/120.01
- 引擎后向兼容扩展:onGesture补{x,y}坐标(单参回调零感知,15游戏+3引擎测试全量回归零失败)
- 判定简化:全部音符=分区点按零延迟,旧双击补偿/下滑折中退役;键盘Digit1-6+空格=家区
- 教学摸鼓认区(摸哪报哪,8s无触摸自动进阶)+重教≤2+降速DDA+归因真话律
- 保留旧作好评:跟随领奏核心/S~C评级/解锁制/判定口径/guide工程遗产
- 语音49id双性别+8key;R1+R2连续2轮4/4愿意玩P0=0裁定可交付;live直证33 Perfect dt≈0.00s
- 03号文档仅文末加取代指针;新设计24号;档案research/rhythm-drummer/;Mureka累计$1.35
This commit is contained in:
xsl
2026-09-14 20:17:10 +08:00
parent ddada6980c
commit ae850a4ce0
280 changed files with 5671 additions and 1121 deletions
+18 -10
View File
@@ -32,9 +32,14 @@
*
* 用法:
* import { attachGestures } from '../engine/gesture.js';
* const detach = attachGestures(element, (g) => game.onGesture(g));
* const detach = attachGestures(element, (g, info) => game.onGesture(g, info));
* // detach() 可解除绑定
*
* 第二参 info(24-rhythm 坐标透传,缺省行为逐字节不变):
* { x, y } 视口像素坐标——tap 类/长按 = 按下点,滑动 = 滑动起点(出手瞄准
* 位置),键盘通道 = 虚拟指针坐标(元素中心)。需要触点位置的游戏(如 rhythm
* 分区打击)使用;单参回调完全不受影响。
*
* 基础震动反馈(haptics.js,不支持的平台自动 no-op):
* 滑动识别成功→swipe,tap→confirm,doubletap→confirm。
* 游戏逻辑层的语义反馈(error/success/warning 等)由游戏自行调用 vibrate()。
@@ -161,6 +166,7 @@ export function attachGestures(element, onGesture, config = {}) {
// 单指/单指针跟踪状态
let tracking = false;
let startX = 0, startY = 0, startTime = 0;
let downX = 0, downY = 0; // 本次按下的坐标(onGesture 第二参 info 透传用)
let moved = false; // 位移是否已超 tapSlop(超过则本次不可能是点按)
// 长按状态
@@ -194,14 +200,14 @@ export function attachGestures(element, onGesture, config = {}) {
if (!longPressActive) return;
longPressActive = false;
_dispatchLongpress('longpressend');
onGesture('longpressend');
onGesture('longpressend', { x: downX, y: downY });
}
/** immediateTap:本次按下的序曲 tap 确定作废(转滑动/长按/触摸取消)时补发 tapcancel。 */
function cancelImmediateTap() {
if (!tapPending) return;
tapPending = false;
onGesture('tapcancel');
onGesture('tapcancel', { x: downX, y: downY });
}
function handleDown(x, y) {
@@ -209,6 +215,8 @@ export function attachGestures(element, onGesture, config = {}) {
moved = false;
startX = x;
startY = y;
downX = x;
downY = y;
startTime = performance.now();
if (cfg.immediateTap) {
// 即时模式:按下立即回调 tap,不等双击窗口关闭。
@@ -216,7 +224,7 @@ export function attachGestures(element, onGesture, config = {}) {
// 这声 tap 已发出不可撤回,但引擎会补 tapcancel 让游戏层撤销判定。
tapPending = true;
vibrate('confirm');
onGesture('tap');
onGesture('tap', { x: downX, y: downY });
}
cancelLongPress();
longPressTimer = setTimeout(() => {
@@ -227,7 +235,7 @@ export function attachGestures(element, onGesture, config = {}) {
cancelImmediateTap();
longPressActive = true;
_dispatchLongpress('longpress');
onGesture('longpress');
onGesture('longpress', { x: downX, y: downY });
}, cfg.longpressMs);
}
@@ -267,7 +275,7 @@ export function attachGestures(element, onGesture, config = {}) {
if (Math.abs(dx) > Math.abs(dy)) g = dx > 0 ? 'right' : 'left';
else g = dy > 0 ? 'down' : 'up';
vibrate('swipe'); // 滑动识别成功
onGesture(g);
onGesture(g, { x: startX, y: startY });
return;
}
@@ -281,7 +289,7 @@ export function attachGestures(element, onGesture, config = {}) {
cancelPendingTap();
lastTapTime = 0;
vibrate('confirm'); // 双击
onGesture('doubletap');
onGesture('doubletap', { x: downX, y: downY });
} else if (cfg.immediateTap) {
lastTapTime = now; // tap 已在 handleDown 发出,这里只记双击窗起点
} else {
@@ -292,7 +300,7 @@ export function attachGestures(element, onGesture, config = {}) {
pendingTapTimer = null;
lastTapTime = 0;
vibrate('confirm'); // 单击
onGesture('tap');
onGesture('tap', { x: downX, y: downY });
}, cfg.doubleTapWindow);
}
return;
@@ -395,7 +403,7 @@ export function attachGestures(element, onGesture, config = {}) {
cancelPendingTap(); // 与触屏滑动一致:滑动作废等待中的单击
lastTapTime = 0;
vibrate('swipe');
onGesture(dir); // 即时,无位移/速度判定(零延迟)
onGesture(dir, keyCenter()); // 即时,无位移/速度判定(零延迟)
return;
}
if (tracking) return;
@@ -405,7 +413,7 @@ export function attachGestures(element, onGesture, config = {}) {
cancelPendingTap();
lastTapTime = 0;
vibrate('confirm');
onGesture('doubletap');
onGesture('doubletap', keyCenter());
return;
}
// 空格:虚拟指针按下,进同一状态机(起 450ms 长按定时器;immediateTap