775 lines
24 KiB
JavaScript
775 lines
24 KiB
JavaScript
// engine.js — 游戏状态机
|
||
|
||
class GameEngine {
|
||
constructor() {
|
||
// 游戏状态
|
||
this.state = 'idle'; // idle→playing→ringing→in_call→msg_incoming→msg_playing→await_reply→detail_reply→inbox→ending
|
||
|
||
// 剧情变量
|
||
this.vars = {
|
||
MOM_LINK: false,
|
||
HE_BACK: null, // null=未定, true=来了, false=拒了
|
||
GROUP_REPLY: false,
|
||
ZHOUNAN_DEPTH: 0, // 0-3
|
||
ZHOUNAN_SHARE: false,
|
||
SELF_RECORD: false,
|
||
UNREAD: 0, // 未读消息计数
|
||
};
|
||
|
||
this.profile = new ProfileTracker();
|
||
|
||
// 消息状态
|
||
this.msgState = {}; // msgId → 'unread'|'read'|'replied'|'missed'
|
||
this.replayCount = {};// msgId → 重听次数
|
||
|
||
// 当前处理的消息
|
||
this.currentMsg = null;
|
||
this.currentChoices = null; // 细回复选项列表
|
||
this.awaitingChoice = false;
|
||
|
||
// 定时器
|
||
this._timers = [];
|
||
this._ringTimer = null;
|
||
this._startTime = null;
|
||
|
||
// 手势引用
|
||
this.gest = null;
|
||
|
||
// 特殊事件追踪
|
||
this._azheFollowUpDone = false;
|
||
this._xiaomeiRecallDone = false;
|
||
this._batteryWarned = false;
|
||
this._doorbell = false;
|
||
}
|
||
|
||
// ══════════════════════════════════════════
|
||
// 初始化
|
||
// ══════════════════════════════════════════
|
||
init(gestureDetector) {
|
||
this.gest = gestureDetector;
|
||
this._bindGestures();
|
||
}
|
||
|
||
_bindGestures() {
|
||
const G = this.gest;
|
||
G.on('singletap', () => this._onSingleTap());
|
||
G.on('doubletap', () => this._onDoubleTap());
|
||
G.on('longpress1s', () => this._onLongPress1s());
|
||
G.on('longpress5s', () => this._onLongPress5s());
|
||
G.on('swipe_left', () => this._onSwipe('left'));
|
||
G.on('swipe_right', () => this._onSwipe('right'));
|
||
G.on('swipe_up', () => this._onSwipe('up'));
|
||
G.on('swipe_down', () => this._onSwipe('down'));
|
||
}
|
||
|
||
// ══════════════════════════════════════════
|
||
// 游戏开始
|
||
// ══════════════════════════════════════════
|
||
async start() {
|
||
this._startTime = Date.now();
|
||
this.state = 'playing';
|
||
|
||
// 启动环境底噪
|
||
await Snd.startAmbience(AMBIENCE_DEFAULT, { vol: 0.2 });
|
||
|
||
// 调度所有消息
|
||
for (const msg of MESSAGES) {
|
||
this._schedule(msg.triggerAt * 1000, () => this._triggerMessage(msg));
|
||
}
|
||
|
||
// 调度特殊事件
|
||
this._scheduleSpecialEvents();
|
||
|
||
// 02:05 兜底:确保结局被触发
|
||
const endingDelay = (gt(2, 5)) * 1000;
|
||
this._schedule(endingDelay, () => {
|
||
if (!this._endingTriggered) this.triggerFinalEnding();
|
||
});
|
||
|
||
// 夜间启动彩蛋
|
||
const hour = new Date().getHours();
|
||
if (hour >= 22 || hour < 4) {
|
||
await sleep(2000);
|
||
await TTS.speak('你也还醒着啊。', { rate: 0.8, volume: 0.6 });
|
||
}
|
||
|
||
// 开始主BGM
|
||
await sleep(3000);
|
||
Snd.startBGM(`${MUS}/lv11_bgm_m1_main_loop_v1.mp3`, { vol: 0.3 });
|
||
}
|
||
|
||
_scheduleSpecialEvents() {
|
||
// 阿哲撤回(消息4未回复时,10游戏分钟后)
|
||
this._schedule(gt(21, 40) * 1000, () => {
|
||
if (!this._azheFollowUpDone && this.msgState[4] !== 'replied') {
|
||
this._azheFollowUpDone = true;
|
||
Snd.startTyping();
|
||
sleep(8000).then(() => {
|
||
Snd.stopTyping();
|
||
TTS.speak('阿哲撤回了一条消息。', { rate: 0.85, volume: 0.7 });
|
||
});
|
||
}
|
||
});
|
||
|
||
// HE_BACK 为 true 时,门铃音效(23:30左右)
|
||
this._schedule(gt(23, 28) * 1000, () => {
|
||
if (this.vars.HE_BACK === true && !this._doorbell) {
|
||
this._doorbell = true;
|
||
Snd.playFX ? null : null;
|
||
Snd.playVoice([`${SFX}/sfx_door_knock_gentle_v1.mp3`]);
|
||
Haptic.ring();
|
||
}
|
||
});
|
||
|
||
// 电量1%警告(结局D触发条件之一)
|
||
this._schedule(gt(1, 0) * 1000, () => {
|
||
if (!this._batteryWarned) {
|
||
this._batteryWarned = true;
|
||
const unread = Object.values(this.msgState).filter(s => s === 'unread').length
|
||
+ (Object.keys(this.msgState).length === 0 ? MESSAGES.length : 0);
|
||
if (this.vars.UNREAD >= 8) {
|
||
Snd.playVoice([`${TDIR}/lv11_sys_battery_01_wav_v1.mp3`]);
|
||
Haptic.system();
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
_schedule(ms, fn) {
|
||
const t = setTimeout(fn, ms);
|
||
this._timers.push(t);
|
||
return t;
|
||
}
|
||
|
||
// ══════════════════════════════════════════
|
||
// 消息触发
|
||
// ══════════════════════════════════════════
|
||
async _triggerMessage(msg) {
|
||
if (this.state === 'ringing' || this.state === 'in_call') {
|
||
// 已在通话中,延迟处理(放入队列)
|
||
this._schedule(5000, () => this._triggerMessage(msg));
|
||
return;
|
||
}
|
||
|
||
// 消息16(妈妈长语音):MOM_LINK=false时跳过,直接触发结局
|
||
if (msg.requiresMomLink && !this.vars.MOM_LINK) {
|
||
this.triggerFinalEnding();
|
||
return;
|
||
}
|
||
|
||
// 记录为未读
|
||
this.msgState[msg.id] = 'unread';
|
||
this.vars.UNREAD++;
|
||
this.currentMsg = msg;
|
||
|
||
if (msg.type === 'call') {
|
||
await this._startRinging(msg);
|
||
} else if (msg.type === 'wechat_voice' || msg.type === 'wechat_text') {
|
||
await this._notifyMessage(msg);
|
||
} else if (msg.type === 'system_notification') {
|
||
await this._notifySystem(msg);
|
||
}
|
||
}
|
||
|
||
// ── 来电流程 ──
|
||
async _startRinging(msg) {
|
||
this.state = 'ringing';
|
||
this.currentMsg = msg;
|
||
|
||
Haptic.ring();
|
||
await Snd.startRingtone(msg.ringtone);
|
||
|
||
// 超时自动漏接
|
||
this._ringTimer = setTimeout(() => {
|
||
this._missCall(msg);
|
||
}, msg.ringDuration * 1000);
|
||
|
||
await TTS.speak(`${msg.senderName},来电话了。`, { rate: 0.9, volume: 0.7 });
|
||
}
|
||
|
||
_missCall(msg) {
|
||
Snd.stopRingtone();
|
||
Haptic.stop();
|
||
this.state = 'playing';
|
||
this.msgState[msg.id] = 'missed';
|
||
this.profile.add(msg.profileOnMiss);
|
||
if (msg.id === 1 || msg.id === 14) {
|
||
// 妈妈漏接不影响 MOM_LINK(需主动接才算)
|
||
}
|
||
}
|
||
|
||
_answerCall(msg) {
|
||
clearTimeout(this._ringTimer);
|
||
Snd.stopRingtone();
|
||
Haptic.confirm();
|
||
this.state = 'in_call';
|
||
this.msgState[msg.id] = 'read';
|
||
this.vars.UNREAD = Math.max(0, this.vars.UNREAD - 1);
|
||
this.profile.add(msg.profileOnAnswer);
|
||
|
||
if (msg.stateOnAnswer) this._applyState(msg.stateOnAnswer);
|
||
|
||
// 播放通话内容
|
||
Snd.playVoice(msg.audio, {
|
||
onEnd: () => {
|
||
if (msg.inCallChoices) {
|
||
this._startInCallChoice(msg);
|
||
} else if (msg.autoHangup) {
|
||
setTimeout(() => this._endCall(msg), msg.autoHangup * 1000);
|
||
} else {
|
||
this._endCall(msg);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
async _startInCallChoice(msg) {
|
||
this.state = 'detail_reply';
|
||
this.currentChoices = msg.inCallChoices;
|
||
this.awaitingChoice = true;
|
||
await this._readChoices(msg.inCallChoices);
|
||
}
|
||
|
||
_endCall(msg) {
|
||
this.state = 'playing';
|
||
this.currentMsg = null;
|
||
if (msg.sfxAfter) Snd.playVoice([msg.sfxAfter]);
|
||
}
|
||
|
||
_rejectCall(msg) {
|
||
clearTimeout(this._ringTimer);
|
||
Snd.stopRingtone();
|
||
Haptic.reject();
|
||
this.state = 'playing';
|
||
this.msgState[msg.id] = 'missed';
|
||
this.profile.add(msg.profileOnMiss);
|
||
}
|
||
|
||
// ── 微信消息流程 ──
|
||
async _notifyMessage(msg) {
|
||
Haptic.message();
|
||
const type = msg.type === 'wechat_text' ? '文字消息' : '语音';
|
||
await TTS.speak(`${msg.senderName},发来${type}。`, { rate: 0.9, volume: 0.7 });
|
||
this.state = 'msg_incoming';
|
||
// 等待玩家交互(单点播放 / 不动保持未读)
|
||
}
|
||
|
||
async _playMessage(msg) {
|
||
this.state = 'msg_playing';
|
||
this.msgState[msg.id] = 'read';
|
||
this.vars.UNREAD = Math.max(0, this.vars.UNREAD - 1);
|
||
|
||
// 开始BGM(如果消息指定)
|
||
if (msg.bgm) Snd.startBGM(msg.bgm, { vol: 0.25 });
|
||
|
||
if (msg.type === 'wechat_text') {
|
||
// 文字消息:TTS 朗读
|
||
await TTS.speak(msg.text, { rate: 0.85 });
|
||
this._afterMessagePlayed(msg);
|
||
} else {
|
||
// 语音消息:播放音频
|
||
Snd.playVoice(msg.audio, {
|
||
onEnd: () => {
|
||
// 如果是小美醉语音,5秒后撤回
|
||
if (msg.followUp?.action === 'xiaomei_recall') {
|
||
setTimeout(() => {
|
||
TTS.speak('小美撤回了刚才的语音。但你已经听过了。', { rate: 0.85, volume: 0.7 });
|
||
}, msg.followUp.delaySec * 1000);
|
||
}
|
||
this._afterMessagePlayed(msg);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
_afterMessagePlayed(msg) {
|
||
this.state = 'await_reply';
|
||
this.currentMsg = msg;
|
||
// 等待玩家选择回应方式
|
||
// 单点=嗯 / 双点=标准回复 / 长按=细回复 / 双指上滑=已读不回 / 不动=本已读
|
||
}
|
||
|
||
// ── 系统通知(时光胶囊)──
|
||
async _notifySystem(msg) {
|
||
Haptic.system();
|
||
Snd.playVoice([msg.systemAudio]);
|
||
await sleep(3000);
|
||
await TTS.speak(msg.text, { rate: 0.85, volume: 0.8 });
|
||
this.state = 'msg_incoming';
|
||
this.currentMsg = msg;
|
||
// 单点=播放, 双指上滑=跳过
|
||
}
|
||
|
||
async _playSystemMessage(msg) {
|
||
const profileLabel = this.profile.label;
|
||
const audioUrl = msg.audioByProfile[profileLabel];
|
||
this.msgState[msg.id] = 'read';
|
||
this.vars.UNREAD = Math.max(0, this.vars.UNREAD - 1);
|
||
this._applyState(msg.stateOnPlay || {});
|
||
this.profile.add(msg.profileOnPlay);
|
||
|
||
this.state = 'msg_playing';
|
||
Snd.playVoice([audioUrl], {
|
||
onEnd: () => {
|
||
this.state = 'playing';
|
||
this._checkEndingConditions();
|
||
}
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════
|
||
// 细回复流程
|
||
// ══════════════════════════════════════════
|
||
async _enterDetailReply(msg) {
|
||
if (!msg.detailReplies) return;
|
||
this.state = 'detail_reply';
|
||
this.currentChoices = msg.detailReplies;
|
||
this.awaitingChoice = true;
|
||
Haptic.detailMode();
|
||
await this._readChoices(msg.detailReplies);
|
||
}
|
||
|
||
async _readChoices(choices) {
|
||
const nums = ['一', '二', '三', '四'];
|
||
for (let i = 0; i < choices.length; i++) {
|
||
if (!this.awaitingChoice) return;
|
||
await TTS.speak(`${nums[i]}……`, { rate: 0.75, volume: 0.65 });
|
||
await sleep(200);
|
||
await TTS.speak(choices[i].label, { rate: 0.85, volume: 0.85 });
|
||
await sleep(400);
|
||
}
|
||
// 读完后震动提示"可以选了"
|
||
if (this.awaitingChoice) Haptic.confirm();
|
||
}
|
||
|
||
_selectDetailChoice(dir) {
|
||
if (!this.awaitingChoice || !this.currentChoices) return;
|
||
const choice = this.currentChoices.find(c => c.dir === dir);
|
||
if (!choice) { Haptic.error(); return; }
|
||
|
||
this.awaitingChoice = false;
|
||
TTS.stop();
|
||
Haptic.select();
|
||
|
||
const msg = this.currentMsg;
|
||
|
||
if (choice.isSilence) {
|
||
// 玩家选择不说话
|
||
TTS.speak('(沉默)', { rate: 0.8, volume: 0.5 });
|
||
} else {
|
||
TTS.speak(`发送:${choice.label}`, { rate: 0.85, volume: 0.7 });
|
||
this.msgState[msg.id] = 'replied';
|
||
this.profile.onDetailReply();
|
||
}
|
||
|
||
if (choice.state) this._applyState(choice.state);
|
||
if (choice.profile) this.profile.add(choice.profile);
|
||
|
||
// 周南相关深度追踪
|
||
if (choice.state?.ZHOUNAN_DEPTH_ADD) {
|
||
this.vars.ZHOUNAN_DEPTH += 1;
|
||
}
|
||
if (choice.state?.ZHOUNAN_SHARE) {
|
||
this.vars.ZHOUNAN_SHARE = true;
|
||
}
|
||
|
||
this.state = 'playing';
|
||
this.currentChoices = null;
|
||
this._checkEndingConditions();
|
||
}
|
||
|
||
// ══════════════════════════════════════════
|
||
// 手势处理器
|
||
// ══════════════════════════════════════════
|
||
_onSingleTap() {
|
||
const s = this.state;
|
||
const msg = this.currentMsg;
|
||
|
||
if (s === 'ringing') {
|
||
this._answerCall(msg);
|
||
}
|
||
else if (s === 'msg_incoming') {
|
||
if (msg.type === 'system_notification') {
|
||
this._playSystemMessage(msg);
|
||
} else {
|
||
this._playMessage(msg);
|
||
}
|
||
}
|
||
else if (s === 'await_reply') {
|
||
// 单点 = 发"嗯"
|
||
if (msg?.type !== 'call') {
|
||
TTS.speak('发送:嗯。', { rate: 0.9, volume: 0.7 });
|
||
this.msgState[msg.id] = 'replied';
|
||
this.profile.onSimpleReply();
|
||
if (msg.stateOnStdReply) this._applyState(msg.stateOnStdReply);
|
||
this.state = 'playing';
|
||
this._checkEndingConditions();
|
||
}
|
||
}
|
||
else if (s === 'detail_reply' && this.awaitingChoice) {
|
||
// 在细回复中单点 = 重听选项
|
||
this._readChoices(this.currentChoices);
|
||
}
|
||
else if (s === 'inbox') {
|
||
this._inboxSelect();
|
||
}
|
||
else if (s === 'playing') {
|
||
const unread = this.vars.UNREAD;
|
||
if (unread > 0) {
|
||
TTS.speak(`${unread}条未读消息。上滑打开收件箱。`, { rate: 0.9, volume: 0.6 });
|
||
} else {
|
||
TTS.speak('暂无新消息。', { rate: 0.9, volume: 0.6 });
|
||
}
|
||
}
|
||
}
|
||
|
||
_onDoubleTap() {
|
||
const s = this.state;
|
||
const msg = this.currentMsg;
|
||
|
||
if (s === 'await_reply' && msg) {
|
||
// 双点 = 发标准回复
|
||
const reply = msg.standardReply || '嗯,好的。';
|
||
TTS.speak(`发送:${reply}`, { rate: 0.9, volume: 0.7 });
|
||
this.msgState[msg.id] = 'replied';
|
||
this.profile.onDetailReply();
|
||
if (msg.stateOnStdReply) this._applyState(msg.stateOnStdReply);
|
||
this.state = 'playing';
|
||
this._checkEndingConditions();
|
||
}
|
||
else if (s === 'msg_incoming') {
|
||
// 双点 = 直接回"嗯"不听
|
||
if (msg?.type === 'wechat_text') {
|
||
TTS.speak('发送:嗯。', { rate: 0.9, volume: 0.7 });
|
||
this.msgState[msg.id] = 'replied';
|
||
this.profile.onSimpleReply();
|
||
this.state = 'playing';
|
||
}
|
||
}
|
||
else if (s === 'ringing') {
|
||
// 双点来电 = 同样接听
|
||
this._answerCall(msg);
|
||
}
|
||
}
|
||
|
||
_onLongPress1s() {
|
||
const s = this.state;
|
||
const msg = this.currentMsg;
|
||
|
||
if (s === 'await_reply' && msg?.detailReplies) {
|
||
this._enterDetailReply(msg);
|
||
}
|
||
else if (s === 'in_call' && msg?.inCallChoices) {
|
||
this._startInCallChoice(msg);
|
||
}
|
||
else if (s === 'detail_reply' && this.awaitingChoice) {
|
||
// 重听选项
|
||
TTS.stop();
|
||
this._readChoices(this.currentChoices);
|
||
}
|
||
}
|
||
|
||
_onLongPress5s() {
|
||
// 强制挂断
|
||
if (this.state === 'in_call' || this.state === 'ringing') {
|
||
clearTimeout(this._ringTimer);
|
||
Snd.stopRingtone();
|
||
Snd.stopVoice();
|
||
Haptic.reject();
|
||
TTS.speak('已挂断。', { rate: 0.9, volume: 0.7 });
|
||
this.state = 'playing';
|
||
}
|
||
}
|
||
|
||
_onSwipe(dir) {
|
||
const s = this.state;
|
||
const msg = this.currentMsg;
|
||
|
||
// 细回复 / 通话选项:四个方向用于选择
|
||
if ((s === 'detail_reply' || (s === 'in_call' && this.awaitingChoice)) && this.currentChoices) {
|
||
this._selectDetailChoice(dir);
|
||
return;
|
||
}
|
||
|
||
// 下滑 = 查询当前状态(任意场景可用)
|
||
if (dir === 'down') {
|
||
this._announceStatus();
|
||
return;
|
||
}
|
||
|
||
// 上滑 = 打开收件箱(主屏幕时)
|
||
if (dir === 'up') {
|
||
if (s === 'playing') this._onInboxOpen();
|
||
return;
|
||
}
|
||
|
||
// 右滑 = 接听(来电时)
|
||
if (dir === 'right') {
|
||
if (s === 'ringing') this._answerCall(msg);
|
||
return;
|
||
}
|
||
|
||
// 左滑 = 拒接 / 挂断 / 忽略 / 已读不回 / 返回
|
||
if (dir === 'left') {
|
||
if (s === 'ringing') {
|
||
this._rejectCall(msg);
|
||
}
|
||
else if (s === 'in_call') {
|
||
Snd.stopVoice();
|
||
Haptic.reject();
|
||
TTS.speak('已挂断。', { rate: 0.9, volume: 0.7 });
|
||
this.state = 'playing';
|
||
this.currentMsg = null;
|
||
this.awaitingChoice = false;
|
||
}
|
||
else if (s === 'msg_incoming') {
|
||
if (msg?.type === 'system_notification') {
|
||
TTS.speak('已跳过。', { rate: 0.9, volume: 0.6 });
|
||
this.msgState[msg.id] = 'read';
|
||
this.profile.add(msg.profileOnSkip);
|
||
} else {
|
||
TTS.speak('(消息保持未读)', { rate: 0.85, volume: 0.5 });
|
||
}
|
||
this.state = 'playing';
|
||
this.currentMsg = null;
|
||
}
|
||
else if (s === 'await_reply' && msg) {
|
||
TTS.speak('(已读不回)', { rate: 0.85, volume: 0.6 });
|
||
this.msgState[msg.id] = 'read';
|
||
this.profile.onReadNoReply();
|
||
this.state = 'playing';
|
||
}
|
||
else if (s === 'inbox') {
|
||
this.state = 'playing';
|
||
this._inboxMessages = null;
|
||
TTS.speak('返回主屏幕。', { rate: 0.9, volume: 0.6 });
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── 下滑:播报当前状态和可用操作 ──
|
||
_announceStatus() {
|
||
const s = this.state;
|
||
const msg = this.currentMsg;
|
||
|
||
if (s === 'playing') {
|
||
const unread = this.vars.UNREAD;
|
||
if (unread > 0) {
|
||
TTS.speak(`主屏幕,${unread}条未读。上滑查看收件箱。`, { rate: 0.9, volume: 0.7 });
|
||
} else {
|
||
TTS.speak('主屏幕,暂无新消息。', { rate: 0.9, volume: 0.7 });
|
||
}
|
||
}
|
||
else if (s === 'ringing') {
|
||
TTS.speak(`${msg?.senderName}来电中。单点接听,左滑拒接。`, { rate: 0.9, volume: 0.7 });
|
||
}
|
||
else if (s === 'in_call') {
|
||
TTS.speak('通话中。左滑挂断。', { rate: 0.9, volume: 0.7 });
|
||
}
|
||
else if (s === 'msg_incoming') {
|
||
TTS.speak(`${msg?.senderName}的新消息。单点播放,左滑忽略。`, { rate: 0.9, volume: 0.7 });
|
||
}
|
||
else if (s === 'msg_playing') {
|
||
TTS.speak('消息播放中。', { rate: 0.9, volume: 0.7 });
|
||
}
|
||
else if (s === 'await_reply') {
|
||
TTS.speak(`${msg?.senderName}的消息已播完。单点回嗯,双点标准回复,长按细回复,左滑已读不回。`, { rate: 0.9, volume: 0.7 });
|
||
}
|
||
else if (s === 'detail_reply') {
|
||
TTS.speak('细回复选项中。滑动选择,单点重听。', { rate: 0.9, volume: 0.7 });
|
||
}
|
||
else if (s === 'inbox') {
|
||
TTS.speak('收件箱。单点进入最新消息,左滑返回。', { rate: 0.9, volume: 0.7 });
|
||
}
|
||
}
|
||
|
||
async _onInboxOpen() {
|
||
if (this.state === 'in_call') return;
|
||
|
||
const prevState = this.state;
|
||
this.state = 'inbox';
|
||
Haptic.system();
|
||
|
||
// 统计未读/未回
|
||
const unreadMsgs = MESSAGES.filter(m =>
|
||
!this.msgState[m.id] || this.msgState[m.id] === 'unread'
|
||
);
|
||
|
||
if (unreadMsgs.length === 0) {
|
||
await TTS.speak('收件箱已空。', { rate: 0.9, volume: 0.7 });
|
||
this.state = prevState;
|
||
return;
|
||
}
|
||
|
||
await TTS.speak(`收件箱:${unreadMsgs.length}条未读。`, { rate: 0.9, volume: 0.7 });
|
||
|
||
for (const m of unreadMsgs.slice(0, 5)) {
|
||
const sender = SENDERS[m.sender];
|
||
const type = m.type === 'call' ? '未接来电' : m.type === 'wechat_text' ? '文字' : '语音';
|
||
await TTS.speak(`${sender?.name || m.senderName},${type}。`, { rate: 0.85, volume: 0.7 });
|
||
await sleep(200);
|
||
}
|
||
|
||
await TTS.speak('单点进入最新一条,左滑返回。', { rate: 0.85, volume: 0.6 });
|
||
|
||
this._inboxMessages = unreadMsgs;
|
||
// 5秒后自动退出收件箱
|
||
setTimeout(() => {
|
||
if (this.state === 'inbox') {
|
||
this.state = prevState;
|
||
this._inboxMessages = null;
|
||
}
|
||
}, 8000);
|
||
}
|
||
|
||
_inboxSelect() {
|
||
if (!this._inboxMessages || this._inboxMessages.length === 0) {
|
||
this.state = 'playing';
|
||
return;
|
||
}
|
||
const msg = this._inboxMessages[0];
|
||
this._inboxMessages = null;
|
||
this.currentMsg = msg;
|
||
this.state = 'playing';
|
||
// 触发该消息
|
||
this._triggerMessage(msg);
|
||
}
|
||
|
||
// ══════════════════════════════════════════
|
||
// 状态变量应用
|
||
// ══════════════════════════════════════════
|
||
_applyState(changes) {
|
||
for (const [k, v] of Object.entries(changes)) {
|
||
if (k === 'ZHOUNAN_DEPTH_ADD') {
|
||
this.vars.ZHOUNAN_DEPTH += (v || 1);
|
||
} else if (v !== null) {
|
||
this.vars[k] = v;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ══════════════════════════════════════════
|
||
// 结局判定
|
||
// ══════════════════════════════════════════
|
||
_checkEndingConditions() {
|
||
// 只在消息16触发后真正判定结局
|
||
// 这里只做中途检测
|
||
const unread = Object.values(this.msgState).filter(s => s === 'unread').length;
|
||
|
||
// 结局D:未读≥8时在01:00前触发
|
||
if (unread >= 8 && Date.now() - this._startTime >= gt(1, 0) * 1000) {
|
||
if (!this._endingTriggered) this._triggerEnding('D');
|
||
}
|
||
}
|
||
|
||
async triggerFinalEnding() {
|
||
if (this._endingTriggered) return;
|
||
|
||
const v = this.vars;
|
||
const unread = Object.values(this.msgState).filter(s => s === 'unread' || !this.msgState[s]).length;
|
||
|
||
// 结局 E(隐藏)
|
||
if (v.ZHOUNAN_DEPTH >= 2 && v.ZHOUNAN_SHARE &&
|
||
(this.profile.label === 'engagement' || this.profile.label === 'nostalgia')) {
|
||
return this._triggerEnding('E');
|
||
}
|
||
|
||
// 结局 A:妈妈连接+自己录音+有回复
|
||
if (v.MOM_LINK && v.SELF_RECORD) {
|
||
return this._triggerEnding('A');
|
||
}
|
||
|
||
// 结局 B:拒了阿哲+回了周南
|
||
if (v.HE_BACK === false && v.ZHOUNAN_DEPTH >= 1) {
|
||
return this._triggerEnding('B');
|
||
}
|
||
|
||
// 结局 C:全部已读但回复≤2
|
||
const repliedCount = Object.values(this.msgState).filter(s => s === 'replied').length;
|
||
const readCount = Object.values(this.msgState).filter(s => s !== 'unread').length;
|
||
if (readCount >= MESSAGES.length - 2 && repliedCount <= 2) {
|
||
return this._triggerEnding('C');
|
||
}
|
||
|
||
// 结局 D(兜底)
|
||
this._triggerEnding('D');
|
||
}
|
||
|
||
async _triggerEnding(type) {
|
||
if (this._endingTriggered) return;
|
||
this._endingTriggered = true;
|
||
this.state = 'ending';
|
||
|
||
Snd.stopBGM(3000);
|
||
await sleep(2000);
|
||
|
||
const ENDINGS = {
|
||
A: this._endingA.bind(this),
|
||
B: this._endingB.bind(this),
|
||
C: this._endingC.bind(this),
|
||
D: this._endingD.bind(this),
|
||
E: this._endingE.bind(this),
|
||
};
|
||
|
||
if (ENDINGS[type]) await ENDINGS[type]();
|
||
}
|
||
|
||
async _endingA() {
|
||
// 妈妈语音播完→厨房→烧水
|
||
Snd.startAmbience(`${AMB}/amb_kitchen_morning_v1.mp3`, { vol: 0.3 });
|
||
await sleep(2000);
|
||
Snd.playVoice([`${SFX}/sfx_kettle_whistle_v1.mp3`]);
|
||
await sleep(4000);
|
||
this._showCaption('明天再说。');
|
||
await sleep(3000);
|
||
Snd.playVoice([`${SFX}/sfx_ending_chime_v1.mp3`]);
|
||
}
|
||
|
||
async _endingB() {
|
||
await TTS.speak('寄出。', { rate: 0.8, volume: 0.8 });
|
||
await sleep(4000);
|
||
this._showCaption('寄出。');
|
||
}
|
||
|
||
async _endingC() {
|
||
await TTS.speak(
|
||
`今晚共${MESSAGES.length}条新消息,已读${Object.values(this.msgState).filter(s => s !== 'unread').length},未回复${MESSAGES.length - Object.values(this.msgState).filter(s => s === 'replied').length}。`,
|
||
{ rate: 0.85, volume: 0.8 }
|
||
);
|
||
await sleep(6000);
|
||
this._showCaption('今晚就这样。');
|
||
}
|
||
|
||
async _endingD() {
|
||
Snd.playVoice([`${TDIR}/lv11_sys_battery_01_wav_v1.mp3`]);
|
||
await sleep(2000);
|
||
Haptic.ring();
|
||
await sleep(1000);
|
||
// 屏幕"彻底黑了"→清晨鸟叫
|
||
await sleep(3000);
|
||
Snd.startAmbience(`${AMB}/amb_dawn_birds_v1.mp3`, { vol: 0.4 });
|
||
await sleep(4000);
|
||
this._showCaption('早上好。');
|
||
}
|
||
|
||
async _endingE() {
|
||
// 周南秒回
|
||
await sleep(2000);
|
||
await TTS.speak('周南发来语音。', { rate: 0.9, volume: 0.7 });
|
||
await TTS.speak('那我下次出差来北京,请你吃饭好不好。', { rate: 0.9 });
|
||
await sleep(2000);
|
||
// 林夏的轻笑(用self3y音色)
|
||
Snd.playVoice([`${TDIR}/lv11_sys_goodnight_wav_v1.mp3`]);
|
||
await sleep(3000);
|
||
this._showCaption('好。');
|
||
await sleep(2000);
|
||
Snd.playVoice([`${SFX}/sfx_ending_chime_v1.mp3`]);
|
||
}
|
||
|
||
_showCaption(text) {
|
||
const el = document.getElementById('caption');
|
||
el.textContent = text;
|
||
el.style.display = 'flex';
|
||
}
|
||
}
|