research(new8/batch2): abyss + detective 迭代闭环收口——双双 R1-R2 连续达标裁定可交付
abyss《深海回音》: - R1 补 synthesis(5/5 愿意玩 P0=0)+ 补录 R1 缺失裁决材料 - R2 十修全部现场坐实:F1 ping迟到/尾句保护首播制/链末看门狗不拆/回波静默一拍/dv4_dual扣账口播/crit水面变体/看门狗13s/震循环1.1s/震去重/状态链人数句 - 补录语音18段(flock串行,QC无离群);playtest 242→278 - 终裁:4/4 人设满分档,连续 2 轮达标 → 可交付 ✅ detective《雨夜专案》: - R1 取证轮:补跑 live 两会话(认真新手33分钟全弧线/乱按200s三级保底抢救),5/5 愿意玩 P0=0 - R2 十一修全部坐实:intro完整落声/tut不被掐/按轴取句/人名指认/结案确认闸/指路句/选句记忆位/hint轻震/st_wrong当场记账/tut_skip补动词/L2疑点齐分支 - 补录+改词33条(29新增+4重录);playtest 265→279 - 终裁:6/6 愿意玩,连续 2 轮达标 → 可交付 ✅ 公共:00-program 批次2进度账收账、13号文档 R2 断言关键词、gen_voice_dual 两游戏段、voice_text.json 验证:abyss 278/278 + detective 279/279 + guide 122/122 + check_all_assets 全项目全绿
This commit is contained in:
+105
-35
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 《雨夜专案》—— 纯音频本格推理游戏(按 docs/design/18-detective.md 实现;
|
||||
* 台词 id 见 18-detective-voice-ids.md,158 句 ≤160 上限)。
|
||||
* 台词 id 见 18-detective-voice-ids.md,R2 起 171 句 ≤176 上限)。
|
||||
*
|
||||
* 你是专案组探长:翻听六份卷宗(4 证人证词 + 现场勘验 + 法医报告),逐句比对,
|
||||
* 指出「同轴异值」的证词对,锁定真凶,结案三问(真凶/凶器/动机)。
|
||||
@@ -11,13 +11,16 @@
|
||||
* _buildCase 生成期断言:全案卷同轴异值对集合 ≡ 预设计矛盾表(4 条)。
|
||||
*
|
||||
* 状态机(§4):TITLE / DRILL / BOARD(空闲|播放) / CONFRONT / FINISH / VERDICT /
|
||||
* SETTLE + MENU 挂起 overlay。证词链 = onComplete 串行(句间 0.8s)+ 单句 8s
|
||||
* 兜底(资产缺失告警续链,§4.3 契约:屏蔽任意 voice 资产流程不卡死)。
|
||||
* SETTLE + MENU 挂起 overlay。证词链 = onComplete 串行(句间 0.8s)+ 单句 15s
|
||||
* 兜底(资产缺失告警续链,§4.3 契约:屏蔽任意 voice 资产流程不卡死;R2 F1:
|
||||
* 兜底须高于全部真实资产,原 8s 会掐 male/intro_1 8.8s 等真实长句)。
|
||||
*
|
||||
* 手势(§2.5 总表,全游戏零手势偏离,仅 2 条已教学豁免):
|
||||
* - E1:BOARD 单击=播放/停止(「检查/翻阅」类非即时操作,escape/dungeon 先例);
|
||||
* R2 F5 起 BOARD 停在「结案陈词」单击=确认闸(fin_pre),闸内再单击才进 FINISH。
|
||||
* - E2:BOARD 空闲长按=进入指证(核心动词快捷入口;绑定条件=无证词链会话在播,
|
||||
* 与长按快进(仅播放中生效)时间互斥零冲突——播放中长按走引擎快进不受理)。
|
||||
* R2 F7 起 CONFRONT 选句相位长按同样放行引擎快进;选句位置按条目记忆。
|
||||
* - 上滑状态链 / 下滑菜单 / 双击重听全状态保留(手势宪法三件套不豁免)。
|
||||
*
|
||||
* 随机性(§6.2):mulberry32(seed) 生成期一次性洗牌(filler 句位置 / 凶器·动机
|
||||
@@ -27,10 +30,13 @@
|
||||
* 评级 S-C(§7.2)六变量条件式(doubts/weaponOk/motiveOk/wrongWho/cfMiss/hints),
|
||||
* 播报值=运行值(账目真实);VERDICT 认罪链 800ms 手势死窗+链锁(dungeon H1 同款,
|
||||
* §7.4:链完成前一切手势吞掉,settle_hook「单击接新案」言行一致)。
|
||||
* R2 F3 起 hit 链 react 按轴取句(慌在刚戳中的那条轴上),4/4 齐追加 reactFinal。
|
||||
*
|
||||
* 看门狗双轨(§9):无操作轨(guide.watchdog:BOARD [10,25] / CONFRONT [15,30] /
|
||||
* FINISH [20,40],手势重置)+ 无进展轨(游戏层 [45,90,150],只由进展事件重置——
|
||||
* 听完新条目/指证判定/结案相位推进;乱按饿不死,150s L3 直接给出可指证的矛盾对)。
|
||||
* FINISH [20,40],手势重置;R2 F6 起听齐未破时一级提示换指路词 gd_board_cf)
|
||||
* + 无进展轨(游戏层 [45,90,150],只由进展事件重置——听完新条目/指证判定/结案
|
||||
* 相位推进;乱按饿不死,150s L3 直接给出可指证的矛盾对;R2 F11 起疑点齐时 L2
|
||||
* 直指 hint_fin)。
|
||||
*/
|
||||
import { Game } from '../engine/game.js';
|
||||
import { Guide } from '../engine/guide.js';
|
||||
@@ -39,7 +45,10 @@ import { vibrate } from '../engine/haptics.js';
|
||||
// ================================ 常量表 ================================
|
||||
|
||||
const SENTENCE_GAP_MS = 800; // 证词链播句间停顿(§3.1)
|
||||
const SENTENCE_FALLBACK_MS = 8000; // 单句 8s 兜底(§4.3:资产缺失续链)
|
||||
// 单句兜底(§4.3:资产缺失/引擎异常时告警续链)。R2 F1:原 8s 会掐真实长句
|
||||
// (male/intro_1 实测 8.8s、male/sys_help 9.28s,R1 live 开场白丢「川死了」)——
|
||||
// 兜底必须高于全部真实资产,只防缺失/异常,不当节奏装置。
|
||||
const SENTENCE_FALLBACK_MS = 15000;
|
||||
const SIG_LEAD_MS = 450; // 签名 earcon → 条目名语音 串行间隔(§8.2)
|
||||
const VERDICT_GRACE_MS = 800; // VERDICT 手势死窗(§7.4:450ms 长按判定+300ms 双击窗全盖)
|
||||
const PROG_LEVELS = [45000, 90000, 150000]; // 无进展轨三级(§9.3:earcon→短句→保底矛盾对)
|
||||
@@ -131,7 +140,8 @@ const PACK_A = {
|
||||
],
|
||||
verdict: { accuseLine: 'fin_right', break: ['z_break_1', 'z_break_2', 'z_break_3'] },
|
||||
wrongAccuse: { w1: 'acc_ge', w2: 'acc_shun', w4: 'acc_wen' },
|
||||
reacts: ['z_react_1', 'z_react_2', 'z_react_3', 'z_react_4'],
|
||||
reacts: { lamp: 'z_react_1', key: 'z_react_2', weapon: 'z_react_w', scale: 'z_react_3' },
|
||||
reactFinal: 'z_react_4', // R2 F3:react 改按轴取句(慌要慌在刚戳中的事上);4/4 齐时追加收官句
|
||||
questions: {
|
||||
weapon: { options: ['wp_1', 'wp_2', 'wp_3', 'wp_4'], answer: 'wp_2' },
|
||||
motive: { options: ['mv_1', 'mv_2', 'mv_3'], answer: 'mv_1' },
|
||||
@@ -167,7 +177,8 @@ const PACK_B = {
|
||||
],
|
||||
verdict: { accuseLine: 'fin_right_b', break: ['b_shun_break_1', 'b_shun_break_2', 'b_shun_break_3'] },
|
||||
wrongAccuse: { w1: 'acc_ge', w3: 'acc_zhou', w4: 'acc_wen' },
|
||||
reacts: ['b_shun_react_1', 'b_shun_react_2', 'b_shun_react_2', 'b_shun_react_2'],
|
||||
reacts: { lamp: 'b_shun_react_1', key: 'b_shun_react_2', weapon: 'b_shun_react_2', scale: 'b_shun_react_2' },
|
||||
reactFinal: 'b_shun_react_2', // pack B 仅 2 条 react(内容弱绑定,非轴假话;§6.3 成本口径)
|
||||
},
|
||||
};
|
||||
|
||||
@@ -185,7 +196,8 @@ function resolvePack(idx) {
|
||||
contradictions: deepClone(ov.contradictions),
|
||||
verdict: deepClone(ov.verdict),
|
||||
wrongAccuse: deepClone(ov.wrongAccuse),
|
||||
reacts: [...ov.reacts],
|
||||
reacts: { ...ov.reacts },
|
||||
reactFinal: ov.reactFinal,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -218,6 +230,9 @@ export class DetectiveGame extends Game {
|
||||
this._gradeInfo = null; // 本局评级快照(SETTLE 上滑重听评级段)
|
||||
this._verdictGraceUntil = 0;
|
||||
this._menuOpenedAt = null; // 菜单挂起计时(_progAdjustMenuPause 消费)
|
||||
this._caseGate = false; // R2 F5:结案陈词确认闸(armed=再单击才进 FINISH)
|
||||
this._cfLineMem = {}; // R2 F7:选句预览位置按条目记忆(本 case 内;新案清空)
|
||||
this._drillPassName = null; // R2 F2:D0 报名移交 D1 链串行播(防报名顶掉 tut_listen)
|
||||
// 无进展轨(§9.3):只由进展事件重置(_progressEvent),任何手势都不重置。
|
||||
this._prog = { lastAt: 0, l1: false, l2: false, l3: false, counts: {}, armed: false, armedSession: -1 };
|
||||
}
|
||||
@@ -297,7 +312,8 @@ export class DetectiveGame extends Game {
|
||||
seed, pack: packIdx, name: pack.name, culprit: pack.culprit, culpritName: pack.culpritName,
|
||||
items, layout, lineClues, lineItem,
|
||||
contradictions: pack.contradictions, verdict: pack.verdict,
|
||||
wrongAccuse: pack.wrongAccuse, reacts: pack.reacts, questions: pack.questions,
|
||||
wrongAccuse: pack.wrongAccuse, reacts: pack.reacts, reactFinal: pack.reactFinal,
|
||||
questions: pack.questions,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -310,13 +326,17 @@ export class DetectiveGame extends Game {
|
||||
_heardCount() { return this.run ? this.run.heard.length : 0; }
|
||||
_lineIds(itemId) { return this._case.layout.lines[itemId] || []; }
|
||||
|
||||
/** 上滑状态链 = 中断复述链同构(§10:st_asked_n → 未听 ≤3 时逐个条目名 → st_doubt_m → recap_go)。 */
|
||||
/** 上滑状态链 = 中断复述链同构(§10:st_asked_n → 未听 ≤3 时逐个条目名 → st_doubt_m → 指路尾句)。
|
||||
* R2 F6:尾句按局面指路——疑点齐=hint_fin(去结案);六份听齐但疑点未齐=recap_cf
|
||||
* (「挑一句你不信的,按住指证」,治「都听完了不知道该干嘛」空档,R1 三人设同撞);
|
||||
* 其余=recap_go「接着查」。 */
|
||||
_statusChain() {
|
||||
const ids = ['voice/st_asked_' + this._heardCount()];
|
||||
const unheard = LISTEN_ITEMS.filter((id) => !this.run.heard.includes(id));
|
||||
if (unheard.length <= 3) for (const id of unheard) ids.push(this._itemName(this._itemOf(id)));
|
||||
ids.push('voice/st_doubt_' + this._doubtCount());
|
||||
ids.push('voice/recap_go');
|
||||
const d = this._doubtCount();
|
||||
ids.push(d === 4 ? 'voice/hint_fin' : (this._heardCount() >= 6 ? 'voice/recap_cf' : 'voice/recap_go'));
|
||||
return ids;
|
||||
}
|
||||
|
||||
@@ -398,6 +418,8 @@ export class DetectiveGame extends Game {
|
||||
};
|
||||
this.itemIdx = 0;
|
||||
this._play = null; this._cf = null; this._fin = null; this._drill = null;
|
||||
this._caseGate = false;
|
||||
this._cfLineMem = {}; // 新案清空选句记忆(R2 F7)
|
||||
this._save();
|
||||
this._speakChain(['voice/sys_new_case'], () => {
|
||||
if (!this.meta.drillDone) this._enterDrill();
|
||||
@@ -422,6 +444,7 @@ export class DetectiveGame extends Game {
|
||||
}
|
||||
this.itemIdx = 0;
|
||||
this._play = null; this._cf = null; this._fin = null;
|
||||
this._caseGate = false;
|
||||
this._progressEvent(); // 读档进板 = 基线重置
|
||||
this._enterBoard(this._statusChain()); // 局面复述链(§10:≤10s 回到可操作)
|
||||
}
|
||||
@@ -464,6 +487,7 @@ export class DetectiveGame extends Game {
|
||||
this._clearTimers();
|
||||
this._play = null; this._cf = null; this._fin = null;
|
||||
this.menuOpen = false;
|
||||
this._caseGate = false; // 回板即撤闸(R2 F5)
|
||||
this.state = 'BOARD';
|
||||
this.audio.stopVoice();
|
||||
this.audio.play('bgm/board');
|
||||
@@ -487,10 +511,20 @@ export class DetectiveGame extends Game {
|
||||
// left/right 静默忽略(无 error 震);longpress 走引擎快进(E2 时间互斥)
|
||||
return;
|
||||
}
|
||||
// R2 F5:结案确认闸在播时,非单击/双击手势先撤闸再走正常映射(滑走=继续查)
|
||||
if (this._caseGate && g !== 'tap' && g !== 'doubletap') this._caseGate = false;
|
||||
if (g === 'tap') {
|
||||
if (ITEM_ORDER[this.itemIdx] === 'case') { this._enterFinish(); return; }
|
||||
if (ITEM_ORDER[this.itemIdx] === 'case') {
|
||||
// R2 F5:「结案陈词」是循环列表必经站——第一次单击只进确认闸(fin_pre),
|
||||
// 闸内再单击才进三问;滑走撤闸留板(R1:压力线误闯 6-7 次,代价真实)
|
||||
if (this._caseGate) { this._caseGate = false; this._enterFinish(); return; }
|
||||
this._caseGate = true;
|
||||
this._speakChain(['voice/fin_pre']);
|
||||
return;
|
||||
}
|
||||
this._startPlayback();
|
||||
} else if (g === 'doubletap') {
|
||||
if (this._caseGate) { this._speakChain(['voice/fin_pre']); return; } // 闸内双击=重听闸句
|
||||
this._replayLastLine();
|
||||
} else if (g === 'left') {
|
||||
this._navItem(-1);
|
||||
@@ -508,6 +542,7 @@ export class DetectiveGame extends Game {
|
||||
}
|
||||
|
||||
_navItem(d) {
|
||||
this._caseGate = false; // 换条目即撤闸
|
||||
this.itemIdx = (this.itemIdx + d + ITEM_ORDER.length) % ITEM_ORDER.length;
|
||||
this._announceItem(this.itemIdx);
|
||||
}
|
||||
@@ -613,6 +648,7 @@ export class DetectiveGame extends Game {
|
||||
this._clearTimers();
|
||||
this._play = null; this._fin = null;
|
||||
this.menuOpen = false;
|
||||
this._caseGate = false;
|
||||
this.state = 'CONFRONT';
|
||||
this.audio.stopVoice();
|
||||
this.guide.state('choice');
|
||||
@@ -666,7 +702,13 @@ export class DetectiveGame extends Game {
|
||||
} else if (g === 'down') {
|
||||
this._openMenu();
|
||||
} else if (g === 'longpress') {
|
||||
this._invalid(); // 选择语境长按不作输入(§2.2)
|
||||
// R2 F7:选句相位(line1/line2)长按放行=引擎快进(仅语音在播生效——预览句也是
|
||||
// 语音,宪法内加速通道;空闲则无可快进,error 震)。条目/确认相位维持不作输入。
|
||||
if (cf.phase === 'line1' || cf.phase === 'line2') {
|
||||
if (!this.audio.isVoicePlaying()) this._invalid();
|
||||
} else {
|
||||
this._invalid(); // 选择语境长按不作输入(§2.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -678,7 +720,8 @@ export class DetectiveGame extends Game {
|
||||
_cfConfirmItem() {
|
||||
const cf = this._cf;
|
||||
cf.phase = cf.phase === 'item1' ? 'line1' : 'line2';
|
||||
cf.lineIdx = 0;
|
||||
// R2 F7:选句入口从该条目上次预览/定下的位置接着走(不再回回从第一句扫起)
|
||||
cf.lineIdx = this._cfLineMem[LISTEN_ITEMS[cf.idx]] || 0;
|
||||
const lead = cf.pickSaid ? [] : ['voice/cf_pick_line']; // 选句相位入口(首次)
|
||||
cf.pickSaid = true;
|
||||
this._speakChain(lead, () => this._playSentence(this._cfCurLineId())); // 播该句全文(句序号不播)
|
||||
@@ -688,6 +731,7 @@ export class DetectiveGame extends Game {
|
||||
const cf = this._cf;
|
||||
const n = this._lineIds(LISTEN_ITEMS[cf.idx]).length;
|
||||
cf.lineIdx = (cf.lineIdx + d + n) % n;
|
||||
this._cfLineMem[LISTEN_ITEMS[cf.idx]] = cf.lineIdx; // 预览位置记忆(R2 F7)
|
||||
this._playSentence(this._cfCurLineId());
|
||||
}
|
||||
|
||||
@@ -708,6 +752,7 @@ export class DetectiveGame extends Game {
|
||||
const cf = this._cf;
|
||||
const itemId = LISTEN_ITEMS[cf.idx];
|
||||
const lineId = this._cfCurLineId();
|
||||
this._cfLineMem[itemId] = cf.lineIdx; // 定下=最强记忆点(R2 F7)
|
||||
if (cf.phase === 'confirm1') {
|
||||
cf.fixed = { item: itemId, line: lineId };
|
||||
cf.phase = 'item2';
|
||||
@@ -771,12 +816,18 @@ export class DetectiveGame extends Game {
|
||||
newDoubt = true;
|
||||
this.run.zHits++;
|
||||
}
|
||||
const reactIdx = Math.max(1, Math.min(this.run.zHits, this._case.reacts.length));
|
||||
const chain = ['voice/cf_hit', 'voice/' + this._case.reacts[reactIdx - 1]];
|
||||
// 疑点 4/4 齐:自动播一次 hint_fin(引导去结案;庆祝性播报不计入保底提示账,§3.5)
|
||||
if (newDoubt && this._doubtCount() === 4 && !this.run.hintFinShown) {
|
||||
this.run.hintFinShown = true;
|
||||
chain.push('voice/hint_fin');
|
||||
// R2 F3:react 按轴取句——慌要慌在刚戳中的那条轴上;按累计次数取句会在换顺序
|
||||
// 坐实时答非所问(R1 人设 P1:先戳凶器却慌「灯」= 演绎层假话)。
|
||||
const axisReact = this._case.reacts[contra.axis] || this._case.reactFinal;
|
||||
const chain = ['voice/cf_hit', 'voice/' + axisReact];
|
||||
// 疑点 4/4 齐:追加收官句 reactFinal(与轴句不同才不重复)+ 自动播一次 hint_fin
|
||||
// (引导去结案;庆祝性播报不计入保底提示账,§3.5)
|
||||
if (newDoubt && this._doubtCount() === 4) {
|
||||
if (this._case.reactFinal && this._case.reactFinal !== axisReact) chain.push('voice/' + this._case.reactFinal);
|
||||
if (!this.run.hintFinShown) {
|
||||
this.run.hintFinShown = true;
|
||||
chain.push('voice/hint_fin');
|
||||
}
|
||||
}
|
||||
this._progressEvent();
|
||||
this._save();
|
||||
@@ -792,6 +843,7 @@ export class DetectiveGame extends Game {
|
||||
this._clearTimers();
|
||||
this._play = null; this._cf = null;
|
||||
this.menuOpen = false;
|
||||
this._caseGate = false;
|
||||
this.state = 'FINISH';
|
||||
this.audio.stopVoice();
|
||||
this.guide.state('choice');
|
||||
@@ -799,18 +851,21 @@ export class DetectiveGame extends Game {
|
||||
this._fin = { phase: 'who', optIdx: 0, busy: false };
|
||||
this._armFinWatchdog();
|
||||
this._speakChain(['voice/fin_start'], () => {
|
||||
this.audio.play(this._itemName(this._itemOf(WHO_ITEMS[0])));
|
||||
this.audio.play(this._whoName(WHO_ITEMS[0])); // R2 F4:指认的是人,播人名不播卷宗名
|
||||
});
|
||||
}
|
||||
|
||||
/** 真凶问选项名=纯人名(R2 F4:item_who_*;原复用条目名「学徒小顺的证词」语义错位)。 */
|
||||
_whoName(itemId) { return 'voice/item_who_' + itemId; }
|
||||
|
||||
_onFinishGesture(g) {
|
||||
const fin = this._fin;
|
||||
if (!fin) return;
|
||||
if (fin.busy) return; // 归因/反馈链播报中:吞掉(防重复答题重复记账)
|
||||
if (g === 'tap') {
|
||||
if (fin.phase === 'who') {
|
||||
fin.phase = 'whoConfirm'; // 两段确认(§2.2:重播选项名 + fin_who_confirm)
|
||||
this._speakChain([this._itemName(this._itemOf(WHO_ITEMS[fin.optIdx])), 'voice/fin_who_confirm']);
|
||||
fin.phase = 'whoConfirm'; // 两段确认(§2.2:重播人名 + fin_who_confirm「就指认这位?」R2 F4)
|
||||
this._speakChain([this._whoName(WHO_ITEMS[fin.optIdx]), 'voice/fin_who_confirm']);
|
||||
} else if (fin.phase === 'whoConfirm') {
|
||||
this._resolveWho(WHO_ITEMS[fin.optIdx]);
|
||||
} else if (fin.phase === 'weapon') {
|
||||
@@ -824,14 +879,14 @@ export class DetectiveGame extends Game {
|
||||
: fin.phase === 'weapon' ? this._case.layout.wpOpts : WHO_ITEMS;
|
||||
fin.optIdx = (fin.optIdx + d + opts.length) % opts.length;
|
||||
if (fin.phase === 'who' || fin.phase === 'whoConfirm') {
|
||||
this.audio.play(this._itemName(this._itemOf(WHO_ITEMS[fin.optIdx])));
|
||||
this.audio.play(this._whoName(WHO_ITEMS[fin.optIdx]));
|
||||
} else {
|
||||
this.audio.play('voice/' + opts[fin.optIdx]);
|
||||
}
|
||||
} else if (g === 'doubletap') {
|
||||
// 重听当前问句/选项(宪法语义保留)
|
||||
if (fin.phase === 'who' || fin.phase === 'whoConfirm') {
|
||||
this.audio.play(this._itemName(this._itemOf(WHO_ITEMS[fin.optIdx])));
|
||||
this.audio.play(this._whoName(WHO_ITEMS[fin.optIdx]));
|
||||
} else if (fin.phase === 'weapon') {
|
||||
this._speakChain(['voice/fin_ask_weapon', 'voice/' + this._case.layout.wpOpts[fin.optIdx]]);
|
||||
} else if (fin.phase === 'motive') {
|
||||
@@ -863,12 +918,13 @@ export class DetectiveGame extends Game {
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 错指:不在场证明复述 + fin_clear → 回 BOARD(无限再机会,§7.1)
|
||||
// 错指:不在场证明复述 + fin_clear + st_wrong 当场记账 → 回 BOARD(无限再机会,§7.1)
|
||||
// R2 F9:st_wrong「错指一回,记档。」让评级账目当场可闻(原结算才揭账,S/A 差距靠悟)
|
||||
fin.busy = true;
|
||||
this.run.wrongWho++;
|
||||
this._progressEvent();
|
||||
this._save();
|
||||
this._speakChain(['voice/' + this._case.wrongAccuse[itemId], 'voice/fin_clear'], () => this._backToBoard());
|
||||
this._speakChain(['voice/' + this._case.wrongAccuse[itemId], 'voice/fin_clear', 'voice/st_wrong'], () => this._backToBoard());
|
||||
}
|
||||
|
||||
_resolveWeapon(optId) {
|
||||
@@ -1004,9 +1060,13 @@ export class DetectiveGame extends Game {
|
||||
// FTUE:intro 三句(~13s)+ D0 指令 → 首输入达成点 ≈t+20s(§5)
|
||||
this._speakChain(['voice/intro_1', 'voice/intro_2', 'voice/intro_3', 'voice/tut_d0']);
|
||||
} else if (step === 1) {
|
||||
// D1 听取老葛真实证词:无论 D0 滑到哪,回正到第一份并重新报名
|
||||
// D1 听取老葛真实证词:无论 D0 滑到哪,回正到第一份并重新报名。
|
||||
// R2 F2:D0 划到的条目名(_drillPassName)接在链首串行补播——原实现里
|
||||
// _navItem 的 450ms 报名与 tut_listen 并行互掐(live R1:指令句只播 0.4s)。
|
||||
const lead = this._drillPassName ? ['voice/' + this._drillPassName] : [];
|
||||
this._drillPassName = null;
|
||||
this.itemIdx = 0;
|
||||
this._speakChain(['voice/tut_listen'], () => this._announceItem(0));
|
||||
this._speakChain([...lead, 'voice/tut_listen'], () => this._announceItem(0));
|
||||
} else if (step === 2) {
|
||||
this._speakChain(['voice/tut_replay']);
|
||||
} else if (step === 3) {
|
||||
@@ -1071,7 +1131,12 @@ export class DetectiveGame extends Game {
|
||||
switch (d.step) {
|
||||
case 0:
|
||||
if (g === 'left' || g === 'right') {
|
||||
this._navItem(g === 'right' ? 1 : -1); // 签名 earcon + 条目名(真实内容)
|
||||
// 签名 earcon + 条目名(真实内容);R2 F2:报名移交 D1 链串行播(不再互掐)
|
||||
this.itemIdx = (this.itemIdx + (g === 'right' ? 1 : -1) + ITEM_ORDER.length) % ITEM_ORDER.length;
|
||||
const item = this._itemAt(this.itemIdx);
|
||||
this.audio.play('sfx/paper_turn');
|
||||
if (item.sig) this.audio.play('sfx/' + item.sig);
|
||||
this._drillPassName = item.name;
|
||||
vibrate('confirm');
|
||||
this._drillPass();
|
||||
} else this._drillWrong();
|
||||
@@ -1112,6 +1177,7 @@ export class DetectiveGame extends Game {
|
||||
this._menuItems = (this.state === 'TITLE' || this.state === 'SETTLE') ? TITLE_MENU : MENU_ITEMS;
|
||||
this._menuIdx = 0;
|
||||
this._menuOpenedAt = performance.now(); // 无进展轨挂起计时(关菜单时剔除时长,§9.3)
|
||||
this._caseGate = false; // 开菜单撤结案闸(R2 F5)
|
||||
this._session++; // 打开即停播报/停看门狗(菜单=天然暂停,本作无实时玩法)
|
||||
this._clearTimers();
|
||||
this._play = null;
|
||||
@@ -1253,8 +1319,10 @@ export class DetectiveGame extends Game {
|
||||
// ---- 无操作轨(guide.watchdog:手势重置;guide=off 全静零定时器) ----
|
||||
|
||||
_armBoardWatchdog() {
|
||||
// R2 F6:六份听齐且疑点未齐时,一级提示换指路词(「换卷宗」此时是错指令)
|
||||
const say0 = (this.run && this._heardCount() >= 6 && this._doubtCount() < 4) ? 'gd_board_cf' : 'gd_board';
|
||||
this.guide.watchdog({
|
||||
arm: true, at: [10000, 25000], say: ['gd_board', null],
|
||||
arm: true, at: [10000, 25000], say: [say0, null],
|
||||
cond: () => this.state === 'BOARD' && !this.menuOpen && !this._play,
|
||||
});
|
||||
}
|
||||
@@ -1329,7 +1397,9 @@ export class DetectiveGame extends Game {
|
||||
if (t >= PROG_LEVELS[1] && !p.l2) {
|
||||
if (chainBusy) return; // 链空了再发(不标记,顺延不丢级)
|
||||
p.l2 = true;
|
||||
const id = LISTEN_ITEMS.some((x) => !this.run.heard.includes(x)) ? 'hint_a' : 'hint_b';
|
||||
// R2 F11:疑点已齐还播「逐句比对」是指引过时——此情直指结案(hint_fin 限次口径沿用)
|
||||
const id = this._doubtCount() === 4 ? 'hint_fin'
|
||||
: LISTEN_ITEMS.some((x) => !this.run.heard.includes(x)) ? 'hint_a' : 'hint_b';
|
||||
this._hintSpeak(id);
|
||||
return;
|
||||
}
|
||||
@@ -1364,7 +1434,7 @@ export class DetectiveGame extends Game {
|
||||
this._prog.counts[id] = (this._prog.counts[id] || 0) + 1;
|
||||
if (this._prog.counts[id] > HINT_MAX_TIMES) { this._prog.counts[id] = HINT_MAX_TIMES; return; }
|
||||
}
|
||||
vibrate('warning');
|
||||
vibrate('confirm'); // R2 F8:递答案不用 warning 三连震(像训人),轻震即可
|
||||
this.run.hintsGiven.push(id); // 保底提示入账(评级变量 hints,§7.2)
|
||||
this._save();
|
||||
this._speakChain(['voice/' + id]);
|
||||
@@ -1372,7 +1442,7 @@ export class DetectiveGame extends Game {
|
||||
|
||||
// ================================ 语音链 / 计时器基础设施 ================================
|
||||
|
||||
/** 单句播放:自然播完推进;8s 兜底(资产缺失告警续链,§4.3 契约)。 */
|
||||
/** 单句播放:自然播完推进;15s 兜底(资产缺失告警续链,§4.3 契约;R2 F1:高于全部真实资产)。 */
|
||||
_playSentence(lineId, onDone) {
|
||||
const s = this._session;
|
||||
let fired = false;
|
||||
|
||||
Reference in New Issue
Block a user