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:
+86
-18
@@ -39,8 +39,8 @@
|
||||
* 死窗,窗内手势一律吞掉,结算链完整播出。
|
||||
*
|
||||
* 看门狗双轨(§7.2):无操作轨(guide watchdog,任何手势复位;搜索等待
|
||||
* [10,25] gd_ping1/gd_ping2 两级不同句;叙事链末 [25,50]/[13,30]
|
||||
* gd_continue/gd_again)+ 无进展轨(游戏层计时 60/120s gd_prog1/gd_prog2,
|
||||
* [10,25] gd_ping1/gd_ping2 两级不同句;叙事链末 [13,30](R2-F7 起 full 档与
|
||||
* standard 对齐)gd_continue/gd_again)+ 无进展轨(游戏层计时 60/120s gd_prog1/gd_prog2,
|
||||
* 仅挂搜索等待与 COMMS 链末两个可饿死点;无效手势不复位——乱按 96s 也必被
|
||||
* 抢救;两轨同级交叉登记去重;guide=off 不挂任何定时器)。
|
||||
*
|
||||
@@ -51,6 +51,15 @@
|
||||
* 纯音频标记)、G2 oxy_crit 附 warning 震、G4 不可跳句空点 beat 轻震、
|
||||
* G5 首播必完整复播可即掐、G6 状态链顺序。
|
||||
*
|
||||
* R2 修复(research/new8/abyss/round1/synthesis.md F1-F10):F1 向导散句在播
|
||||
* 时发脉冲先掐散句立即出音(ping 不再迟到 12~17s);F2 F5 尾句保护改认实际
|
||||
* 播放链尾(dyn 注入的档位句纳入保护)+ 签收链入口 800ms 死窗;F3 回波
|
||||
* wait = 资产实长+静默一拍(「回波→静默一拍→解读」兑现);F4 双载 D4 补播
|
||||
* dv4_dual 扣账句(3+1 不再无声);F5 crit 水面变体 oxy_crit_deck;F6 叙事
|
||||
* 尾句保护改首播制(_tailHeard 复播可即掐,G5 后半句兑现)+ sys_skip 改词;
|
||||
* F7 叙事链末 full 档看门狗 [13,30];F8 danger 震循环 1100ms;F9 状态链
|
||||
* crit 震按档进入去重;F10 状态链插入已救人数 prog_saved1..5。
|
||||
*
|
||||
* 资产缺失兜底:引擎 console.error 静默跳过且不回调 onComplete;单句语音
|
||||
* 看门狗 30s 且 isVoicePlaying()===false 才自动续链(storyline N1 先例照抄)
|
||||
* ——屏蔽任意 voice 资产仍可一路走到结局,不卡死。环境音循环(amb_deck /
|
||||
@@ -64,13 +73,16 @@ import { vibrate } from '../engine/haptics.js';
|
||||
// ---------------------------------------------------------------- 声呐时序常量
|
||||
|
||||
// 声呐链驻留时长(ms):脉冲→回波静默一拍 / 回波余音 / 危机前置 2s(L2)
|
||||
// R2-F3:回波 wait = 资产实长 + 静默一拍(§6.2 R2「回波→静默一拍→解读」)——
|
||||
// echo_metal/echo_debris 2.76-2.78s、pipe_knock 2.96s、echo_groan 4.42s
|
||||
// (R1 实测尾巴压解读句开头 0.35~0.55s,wait 常数全部低于资产实长)
|
||||
const PING_GAP_MS = 900;
|
||||
const W_Metal = 2200;
|
||||
const W_Knock = 2600;
|
||||
const W_Groan = 4000;
|
||||
const W_Metal = 3300;
|
||||
const W_Knock = 3500;
|
||||
const W_Groan = 5000;
|
||||
const W_Crisis = 2000;
|
||||
const W_Bell = 3200;
|
||||
const DANGER_LOOP_MS = 500; // danger 震循环间隔(≥400ms,§5.2)
|
||||
const DANGER_LOOP_MS = 1100; // danger 震循环间隔(≥400ms,§5.2;R2-F8:500ms 约 10 连震过密)
|
||||
|
||||
// ---------------------------------------------------------------- 节点表
|
||||
|
||||
@@ -195,7 +207,9 @@ const NODES = {
|
||||
s3_surf: N(['voice/d3_surf'], { auto: true, next: 's_tier' }),
|
||||
|
||||
// ---- D4 指挥舱·高船长(高潮:滑坡危机拍前置 2s 渐强 + danger 震循环)
|
||||
dv4_down: N(['voice/dv4_down'], { auto: true, next: '@search' }),
|
||||
// dv4_down 链内条件选播(R2-F4):双载形态追加 dv4_dual「这趟往返,扣四格气」
|
||||
// ——D4 双载下潜 3+1 扣账不再是无声扣款(R1 P1 心算账对不上耳朵)
|
||||
dv4_down: N([], { auto: true, next: '@search', dyn: 'dv4down' }),
|
||||
// 回波渐强 + 金属回波(§6.3 承诺:D4 接近指挥舱必播 echo_metal*,取 _r 声像)
|
||||
sv4_echo: N([], {
|
||||
auto: true,
|
||||
@@ -259,6 +273,14 @@ const SEGS = ['找船', '机舱', '病舱', '指挥舱'];
|
||||
const PROG_VOICES = ['voice/prog_d1', 'voice/prog_d2', 'voice/prog_d3', 'voice/prog_d4'];
|
||||
/** 氧气档位分档整句(floor(oxy/3)=可下潜次数划界,句义逐格为真,§4.3)。 */
|
||||
const TIER_VOICES = { full: 'voice/oxy_full', ok: 'voice/oxy_ok', low: 'voice/oxy_low', crit: 'voice/oxy_crit' };
|
||||
/** R2-F5:crit 水面变体——人已出水后「必须上浮」是错场景措辞(甲板=一潜都下不去)。 */
|
||||
const TIER_CRIT_DECK = 'voice/oxy_crit_deck';
|
||||
|
||||
/** 上滑状态链「已救人数」分档整句(R2-F10;saved=0 不播,数字写死禁拼接)。 */
|
||||
const SAVED_VOICES = [
|
||||
'voice/prog_saved1', 'voice/prog_saved2', 'voice/prog_saved3',
|
||||
'voice/prog_saved4', 'voice/prog_saved5',
|
||||
];
|
||||
|
||||
const MENU_ITEMS = [
|
||||
{ id: 'resume', voice: 'voice/sys_menu_resume' }, // 回到水下
|
||||
@@ -294,7 +316,9 @@ const VOICE_WATCHDOG_MS = 30000;
|
||||
const SETTLE_GRACE_MS = 800;
|
||||
|
||||
// 向导层看门狗(§7.2;full 档 say 由 guide 播,standard 档 onEscalate 游戏层补播)
|
||||
const GUIDE_WATCHDOG_AT = [25000, 50000]; // full:叙事链末 / ENDING 结算等待
|
||||
// R2-F7:叙事链末/ENDING full 档与 standard 对齐 [13,30]——R1 主线 25s 首轮静默
|
||||
// 掐在钩子落地上(两人设点名),标准档 13s 已被 live 局 B 验证不烦人
|
||||
const GUIDE_WATCHDOG_AT = [13000, 30000]; // full:叙事链末 / ENDING 结算等待
|
||||
const IDLE_HINT_AT = [13000, 30000]; // standard:同 storyline P2-3 先例
|
||||
const CHOICE_HINT_AT = [25000, 50000]; // 抉择点:两级都重播当前 p* 提示句本身
|
||||
const SEARCH_WATCHDOG_AT = [10000, 25000]; // 搜索等待两级**不同句**(gd_ping1≠gd_ping2)
|
||||
@@ -371,6 +395,9 @@ export class AbyssGame extends Game {
|
||||
this._resumeGuard = false;
|
||||
this._skipLockId = null;
|
||||
this._graceUntil = 0; // H1:结算手势死窗截止时刻
|
||||
this._chainTail = null; // 实际播放链的尾句(R2-F2:dyn 注入句也算尾句)
|
||||
this._tailHeard = new Set(); // R2-F6:链自然播完登记的节点(复播可即掐,G5)
|
||||
this._critWarned = false; // R2-F9:状态链 crit warning 震按档进入去重
|
||||
|
||||
// 声呐双通道(§6.2):空闲门 + 排队补发(回波与语音零重叠,R1)
|
||||
this._sonarQueue = [];
|
||||
@@ -519,6 +546,11 @@ export class AbyssGame extends Game {
|
||||
/** 单击=发脉冲:搜索等待 → 回波链(脉冲 → 静默一拍 → 回波 → 解读)。 */
|
||||
_firePing() {
|
||||
if (!this.searchArmed) return;
|
||||
// R2-F1:向导看门狗行(gd_ping*/gd_prog* 散句)在播时玩家照做单击——先掐
|
||||
// 散句再发脉冲,玩家脉冲立即出音。R1 根因:ping 走 sonarGate 见 voice 忙入队,
|
||||
// 排水要等链真歇下 → 迟到 12~17s 落在 COMMS 句隙,「回波先于脉冲」错序。
|
||||
// 搜索等待态游戏链恒不在播(voicePlaying=false),voice 层只可能是向导散句。
|
||||
if (!this.voicePlaying && this.audio.isVoicePlaying()) this.audio.stopVoice();
|
||||
this.searchArmed = false;
|
||||
this._clearProg(); // 有真实进展:无进展轨撤防(当次等待点作废)
|
||||
this.playNode(`sv${this.diveN}_echo`);
|
||||
@@ -612,17 +644,20 @@ export class AbyssGame extends Game {
|
||||
}
|
||||
|
||||
/**
|
||||
* F5 链末句保护:当前句是本节点末句、且节点把控制权交出(链末等待单击 /
|
||||
* @x 过场转移 / 直入抉择提示)时,屏蔽「跳过本句」。自动链中段不受保护。
|
||||
* F5 链末句保护:当前句是**实际播放链**的末句、且节点把控制权交出(链末等待
|
||||
* 单击 / @x 过场转移 / 直入抉择提示)时,屏蔽「跳过本句」。自动链中段不受保护。
|
||||
* R2-F2:尾句改认 _chainTail(_playSeqSteps 按实际步序记录)——R1 根因是
|
||||
* 用 _nodeSteps 重算,dyn 注入的档位句(s_tier)不在其中,保护形同虚设。
|
||||
* R2-F6(G5 复播可即掐):非 auto 叙事节点的尾句保护仅限**首播**——链自然
|
||||
* 播完登记 _tailHeard,菜单/状态复述等重播时尾句可跳(sys_skip 教的必须真);
|
||||
* auto 过场/直入抉择的末句与 p* 提示恒保护(抢跑吃钩子根因不回潮)。
|
||||
*/
|
||||
_isTailSentence() {
|
||||
const node = NODES[this.nodeId];
|
||||
if (!node || !this.voicePlaying || this.promptPlaying) return false;
|
||||
if (this.currentVoiceId == null) return false; // 声呐步不适用(由快进语义接管)
|
||||
const voices = this._nodeSteps(this.nodeId).filter((s) => typeof s === 'string');
|
||||
const tail = voices.length ? voices[voices.length - 1] : null;
|
||||
if (tail == null || this.currentVoiceId !== tail) return false; // 仅护末句
|
||||
if (!node.auto) return true; // 链末等待型节点(本作叙事句)
|
||||
if (this._chainTail == null || this.currentVoiceId !== this._chainTail) return false;
|
||||
if (!node.auto) return !this._tailHeard.has(this.nodeId); // 链末等待型:首播保护
|
||||
if (node.next == null || this._isTransition(node.next)) return true; // 自动链终点
|
||||
const nx = NODES[node.next];
|
||||
return !!(nx && (nx.prompt != null || nx.dyn === 'p7')); // 自动链直入抉择提示
|
||||
@@ -1061,6 +1096,9 @@ export class AbyssGame extends Game {
|
||||
console.error('[AbyssGame] 未知节点:', id);
|
||||
return;
|
||||
}
|
||||
// R2-F2:签收链入口盖 800ms 死窗——SURFACE(4)/取消链过场的在途 tap 不得
|
||||
// 剁断 g9_in(R1 live:g9_in 只播 0.28s 被掐)
|
||||
if (id === 'g9_in') this._grace();
|
||||
this.nodeId = id;
|
||||
this.choiceArmed = false;
|
||||
this.pendingChoice = null;
|
||||
@@ -1127,6 +1165,11 @@ export class AbyssGame extends Game {
|
||||
switch (node.dyn) {
|
||||
case 'd2pry':
|
||||
return [this.flags.knowsPipe ? 'voice/d2_pry_ok' : 'voice/d2_pry_slow'];
|
||||
case 'dv4down':
|
||||
// R2-F4:双载形态补播扣账句(D4 双载起播 3+1=4 格,不再无声扣款)
|
||||
return this.diveForm === 'double'
|
||||
? ['voice/dv4_down', 'voice/dv4_dual']
|
||||
: ['voice/dv4_down'];
|
||||
case 'gaoconfess':
|
||||
return [this.flags.hasLog ? 'voice/gao_confess' : 'voice/gao_c'];
|
||||
case 'stayleave':
|
||||
@@ -1195,6 +1238,9 @@ export class AbyssGame extends Game {
|
||||
this._setAmbDucked(false);
|
||||
const voices = steps.filter((x) => typeof x === 'string');
|
||||
if (voices.length > 0) this.lastCloser = voices[voices.length - 1];
|
||||
// R2-F6:链自然播完 = 本节点尾句已首播完整(尾句受 F5 保护必完整),登记后
|
||||
// 该节点重播(菜单/状态复述)尾句可即掐(G5 复播可即掐)
|
||||
if (voices.length > 0) this._tailHeard.add(this.nodeId);
|
||||
if (this._isPromptNode(node)) {
|
||||
this.choiceArmed = true; // W2 契约:播完当刻即可选,零插播
|
||||
if (this.pendingChoice) {
|
||||
@@ -1312,14 +1358,25 @@ export class AbyssGame extends Game {
|
||||
|
||||
// ---------------------------------------------------------------- 档位 / 状态链
|
||||
|
||||
/** 档位句选播(R2-F5):crit 档在水面场景(SURFACE/ENDING/INTRO)用「一潜都
|
||||
* 下不去了」变体——人已出水再说「必须上浮」是错场景措辞;水下保留原句。 */
|
||||
_tierVoice(tier) {
|
||||
if (tier === 'crit'
|
||||
&& (this.phase === 'SURFACE' || this.phase === 'ENDING' || this.phase === 'INTRO')) {
|
||||
return TIER_CRIT_DECK;
|
||||
}
|
||||
return TIER_VOICES[tier];
|
||||
}
|
||||
|
||||
/** 跨档待播档位句(无跨档返回 null;副作用由 playNode('s_tier') 结清)。 */
|
||||
_tierPending() {
|
||||
const tier = tierOf(this.oxy);
|
||||
if (tier === this.lastAnnouncedTier) return null;
|
||||
return TIER_VOICES[tier];
|
||||
return this._tierVoice(tier);
|
||||
}
|
||||
|
||||
/** 上滑状态链(G6 顺序:prog_dN → oxy 档 → sys_help;G2:crit 附 warning 震)。 */
|
||||
/** 上滑状态链(G6 顺序:prog_dN → 已救人数 → oxy 档 → sys_help;
|
||||
* G2:crit 附 warning 震——R2-F9 按「进入 crit 档」去重,同档复听不重复震)。 */
|
||||
_statusIds() {
|
||||
const ids = [];
|
||||
if (this.phase === 'ENDING') {
|
||||
@@ -1328,8 +1385,14 @@ export class AbyssGame extends Game {
|
||||
} else if (this.diveN >= 1 && this.diveN <= 4) {
|
||||
ids.push(PROG_VOICES[this.diveN - 1]);
|
||||
}
|
||||
if (tierOf(this.oxy) === 'crit') vibrate('warning');
|
||||
ids.push(TIER_VOICES[tierOf(this.oxy)]);
|
||||
// R2-F10:已救人数(P3 被打断后要捡回的三样局面:潜次/氧气/船上躺着谁)
|
||||
const savedCount = ['zhou', 'bai', 'chen', 'zheng', 'gao']
|
||||
.filter((k) => this.saved[k]).length;
|
||||
if (savedCount >= 1) ids.push(SAVED_VOICES[savedCount - 1]);
|
||||
const tier = tierOf(this.oxy);
|
||||
if (tier === 'crit' && !this._critWarned) { vibrate('warning'); this._critWarned = true; }
|
||||
if (tier !== 'crit') this._critWarned = false;
|
||||
ids.push(this._tierVoice(tier));
|
||||
return ids;
|
||||
}
|
||||
|
||||
@@ -1467,6 +1530,11 @@ export class AbyssGame extends Game {
|
||||
const firstVoice = steps.find((x) => typeof x === 'string') || null;
|
||||
this._skipLockId = this._resumeGuard ? firstVoice : null; // F4:续玩锁
|
||||
this._resumeGuard = false;
|
||||
// R2-F2:记录实际播放链的尾句(含 dyn 注入的档位句),供 F5 保护判定
|
||||
this._chainTail = null;
|
||||
for (let k = steps.length - 1; k >= 0; k--) {
|
||||
if (typeof steps[k] === 'string') { this._chainTail = steps[k]; break; }
|
||||
}
|
||||
this.voicePlaying = true;
|
||||
this._sonarSkip = null;
|
||||
const step = (i) => {
|
||||
|
||||
Reference in New Issue
Block a user