从github迁移过来

This commit is contained in:
xsl
2026-05-18 18:39:49 +08:00
commit 9c5d9e586c
89 changed files with 9035 additions and 0 deletions
+774
View File
@@ -0,0 +1,774 @@
// 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.wav`, { 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.wav`]);
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.wav`]);
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.wav`, { vol: 0.3 });
await sleep(2000);
Snd.playVoice([`${SFX}/sfx_kettle_whistle_v1.wav`]);
await sleep(4000);
this._showCaption('明天再说。');
await sleep(3000);
Snd.playVoice([`${SFX}/sfx_ending_chime_v1.wav`]);
}
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.wav`]);
await sleep(2000);
Haptic.ring();
await sleep(1000);
// 屏幕"彻底黑了"→清晨鸟叫
await sleep(3000);
Snd.startAmbience(`${AMB}/amb_dawn_birds_v1.wav`, { 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.wav`]);
await sleep(3000);
this._showCaption('好。');
await sleep(2000);
Snd.playVoice([`${SFX}/sfx_ending_chime_v1.wav`]);
}
_showCaption(text) {
const el = document.getElementById('caption');
el.textContent = text;
el.style.display = 'flex';
}
}