fix(escape): 暗室状态引导方案落地(25号策划文档 B1-B8 → F1-F9 实施)
策划轮:25-escape-map-flow.md as-built地图/玩家流程/信息账本+盲区清单(S1-S7)+方案B1-B8 实施(玩法零改动,只加状态引导层): - F1 撞墙方位句「左边是墙」(wall_dir_*×4,同向10s限频) - F2 目标句并入方位「铁柜,在右面的房间」(gd_*_dir×3) - F3 无进展150s活跃计时器+gd_prog_1..3进度重述(只重述不泄题,专治活跃迷路玩家——修卡关计时从「无进展」退化为「无操作」的既定偏差) - F4 双击兜底直通L2降级L1(不再白送答案) - F5 空房首访描述(4空房可区分) - F7 busy窗口手势缓存补判(原静默吞手势) - F8 结算链单击守卫(time_band/again完整到达) - F9 跳过引导补播目标句 - F6 空房方位特征句按25号裁定留档后续轮 新增语音10句×2性别(flock串行QC无离群,ASR逐字正确) 验证:escape playtest 235/235(+85新断言)+ guide 132/132 + 终检全绿 + 14游戏回归零回退 live复跑10.4分钟通关:零截断,S1-S7盲区逐条核销 档案 research/escape-state/round1-2;01号文档追加修订记录R1
This commit is contained in:
+163
-40
@@ -8,14 +8,17 @@
|
||||
*
|
||||
* 手势映射(偏离全局默认,引导中讲清):
|
||||
* - 四向滑动:在相邻房间间移动,撞墙播闷响 + ERROR 震动 + wall 语音(每次都播;
|
||||
* PLAY 阶段只播「那边是墙」,教学腔方位句 guide_blunt_* 仅限引导期)
|
||||
* PLAY 阶段 wall 播完接播陈述式方位句 wall_dir_<dir>「左边是墙。」(escape-state
|
||||
* round2 B1,同向 10s 限频,被打断自然作废);教学腔 guide_blunt_* 仅限引导期)
|
||||
* - 单击:重播当前房间环境音 + 检查描述(储物间拾钥匙);
|
||||
* voice 播放中单击 = 跳过本句(audio.skipVoice(),链播继续,12 号文档)
|
||||
* 并补发检查——环境音即时重播、描述进待播队列等链播播完(不顶句)。
|
||||
* 不可跳:引导 G0-G7(引导期单击是答题)、关键获得(found_key/
|
||||
* unlock_cabinet/fix_box/escape 结算链)、L3 带路(stuck_*_3 + nav_*)
|
||||
* - 双击:使用物品(钥匙开柜 / 保险丝修电箱 / 通电后开大门);
|
||||
* 无可用目标时播报背包 + L2 级明示提示
|
||||
* 无可用目标时播报背包/use_fail + L1 级意象暗示(escape-state round2 B4:
|
||||
* 原直通 L2 明示把答案白送给没卡关的玩家,降级为 L1;答案仍锁在计时器
|
||||
* L2/L3 与进展链里)
|
||||
* - 引导阶段下滑:跳过引导(仅限答案不含滑动的步骤 G0/G3/G5/G6;G1/G2 任意
|
||||
* 滑动即答案、G4 的答案就是下滑——这些步骤下滑 = 作答,不可跳过);
|
||||
* ENDING 下滑:菜单
|
||||
@@ -31,12 +34,23 @@
|
||||
* 修电箱/首到关键房间/逃脱)清空已播级别,下段静默期从 L1 重新分级。
|
||||
* 提示到点只进待播队列:等当前语音自然播完再播、绝不顶掉进行中的语音;
|
||||
* 待播期间玩家任何操作即作废该提示(未播级别不计入已播)。
|
||||
* escape-state round2 新增平行通道(B3):无进展 150s 且期间 ≥5 次有效手势
|
||||
* (活跃但零进展,挂机仍走 60s L1 体系)→ 播不泄题的进度重述 gd_prog_1/2/3
|
||||
* (stage=cabinet/box/gate,key 阶段不走本通道),每进展段限 1 次,
|
||||
* 队列语义与卡关提示同款(不顶句、待播期间有操作即作废)。
|
||||
*
|
||||
* round4 H1(docs/design/14-guide-layer.md「round4 v1.3」):阶段目标句
|
||||
* (found_key / gd_next_fuse / fix_box)播完后,按房间图计算目标房间相对当前
|
||||
* 房间的真实方位,接播 gd_room_l/r/u/d 之一(游戏反馈层全档位;目标在同一
|
||||
* 房间不播;方向 = escape 移动坐标系,up/down/left/right 滑动方向即方位)。
|
||||
* 引导期 g7_goal 不接(引导本身逐句带路,且引导链被 playtest 断言冻结)。
|
||||
* 房间的真实方位接播方位句;escape-state round2 B2 把三处固定接线点升级为
|
||||
* "目标名+方位"一体句(gd_cabinet_dir/gd_box_dir/gd_gate_dir),异常路径方位
|
||||
* 与枚举不符时回落 gd_room_l/r/u/d(原句保留)。引导期 g7_goal 不接(引导链
|
||||
* 被 playtest 断言冻结);但跳过引导/REOPEN 直进路径补播 g7_goal(round2 B8:
|
||||
* 任何路径进 PLAY 都至少听过一次目标句)。
|
||||
*
|
||||
* escape-state round2 逻辑修复:B6 引导 busy 窗口缓存首个非跳过分手势、步骤
|
||||
* 解锁后按新步骤补判(合法=作答/不合法=答错路径,缓存手势不触发下滑跳过);
|
||||
* B7 结算链播完前 ENDING 单击不重开(_settleBusy 守卫,链完/30s 兜底解除);
|
||||
* B5a 空房首访补播 empty_* 一次性听觉锚(不算进展事件)。
|
||||
*
|
||||
* 语音 id 权威清单:docs/audio/round2-voice-ids.md(已删除的 n0–n9 数字拼接链、
|
||||
* hint_ 四级提示、intro/tutorial 旧开场不再引用)。震动分工:滑动/点按的识别反馈由 gesture.js 内置,本层只发语义震动
|
||||
@@ -80,6 +94,16 @@ const KEY_ROOMS = {
|
||||
// 空房环境音轮换(弱化版风声/嗡鸣)
|
||||
const EMPTY_AMB = { '1,0': 'sfx/amb_wind', '0,1': 'sfx/amb_hum', '1,2': 'sfx/amb_wind', '2,0': 'sfx/amb_hum' };
|
||||
|
||||
// 空房首访描述(escape-state round2 B5a:复用现有 empty_*,进入时一次性播;
|
||||
// 与 _inspect 单击检查映射一致——两间"远处水滴"空房 (1,2)/(2,0) 对称分布,
|
||||
// 共用 empty_3 也是合法的记忆点)
|
||||
const EMPTY_FIRST_VOICE = {
|
||||
'1,0': 'voice/empty_1', '0,1': 'voice/empty_2', '1,2': 'voice/empty_3', '2,0': 'voice/empty_3',
|
||||
};
|
||||
|
||||
// B3 无进展进度重述:stage → gd_prog id(key 阶段不走本通道)
|
||||
const GD_PROG = { cabinet: 'voice/gd_prog_1', box: 'voice/gd_prog_2', gate: 'voice/gd_prog_3' };
|
||||
|
||||
const PREWARM_SFX = [
|
||||
'sfx/step', 'sfx/wall', 'sfx/pickup', 'sfx/unlock', 'sfx/door_creak',
|
||||
'sfx/spark', 'sfx/power_on', 'sfx/gate_open', 'sfx/win_chime',
|
||||
@@ -110,6 +134,10 @@ export class EscapeGame extends Game {
|
||||
this.cabinetOpen = false;
|
||||
this.powerOn = false;
|
||||
this.visited = new Set(); // 首到关键房间记录(进展事件 + room_* 描述)
|
||||
this.emptyVisited = new Set(); // 首到空房记录(B5a 首访描述,不算进展事件)
|
||||
this._freshEmpty = null; // 首访句刚播过的空房 key(本房间内单击不重复同一句)
|
||||
this._wallDirAt = {}; // B1 撞墙方位句同向 10s 限频:dir -> 上次播出时刻
|
||||
this._settleBusy = false; // B7 结算链守卫:播完前 ENDING 单击不重开
|
||||
this.startElapsedMs = 0;
|
||||
// 单击跳过:当前 voice 句是否可跳(每次 voice 播放时更新,见 _playVoice)
|
||||
this._voiceSkippable = false;
|
||||
@@ -118,10 +146,15 @@ export class EscapeGame extends Game {
|
||||
this.guideSwipes = 0; // G2 撞墙计数
|
||||
this.guideErrs = 0; // 当前步骤连续错误数
|
||||
this._guideBusy = false; // 步骤推进语音播放中(忽略非跳过分手势)
|
||||
this._guidePending = null; // B6:busy 窗口缓存的首个非跳过分手势(仅 1 个、不跨步存活)
|
||||
// 卡关检测(按"无操作时长"计时,见 _checkStuck)
|
||||
this._lastActiveAt = Date.now();
|
||||
this._stuckShown = new Set(); // 当前进展段内已自动播过的级别 'L1'|'L2'|'L3'
|
||||
this._lastL3At = 0;
|
||||
// B3 无进展计时(与无操作计时并存互不复位)
|
||||
this._lastProgressAt = Date.now(); // 上一次进展事件时刻
|
||||
this._gesturesSinceProgress = 0; // 进展段内有效手势数(≥5 才触发 gd_prog)
|
||||
this._progShown = new Set(); // 已播过 gd_prog 的阶段(每进展段限 1 次)
|
||||
this._pendingVoice = []; // 待播语音队列:排队等当前语音播完,绝不顶句
|
||||
this._playSeq = 0; // voice 播放请求序号(onComplete 内判断是否链播新句)
|
||||
// 菜单
|
||||
@@ -160,12 +193,14 @@ export class EscapeGame extends Game {
|
||||
if (this.phase === Phase.PLAY) {
|
||||
// 卡关提示按"无操作时长"计时:任何有效手势重置计时,并作废待播提示
|
||||
this._lastActiveAt = Date.now();
|
||||
this._gesturesSinceProgress++; // B3:活跃手势计数(无进展 150s 通道的闸门)
|
||||
this._dropPendingStuck();
|
||||
}
|
||||
switch (this.phase) {
|
||||
case Phase.REOPEN:
|
||||
if (g === G.UP) { this.audio.stopVoice(); this._startGuide(); }
|
||||
else if (g === G.TAP) { this.audio.stopVoice(); this._enterPlay(true); }
|
||||
// B8:REOPEN 单击直进路径在 start_room 后接播目标句 g7_goal
|
||||
else if (g === G.TAP) { this.audio.stopVoice(); this._enterPlay(true, true); }
|
||||
break;
|
||||
case Phase.GUIDE:
|
||||
this._onGuideGesture(g);
|
||||
@@ -174,7 +209,9 @@ export class EscapeGame extends Game {
|
||||
this._onPlayGesture(g);
|
||||
break;
|
||||
case Phase.ENDING:
|
||||
if (g === G.TAP) this._restart();
|
||||
// B7(escape-state round2):结算链播完前单击不立即重开——time_band/again
|
||||
// 的完整到达权(live round1 S7:习惯性单击把结算链截断,两句被永久吞掉)
|
||||
if (g === G.TAP) { if (!this._settleBusy) this._restart(); }
|
||||
else if (g === G.DOWN) this._openMenu(Phase.ENDING);
|
||||
break;
|
||||
case Phase.MENU:
|
||||
@@ -227,6 +264,13 @@ export class EscapeGame extends Game {
|
||||
this._guideBusy = false;
|
||||
this.guide.turn({}); // turn cue:引导步骤解锁输入(W2 播完即静默,向导补标记)
|
||||
if (onDone) onDone();
|
||||
// B6(escape-state round2):busy 窗口缓存的手势在步骤解锁后按新步骤补判
|
||||
// (onDone 若已离开 GUIDE——G6 链尾进 PLAY——缓存直接作废,不跨阶段存活)
|
||||
if (this._guidePending !== null) {
|
||||
const p = this._guidePending;
|
||||
this._guidePending = null;
|
||||
if (this.phase === Phase.GUIDE) this._onGuideGesture(p, { fromCache: true });
|
||||
}
|
||||
};
|
||||
this._timer(() => {
|
||||
if (this._guideBusy && this.guideStep === fromStep) done();
|
||||
@@ -236,16 +280,24 @@ export class EscapeGame extends Game {
|
||||
else done();
|
||||
}
|
||||
|
||||
_onGuideGesture(g) {
|
||||
_onGuideGesture(g, { fromCache = false } = {}) {
|
||||
// 下滑跳过仅限"答案不含滑动"的步骤(G0/G3/G5/G6):G1/G2 的正确答案是
|
||||
// 任意滑动、G4 的答案就是下滑——这些步骤下滑 = 作答,不可跳过(13 号文档
|
||||
// 裁定;round2-live:g1_move 明说"朝任意方向滑一下",玩家下滑却被保送
|
||||
// 自由探索,教学链就此断裂)
|
||||
if (g === G.DOWN && this.guideStep !== 1 && this.guideStep !== 2 && this.guideStep !== 4) {
|
||||
// 自由探索,教学链就此断裂)。B6:busy 缓存补判的手势不再触发下滑跳过
|
||||
// (live round1 S6:答案在 busy 窗口被吞后,玩家紧接着的下滑被当成跳过引导)
|
||||
if (!fromCache && g === G.DOWN && this.guideStep !== 1 && this.guideStep !== 2 && this.guideStep !== 4) {
|
||||
this._skipGuide();
|
||||
return;
|
||||
}
|
||||
if (this._guideBusy) return; // 推进语音播放中,忽略
|
||||
if (this._guideBusy) {
|
||||
// B6:busy 窗口缓存首个非跳过分手势,推进语音播完(_advance done)后按
|
||||
// 新步骤补判——合法则直接作答,不合法走答错路径(有纠正语音,可归因)。
|
||||
// 仅缓存 1 个、只存活到本步骤解锁,杜绝跨步幽灵输入(00 §5.4:做了手势
|
||||
// 3 秒内必须有反馈——原 busy 静默吞手势违反此条,本缓存是合规修复)
|
||||
if (!fromCache && this._guidePending === null) this._guidePending = g;
|
||||
return;
|
||||
}
|
||||
switch (this.guideStep) {
|
||||
case 0: // G0:单击继续(intro 播完 1.2s 未操作也会自动推进 G1,见 _startGuide)
|
||||
if (g === G.TAP) this._advance(1, ['voice/g1_move', 'voice/skip_hint']); // skip_hint 首局插播一次
|
||||
@@ -316,10 +368,17 @@ export class EscapeGame extends Game {
|
||||
this._session++; // 作废进行中的引导语音链
|
||||
this._clearTimers();
|
||||
this.audio.stopVoice();
|
||||
this._guidePending = null;
|
||||
try { localStorage.setItem(GUIDE_DONE_KEY, '1'); } catch (_) { /* 隐私模式 */ }
|
||||
this._speak(['voice/guide_skip'], () => this._enterPlay(true), { skippable: false });
|
||||
// 语音缺失兜底
|
||||
this._timer(() => { if (this.phase === Phase.GUIDE) this._enterPlay(true); }, 5000);
|
||||
this.guideStep = 7; // 跳过链期间视作 G7 等待:滑动手势全部忽略(防链中误触误答/二次跳过)
|
||||
// B8(escape-state round2):跳过确认句后、进 PLAY 前补播目标句 g7_goal——
|
||||
// 目标陈述原只在引导 G7 链尾出现一次,跳过的玩家(恰是最需要目标的一群)
|
||||
// 永远听不到(live round1 S4:跳过后目标维度完全空白)
|
||||
this._speak(['voice/guide_skip'], () => {
|
||||
this._playVoice('voice/g7_goal', { onComplete: () => this._enterPlay(true) });
|
||||
}, { skippable: false });
|
||||
// 语音缺失兜底(两句链:覆盖 guide_skip/g7_goal 缺失情形)
|
||||
this._timer(() => { if (this.phase === Phase.GUIDE) this._enterPlay(true); }, 9000);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -341,14 +400,26 @@ export class EscapeGame extends Game {
|
||||
|
||||
// ---------- 进入 / 重开 PLAY ----------
|
||||
|
||||
/** @param {boolean} announce 是否播 start_room(跳过引导/二周目直接进入时) */
|
||||
_enterPlay(announce) {
|
||||
/**
|
||||
* @param {boolean} announce 是否播 start_room(跳过引导/二周目直接进入时)
|
||||
* @param {boolean} withGoal B8:start_room 后接播目标句 g7_goal(REOPEN 直进
|
||||
* 路径;跳过引导路径的 g7_goal 已在 _enterPlay 前播过,此处不重复)
|
||||
*/
|
||||
_enterPlay(announce, withGoal = false) {
|
||||
this._session++;
|
||||
this._clearTimers();
|
||||
this.phase = Phase.PLAY;
|
||||
this.startElapsedMs = Date.now();
|
||||
this._resetStuck();
|
||||
if (announce) this._playVoice('voice/start_room');
|
||||
if (announce) {
|
||||
if (withGoal) {
|
||||
// B8:start_room(可跳)自然播完/被跳过后接播 g7_goal(可跳,
|
||||
// 二周目老手可单击跳过——教学义务句已随首局完成,此处是状态告知)
|
||||
this._playVoice('voice/start_room', { onComplete: () => this._playVoice('voice/g7_goal') });
|
||||
} else {
|
||||
this._playVoice('voice/start_room');
|
||||
}
|
||||
}
|
||||
this._playAmbient();
|
||||
}
|
||||
|
||||
@@ -397,22 +468,37 @@ export class EscapeGame extends Game {
|
||||
if (nx < 0 || nx > 2 || ny < 0 || ny > 2) {
|
||||
this.audio.play('sfx/wall');
|
||||
vibrate('error');
|
||||
// 撞墙极简反馈:只播「那边是墙,换个方向。」(round1-live 修复:原接播
|
||||
// guide_blunt_<dir>「手指贴着屏幕,向 X 推一下」教学腔方位句,中后期出戏;
|
||||
// 教学腔仅限引导期 G2/G3 纠错路径,见 _onGuideGesture/_guideWrong)
|
||||
this._playVoice('voice/wall');
|
||||
// 撞墙反馈:先播「那边是墙,换个方向。」,播完接播陈述式方位句
|
||||
// wall_dir_<dir>「左边是墙。」(escape-state round2 B1:round1-live 回退掉的
|
||||
// 是教学腔措辞,方位信息本身保留;复用 14-F2.3 原接线模式——被下一次语音
|
||||
// 顶掉则自然作废不迟到误导;同方向 10s 限频 1 次防贴墙连撞连播)。
|
||||
// 方位 = 滑动方向(escape 移动坐标系),按真实撞墙方向取句,归因真话。
|
||||
const dir = dy < 0 ? 'u' : dy > 0 ? 'd' : dx < 0 ? 'l' : 'r';
|
||||
const allowDir = !this._wallDirAt[dir] || Date.now() - this._wallDirAt[dir] >= 10000;
|
||||
this._playVoice('voice/wall', {
|
||||
onComplete: allowDir
|
||||
? () => { this._wallDirAt[dir] = Date.now(); this._playVoice(`voice/wall_dir_${dir}`); }
|
||||
: undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.x = nx;
|
||||
this.y = ny;
|
||||
this.audio.play('sfx/step', { volume: 0.8 });
|
||||
this._playAmbient();
|
||||
this._freshEmpty = null; // 离开首访句刚播过的空房:单击检查恢复播 empty_*
|
||||
// 首次到达关键房间:进展事件 + 房间描述
|
||||
const key = `${nx},${ny}`;
|
||||
if (KEY_ROOMS[key] && !this.visited.has(key)) {
|
||||
this.visited.add(key);
|
||||
this._progressEvent();
|
||||
this._playVoice(KEY_ROOMS[key].room);
|
||||
} else if (EMPTY_FIRST_VOICE[key] && !this.emptyVisited.has(key)) {
|
||||
// B5a(escape-state round2):空房首访补播 empty_* 一次性听觉锚
|
||||
// (不算进展事件,不清卡关已播级别;重访零语音与关键房间同款)
|
||||
this.emptyVisited.add(key);
|
||||
this._freshEmpty = key;
|
||||
this._playVoice(EMPTY_FIRST_VOICE[key]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,10 +540,12 @@ export class EscapeGame extends Game {
|
||||
this._progressEvent();
|
||||
this.audio.play('sfx/pickup');
|
||||
vibrate('pickup');
|
||||
// H1(round4):钥匙到手句后接播铁柜房方位(下一目标 = cabinet (2,1))。
|
||||
// H1(round4)/ B2(escape-state round2):钥匙到手句后接播铁柜房
|
||||
// "目标名+方位"一体句 gd_cabinet_dir(下一目标 = cabinet (2,1),固定为右)。
|
||||
this._timer(() => {
|
||||
if (s !== this._session) return;
|
||||
emit('voice/found_key', false, () => this._playGoalDir(STAGES.cabinet.target));
|
||||
emit('voice/found_key', false,
|
||||
() => this._playGoalDir(STAGES.cabinet.target, 'voice/gd_cabinet_dir', 'voice/gd_room_r'));
|
||||
}, 800); // 先让环境音进耳朵再播描述
|
||||
} else {
|
||||
describe('voice/empty_2');
|
||||
@@ -470,12 +558,11 @@ export class EscapeGame extends Game {
|
||||
describe(this.powerOn ? 'voice/inspect_gate_ready' : 'voice/inspect_gate');
|
||||
} else if (x === 1 && y === 1) {
|
||||
describe('voice/start_room');
|
||||
} else if (x === 1 && y === 0) {
|
||||
describe('voice/empty_1');
|
||||
} else if (x === 0 && y === 1) {
|
||||
describe('voice/empty_2');
|
||||
} else {
|
||||
describe('voice/empty_3'); // (1,2)、(2,0)
|
||||
// 空房 (1,0)/(0,1)/(1,2)/(2,0):B5a 首访句刚播过的房间内,单击不再重复
|
||||
// 同一句(_freshEmpty 离开即清;重访后单击检查语义不变,照常播 empty_*)
|
||||
const key = `${x},${y}`;
|
||||
if (this._freshEmpty !== key) describe(EMPTY_FIRST_VOICE[key] || 'voice/empty_3');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,11 +580,12 @@ export class EscapeGame extends Game {
|
||||
// G4(round3):拾取反馈链尾接下一目标句「保险丝到手。接下来,去找电箱。」
|
||||
// (游戏反馈层全档位,gd_economy 先例——走游戏语音链不走向导通道)——堵上
|
||||
// P2-round2 P0「保险丝到手后目标悬空:没有新指令、没有下文」。
|
||||
// H1(round4):目标句播完接播目标房间方位句(P3 唯一拒因:目标无方位)。
|
||||
// H1(round4)/ B2(escape-state round2):目标句播完接播"目标名+方位"一体句
|
||||
// gd_box_dir(电箱房 (0,0) 固定为左)。
|
||||
this._playVoice('voice/unlock_cabinet', {
|
||||
skippable: false, // 关键获得不可跳
|
||||
onComplete: () => this._playVoice('voice/gd_next_fuse', {
|
||||
onComplete: () => this._playGoalDir(STAGES.box.target),
|
||||
onComplete: () => this._playGoalDir(STAGES.box.target, 'voice/gd_box_dir', 'voice/gd_room_l'),
|
||||
}),
|
||||
});
|
||||
} else if (x === 0 && y === 0 && this.hasFuse && !this.powerOn) {
|
||||
@@ -508,22 +596,28 @@ export class EscapeGame extends Game {
|
||||
this.audio.play('sfx/spark');
|
||||
this.audio.play('sfx/power_on', { volume: 0.8 });
|
||||
vibrate('success');
|
||||
// H1(round4):通电目标句后接播大门方位(下一目标 = gate (2,2))。
|
||||
// H1(round4)/ B2(escape-state round2):通电目标句后接播"目标名+方位"
|
||||
// 一体句 gd_gate_dir(大门 (2,2) 固定为右)。
|
||||
this._playVoice('voice/fix_box', {
|
||||
skippable: false, // 关键获得不可跳
|
||||
onComplete: () => this._playGoalDir(STAGES.gate.target),
|
||||
onComplete: () => this._playGoalDir(STAGES.gate.target, 'voice/gd_gate_dir', 'voice/gd_room_r'),
|
||||
});
|
||||
} else if (x === 2 && y === 2 && this.powerOn) {
|
||||
// 通电后开大门 -> 逃脱
|
||||
this._escape();
|
||||
} else {
|
||||
// 兜底:ERROR 震动 + 背包/use_fail + L2 级明示(ui_deny 已退役)
|
||||
// 兜底:ERROR 震动 + 背包/use_fail + L1 级意象暗示(escape-state round2 B4:
|
||||
// 原直通 L2 明示把答案白送给没卡关的玩家——live round1 两次实录;降级为 L1
|
||||
// 意象句(不点明房间与动作,零泄漏),答案仍锁在计时器 L2/L3 与进展链里。
|
||||
// 本进展段已播过 L1 则不重复;播了即记账(沿用 _stuckShown 既有去重))
|
||||
vibrate('error');
|
||||
const inKeyRoom = `${x},${y}` in KEY_ROOMS;
|
||||
this._speak([
|
||||
inKeyRoom ? 'voice/use_fail' : this._inventoryVoice(),
|
||||
`voice/stuck_${this._stage()}_2`,
|
||||
]);
|
||||
const ids = [inKeyRoom ? 'voice/use_fail' : this._inventoryVoice()];
|
||||
if (!this._stuckShown.has('L1')) {
|
||||
this._stuckShown.add('L1');
|
||||
ids.push(`voice/stuck_${this._stage()}_1`);
|
||||
}
|
||||
this._speak(ids);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,10 +650,16 @@ export class EscapeGame extends Game {
|
||||
return dy < 0 ? 'voice/gd_room_u' : 'voice/gd_room_d';
|
||||
}
|
||||
|
||||
/** H1:目标句播完后接播目标房间方位句(游戏反馈层全档位,gd_next_fuse 先例)。 */
|
||||
_playGoalDir(target) {
|
||||
/**
|
||||
* H1(round4)/ B2(escape-state round2):目标句播完后接播目标房间方位句
|
||||
* (游戏反馈层全档位,gd_next_fuse 先例)。B2:三处固定接线点传 namedId +
|
||||
* expectedId,计算方位与枚举一致时播"目标名+方位"一体句;异常路径不符时
|
||||
* 回落原 gd_room_*(归因真话,原句保留不删)。
|
||||
*/
|
||||
_playGoalDir(target, namedId = null, expectedId = null) {
|
||||
const dir = this._roomDirVoice(target);
|
||||
if (dir) this._playVoice(dir);
|
||||
if (!dir) return;
|
||||
this._playVoice(namedId && dir === expectedId ? namedId : dir);
|
||||
}
|
||||
|
||||
// ---------- 卡关检测与三级提示 ----------
|
||||
@@ -573,12 +673,19 @@ export class EscapeGame extends Game {
|
||||
this._lastActiveAt = Date.now();
|
||||
this._stuckShown = new Set();
|
||||
this._lastL3At = 0;
|
||||
// B3 无进展计时:进入/重开 PLAY 时起算点与闸门一并复位
|
||||
this._lastProgressAt = Date.now();
|
||||
this._gesturesSinceProgress = 0;
|
||||
this._progShown = new Set();
|
||||
}
|
||||
|
||||
/** 进展事件:清空已播提示级别,下段静默期从 L1 重新分级(计时由手势重置)。 */
|
||||
_progressEvent() {
|
||||
this._stuckShown.clear();
|
||||
this._lastL3At = 0;
|
||||
// B3:无进展计时起算点与活跃手势计数复位(_progShown 按阶段记账,不清)
|
||||
this._lastProgressAt = Date.now();
|
||||
this._gesturesSinceProgress = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -609,6 +716,18 @@ export class EscapeGame extends Game {
|
||||
vibrate('warning');
|
||||
this._enqueueVoice('stuck', ['voice/stuck_ask', `voice/stuck_${stage}_1`], { level: 'L1' });
|
||||
}
|
||||
// B3(escape-state round2):无进展 150s 活跃计时器(与无操作计时并存互不复位)
|
||||
// ——活跃但零进展的玩家收到一条不泄题的进度重述 gd_prog_*(完成数由 _stage()
|
||||
// 反推,归因真话)。闸门:期间 ≥5 次有效手势(=活跃探索;挂机交给 60s L1
|
||||
// 体系);key 阶段不走本通道(L1/L2/L3 已覆盖"找不到钥匙",避免双计时器
|
||||
// 对同一阶段轰炸);每进展段限 1 次(触发即记账,待播被操作作废也不重发);
|
||||
// 队列语义与卡关提示同款(绝不顶句,待播期间有操作即作废),warning 震同款。
|
||||
if (stage !== 'key' && !this._progShown.has(stage)
|
||||
&& Date.now() - this._lastProgressAt >= 150000 && this._gesturesSinceProgress >= 5) {
|
||||
this._progShown.add(stage);
|
||||
vibrate('warning');
|
||||
this._enqueueVoice('stuck', [GD_PROG[stage]], { level: 'PROG' });
|
||||
}
|
||||
}
|
||||
|
||||
/** 从当前坐标到目标房间的导航语音(每轴一句,最多两句)。 */
|
||||
@@ -677,6 +796,9 @@ export class EscapeGame extends Game {
|
||||
this.audio.stopBgm(2500);
|
||||
this.guide.state('settle'); // 状态签名:结算(逃脱)
|
||||
this._endingBgmStarted = false;
|
||||
// B7(escape-state round2):结算链守卫——播完前 ENDING 单击不立即重开
|
||||
// (_startEndingBgm 处解除:链自然播完或 30s 资产缺失兜底,两条路径汇合)
|
||||
this._settleBusy = true;
|
||||
const totalSec = Math.floor((Date.now() - this.startElapsedMs) / 1000);
|
||||
this._saveEscape(totalSec);
|
||||
// 结算语音链(关键获得,不可跳);播完淡入 menu pad(10 号文档:volume 0.5,1500ms)
|
||||
@@ -696,6 +818,7 @@ export class EscapeGame extends Game {
|
||||
/** 逃脱结算 BGM:drone 淡出后淡入 bgm/menu(幂等,仅在仍是结算态时生效)。 */
|
||||
_startEndingBgm() {
|
||||
if (this._endingBgmStarted || this.phase !== Phase.ENDING) return;
|
||||
this._settleBusy = false; // B7:结算链播完(或 30s 兜底)后解除单击守卫
|
||||
this._endingBgmStarted = true;
|
||||
this.audio.play('bgm/menu', { volume: 0.5, fadeMs: 1500 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user