新版本

This commit is contained in:
xsl
2026-05-19 00:34:23 +08:00
parent b90238975f
commit 0f1b70b1a9
124 changed files with 1543 additions and 170 deletions
+224 -93
View File
@@ -1,32 +1,40 @@
// engine.js — 游戏引擎(玩家驱动,无时间依赖
// engine.js — 游戏引擎 v2(多轮对话 + 林夏语音 + NPC反应
//
// 状态流:idle → notification/ringing → playing → choosing → idle → …… → ending
// 所有 UI 反馈使用合成短音效(SFX_UI),不使用 TTS 语音播报
// 状态流:idle → notification/ringing → playing → choosing
// → responding → reacting → [choosing | idle] → …… → ending
//
// 新增状态:
// responding — 林夏说话中(打字音效 + 语音播放)
// reacting — NPC反应播放中
class GameEngine {
constructor() {
// 状态(通过 setter 自动更新提示栏)
this._state = 'idle';
// 剧情变量
this.vars = {
MOM_LINK: false,
HE_BACK: null, // null=未定, true=来了, false=拒了
HE_BACK: null,
GROUP_REPLY: false,
ZHOUNAN_DEPTH: 0, // 0-3
ZHOUNAN_DEPTH: 0,
ZHOUNAN_SHARE: false,
SELF_RECORD: false,
UNREAD: 0,
};
this.profile = new ProfileTracker();
this.msgState = {}; // msgId → 'unread'|'read'|'replied'|'missed'
this.msgState = {};
this.currentMsg = null;
this._queue = []; // 消息队列
this._queue = [];
this._endingTriggered = false;
this._reminderTimer = null;
// v2: 对话系统
this._currentChoices = null; // 当前可用的选项集
this._pendingResolve = null; // 用于中断语音播放的 resolve
this._skipRequested = false; // 跳过标志
this.gest = null;
}
@@ -43,6 +51,8 @@ class GameEngine {
ringing: '单击 接听 · 左滑 拒接',
playing: '双击 跳过',
choosing: '→温暖 ←冷淡 ↑追问 ↓沉默',
responding: '双击 跳过',
reacting: '双击 跳过',
ending: '',
};
el.textContent = H[s] || '';
@@ -66,16 +76,48 @@ class GameEngine {
async start() {
this._queue = [...MESSAGES];
// 环境音 + BGM
await Snd.startAmbience(AMBIENCE_DEFAULT, { vol: 0.15 });
await sleep(1500);
Snd.startBGM(`${MUS}/lv11_bgm_m1_main_loop_v1.mp3`, { vol: 0.2 });
await sleep(1500);
// 开始第一条消息
this._next();
}
// ══════════════════════════════════════════
// 语音播放(异步 + 可中断)
// ══════════════════════════════════════════
_playVoiceAsync(urls) {
return new Promise(resolve => {
this._pendingResolve = resolve;
Snd.playVoice(urls, {
onEnd: () => {
this._pendingResolve = null;
resolve();
},
});
});
}
_interruptVoice() {
Snd.stopVoice();
TTS.stop();
if (this._pendingResolve) {
this._pendingResolve();
this._pendingResolve = null;
}
}
// 打字 + 发送音效
async _playTypingSend() {
SFX_UI.typing();
await sleep(800);
if (this._skipRequested) return;
SFX_UI.send();
await sleep(300);
}
// ══════════════════════════════════════════
// 消息队列推进
// ══════════════════════════════════════════
@@ -89,13 +131,13 @@ class GameEngine {
return;
}
// 消息16需要 MOM_LINK
if (msg.requiresMomLink && !this.vars.MOM_LINK) {
this.triggerFinalEnding();
return;
}
this.currentMsg = msg;
this._currentChoices = msg.choices || null;
this.msgState[msg.id] = 'unread';
this.vars.UNREAD++;
@@ -106,7 +148,6 @@ class GameEngine {
}
}
// ── 非通话消息通知 ──
_notifyMessage(msg) {
this.state = 'notification';
Haptic.message();
@@ -114,7 +155,6 @@ class GameEngine {
this._startReminder();
}
// ── 来电铃声 ──
async _startRinging(msg) {
this.state = 'ringing';
Haptic.ring();
@@ -123,10 +163,10 @@ class GameEngine {
}
// ══════════════════════════════════════════
// 通话流程
// 通话流程(异步)
// ══════════════════════════════════════════
_answerCall(msg) {
async _answerCall(msg) {
Snd.stopRingtone();
Haptic.stop();
Haptic.confirm();
@@ -140,22 +180,57 @@ class GameEngine {
if (msg.stateOnAnswer) this._applyState(msg.stateOnAnswer);
if (msg.profileOnAnswer) this.profile.add(msg.profileOnAnswer);
Snd.playVoice(msg.audio, {
onEnd: () => {
if (this.state !== 'playing') return; // 已被跳过
if (msg.autoHangup && msg.sfxAfter) {
const sfx = Array.isArray(msg.sfxAfter) ? msg.sfxAfter[0] : msg.sfxAfter;
Snd.playVoice([sfx], { onEnd: () => this._proceedToNext() });
} else if (msg.choices) {
this.state = 'choosing';
SFX_UI.choiceReady();
Haptic.confirm();
this._startReminder();
} else {
this._proceedToNext();
this._skipRequested = false;
// 播放通话内容
await this._playVoiceAsync(msg.audio);
// _onDoubleTap 在 'playing' 状态下会直接处理状态转换
// 所以如果状态已不是 'playing',说明跳过已被处理,直接返回
if (this.state !== 'playing') return;
// 自动挂断(MSG-08 未知号码)
if (msg.autoHangup) {
if (msg.sfxAfter) {
const sfx = Array.isArray(msg.sfxAfter) ? msg.sfxAfter[0] : msg.sfxAfter;
await this._playVoiceAsync([sfx]);
if (this.state !== 'playing') return;
}
this._proceedToNext();
return;
}
// 自动回复(MSG-01/03/14 妈妈电话、外卖、妈妈第二次电话)
if (msg.autoReply) {
await this._playTypingSend();
if (this.state !== 'playing') return;
this.state = 'responding';
await this._playVoiceAsync([msg.autoReply]);
// 条件反应(MSG-14:根据 MOM_LINK 播放不同反应)
if (!this._skipRequested && msg.autoReact) {
const val = this.vars[msg.autoReact.condition];
const reactUrl = val ? msg.autoReact['true'] : msg.autoReact['false'];
if (reactUrl) {
this.state = 'reacting';
await sleep(500);
if (!this._skipRequested) {
await this._playVoiceAsync([reactUrl]);
}
}
}
});
this._proceedToNext();
return;
}
// 通话后有选择(MSG-11 阿哲)
if (msg.choices) {
this._enterChoosing(msg.choices);
return;
}
this._proceedToNext();
}
_rejectCall(msg) {
@@ -169,7 +244,7 @@ class GameEngine {
}
// ══════════════════════════════════════════
// 消息播放
// 消息播放(异步)
// ══════════════════════════════════════════
async _playMessage(msg) {
@@ -186,32 +261,27 @@ class GameEngine {
return;
}
// 文字消息(用 TTS 朗读内容)
if (msg.type === 'wechat_text' && msg.text && (!msg.audio || msg.audio.length === 0)) {
TTS.speak(msg.text).then(() => {
if (this.state === 'playing') this._afterPlayed(msg);
});
return;
this._skipRequested = false;
// 文字消息(TTS 朗读)
if (msg.type === 'wechat_text' && msg.text) {
await TTS.speak(msg.text);
}
// 语音消息
else if (msg.audio?.length > 0) {
await this._playVoiceAsync(msg.audio);
}
// 语音消息
if (msg.audio && msg.audio.length > 0) {
Snd.playVoice(msg.audio, {
onEnd: () => {
if (this.state === 'playing') this._afterPlayed(msg);
}
});
} else {
this._afterPlayed(msg);
}
if (this._skipRequested) return;
if (this.state !== 'playing') return;
this._afterPlayed(msg);
}
// ── 系统通知特殊处理 ──
_playSystemNotification(msg) {
Snd.playVoice([msg.systemAudio], {
onEnd: () => {
if (this.state !== 'playing') return;
// 根据画像播放对应版本
const audio = msg.audioByProfile?.[this.profile.label];
if (audio) {
if (msg.stateOnPlay) this._applyState(msg.stateOnPlay);
@@ -219,19 +289,18 @@ class GameEngine {
setTimeout(() => {
if (this.state !== 'playing') return;
Snd.playVoice([audio], {
onEnd: () => this._proceedToNext()
onEnd: () => this._proceedToNext(),
});
}, 800);
} else {
this._proceedToNext();
}
}
},
});
}
// ── 消息播放完成 ──
_afterPlayed(msg) {
if (this.state !== 'playing') return; // 防止重复调用
if (this.state !== 'playing') return;
if (msg.isEnding) {
this.triggerFinalEnding();
@@ -239,28 +308,34 @@ class GameEngine {
}
if (msg.choices) {
this.state = 'choosing';
SFX_UI.choiceReady();
Haptic.confirm();
this._startReminder();
} else {
this._proceedToNext();
this._enterChoosing(msg.choices);
return;
}
this._proceedToNext();
}
// ══════════════════════════════════════════
// 选择回复
// 选择系统(v2 多轮对话核心)
// ══════════════════════════════════════════
_choose(dir) {
const msg = this.currentMsg;
if (!msg?.choices) {
_enterChoosing(choices) {
this._currentChoices = choices;
this.state = 'choosing';
SFX_UI.choiceReady();
Haptic.confirm();
this._startReminder();
}
async _handleChoice(dir) {
const choices = this._currentChoices;
if (!choices) {
SFX_UI.error();
Haptic.error();
return;
}
const choice = msg.choices[dir];
const choice = choices[dir];
if (!choice) {
SFX_UI.error();
Haptic.error();
@@ -268,8 +343,9 @@ class GameEngine {
}
this._clearReminder();
this._skipRequested = false;
// 播放方向对应的情感音效
// 1. 播放方向对应的情感音效
const sfxMap = {
tap: 'tap',
right: 'choiceWarm',
@@ -280,13 +356,63 @@ class GameEngine {
if (sfxMap[dir]) SFX_UI[sfxMap[dir]]();
Haptic.select();
// 2. 应用状态和画像
if (!choice.isSilence) {
this.msgState[msg.id] = 'replied';
this.msgState[this.currentMsg.id] = 'replied';
}
if (choice.state) this._applyState(choice.state);
if (choice.profile) this.profile.add(choice.profile);
// 3. 林夏回应
this.state = 'responding';
if (choice.isSilence) {
// 沉默:短暂停顿
await sleep(1500);
} else if (choice.linxia) {
// 打字 → 发送 → 林夏语音
await this._playTypingSend();
if (!this._skipRequested) {
await this._playVoiceAsync([choice.linxia]);
}
}
if (this._skipRequested) {
// 跳过了林夏回应 → 如有 followUp 进入选择,否则跳到下一条
if (choice.followUp?.choices) {
this._enterChoosing(choice.followUp.choices);
return;
}
this._proceedToNext();
return;
}
// 4. NPC 反应
if (choice.npcReact?.length > 0) {
this.state = 'reacting';
await sleep(400);
if (!this._skipRequested) {
await this._playVoiceAsync(choice.npcReact);
}
}
if (this._skipRequested) {
if (choice.followUp?.choices) {
this._enterChoosing(choice.followUp.choices);
return;
}
this._proceedToNext();
return;
}
// 5. 如有后续对话 → 再次进入选择
if (choice.followUp?.choices) {
await sleep(300);
this._enterChoosing(choice.followUp.choices);
return;
}
// 6. 推进到下一条
this._proceedToNext();
}
@@ -305,6 +431,7 @@ class GameEngine {
async _proceedToNext() {
const prev = this.currentMsg;
this.currentMsg = null;
this._currentChoices = null;
this.state = 'idle';
this._clearReminder();
@@ -318,7 +445,7 @@ class GameEngine {
await sleep(3000);
}
// 检查结局D:未读过多14条消息处理后检查)
// 检查结局D:未读过多
const triggered = MESSAGES.length - this._queue.length;
if (this.vars.UNREAD >= 8 && triggered >= 14) {
this._triggerEnding('D');
@@ -332,7 +459,7 @@ class GameEngine {
}
// ══════════════════════════════════════════
// 提醒定时器(玩家不操作时重复提示音)
// 提醒定时器
// ══════════════════════════════════════════
_startReminder() {
@@ -379,23 +506,26 @@ class GameEngine {
_onTap() {
const s = this.state;
if (s === 'ringing') this._answerCall(this.currentMsg);
else if (s === 'notification') {
if (s === 'ringing') {
this._answerCall(this.currentMsg);
} else if (s === 'notification') {
SFX_UI.tap();
Haptic.confirm();
this._playMessage(this.currentMsg);
} else if (s === 'choosing') {
this._handleChoice('tap');
}
else if (s === 'choosing') this._choose('tap');
}
_onDoubleTap() {
const s = this.state;
if (s === 'ringing') {
// 双击来电 = 拒接
this._rejectCall(this.currentMsg);
return;
}
else if (s === 'notification') {
// 跳过消息
if (s === 'notification') {
this._clearReminder();
SFX_UI.skip();
Haptic.confirm();
@@ -403,28 +533,36 @@ class GameEngine {
if (msg?.profileOnSkip) this.profile.add(msg.profileOnSkip);
else this.profile.add({ avoidance: 1 });
this._proceedToNext();
return;
}
else if (s === 'playing') {
// 跳过当前播放
Snd.stopVoice();
TTS.stop();
if (s === 'playing') {
this._skipRequested = true;
this._interruptVoice();
SFX_UI.skip();
Haptic.confirm();
const msg = this.currentMsg;
// 直接进入后续流程
if (msg?.isEnding) {
this.triggerFinalEnding();
} else if (msg?.choices) {
this.state = 'choosing';
SFX_UI.choiceReady();
Haptic.confirm();
this._startReminder();
this._enterChoosing(msg.choices);
} else {
this._proceedToNext();
}
return;
}
else if (s === 'choosing') {
if (s === 'responding' || s === 'reacting') {
this._skipRequested = true;
this._interruptVoice();
SFX_UI.skip();
Haptic.confirm();
return;
}
if (s === 'choosing') {
this._skipChoice();
return;
}
}
@@ -439,7 +577,6 @@ class GameEngine {
if (s === 'notification') {
if (dir === 'left') {
// 左滑忽略消息
this._clearReminder();
SFX_UI.skip();
Haptic.confirm();
@@ -450,7 +587,7 @@ class GameEngine {
}
if (s === 'choosing') {
this._choose(dir);
this._handleChoice(dir);
return;
}
}
@@ -497,9 +634,8 @@ class GameEngine {
this.state = 'ending';
this._clearReminder();
Snd.stopVoice();
this._interruptVoice();
Snd.stopRingtone();
TTS.stop();
Snd.stopBGM(3000);
await sleep(2000);
@@ -515,7 +651,6 @@ class GameEngine {
if (fn) await fn();
}
// ── 结局A:明天再说 ──
async _endingA() {
Snd.startAmbience(`${AMB}/amb_kitchen_morning_v1.mp3`, { vol: 0.3 });
await sleep(2000);
@@ -525,7 +660,6 @@ class GameEngine {
this._showCaption('明天再说。');
}
// ── 结局B:寄出 ──
async _endingB() {
await sleep(2000);
SFX_UI.ending();
@@ -533,7 +667,6 @@ class GameEngine {
this._showCaption('寄出。');
}
// ── 结局C:今晚就这样 ──
async _endingC() {
await sleep(3000);
SFX_UI.ending();
@@ -541,7 +674,6 @@ class GameEngine {
this._showCaption('今晚就这样。');
}
// ── 结局D:早上好 ──
async _endingD() {
Haptic.system();
await sleep(2000);
@@ -552,7 +684,6 @@ class GameEngine {
this._showCaption('早上好。');
}
// ── 结局E(隐藏):好 ──
async _endingE() {
Haptic.message();
SFX_UI.notify();