feat(playbook): 26 号交互手册首批落地——storyline/escape/simon 全面重构 + 引擎六模块
按 docs/design/26-audio-interaction-playbook.md 重构三款游戏(对白重写 + 双性别语音重生成 580 文件)。 引擎层(web/engine/,其余 12 款游戏零改动): - floor.js 发言权管理器:SPEAKING/YOUR_TURN/BUSY/ACTION 四态、交接提示音、 等待声(220/330Hz 嘟-嘟,20s 后放缓)、跨链音乐压低(audio.js holdDuck) - receipts.js 输入回执三件套(ok/invalid/busy,100ms 内,原则 4) - confusion.js 迷茫检测(乱点/无效/静默信号 → 三级接管,§4.5) - hints.js 等待点 + 三档提示(full→short→提示音,30s 台账跨点去重,原则 9) - novice.js 手势解锁阶梯(up/down/doubletap 跨局持久,§4.3) - pipeline.js 输入管道(回执收口、防连点 400ms、双击折算、explainOnce) 游戏层: - storyline R3:开场 71.7s 规则念白 → 3 秒内动手;双击=重播(反模式5)废除; 抉择点三档提示+主动重听;断点存档复述(§4.12) - escape R3:单击=检查+使用合并(双击用法废除);长按=我在哪(四向玩法裁定); 撞墙归因每方向一次(原则 7);迷茫 L2 缩小/L3 演示;坐标存档续玩 - simon R3:新手三音色,按实力渐进解锁(+up/down/dt);失败表替代机会账 (归因 2s 内→免费重听→放慢→结算,原则 15);播放期手势=busy 回执 测试:三款 playtest 重写(含 §6.2 乱点机器人:无声操作/被动重复/打断失败全 0, 首操作 ~2s);guide.test 改为 Guide 层单测+rhythm/space 段。全部通过 (38+33+22+72+19+35 断言)。设计单见 docs/design/26-apply-*.md。
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* novice.js — 新手手势模式与解锁阶梯(26 号文档 §4.3,P0 第 1 项 + P3 第 11 项)。
|
||||
*
|
||||
* 新手手势表(全部游戏共用,原则 6):
|
||||
* | 手势 | 唯一含义 | 新手期(阶段 0) |
|
||||
* |------------|------------------------|---------------------------------|
|
||||
* | tap | 继续 / 确定 / 听一下 | 开放 |
|
||||
* | left/right | 选择 / 移动 | 开放 |
|
||||
* | up | 我在哪、我该干嘛 | 阶段 1 解锁(完成第一个小目标) |
|
||||
* | down | 菜单 | 阶段 2 解锁(完成第一局) |
|
||||
* | doubletap | 重听 | 阶段 3 解锁(累计 3 局);未解锁按单击处理 |
|
||||
* | longpress | 帮助(四向玩法类游戏) | 仅「滑动是玩法」的游戏开放(§4.3 裁定) |
|
||||
*
|
||||
* 解锁进度跨局持久(localStorage),全局共享(手势表全项目一致,学会一次就算会)。
|
||||
* 游戏在对应时机调 onGoal() / onGameDone(),返回新解锁的手势(null=无新解锁),
|
||||
* 随后按原则 11 教学:先说一句(teachLine),马上用一次。
|
||||
*/
|
||||
const STORAGE_KEY = 'soundgame.gestures';
|
||||
|
||||
/** 解锁教学句 id(每游戏目录各存一份同文案资产,§4.3 阶梯表)。 */
|
||||
const TEACH_IDS = {
|
||||
up: 'sys_unlock_up', // 「想知道自己在哪,就向上滑。」
|
||||
down: 'sys_unlock_down', // 「向下滑,可以打开菜单。」
|
||||
doubletap: 'sys_unlock_dt', // 「没听清,就双击,再听一遍。」
|
||||
};
|
||||
|
||||
const UNLOCK_AT = { 1: 'up', 2: 'down', 3: 'doubletap' };
|
||||
|
||||
export class GestureLadder {
|
||||
constructor(key = STORAGE_KEY) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
_load() {
|
||||
try {
|
||||
const raw = localStorage.getItem(this.key);
|
||||
if (!raw) return { stage: 0, games: 0 };
|
||||
const o = JSON.parse(raw);
|
||||
return {
|
||||
stage: Math.max(0, Math.min(3, Number(o.stage) || 0)),
|
||||
games: Math.max(0, Number(o.games) || 0),
|
||||
};
|
||||
} catch (_) {
|
||||
return { stage: 0, games: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
_save(o) {
|
||||
try { localStorage.setItem(this.key, JSON.stringify(o)); } catch (_) { /* 隐私模式 */ }
|
||||
}
|
||||
|
||||
/** 当前阶段 0..3。 */
|
||||
stage() { return this._load().stage; }
|
||||
|
||||
/** 手势是否已解锁(tap/left/right 永远开放;longpress 由游戏按玩法裁定)。 */
|
||||
has(g) {
|
||||
if (g === 'tap' || g === 'left' || g === 'right') return true;
|
||||
if (g === 'up') return this.stage() >= 1;
|
||||
if (g === 'down') return this.stage() >= 2;
|
||||
if (g === 'doubletap') return this.stage() >= 3;
|
||||
return false; // longpress 等:走游戏自己的核心玩法裁定
|
||||
}
|
||||
|
||||
/** 完成第一个小目标 → 阶段 1。返回新解锁手势或 null。 */
|
||||
onGoal() { return this._unlock(1); }
|
||||
|
||||
/** 完成一局 → games+1;第 1 局解锁阶段 2,第 3 局解锁阶段 3。 */
|
||||
onGameDone() {
|
||||
const o = this._load();
|
||||
o.games += 1;
|
||||
this._save(o);
|
||||
let got = null;
|
||||
if (o.games >= 1) got = this._unlock(2) || got;
|
||||
if (o.games >= 3) got = this._unlock(3) || got;
|
||||
return got;
|
||||
}
|
||||
|
||||
_unlock(stage) {
|
||||
const o = this._load();
|
||||
if (o.stage >= stage) return null;
|
||||
o.stage = stage;
|
||||
this._save(o);
|
||||
return UNLOCK_AT[stage];
|
||||
}
|
||||
|
||||
/** 老玩家在大厅一键全部解锁。 */
|
||||
unlockAll() {
|
||||
const o = this._load();
|
||||
o.stage = 3;
|
||||
this._save(o);
|
||||
}
|
||||
|
||||
/** 解锁教学句 voice id。 */
|
||||
teachLine(g) { return TEACH_IDS[g] || null; }
|
||||
}
|
||||
|
||||
export default GestureLadder;
|
||||
Reference in New Issue
Block a user