storyline/escape/simon 首批之后的推广收官:15 款全部按 26 号模式运行 (发言权/回执/三档提示/迷茫接管/手势阶梯/断点复述),旧「双击重听/格挡/ 菜单确认」等反模式口径全线废除,开场零规则念白(首句即动手)。 游戏层(12 款):rpg(格挡=前摇窗口内再单击)/ rhythm / space / radio / dungeon / abyss / detective / cipher / teahouse / keeper / bridge / hunt(联机切片): - 每款 game.js 接引擎六模块 + makeInputPipeline(doubleTapWindow:0); 四向玩法类 swipeCore + 长按帮助;双击是玩法的(space 护盾 / hunt 旋风斩)按手势阶梯阶段 3 分档恢复双击窗口(doubletapCore) - 每款 playtest 重写:玩法断言 + §6.2 乱点机器人(无声操作 / 被动 原句重复 / 打断失败 = 0);15 款 3900+ 断言全绿 - 12 份 docs/design/26-apply-*.md 施工单 + tools/voice_lines_26/ 台词清单 + voice-ids 文档同步 + 转写会话剧本按新口径重写 引擎层(web/engine/,6 处修复):pipeline 增 doubletapCore;novice teachLine 带 voice/ 前缀(此前教学句被静默丢弃);confusion.record 前置到新手门(锁定手势的乱点也计信号);InvalidHints 播完恢复解释前 状态(根治孤儿 BUSY);WaitPoint takeover(2) 台账去重;台账只在 speak 真正播出时登记。遗留引擎债登记 roadmap §3.1。 语音资产:台词表合并(新增 ~250 系统句 / 改 ~50 旧口径 / 删 ~110 废止句;合并器 tools/merge_voice_lines_26.py + 引用校验器 tools/validate_voice_refs.py),GPU 重录 1536 wav → 4204 ogg/m4a 落地,check_all_assets + validate_voice_refs 双绿;键盘变体同步扩表。 大厅/文档:15 款统一新交互口径(「新交互」徽标退役);README / HANDOVER / rpg README 旧口径修正;roadmap P0-P3 置完成。
99 lines
3.6 KiB
JavaScript
99 lines
3.6 KiB
JavaScript
/**
|
||
* 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 阶梯表)。 */
|
||
// 带 voice/ 层前缀,可直接进 floor.speak(2026-10 修复:此前返回裸 id 被 audio.play 按未知层丢弃)
|
||
const TEACH_IDS = {
|
||
up: 'voice/sys_unlock_up', // 「想知道自己在哪,就向上滑。」
|
||
down: 'voice/sys_unlock_down', // 「向下滑,可以打开菜单。」
|
||
doubletap: 'voice/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;
|