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:
+18
-10
@@ -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
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
/**
|
||||
* 《节拍行者》新手会话剧本:首局完整走教学 T1-T3(第 2 句先整拍不出手——
|
||||
* round4 H5 归因真话律:听到的是「这拍没接」而不是「慢了半拍」;再连错一遍
|
||||
* 触发 round3 G2 的「降速 25% 重演 + gd_slow」,跟着慢速领奏做对后恢复正常)
|
||||
* → 曲目一(90 BPM);曲中下滑呼出菜单逛一圈再恢复(round3 G3:恢复先听
|
||||
* gd_count「四拍之后,继续」,再跟 4 拍底鼓 count-in 进场;首个应答窗口前
|
||||
* 整整 1 小节有 gd_answer+turn 交接标记);正片第一乐句故意整句漏掉(连 2 Miss
|
||||
* 触发向导 gd_miss「慢了半拍」——正片规则不变),之后跟着领奏把一曲打完到结算。
|
||||
* 《节拍行者》鼓手模式新手会话剧本:首局完整走「摸鼓认区 → 教学 T1-T3 →
|
||||
* 曲目一(90 BPM)」全弧线——探索阶段把六区摸遍(区名每区只播一次)、曲中
|
||||
* 下滑逛菜单再恢复(gd_count + 4 拍底鼓 count-in 进场)、正片第一乐句故意
|
||||
* 整句漏掉(连 2 Miss → 向导 gd_miss)、之后逐音跟打(含终曲加花)到结算。
|
||||
* 分区敲击带坐标注入(引擎坐标透传契约,24 号文档 §0.3)。
|
||||
*/
|
||||
const ZC = {
|
||||
TL: { x: 50, y: 100 }, TC: { x: 150, y: 100 }, TR: { x: 250, y: 100 },
|
||||
BL: { x: 50, y: 500 }, BC: { x: 150, y: 500 }, BR: { x: 250, y: 500 },
|
||||
};
|
||||
|
||||
export function describe(game) {
|
||||
switch (game.phase) {
|
||||
case 'intro': return '开场旁白在播,单击可以跳过';
|
||||
case 'tut': return game._cues.length
|
||||
? `教学:听领奏,下一小节照样打(${game._cues.map((c) => c.gesture).join('→')})${game._tutRate < 1 ? '(领奏已放慢 25%)' : ''}`
|
||||
: '教学准备中,听鼓点';
|
||||
case 'intro': return '开场旁白在播(认识你的鼓),单击可以跳过';
|
||||
case 'tut':
|
||||
if (game._tutStage === 'probe') {
|
||||
return `摸鼓认区:随便摸,摸到 3 个不同的地方就开始(已摸 ${game._probeHeard.size} 区)`;
|
||||
}
|
||||
return game._cues.length
|
||||
? `教学:听领奏,下一小节照样敲(${game._cues.map((c) => c.zone).join('→')})${game._tutRate < 1 ? '(领奏已放慢 25%)' : ''}`
|
||||
: '教学准备中,听数拍';
|
||||
case 'select': return '选曲:左右滑换曲,单击开始,下滑回大厅';
|
||||
case 'main': {
|
||||
const left = game._cues.filter((c) => !c.judged).length;
|
||||
return `演奏中(${game._song?.title ?? ''}):还剩 ${left} 个应答音。领奏停了、轮到空拍,就该我出手了`;
|
||||
const fillNext = game._timed.some((e) => e.fn && game._song?.units?.some((u) => u.fill));
|
||||
return `演奏中(${game._song?.title ?? ''}):还剩 ${left} 个应答音。领奏停了、轮到空拍,就该我出手了${fillNext ? ';有一段加花要连打' : ''}`;
|
||||
}
|
||||
case 'menu': return '暂停菜单:单击切选项,双击确认,上滑帮助,下滑恢复演奏';
|
||||
case 'end': return '结算播报中:听完单击再来一遍,右滑下一首,下滑回大厅';
|
||||
@@ -27,60 +35,61 @@ export function describe(game) {
|
||||
export async function session(ctx, mock) {
|
||||
const { game, audio } = ctx;
|
||||
const t = () => audio.ctx.currentTime;
|
||||
// transcribe 的 root 是 {}:补上布局让分区坐标生效(引擎坐标契约的游戏层语义)
|
||||
game.root = { getBoundingClientRect: () => ({ left: 0, top: 0, width: 300, height: 600 }) };
|
||||
const hit = async (zone) => {
|
||||
game.onGesture('tap', ZC[zone]);
|
||||
await mock.flush();
|
||||
};
|
||||
|
||||
// 开场:welcome 听完进教学
|
||||
await ctx.waitCond(() => game.phase === 'tut');
|
||||
ctx.status('教学开始了,先听领奏');
|
||||
// 开场:welcome → tut_map → tut_probe 三连旁白,逐句跳过
|
||||
await ctx.waitCond(() => game.phase === 'intro' && ctx.voiceBusy());
|
||||
ctx.status('开场旁白:我是鼓手,整个屏幕都是鼓。单击跳过');
|
||||
for (let i = 0; i < 3; i++) { await ctx.gesture('tap'); await ctx.waitVirtual(600); }
|
||||
await ctx.waitCond(() => game.phase === 'tut' && game._tutStage === 'probe');
|
||||
ctx.status('进入摸鼓认区:先四处摸摸,听到哪面鼓就报哪面鼓的名');
|
||||
ctx.note('背景音乐《启程》无鼓伴奏已在轻声循环——拍子网格的底,鼓由我来打');
|
||||
|
||||
// 教学 3 步:等领奏排程好,在应答窗口内跟打
|
||||
// T0 探索:六区全摸一遍(区名每区一次;重复摸只出声),再摸到 3 区进阶
|
||||
for (const zone of ['TL', 'TC', 'TR', 'BL', 'BC', 'BR']) await hit(zone);
|
||||
ctx.status('六面鼓摸遍了:左上踩镲、上中高嗵、右上吊镲、左下军鼓、下中底鼓、右下低嗵');
|
||||
await hit('TL'); // 重复摸:只出声不报名
|
||||
await hit('BR');
|
||||
ctx.status('又摸了两下重复的,只响不报名');
|
||||
await ctx.waitCond(() => game._tutStage === 'step' && game._cues.length > 0);
|
||||
ctx.status('摸到三个不同的地方,自动开始教学第一步');
|
||||
|
||||
// 教学 3 步:等领奏排程好,在应答窗口内跟敲
|
||||
//(注意:调度泵 interval 常驻,虚拟时钟永不"空闲"——禁止裸 waitIdle)
|
||||
for (let step = 0; step < 3; step++) {
|
||||
await ctx.waitCond(() => game.phase === 'tut' && game._cues.length > 0 && game._tutAttempts.length === 0);
|
||||
ctx.status(`教学第 ${step + 1}/3 句`);
|
||||
if (step === 1) {
|
||||
// round4 H5 剧本:这一句先整拍不出手——归因必须是「这拍没接」(gd_miss_none),
|
||||
// 不是「慢了半拍」;再连错一遍(整句不出手)听它放慢一些再带着打(G2)。
|
||||
// 注意 _tutEvaluate 判定即把 _phaseEnd 置 Infinity 防重入——等待一律挂
|
||||
// 「持久里程碑」条件(_tutFails/_tutSlowUsed),别挂阶段末时刻。
|
||||
await ctx.waitCond(() => game._tutFails === 1); // 失败①:没出手
|
||||
ctx.status('第一遍整拍没出手——听到的是「这拍没接。我停下的时候,就轮到你了」,不是「慢了半拍」');
|
||||
await ctx.waitCond(() => game._phaseEnd < 1e8 && t() < game._phaseEnd - 1); // 纠正链播完、重新领奏
|
||||
ctx.status('又重新领奏了一遍,还是没敢出手');
|
||||
await ctx.waitCond(() => game._tutSlowUsed === true); // 失败② → 触发降速重演
|
||||
await ctx.waitCond(() => game._phaseEnd < 1e8 && t() < game._phaseEnd - 1); // gd_slow 播完、慢速领奏已排
|
||||
ctx.status('听到「我放慢一些,跟着试试」——领奏降速 25% 重演,这回跟着慢拍打');
|
||||
for (const cue of game._cues) {
|
||||
const at = cue.expected - (cue.gesture === 'tap' ? -0.28 : 0.04);
|
||||
await ctx.waitCond(() => t() >= at || cue.judged);
|
||||
if (cue.judged) continue;
|
||||
await ctx.gesture(cue.gesture);
|
||||
}
|
||||
await ctx.waitCond(() => game._tutIdx > step || game.phase !== 'tut');
|
||||
continue;
|
||||
}
|
||||
await ctx.waitCond(() => game._tutIdx === step && game.phase === 'tut'
|
||||
&& game._cues.length > 0 && game._tutAttempts.length === 0);
|
||||
ctx.status(`教学第 ${step + 1}/3 步:听数拍和领奏,下一小节照着敲`);
|
||||
for (const cue of game._cues) {
|
||||
const at = cue.expected - (cue.gesture === 'tap' ? -0.28 : 0.04); // tap 补偿引擎 ~300ms 延迟
|
||||
await ctx.waitCond(() => t() >= at || cue.judged);
|
||||
await ctx.waitCond(() => t() >= cue.expected || cue.judged);
|
||||
if (cue.judged) continue;
|
||||
await ctx.gesture(cue.gesture);
|
||||
await hit(cue.zone);
|
||||
}
|
||||
await ctx.waitCond(() => game._tutIdx > step || game.phase !== 'tut');
|
||||
}
|
||||
await ctx.waitCond(() => game.phase === 'main');
|
||||
ctx.status('教学完毕,曲目一《启程》90 拍开始。领奏响之前先听到「现在跟着我打」');
|
||||
ctx.status('教学完毕,曲目一《启程》90 拍开始。先听四下底鼓数拍,然后是「现在跟着我打」');
|
||||
ctx.note('《启程》无鼓伴奏 BGM 起,四下底鼓数拍对齐小节');
|
||||
|
||||
// 曲中逛菜单:下滑呼出(模态暂停)→ 听选项 → 下滑恢复
|
||||
//(round3 G3:恢复先播「四拍之后,继续」,再跟 4 拍底鼓 count-in 进场)
|
||||
//(gd_count「四拍之后,继续」+ 4 拍底鼓 count-in 进场)
|
||||
await ctx.waitCond(() => t() >= game._bgmT0 + 1.5);
|
||||
await ctx.gesture('down');
|
||||
await ctx.waitCond(() => game.phase === 'menu');
|
||||
ctx.status('曲中暂停,进了菜单');
|
||||
ctx.note('BGM 在断点冻住——谱面、判定、音乐全部暂停');
|
||||
await ctx.waitVirtual(1100); // 菜单 1s 去抖独占期
|
||||
await ctx.gesture('tap'); // 切换选项(听到「回大厅」)
|
||||
await ctx.waitIdle();
|
||||
ctx.note('还是想打完这首,下滑回去');
|
||||
await ctx.gesture('down'); // 恢复
|
||||
await ctx.waitCond(() => game.phase === 'main');
|
||||
ctx.status('听到「四拍之后,继续」,跟着四下底鼓数拍进来了');
|
||||
ctx.note('BGM 从断点续播,节拍网格整体后移了一小节');
|
||||
|
||||
// 第一乐句故意整句不打:连 2 次 Miss → 向导 gd_miss
|
||||
const firstPhrase = game._cues.filter((c) => c.phrase === 0);
|
||||
@@ -88,16 +97,16 @@ export async function session(ctx, mock) {
|
||||
await ctx.waitCond(() => firstPhrase.every((c) => c.judged));
|
||||
ctx.status('第一句全漏了,听到「慢了半拍,下一小节跟上我就行」');
|
||||
|
||||
// 之后逐音跟打(tap 注入留出引擎 ~300ms 回调延迟)
|
||||
// 之后逐音跟打(含终曲加花)
|
||||
for (const cue of game._cues) {
|
||||
if (cue.judged) continue;
|
||||
const at = cue.expected - (cue.gesture === 'tap' ? -0.28 : 0.04);
|
||||
await ctx.waitCond(() => t() >= at || cue.judged);
|
||||
await ctx.waitCond(() => t() >= cue.expected || cue.judged);
|
||||
if (cue.judged) continue;
|
||||
await ctx.gesture(cue.gesture);
|
||||
await hit(cue.zone);
|
||||
}
|
||||
|
||||
await ctx.waitCond(() => game.phase === 'end');
|
||||
await ctx.waitIdle();
|
||||
ctx.status('曲子打完了,结算播报中');
|
||||
ctx.status('曲子打完了(加花收尾是一记吊镲),结算播报中');
|
||||
ctx.note('曲目 BGM 淡出,结算语音链播完会淡入大厅音乐');
|
||||
}
|
||||
|
||||
+36
-33
@@ -1,41 +1,45 @@
|
||||
# 节拍行者(RHYTHM)
|
||||
# 节拍行者(RHYTHM)· 鼓手模式
|
||||
|
||||
跟随领奏的节奏游戏,纯音频。系统奏一句,你在下一小节原样跟打——
|
||||
音要对,拍点也要对。建议戴耳机(有线优先,蓝牙延迟由 Good 窗口吸收)。
|
||||
屏幕分区打击乐节奏游戏,纯音频。整面屏幕是一面鼓:你在无鼓伴奏的乐队里
|
||||
当鼓手,系统(领奏)敲一小节,你在下一小节照着敲回来——鼓要选对,拍点也要对。
|
||||
建议戴耳机(有线优先,蓝牙延迟由 Good 窗口吸收)。
|
||||
|
||||
## 玩法
|
||||
|
||||
- 每个乐句 = **领奏 1 小节 + 应答 1 小节**:先听系统用打击乐器演奏
|
||||
2-4 个音的节奏型,然后在下一小节的相同拍点用相同手势跟打。
|
||||
- 判定:**Perfect ±150ms**(乐器音 + 亮音 accent)/ **Good ±350ms**(只播乐器音)/
|
||||
**Miss**(手势错或错过拍点:该音符不出声 + 一声闷镲 + 震动提醒)。
|
||||
判定时刻按真实触摸时刻算(引擎 immediateTap 按下即发 tap;滑动/双击按
|
||||
识别延迟补偿回触摸时刻)。Miss 不中断不掉血,歌照唱完。
|
||||
- 每个乐句 = **领奏 1 小节 + 应答 1 小节**:先听系统用打击乐演奏 2-4 音的
|
||||
节奏型(曲三 4-6 音),然后在下一小节的相同拍点敲相同的鼓。曲子开头有
|
||||
1 小节底鼓数拍。
|
||||
- **加花**:曲中/曲尾有八分音符密集加花(领奏一串快鼓),跟着连打,最后
|
||||
一记吊镲收束。
|
||||
- 判定:**Perfect ±150ms**(鼓音 + 亮音 accent)/ **Good ±350ms**(只播鼓音)/
|
||||
**Miss**(敲错鼓或错过拍点:闷镲 + 震动提醒)。点按按下即判(零识别延迟),
|
||||
Miss 不中断不掉血,歌照唱完。
|
||||
- 计分:Perfect=2 / Good=1 / Miss=0,评级 **S≥95% / A≥85% / B≥70% / C**。
|
||||
- 连击每 16 次有震动鼓励;乐句全 Perfect 有额外震动。
|
||||
- **3 首曲目解锁制**:《启程》90 BPM → 《追风》105 BPM(反拍)→ 《星爆》120 BPM
|
||||
(切分密集)。曲目 n 拿到 B 级以上解锁 n+1,解锁状态本地保存并同步大厅。
|
||||
|
||||
## 手势 → 乐器音
|
||||
## 屏幕分区 → 鼓组(3 列 × 2 行)
|
||||
|
||||
| 手势 | 乐器音 | 声像 |
|
||||
|---|---|---|
|
||||
| 单击 | 底鼓 kick | 中央低频 |
|
||||
| 双击 | 军鼓 snare | 中央偏上 |
|
||||
| 左滑 | 闭合镲 hat | 极左 |
|
||||
| 右滑 | 开放镲 hat | 极右 |
|
||||
| 上滑 | 高通 tom | 升调感 |
|
||||
| 下滑 | 低通 tom | 降调感 |
|
||||
| 长按 | —(非演奏手势) | Miss 反馈音 + 震动提醒 |
|
||||
| 区 | 位置 | 乐器 | 声像 |
|
||||
|---|---|---|---|
|
||||
| 踩镲 | 左上 | 闭合踩镲 | 左 |
|
||||
| 高嗵 | 上中 | 高音嗵 | 中 |
|
||||
| 吊镲 | 右上 | 吊镲(长尾) | 右 |
|
||||
| 军鼓 | 左下 | 军鼓 | 左 |
|
||||
| 底鼓 | 下中 | 底鼓(你的「家」) | 中 |
|
||||
| 低嗵 | 右下 | 低音嗵 | 右 |
|
||||
|
||||
你的每次出手都会实时奏响对应乐器音——跟对了,就能听到自己在演奏。
|
||||
**横着三列 = 左中右声像;竖着两行 = 上清亮下沉闷。** 六件乐器音色各异,
|
||||
声像 + 音高 + 音色三重线索,蒙眼可辨。键盘:数字 1-6 = 六区(1-3 上排,
|
||||
4-6 下排),空格 = 下中底鼓。
|
||||
|
||||
## 流程
|
||||
|
||||
- **首次进入**:一句欢迎后进入 5 步交互教学(单击 → 左右滑 → 上滑 → 下滑 →
|
||||
三音带双击),做错有纠正语音(早了/晚了/不是这个手势),同一步失败 2 次
|
||||
自动跳过;全程零通过时收尾播中性句(不夸"很好");教学中**下滑 = 跳过
|
||||
教学**(教到下滑步、临近下滑音符时除外——那时它是演奏)。
|
||||
- **首次进入**:欢迎 → 摸鼓认区(随便摸,听到哪面鼓就告诉你它在哪,摸到
|
||||
3 个不同的地方开始)→ 3 步跟打教学(底鼓家区 → 左右军鼓低嗵 → 上排
|
||||
踩镲高嗵吊镲),做错有纠正语音(早了/晚了/敲错鼓),同一步失败 2 次会
|
||||
放慢再教一遍,全程零惩罚;教学中**下滑 = 跳过教学**。
|
||||
- **选曲**(二周目起):左右滑切曲目(播曲目名/未解锁提示),单击开始,
|
||||
上滑听规则,下滑回大厅。
|
||||
- **结算**:播报评级(含准确率档位)与连击纪录;**单击 = 再来一遍(同曲),
|
||||
@@ -43,18 +47,17 @@
|
||||
|
||||
## 菜单
|
||||
|
||||
演奏中**下滑呼出**(模态暂停:谱面、判定、BGM 全部冻结,BGM 断点续播)→
|
||||
**单击循环选项**(继续演奏/回大厅)→ **双击确认** → **上滑 = 帮助**(规则 +
|
||||
设置引导)。菜单打开后 **1 秒去抖独占期**:期内不收任何手势(防演奏途中被
|
||||
弹菜单时,已在路上的手势直接穿透菜单误确认)。
|
||||
注意:下滑同时是"低通 tom"的演奏手势——**自下滑音符的领奏响起到判定
|
||||
死线之间**,下滑永远是演奏;音符刚被判定死线拖死的短窗内(识别延迟、手势
|
||||
还在路上)同样按演奏处理,绝不弹菜单;仅当没有悬置、临近或刚拖死的下滑
|
||||
音符时才呼出菜单。
|
||||
演奏中**下滑呼出**(模态暂停:谱面、判定、BGM 全部冻结,BGM 断点续播,
|
||||
恢复前先听 4 拍数拍)→ **单击循环选项**(继续演奏/回大厅)→ **双击确认**
|
||||
→ **上滑 = 帮助**(规则 + 设置引导)。菜单打开后 **1 秒去抖独占期**:期内
|
||||
不收任何手势(防已在路上的手势穿透菜单误确认)。
|
||||
|
||||
## 运行
|
||||
|
||||
```bash
|
||||
python3 -m http.server 8105 --directory /home/ubuntu/SoundGame/web
|
||||
python3 -m http.server 8105 --directory /home/xsl/AutoVideo/SoundGame/web
|
||||
# 浏览器打开 http://localhost:8105/rhythm/ ,建议戴耳机
|
||||
```
|
||||
|
||||
无头试玩:`tools/node/bin/node web/rhythm/test/playtest.mjs`
|
||||
(断言清单 docs/design/24-rhythm-drummer.md §11)。
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user