第二批反馈:引擎改造(限幅/SoundTouch不变调变速/长按快进/跳句/iOS闪光降级) + 6游戏状态BGM与试玩测试全绿
This commit is contained in:
+91
-35
@@ -6,10 +6,16 @@
|
||||
*
|
||||
* 手势映射(偏离全局默认,引导中讲清):
|
||||
* - 四向滑动:在相邻房间间移动,撞墙播闷响 + ERROR 震动 + wall 语音(每次都播)
|
||||
* - 单击:重播当前房间环境音 + 检查描述(储物间拾钥匙)
|
||||
* - 单击:重播当前房间环境音 + 检查描述(储物间拾钥匙);
|
||||
* voice 播放中单击 = 跳过本句(audio.skipVoice(),链播继续,12 号文档)。
|
||||
* 不可跳:引导 G0-G7(引导期单击是答题)、关键获得(found_key/
|
||||
* unlock_cabinet/fix_box/escape 结算链)、L3 带路(stuck_*_3 + nav_*)
|
||||
* - 双击:使用物品(钥匙开柜 / 保险丝修电箱 / 通电后开大门);
|
||||
* 无可用目标时播报背包 + L2 级明示提示
|
||||
* - 引导阶段下滑:跳过引导;ENDING 下滑:菜单
|
||||
* - 引导阶段下滑:跳过引导(G4 除外——G4 的答案就是下滑);ENDING 下滑:菜单
|
||||
*
|
||||
* 状态 BGM(10 号文档):引导/探索 = bgm/drone;菜单 = bgm/menu(400ms);
|
||||
* 逃脱结算 = escape 语音链播完淡入 bgm/menu(volume 0.5,1500ms)。
|
||||
*
|
||||
* 卡关检测:进展事件(拾钥匙/开柜/修电箱/首到关键房间/逃脱)重置计时;
|
||||
* 60s 无进展 L1 暗示(stuck_ask + stuck_*_1)、120s L2 明示(stuck_*_2)、
|
||||
@@ -86,6 +92,8 @@ export class EscapeGame extends Game {
|
||||
this.powerOn = false;
|
||||
this.visited = new Set(); // 首到关键房间记录(进展事件 + room_* 描述)
|
||||
this.startElapsedMs = 0;
|
||||
// 单击跳过:当前 voice 句是否可跳(每次 voice 播放时更新,见 _playVoice)
|
||||
this._voiceSkippable = false;
|
||||
// 引导状态
|
||||
this.guideStep = 0; // 0..7 对应 G0..G7
|
||||
this.guideSwipes = 0; // G2 撞墙计数
|
||||
@@ -98,13 +106,14 @@ export class EscapeGame extends Game {
|
||||
// 菜单
|
||||
this._menuReturn = Phase.ENDING;
|
||||
this._menuIdx = 0;
|
||||
this._endingBgmStarted = false; // 结算 pad 是否已起(_escape 幂等守护)
|
||||
}
|
||||
|
||||
// ---------- Game ----------
|
||||
|
||||
start() {
|
||||
this.audio.prewarm(PREWARM_SFX);
|
||||
this.audio.prewarm(['voice/g0_intro', 'voice/g1_move', 'voice/reopen_prompt', 'voice/start_room']);
|
||||
this.audio.prewarm(['voice/g0_intro', 'voice/g1_move', 'voice/skip_hint', 'voice/reopen_prompt', 'voice/start_room', 'bgm/menu']);
|
||||
this._session++;
|
||||
this._clearTimers();
|
||||
this._startStuckTimer();
|
||||
@@ -113,7 +122,7 @@ export class EscapeGame extends Game {
|
||||
if (guided) {
|
||||
// 二周目:上滑重听引导 / 单击直接开始
|
||||
this.phase = Phase.REOPEN;
|
||||
this.audio.play('voice/reopen_prompt');
|
||||
this._playVoice('voice/reopen_prompt', { skippable: false }); // 抉择前提示必须听完
|
||||
} else {
|
||||
this._startGuide();
|
||||
}
|
||||
@@ -157,7 +166,7 @@ export class EscapeGame extends Game {
|
||||
this._resetState();
|
||||
this.phase = Phase.GUIDE;
|
||||
this.guideStep = 0;
|
||||
this._speak(['voice/g0_intro']);
|
||||
this._speak(['voice/g0_intro'], null, { skippable: false });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,17 +188,20 @@ export class EscapeGame extends Game {
|
||||
this._timer(() => {
|
||||
if (this._guideBusy && this.guideStep === fromStep) done();
|
||||
}, 20000); // 看门狗
|
||||
if (ids && ids.length) this._speak(ids, done);
|
||||
// 引导句不可单击跳过(12 号文档:引导句即操作指令)
|
||||
if (ids && ids.length) this._speak(ids, done, { skippable: false });
|
||||
else done();
|
||||
}
|
||||
|
||||
_onGuideGesture(g) {
|
||||
if (g === G.DOWN) { this._skipGuide(); return; } // 任何步骤可下滑跳过
|
||||
// 任何步骤可下滑跳过——G4 除外:该步的正确答案就是下滑(01 号文档"G4=下滑"
|
||||
// 与"任何步骤下滑跳过"冲突,以 13 号试玩标准为准:G4 下滑 = 作答,不可跳过)
|
||||
if (g === G.DOWN && this.guideStep !== 4) { this._skipGuide(); return; }
|
||||
if (this._guideBusy) return; // 推进语音播放中,忽略
|
||||
switch (this.guideStep) {
|
||||
case 0: // G0:单击继续
|
||||
if (g === G.TAP) this._advance(1, ['voice/g1_move']);
|
||||
else if (this._isSwipe(g)) this.audio.play('voice/guide_offpath'); // "先跟我走"
|
||||
if (g === G.TAP) this._advance(1, ['voice/g1_move', 'voice/skip_hint']); // skip_hint 首局插播一次
|
||||
else if (this._isSwipe(g)) this._playVoice('voice/guide_offpath', { skippable: false }); // "先跟我走"
|
||||
break;
|
||||
case 1: // G1:任意滑动,走一小步
|
||||
if (this._isSwipe(g)) {
|
||||
@@ -203,7 +215,7 @@ export class EscapeGame extends Game {
|
||||
vibrate('error');
|
||||
this.guideSwipes++;
|
||||
if (this.guideSwipes >= 2) this._advance(3, ['voice/wall', 'voice/g3_left']);
|
||||
else this.audio.play('voice/wall');
|
||||
else this._playVoice('voice/wall', { skippable: false });
|
||||
} else this._guideWrong(null);
|
||||
break;
|
||||
case 3: // G3:向左滑 → (0,1)
|
||||
@@ -253,7 +265,7 @@ export class EscapeGame extends Game {
|
||||
this._clearTimers();
|
||||
this.audio.stopVoice();
|
||||
try { localStorage.setItem(GUIDE_DONE_KEY, '1'); } catch (_) { /* 隐私模式 */ }
|
||||
this._speak(['voice/guide_skip'], () => this._enterPlay(true));
|
||||
this._speak(['voice/guide_skip'], () => this._enterPlay(true), { skippable: false });
|
||||
// 语音缺失兜底
|
||||
this._timer(() => { if (this.phase === Phase.GUIDE) this._enterPlay(true); }, 5000);
|
||||
}
|
||||
@@ -267,12 +279,12 @@ export class EscapeGame extends Game {
|
||||
this.guideErrs++;
|
||||
if (this.guideErrs >= 3) {
|
||||
vibrate('warning');
|
||||
this.audio.play(expected ? `voice/guide_blunt_${expected}` : 'voice/guide_wrong_gesture');
|
||||
this._playVoice(expected ? `voice/guide_blunt_${expected}` : 'voice/guide_wrong_gesture', { skippable: false });
|
||||
this.guideErrs = 0;
|
||||
return;
|
||||
}
|
||||
vibrate('error');
|
||||
this.audio.play(expected ? `voice/guide_wrong_${expected}` : 'voice/guide_wrong_gesture');
|
||||
this._playVoice(expected ? `voice/guide_wrong_${expected}` : 'voice/guide_wrong_gesture', { skippable: false });
|
||||
}
|
||||
|
||||
// ---------- 进入 / 重开 PLAY ----------
|
||||
@@ -284,7 +296,7 @@ export class EscapeGame extends Game {
|
||||
this.phase = Phase.PLAY;
|
||||
this.startElapsedMs = Date.now();
|
||||
this._resetStuck();
|
||||
if (announce) this.audio.play('voice/start_room');
|
||||
if (announce) this._playVoice('voice/start_room');
|
||||
this._playAmbient();
|
||||
}
|
||||
|
||||
@@ -297,7 +309,7 @@ export class EscapeGame extends Game {
|
||||
this.startElapsedMs = Date.now();
|
||||
this._resetStuck();
|
||||
this.audio.play('bgm/drone');
|
||||
this.audio.play('voice/replay');
|
||||
this._playVoice('voice/replay');
|
||||
this._playAmbient();
|
||||
}
|
||||
|
||||
@@ -309,7 +321,16 @@ export class EscapeGame extends Game {
|
||||
case G.DOWN: this._move(0, 1); break;
|
||||
case G.LEFT: this._move(-1, 0); break;
|
||||
case G.RIGHT: this._move(1, 0); break;
|
||||
case G.TAP: this._inspect(); break;
|
||||
case G.TAP:
|
||||
// voice 播放中:可跳句单击 = 跳过本句(链播继续);不可跳句(关键获得/
|
||||
// L3 带路)单击 = 忽略——若照旧触发检查,800ms 后的描述会顶掉该句。
|
||||
// 无 voice 时单击 = 原检查语义。
|
||||
if (this.audio._voice) {
|
||||
if (this._voiceSkippable) this.audio.skipVoice();
|
||||
break;
|
||||
}
|
||||
this._inspect();
|
||||
break;
|
||||
case G.DOUBLE_TAP: this._useItem(); break;
|
||||
}
|
||||
}
|
||||
@@ -320,7 +341,7 @@ export class EscapeGame extends Game {
|
||||
if (nx < 0 || nx > 2 || ny < 0 || ny > 2) {
|
||||
this.audio.play('sfx/wall');
|
||||
vibrate('error');
|
||||
this.audio.play('voice/wall'); // 每次撞墙都播(取消一次性限制)
|
||||
this._playVoice('voice/wall'); // 每次撞墙都播(取消一次性限制)
|
||||
return;
|
||||
}
|
||||
this.x = nx;
|
||||
@@ -332,7 +353,7 @@ export class EscapeGame extends Game {
|
||||
if (KEY_ROOMS[key] && !this.visited.has(key)) {
|
||||
this.visited.add(key);
|
||||
this._progressEvent();
|
||||
this.audio.play(KEY_ROOMS[key].room);
|
||||
this._playVoice(KEY_ROOMS[key].room);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,8 +375,9 @@ export class EscapeGame extends Game {
|
||||
_inspect() {
|
||||
this._playAmbient();
|
||||
const s = this._session;
|
||||
const describe = (id) => this._timer(() => {
|
||||
if (s === this._session) this.audio.play(id);
|
||||
// skippable:found_key 是关键获得(不可跳),其余检查描述可跳
|
||||
const describe = (id, skippable = true) => this._timer(() => {
|
||||
if (s === this._session) this._playVoice(id, { skippable });
|
||||
}, 800); // 先让环境音进耳朵再播描述
|
||||
|
||||
const { x, y } = this;
|
||||
@@ -365,7 +387,7 @@ export class EscapeGame extends Game {
|
||||
this._progressEvent();
|
||||
this.audio.play('sfx/pickup');
|
||||
vibrate('pickup');
|
||||
describe('voice/found_key');
|
||||
describe('voice/found_key', false);
|
||||
} else {
|
||||
describe('voice/empty_2');
|
||||
}
|
||||
@@ -397,7 +419,7 @@ export class EscapeGame extends Game {
|
||||
this.audio.play('sfx/unlock');
|
||||
this.audio.play('sfx/door_creak', { volume: 0.7 });
|
||||
vibrate('success');
|
||||
this.audio.play('voice/unlock_cabinet');
|
||||
this._playVoice('voice/unlock_cabinet', { skippable: false }); // 关键获得不可跳
|
||||
} else if (x === 0 && y === 0 && this.hasFuse && !this.powerOn) {
|
||||
// 保险丝修电箱 -> 恢复供电(保险丝消耗)
|
||||
this.powerOn = true;
|
||||
@@ -406,7 +428,7 @@ export class EscapeGame extends Game {
|
||||
this.audio.play('sfx/spark');
|
||||
this.audio.play('sfx/power_on', { volume: 0.8 });
|
||||
vibrate('success');
|
||||
this.audio.play('voice/fix_box');
|
||||
this._playVoice('voice/fix_box', { skippable: false }); // 关键获得不可跳
|
||||
} else if (x === 2 && y === 2 && this.powerOn) {
|
||||
// 通电后开大门 -> 逃脱
|
||||
this._escape();
|
||||
@@ -459,12 +481,12 @@ export class EscapeGame extends Game {
|
||||
const idleMs = Date.now() - this._lastProgressAt;
|
||||
const stage = this._stage();
|
||||
if (idleMs >= 180000) {
|
||||
// L3 带路:首次或每 60s 重复,按当前坐标重算路线
|
||||
// L3 带路:首次或每 60s 重复,按当前坐标重算路线(带路语音不可单击跳过)
|
||||
if (!this._stuckShown.has('L3') || Date.now() - this._lastL3At >= 60000) {
|
||||
this._stuckShown.add('L3');
|
||||
this._lastL3At = Date.now();
|
||||
vibrate('warning');
|
||||
this._speak([`voice/stuck_${stage}_3`, ...this._navVoices(STAGES[stage].target)]);
|
||||
this._speak([`voice/stuck_${stage}_3`, ...this._navVoices(STAGES[stage].target)], null, { skippable: false });
|
||||
}
|
||||
} else if (idleMs >= 120000 && !this._stuckShown.has('L2')) {
|
||||
this._stuckShown.add('L2');
|
||||
@@ -493,22 +515,37 @@ export class EscapeGame extends Game {
|
||||
this._menuReturn = returnPhase;
|
||||
this._menuIdx = 0;
|
||||
this.phase = Phase.MENU;
|
||||
this._speak(['voice/menu_open', 'voice/menu_restart']);
|
||||
// 状态 BGM:菜单 = bgm/menu(10 号文档,400ms crossfade;与结算 pad 同音量 0.5,
|
||||
// 同一首引擎不重启,关菜单回 ENDING 时无缝保持)
|
||||
this.audio.play('bgm/menu', { volume: 0.5, fadeMs: 400 });
|
||||
this._speak(['voice/menu_open', 'voice/menu_restart'], null, { skippable: false });
|
||||
}
|
||||
|
||||
_onMenuGesture(g) {
|
||||
const items = ['voice/menu_restart', 'voice/menu_home', 'voice/menu_back'];
|
||||
if (g === G.TAP) {
|
||||
this._menuIdx = (this._menuIdx + 1) % items.length;
|
||||
this.audio.play(items[this._menuIdx]);
|
||||
this._playVoice(items[this._menuIdx], { skippable: false });
|
||||
} else if (g === G.UP) {
|
||||
this.audio.play('voice/help_ops'); // 操作说明
|
||||
this._playVoice('voice/help_ops', { skippable: false }); // 操作说明
|
||||
} else if (g === G.DOWN) {
|
||||
this._closeMenu(); // 下滑 = 关闭菜单(13 号文档 §7.5 菜单约定)
|
||||
} else if (g === G.DOUBLE_TAP) {
|
||||
const sel = this._menuIdx;
|
||||
this.phase = this._menuReturn;
|
||||
if (sel === 0) this._restart();
|
||||
if (sel === 0) { this.phase = this._menuReturn; this._restart(); } // 重开:_restart 内切回 bgm/drone
|
||||
else if (sel === 1) this._goHome();
|
||||
// sel === 2 返回游戏:phase 已恢复
|
||||
else this._closeMenu(); // "返回"
|
||||
}
|
||||
}
|
||||
|
||||
/** 关闭菜单回到来源状态,并按来源恢复状态 BGM(ENDING 保持 menu pad)。 */
|
||||
_closeMenu() {
|
||||
this.phase = this._menuReturn;
|
||||
if (this._menuReturn === Phase.ENDING) {
|
||||
// 返回结算:结算态 BGM 即 bgm/menu(同 id 引擎不重启,无缝保持)
|
||||
this.audio.play('bgm/menu', { volume: 0.5 });
|
||||
} else {
|
||||
this.audio.play('bgm/drone'); // 关闭菜单切回主题曲
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,9 +563,21 @@ export class EscapeGame extends Game {
|
||||
this.audio.play('sfx/gate_open');
|
||||
this.audio.play('sfx/win_chime', { volume: 0.8 });
|
||||
this.audio.stopBgm(2500);
|
||||
this._endingBgmStarted = false;
|
||||
const totalSec = Math.floor((Date.now() - this.startElapsedMs) / 1000);
|
||||
this._saveEscape(totalSec);
|
||||
this._speak(['voice/escape', this._timeBandVoice(totalSec), 'voice/again']);
|
||||
// 结算语音链(关键获得,不可跳);播完淡入 menu pad(10 号文档:volume 0.5,1500ms)
|
||||
this._speak(['voice/escape', this._timeBandVoice(totalSec), 'voice/again'],
|
||||
() => this._startEndingBgm(), { skippable: false });
|
||||
// 语音资产缺失时链不推进,30s 兜底起结算 pad(escape+time_band+again 正常 ~10s 内播完)
|
||||
this._timer(() => this._startEndingBgm(), 30000);
|
||||
}
|
||||
|
||||
/** 逃脱结算 BGM:drone 淡出后淡入 bgm/menu(幂等,仅在仍是结算态时生效)。 */
|
||||
_startEndingBgm() {
|
||||
if (this._endingBgmStarted || this.phase !== Phase.ENDING) return;
|
||||
this._endingBgmStarted = true;
|
||||
this.audio.play('bgm/menu', { volume: 0.5, fadeMs: 1500 });
|
||||
}
|
||||
|
||||
/** 用时播报:4 段整句(time_band1..4),替代数字拼接。 */
|
||||
@@ -589,14 +638,21 @@ export class EscapeGame extends Game {
|
||||
return g === G.UP || g === G.DOWN || g === G.LEFT || g === G.RIGHT;
|
||||
}
|
||||
|
||||
/** 对白包装:登记本句是否允许单击跳过(12 号文档裁定表,默认可跳)。 */
|
||||
_playVoice(id, { skippable = true, onComplete } = {}) {
|
||||
this._voiceSkippable = skippable;
|
||||
this.audio.play(id, { onComplete });
|
||||
}
|
||||
|
||||
/** 对白排队依次播放(onComplete 链式触发;被打断即作废整条链)。 */
|
||||
_speak(ids, onDone) {
|
||||
_speak(ids, onDone, { skippable = true } = {}) {
|
||||
if (ids.length === 0) { if (onDone) onDone(); return; }
|
||||
const s = this._session;
|
||||
this.audio.play(ids[0], {
|
||||
this._playVoice(ids[0], {
|
||||
skippable,
|
||||
onComplete: () => {
|
||||
if (s !== this._session) return;
|
||||
if (ids.length > 1) this._speak(ids.slice(1), onDone);
|
||||
if (ids.length > 1) this._speak(ids.slice(1), onDone, { skippable });
|
||||
else if (onDone) onDone();
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user