游戏重构
This commit is contained in:
+15
-20
@@ -76,7 +76,7 @@
|
|||||||
transition:opacity 0.4s;
|
transition:opacity 0.4s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 调试条(开发用) ── */
|
/* ── 调试条 ── */
|
||||||
#dbg{
|
#dbg{
|
||||||
display:none;
|
display:none;
|
||||||
position:fixed;bottom:30px;left:0;right:0;text-align:center;
|
position:fixed;bottom:30px;left:0;right:0;text-align:center;
|
||||||
@@ -97,43 +97,38 @@
|
|||||||
<div id="controls">
|
<div id="controls">
|
||||||
<div class="ctrl-title">操 作 说 明</div>
|
<div class="ctrl-title">操 作 说 明</div>
|
||||||
|
|
||||||
<div class="ctrl-group">
|
|
||||||
<div class="ctrl-scene">通用(随时可用)</div>
|
|
||||||
<div class="ctrl-row"><span class="ctrl-gesture">下滑</span><span class="ctrl-action">查看当前状态</span></div>
|
|
||||||
<div class="ctrl-row"><span class="ctrl-gesture">上滑</span><span class="ctrl-action">打开收件箱</span></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ctrl-group">
|
<div class="ctrl-group">
|
||||||
<div class="ctrl-scene">来电时</div>
|
<div class="ctrl-scene">来电时</div>
|
||||||
<div class="ctrl-row"><span class="ctrl-gesture">单点</span><span class="ctrl-action">接听</span></div>
|
<div class="ctrl-row"><span class="ctrl-gesture">单击 / 右滑</span><span class="ctrl-action">接听</span></div>
|
||||||
<div class="ctrl-row"><span class="ctrl-gesture">左滑</span><span class="ctrl-action">拒接</span></div>
|
<div class="ctrl-row"><span class="ctrl-gesture">左滑 / 双击</span><span class="ctrl-action">拒接</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ctrl-group">
|
<div class="ctrl-group">
|
||||||
<div class="ctrl-scene">收到消息</div>
|
<div class="ctrl-scene">收到消息</div>
|
||||||
<div class="ctrl-row"><span class="ctrl-gesture">单点</span><span class="ctrl-action">播放</span></div>
|
<div class="ctrl-row"><span class="ctrl-gesture">单击</span><span class="ctrl-action">播放</span></div>
|
||||||
<div class="ctrl-row"><span class="ctrl-gesture">左滑</span><span class="ctrl-action">忽略</span></div>
|
<div class="ctrl-row"><span class="ctrl-gesture">双击 / 左滑</span><span class="ctrl-action">跳过</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ctrl-group">
|
<div class="ctrl-group">
|
||||||
<div class="ctrl-scene">听完消息</div>
|
<div class="ctrl-scene">选择回复</div>
|
||||||
<div class="ctrl-row"><span class="ctrl-gesture">单点</span><span class="ctrl-action">回复"嗯"</span></div>
|
<div class="ctrl-row"><span class="ctrl-gesture">单击</span><span class="ctrl-action">简单回应</span></div>
|
||||||
<div class="ctrl-row"><span class="ctrl-gesture">双点</span><span class="ctrl-action">标准回复</span></div>
|
<div class="ctrl-row"><span class="ctrl-gesture">右滑</span><span class="ctrl-action">温暖回应</span></div>
|
||||||
<div class="ctrl-row"><span class="ctrl-gesture">长按</span><span class="ctrl-action">更多回复选项</span></div>
|
<div class="ctrl-row"><span class="ctrl-gesture">左滑</span><span class="ctrl-action">冷淡回应</span></div>
|
||||||
<div class="ctrl-row"><span class="ctrl-gesture">左滑</span><span class="ctrl-action">已读不回</span></div>
|
<div class="ctrl-row"><span class="ctrl-gesture">上滑</span><span class="ctrl-action">追问</span></div>
|
||||||
|
<div class="ctrl-row"><span class="ctrl-gesture">下滑</span><span class="ctrl-action">沉默</span></div>
|
||||||
|
<div class="ctrl-row"><span class="ctrl-gesture">双击</span><span class="ctrl-action">不回复</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ctrl-group">
|
<div class="ctrl-group">
|
||||||
<div class="ctrl-scene">选择回复时</div>
|
<div class="ctrl-scene">播放中</div>
|
||||||
<div class="ctrl-row"><span class="ctrl-gesture">上下左右滑</span><span class="ctrl-action">选择选项</span></div>
|
<div class="ctrl-row"><span class="ctrl-gesture">双击</span><span class="ctrl-action">跳过</span></div>
|
||||||
<div class="ctrl-row"><span class="ctrl-gesture">单点</span><span class="ctrl-action">重听选项</span></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hint">· 轻触屏幕开始 ·</div>
|
<div class="hint">· 轻触屏幕开始 ·</div>
|
||||||
<div class="tip">
|
<div class="tip">
|
||||||
建议关灯 · 戴上耳机<br>
|
建议关灯 · 戴上耳机<br>
|
||||||
全程约 60 分钟
|
按你的节奏来
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+120
-62
@@ -1,15 +1,14 @@
|
|||||||
// audio.js — 音频管理器(HTML Audio + Web Audio API 混用)
|
// audio.js — 音频管理器 + 合成 UI 音效
|
||||||
|
|
||||||
class AudioManager {
|
class AudioManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._ctx = null;
|
this._ctx = null;
|
||||||
this._master = null;
|
this._master = null;
|
||||||
this._bgmNode = null;
|
|
||||||
this._ambNode = null;
|
|
||||||
this._voiceQueue = [];
|
this._voiceQueue = [];
|
||||||
this._voiceEl = null;
|
this._voiceEl = null;
|
||||||
this._ringEl = null;
|
this._ringEl = null;
|
||||||
this._ringTimer = null;
|
this._bgmEl = null;
|
||||||
|
this._ambEl = null;
|
||||||
this._preloaded = {};
|
this._preloaded = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,19 +21,44 @@ class AudioManager {
|
|||||||
this._master.connect(this._ctx.destination);
|
this._master.connect(this._ctx.destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 预加载短音效(铃声、UI音)──
|
// ── 合成短音(UI 反馈音效)──
|
||||||
|
synth({ freq = 440, freq2 = null, dur = 100, type = 'sine', vol = 0.3 } = {}) {
|
||||||
|
if (!this._ctx) return;
|
||||||
|
const t = this._ctx.currentTime;
|
||||||
|
const d = dur / 1000;
|
||||||
|
|
||||||
|
const osc = this._ctx.createOscillator();
|
||||||
|
const gain = this._ctx.createGain();
|
||||||
|
|
||||||
|
osc.type = type;
|
||||||
|
osc.frequency.setValueAtTime(freq, t);
|
||||||
|
if (freq2 != null) {
|
||||||
|
osc.frequency.linearRampToValueAtTime(freq2, t + d);
|
||||||
|
}
|
||||||
|
|
||||||
|
gain.gain.setValueAtTime(vol, t);
|
||||||
|
gain.gain.setValueAtTime(vol, t + d * 0.75);
|
||||||
|
gain.gain.linearRampToValueAtTime(0, t + d);
|
||||||
|
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(this._master);
|
||||||
|
osc.start(t);
|
||||||
|
osc.stop(t + d + 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 预加载短音效 ──
|
||||||
async preload(key, url) {
|
async preload(key, url) {
|
||||||
if (this._preloaded[key]) return;
|
if (this._preloaded[key]) return;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
const buf = await res.arrayBuffer();
|
const buf = await res.arrayBuffer();
|
||||||
this._preloaded[key] = await this._ctx.decodeAudioData(buf);
|
this._preloaded[key] = await this._ctx.decodeAudioData(buf);
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
console.warn('[Audio] preload failed:', key, e);
|
console.warn('[Audio] preload failed:', key, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 播放预加载的短音效 ──
|
// ── 播放预加载音效 ──
|
||||||
playFX(key, { vol = 1.0, loop = false } = {}) {
|
playFX(key, { vol = 1.0, loop = false } = {}) {
|
||||||
const buf = this._preloaded[key];
|
const buf = this._preloaded[key];
|
||||||
if (!buf) return null;
|
if (!buf) return null;
|
||||||
@@ -46,17 +70,16 @@ class AudioManager {
|
|||||||
src.connect(gain);
|
src.connect(gain);
|
||||||
gain.connect(this._master);
|
gain.connect(this._master);
|
||||||
src.start();
|
src.start();
|
||||||
return { src, gain, stop: () => { try { src.stop(); } catch(_){} } };
|
return { src, gain, stop: () => { try { src.stop(); } catch (_) {} } };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 铃声(循环直到 stopRingtone)──
|
// ── 铃声(循环)──
|
||||||
async startRingtone(url) {
|
async startRingtone(url) {
|
||||||
this.stopRingtone();
|
this.stopRingtone();
|
||||||
// 使用 HTMLAudio 以支持更大文件
|
|
||||||
this._ringEl = new Audio(url);
|
this._ringEl = new Audio(url);
|
||||||
this._ringEl.loop = true;
|
this._ringEl.loop = true;
|
||||||
this._ringEl.volume = 1.0;
|
this._ringEl.volume = 1.0;
|
||||||
try { await this._ringEl.play(); } catch(e) { console.warn('[Audio] ring play error', e); }
|
try { await this._ringEl.play(); } catch (e) { console.warn('[Audio] ring error', e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
stopRingtone() {
|
stopRingtone() {
|
||||||
@@ -65,17 +88,16 @@ class AudioManager {
|
|||||||
this._ringEl.src = '';
|
this._ringEl.src = '';
|
||||||
this._ringEl = null;
|
this._ringEl = null;
|
||||||
}
|
}
|
||||||
if (this._ringTimer) { clearTimeout(this._ringTimer); this._ringTimer = null; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 语音消息播放(支持多段顺序播放)──
|
// ── 语音播放(多段顺序)──
|
||||||
playVoice(urls, { vol = 1.0, onEnd = null, onSegEnd = null } = {}) {
|
playVoice(urls, { vol = 1.0, onEnd = null } = {}) {
|
||||||
this.stopVoice();
|
this.stopVoice();
|
||||||
this._voiceQueue = [...urls];
|
this._voiceQueue = [...urls];
|
||||||
this._playNextSegment(vol, onEnd, onSegEnd);
|
this._playNext(vol, onEnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
_playNextSegment(vol, onEnd, onSegEnd) {
|
_playNext(vol, onEnd) {
|
||||||
if (this._voiceQueue.length === 0) {
|
if (this._voiceQueue.length === 0) {
|
||||||
this._voiceEl = null;
|
this._voiceEl = null;
|
||||||
if (onEnd) onEnd();
|
if (onEnd) onEnd();
|
||||||
@@ -85,22 +107,19 @@ class AudioManager {
|
|||||||
const el = new Audio(url);
|
const el = new Audio(url);
|
||||||
el.volume = vol;
|
el.volume = vol;
|
||||||
this._voiceEl = el;
|
this._voiceEl = el;
|
||||||
el.onended = () => {
|
el.onended = () => this._playNext(vol, onEnd);
|
||||||
if (onSegEnd) onSegEnd();
|
|
||||||
this._playNextSegment(vol, onEnd, onSegEnd);
|
|
||||||
};
|
|
||||||
el.onerror = () => {
|
el.onerror = () => {
|
||||||
console.warn('[Audio] voice error:', url);
|
console.warn('[Audio] voice error:', url);
|
||||||
this._playNextSegment(vol, onEnd, onSegEnd);
|
this._playNext(vol, onEnd);
|
||||||
};
|
};
|
||||||
el.play().catch(e => console.warn('[Audio] voice play error', e));
|
el.play().catch(e => console.warn('[Audio] play error', e));
|
||||||
}
|
}
|
||||||
|
|
||||||
stopVoice() {
|
stopVoice() {
|
||||||
this._voiceQueue = [];
|
this._voiceQueue = [];
|
||||||
if (this._voiceEl) {
|
if (this._voiceEl) {
|
||||||
this._voiceEl.pause();
|
|
||||||
this._voiceEl.onended = null;
|
this._voiceEl.onended = null;
|
||||||
|
this._voiceEl.pause();
|
||||||
this._voiceEl = null;
|
this._voiceEl = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,7 +130,6 @@ class AudioManager {
|
|||||||
|
|
||||||
// ── BGM(淡入淡出切换)──
|
// ── BGM(淡入淡出切换)──
|
||||||
async startBGM(url, { vol = 0.35, fade = 2000 } = {}) {
|
async startBGM(url, { vol = 0.35, fade = 2000 } = {}) {
|
||||||
// 先淡出现有
|
|
||||||
if (this._bgmEl) {
|
if (this._bgmEl) {
|
||||||
const old = this._bgmEl;
|
const old = this._bgmEl;
|
||||||
this._fadeOut(old, fade / 2).then(() => { old.pause(); old.src = ''; });
|
this._fadeOut(old, fade / 2).then(() => { old.pause(); old.src = ''; });
|
||||||
@@ -123,7 +141,7 @@ class AudioManager {
|
|||||||
try {
|
try {
|
||||||
await el.play();
|
await el.play();
|
||||||
this._fadeTo(el, vol, fade);
|
this._fadeTo(el, vol, fade);
|
||||||
} catch(e) { console.warn('[Audio] BGM error', e); }
|
} catch (e) { console.warn('[Audio] BGM error', e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
stopBGM(fade = 2000) {
|
stopBGM(fade = 2000) {
|
||||||
@@ -134,21 +152,18 @@ class AudioManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 环境音(底噪循环)──
|
// ── 环境音 ──
|
||||||
async startAmbience(url, { vol = 0.25 } = {}) {
|
async startAmbience(url, { vol = 0.25 } = {}) {
|
||||||
if (this._ambEl) { this._ambEl.pause(); this._ambEl.src = ''; }
|
if (this._ambEl) { this._ambEl.pause(); this._ambEl.src = ''; }
|
||||||
const el = new Audio(url);
|
const el = new Audio(url);
|
||||||
el.loop = true;
|
el.loop = true;
|
||||||
el.volume = vol;
|
el.volume = vol;
|
||||||
this._ambEl = el;
|
this._ambEl = el;
|
||||||
try { await el.play(); } catch(e) { console.warn('[Audio] ambience error', e); }
|
try { await el.play(); } catch (e) { console.warn('[Audio] ambience error', e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
stopAmbience() {
|
stopAmbience() {
|
||||||
if (this._ambEl) {
|
if (this._ambEl) { this._ambEl.pause(); this._ambEl.src = ''; this._ambEl = null; }
|
||||||
this._ambEl.pause(); this._ambEl.src = '';
|
|
||||||
this._ambEl = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 音量渐变 ──
|
// ── 音量渐变 ──
|
||||||
@@ -160,8 +175,7 @@ class AudioManager {
|
|||||||
let i = 0;
|
let i = 0;
|
||||||
const t = setInterval(() => {
|
const t = setInterval(() => {
|
||||||
el.volume = Math.max(0, Math.min(1, start + dv * i));
|
el.volume = Math.max(0, Math.min(1, start + dv * i));
|
||||||
i++;
|
if (++i >= steps) clearInterval(t);
|
||||||
if (i >= steps) clearInterval(t);
|
|
||||||
}, dt);
|
}, dt);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,38 +188,82 @@ class AudioManager {
|
|||||||
let i = 0;
|
let i = 0;
|
||||||
const t = setInterval(() => {
|
const t = setInterval(() => {
|
||||||
el.volume = Math.max(0, start - dv * i);
|
el.volume = Math.max(0, start - dv * i);
|
||||||
i++;
|
if (++i >= steps) { clearInterval(t); resolve(); }
|
||||||
if (i >= steps) { clearInterval(t); resolve(); }
|
|
||||||
}, dt);
|
}, dt);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 键盘声(空间音频:左耳)──
|
|
||||||
startTyping() {
|
|
||||||
if (this._typingNode) return;
|
|
||||||
const buf = this._preloaded['sfx_keyboard_slow'];
|
|
||||||
if (!buf) return;
|
|
||||||
const src = this._ctx.createBufferSource();
|
|
||||||
src.buffer = buf;
|
|
||||||
src.loop = true;
|
|
||||||
const gain = this._ctx.createGain();
|
|
||||||
gain.gain.value = 0.15;
|
|
||||||
// 偏左声道
|
|
||||||
const pan = this._ctx.createStereoPanner();
|
|
||||||
pan.pan.value = -0.8;
|
|
||||||
src.connect(gain);
|
|
||||||
gain.connect(pan);
|
|
||||||
pan.connect(this._master);
|
|
||||||
src.start();
|
|
||||||
this._typingNode = { src, gain, pan };
|
|
||||||
}
|
|
||||||
|
|
||||||
stopTyping() {
|
|
||||||
if (this._typingNode) {
|
|
||||||
try { this._typingNode.src.stop(); } catch(_) {}
|
|
||||||
this._typingNode = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Snd = new AudioManager();
|
const Snd = new AudioManager();
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════
|
||||||
|
// SFX_UI — 合成 UI 音效(不依赖音频文件)
|
||||||
|
// ══════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const SFX_UI = {
|
||||||
|
// 确认操作(单击)
|
||||||
|
tap() {
|
||||||
|
Snd.synth({ freq: 880, dur: 45, vol: 0.2 });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 滑动确认
|
||||||
|
swipe() {
|
||||||
|
Snd.synth({ freq: 660, dur: 60, vol: 0.18 });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 无效操作
|
||||||
|
error() {
|
||||||
|
Snd.synth({ freq: 200, dur: 100, type: 'square', vol: 0.1 });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 新消息到达
|
||||||
|
notify() {
|
||||||
|
Snd.synth({ freq: 523, dur: 100, vol: 0.25 });
|
||||||
|
setTimeout(() => Snd.synth({ freq: 659, dur: 130, vol: 0.2 }), 130);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 进入选择模式(三音上行)
|
||||||
|
choiceReady() {
|
||||||
|
Snd.synth({ freq: 440, dur: 80, type: 'triangle', vol: 0.18 });
|
||||||
|
setTimeout(() => Snd.synth({ freq: 523, dur: 80, type: 'triangle', vol: 0.18 }), 100);
|
||||||
|
setTimeout(() => Snd.synth({ freq: 659, dur: 120, type: 'triangle', vol: 0.18 }), 200);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 选择:温暖/接受(右滑)— 上行音
|
||||||
|
choiceWarm() {
|
||||||
|
Snd.synth({ freq: 440, freq2: 659, dur: 140, vol: 0.22 });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 选择:冷淡/拒绝(左滑)— 下行音
|
||||||
|
choiceCold() {
|
||||||
|
Snd.synth({ freq: 523, freq2: 349, dur: 140, vol: 0.22 });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 选择:好奇/追问(上滑)— 快速上行
|
||||||
|
choiceCurious() {
|
||||||
|
Snd.synth({ freq: 523, freq2: 784, dur: 120, vol: 0.22 });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 选择:沉默/退缩(下滑)— 极轻低音
|
||||||
|
choiceSilent() {
|
||||||
|
Snd.synth({ freq: 330, dur: 200, vol: 0.08 });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 跳过 / 忽略
|
||||||
|
skip() {
|
||||||
|
Snd.synth({ freq: 587, freq2: 392, dur: 80, vol: 0.12 });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 下一条消息即将到来
|
||||||
|
next() {
|
||||||
|
Snd.synth({ freq: 440, dur: 80, vol: 0.1 });
|
||||||
|
setTimeout(() => Snd.synth({ freq: 523, dur: 100, vol: 0.13 }), 120);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 结局音效
|
||||||
|
ending() {
|
||||||
|
Snd.synth({ freq: 523, dur: 400, vol: 0.25 });
|
||||||
|
setTimeout(() => Snd.synth({ freq: 659, dur: 400, vol: 0.2 }), 450);
|
||||||
|
setTimeout(() => Snd.synth({ freq: 784, dur: 600, vol: 0.15 }), 900);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
+353
-576
File diff suppressed because it is too large
Load Diff
+62
-69
@@ -1,128 +1,121 @@
|
|||||||
// gestures.js — 触控手势检测器(单指操作)
|
// gestures.js — 触控手势检测器(简化版)
|
||||||
//
|
//
|
||||||
// 支持:单点 · 双点 · 长按1s · 长按5s
|
// 仅支持 6 种手势:单击 · 双击 · 左滑 · 右滑 · 上滑 · 下滑
|
||||||
// 左滑 · 右滑 · 上滑 · 下滑
|
// 无长按,无多指操作
|
||||||
|
|
||||||
class GestureDetector {
|
class GestureDetector {
|
||||||
constructor(el) {
|
constructor(el) {
|
||||||
this._el = el;
|
this._el = el;
|
||||||
this._handlers = {};
|
this._handlers = {};
|
||||||
|
|
||||||
// 状态
|
|
||||||
this._startX = 0;
|
this._startX = 0;
|
||||||
this._startY = 0;
|
this._startY = 0;
|
||||||
this._startTime = 0;
|
this._startTime = 0;
|
||||||
|
|
||||||
this._tapTimer = null;
|
this._tapTimer = null;
|
||||||
this._longTimer1 = null;
|
|
||||||
this._longTimer5 = null;
|
|
||||||
this._longFired = false;
|
|
||||||
this._tapCount = 0;
|
this._tapCount = 0;
|
||||||
|
|
||||||
this._bind();
|
this._bindTouch();
|
||||||
|
this._bindMouse();
|
||||||
|
this._bindKeyboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
on(evt, fn) { this._handlers[evt] = fn; return this; }
|
on(evt, fn) { this._handlers[evt] = fn; return this; }
|
||||||
|
|
||||||
_emit(evt, data) {
|
_emit(evt) {
|
||||||
dbg(evt);
|
dbg(evt);
|
||||||
const fn = this._handlers[evt];
|
const fn = this._handlers[evt];
|
||||||
if (fn) fn(data);
|
if (fn) fn();
|
||||||
}
|
}
|
||||||
|
|
||||||
_bind() {
|
// ── 触摸事件 ──
|
||||||
|
_bindTouch() {
|
||||||
const el = this._el;
|
const el = this._el;
|
||||||
el.addEventListener('touchstart', e => this._onStart(e), { passive: false });
|
el.addEventListener('touchstart', e => {
|
||||||
el.addEventListener('touchend', e => this._onEnd(e), { passive: false });
|
e.preventDefault();
|
||||||
el.addEventListener('touchcancel', e => this._onCancel(e), { passive: false });
|
const t = e.touches[0];
|
||||||
|
this._startX = t.clientX;
|
||||||
|
this._startY = t.clientY;
|
||||||
|
this._startTime = Date.now();
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
|
el.addEventListener('touchend', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const t = e.changedTouches[0];
|
||||||
|
this._process(t.clientX, t.clientY);
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
|
el.addEventListener('touchcancel', () => this._reset(), { passive: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
_onStart(e) {
|
// ── 鼠标事件(桌面调试)──
|
||||||
e.preventDefault();
|
_bindMouse() {
|
||||||
const t = e.touches[0];
|
this._el.addEventListener('mousedown', e => {
|
||||||
this._startX = t.clientX;
|
this._startX = e.clientX;
|
||||||
this._startY = t.clientY;
|
this._startY = e.clientY;
|
||||||
this._startTime = Date.now();
|
this._startTime = Date.now();
|
||||||
this._longFired = false;
|
});
|
||||||
|
this._el.addEventListener('mouseup', e => {
|
||||||
this._clearTimers();
|
this._process(e.clientX, e.clientY);
|
||||||
|
});
|
||||||
// 1s长按
|
|
||||||
this._longTimer1 = setTimeout(() => {
|
|
||||||
this._longFired = true;
|
|
||||||
this._emit('longpress1s');
|
|
||||||
Haptic.detailMode();
|
|
||||||
}, 900);
|
|
||||||
|
|
||||||
// 5s长按(安全机制)
|
|
||||||
this._longTimer5 = setTimeout(() => {
|
|
||||||
this._longFired = true;
|
|
||||||
this._emit('longpress5s');
|
|
||||||
Haptic.reject();
|
|
||||||
}, 5000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_onEnd(e) {
|
// ── 键盘事件(桌面调试)──
|
||||||
e.preventDefault();
|
_bindKeyboard() {
|
||||||
this._clearTimers();
|
document.addEventListener('keydown', e => {
|
||||||
|
switch (e.code) {
|
||||||
|
case 'Space': e.preventDefault(); this._emit('singletap'); break;
|
||||||
|
case 'Enter': e.preventDefault(); this._emit('doubletap'); break;
|
||||||
|
case 'ArrowLeft': e.preventDefault(); this._emit('swipe_left'); break;
|
||||||
|
case 'ArrowRight': e.preventDefault(); this._emit('swipe_right'); break;
|
||||||
|
case 'ArrowUp': e.preventDefault(); this._emit('swipe_up'); break;
|
||||||
|
case 'ArrowDown': e.preventDefault(); this._emit('swipe_down'); break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const t = e.changedTouches[0];
|
// ── 手势判定 ──
|
||||||
const dx = t.clientX - this._startX;
|
_process(endX, endY) {
|
||||||
const dy = t.clientY - this._startY;
|
const dx = endX - this._startX;
|
||||||
|
const dy = endY - this._startY;
|
||||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||||
const dt = Date.now() - this._startTime;
|
const dt = Date.now() - this._startTime;
|
||||||
|
|
||||||
if (this._longFired) return;
|
// 滑动:距离 > 35px
|
||||||
|
if (dist > 35) {
|
||||||
// ── 滑动判断 ──
|
|
||||||
const SWIPE_MIN = 35;
|
|
||||||
const SWIPE_ANG = 45;
|
|
||||||
|
|
||||||
if (dist > SWIPE_MIN) {
|
|
||||||
const angle = Math.abs(Math.atan2(dy, dx) * 180 / Math.PI);
|
const angle = Math.abs(Math.atan2(dy, dx) * 180 / Math.PI);
|
||||||
let dir = null;
|
let dir = null;
|
||||||
|
|
||||||
if (angle < SWIPE_ANG) dir = 'right';
|
if (angle < 45) dir = 'right';
|
||||||
else if (angle > 180 - SWIPE_ANG) dir = 'left';
|
else if (angle > 135) dir = 'left';
|
||||||
else if (dy < 0 && Math.abs(dy) > Math.abs(dx)) dir = 'up';
|
else if (dy < 0 && Math.abs(dy) > Math.abs(dx)) dir = 'up';
|
||||||
else if (dy > 0 && Math.abs(dy) > Math.abs(dx)) dir = 'down';
|
else if (dy > 0 && Math.abs(dy) > Math.abs(dx)) dir = 'down';
|
||||||
|
|
||||||
if (dir) {
|
if (dir) {
|
||||||
|
this._tapCount = 0;
|
||||||
|
clearTimeout(this._tapTimer);
|
||||||
this._emit(`swipe_${dir}`);
|
this._emit(`swipe_${dir}`);
|
||||||
Haptic.confirm();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 点击判断 ──
|
// 点击:距离 < 20px,时间 < 500ms
|
||||||
if (dist < 20 && dt < 500) {
|
if (dist < 20 && dt < 500) {
|
||||||
this._tapCount++;
|
this._tapCount++;
|
||||||
if (this._tapCount === 1) {
|
if (this._tapCount === 1) {
|
||||||
this._tapTimer = setTimeout(() => {
|
this._tapTimer = setTimeout(() => {
|
||||||
this._tapCount = 0;
|
this._tapCount = 0;
|
||||||
this._emit('singletap');
|
this._emit('singletap');
|
||||||
Haptic.confirm();
|
|
||||||
}, 250);
|
}, 250);
|
||||||
} else if (this._tapCount === 2) {
|
} else if (this._tapCount >= 2) {
|
||||||
clearTimeout(this._tapTimer);
|
clearTimeout(this._tapTimer);
|
||||||
this._tapCount = 0;
|
this._tapCount = 0;
|
||||||
this._emit('doubletap');
|
this._emit('doubletap');
|
||||||
Haptic.confirm();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onCancel(e) {
|
_reset() {
|
||||||
this._clearTimers();
|
|
||||||
this._longFired = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_clearTimers() {
|
|
||||||
clearTimeout(this._tapTimer);
|
clearTimeout(this._tapTimer);
|
||||||
clearTimeout(this._longTimer1);
|
this._tapCount = 0;
|
||||||
clearTimeout(this._longTimer5);
|
|
||||||
this._tapTimer = null;
|
|
||||||
this._longTimer1 = null;
|
|
||||||
this._longTimer5 = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-24
@@ -1,13 +1,5 @@
|
|||||||
// main.js — 游戏入口
|
// main.js — 游戏入口
|
||||||
|
|
||||||
// 开发模式(URL加?dev):时间线缩短为1/10
|
|
||||||
const DEV_MODE = new URLSearchParams(location.search).has('dev');
|
|
||||||
if (DEV_MODE) {
|
|
||||||
// 直接缩短所有消息的触发时间
|
|
||||||
MESSAGES.forEach(m => { m.triggerAt = Math.floor(m.triggerAt / 10); });
|
|
||||||
document.title = '林夏 [DEV x10]';
|
|
||||||
}
|
|
||||||
|
|
||||||
let engine = null;
|
let engine = null;
|
||||||
let gestures = null;
|
let gestures = null;
|
||||||
|
|
||||||
@@ -17,30 +9,25 @@ async function main() {
|
|||||||
// 初始化音频(必须在用户手势后)
|
// 初始化音频(必须在用户手势后)
|
||||||
await Snd.init();
|
await Snd.init();
|
||||||
|
|
||||||
// 预加载关键短音效
|
// 请求屏幕常亮
|
||||||
await Promise.allSettled([
|
|
||||||
Snd.preload('sfx_keyboard', `${SFX}/sfx_keyboard_slow_v1.mp3`),
|
|
||||||
Snd.preload('sfx_chime', `${SFX}/sfx_ending_chime_v1.mp3`),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 请求屏幕常亮(Android Chrome 支持)
|
|
||||||
try {
|
try {
|
||||||
if ('wakeLock' in navigator) {
|
if ('wakeLock' in navigator) {
|
||||||
window._wakeLock = await navigator.wakeLock.request('screen');
|
window._wakeLock = await navigator.wakeLock.request('screen');
|
||||||
}
|
}
|
||||||
} catch(_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
// 绑定手势到游戏区域
|
// 绑定手势
|
||||||
const gameEl = document.getElementById('game');
|
const gameEl = document.getElementById('game');
|
||||||
gestures = new GestureDetector(gameEl);
|
gestures = new GestureDetector(gameEl);
|
||||||
|
|
||||||
// 创建并启动游戏引擎
|
// 创建并启动引擎
|
||||||
engine = new GameEngine();
|
engine = new GameEngine();
|
||||||
engine.init(gestures);
|
engine.init(gestures);
|
||||||
await engine.start();
|
await engine.start();
|
||||||
|
|
||||||
if (DEV_MODE) {
|
if (DEV) {
|
||||||
document.getElementById('dbg').style.display = 'block';
|
document.getElementById('dbg').style.display = 'block';
|
||||||
|
document.title = '林夏 [DEV]';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,8 +53,3 @@ document.getElementById('start').addEventListener('click', async function onClic
|
|||||||
// 防止页面滚动/缩放
|
// 防止页面滚动/缩放
|
||||||
document.addEventListener('touchmove', e => e.preventDefault(), { passive: false });
|
document.addEventListener('touchmove', e => e.preventDefault(), { passive: false });
|
||||||
document.addEventListener('gesturestart', e => e.preventDefault());
|
document.addEventListener('gesturestart', e => e.preventDefault());
|
||||||
|
|
||||||
// 页面隐藏时停止打字音效
|
|
||||||
document.addEventListener('visibilitychange', () => {
|
|
||||||
if (document.hidden && Snd) Snd.stopTyping();
|
|
||||||
});
|
|
||||||
|
|||||||
+136
-218
@@ -1,4 +1,12 @@
|
|||||||
// story.js — 《林夏》全部16条消息数据
|
// story.js — 消息数据(玩家驱动,无时间依赖)
|
||||||
|
//
|
||||||
|
// 选择方向的情感含义(全局一致):
|
||||||
|
// 单击 → 简单回应("嗯")
|
||||||
|
// 双击 → 不回复 / 跳过
|
||||||
|
// 右滑 → 温暖 / 接受 / 开放
|
||||||
|
// 左滑 → 冷淡 / 拒绝 / 疏远
|
||||||
|
// 上滑 → 好奇 / 主动 / 追问
|
||||||
|
// 下滑 → 沉默 / 退缩
|
||||||
|
|
||||||
const A = '../audio/mp3';
|
const A = '../audio/mp3';
|
||||||
const TDIR = `${A}/tts`;
|
const TDIR = `${A}/tts`;
|
||||||
@@ -6,129 +14,93 @@ const MUS = `${A}/music`;
|
|||||||
const SFX = `${A}/sfx`;
|
const SFX = `${A}/sfx`;
|
||||||
const AMB = `${A}/ambience`;
|
const AMB = `${A}/ambience`;
|
||||||
|
|
||||||
// 游戏时间压缩比:5小时 → 60分钟(实际秒)
|
|
||||||
// 1 游戏分钟 = 12 实际秒
|
|
||||||
// triggerAt = (游戏时间 - 21:00) 的分钟数 × 12
|
|
||||||
function gt(hh, mm) {
|
|
||||||
const gameMin = (hh < 21 ? hh + 24 : hh) * 60 + mm - 21 * 60;
|
|
||||||
return gameMin * 12; // 实际秒
|
|
||||||
}
|
|
||||||
|
|
||||||
const MESSAGES = [
|
const MESSAGES = [
|
||||||
|
|
||||||
// ── 01 · 21:03 · 妈妈 · 电话 ──────────────────────────────────
|
// ── 01 · 妈妈 · 电话 ──
|
||||||
{
|
{
|
||||||
id: 1, gameTime: '21:03', triggerAt: gt(21, 3),
|
id: 1, type: 'call',
|
||||||
type: 'call',
|
|
||||||
sender: 'mom', senderName: '妈妈',
|
sender: 'mom', senderName: '妈妈',
|
||||||
ringtone: `${MUS}/lv11_ring_mom_v1.mp3`,
|
ringtone: `${MUS}/lv11_ring_mom_v1.mp3`,
|
||||||
ringDuration: 28,
|
|
||||||
audio: [`${TDIR}/lv11_2103_mom_call_01_wav_v1.mp3`],
|
audio: [`${TDIR}/lv11_2103_mom_call_01_wav_v1.mp3`],
|
||||||
note: '今晚煮了你爱吃的排骨',
|
|
||||||
stateOnAnswer: { MOM_LINK: true },
|
stateOnAnswer: { MOM_LINK: true },
|
||||||
profileOnMiss: { avoidance: 2 },
|
profileOnMiss: { avoidance: 2 },
|
||||||
profileOnAnswer:{ engagement: 1 },
|
profileOnAnswer: { engagement: 1 },
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── 02 · 21:15 · 小美 · 微信语音×3(教学关)──────────────────
|
// ── 02 · 小美 · 微信语音×3 ──
|
||||||
{
|
{
|
||||||
id: 2, gameTime: '21:15', triggerAt: gt(21, 15),
|
id: 2, type: 'wechat_voice',
|
||||||
type: 'wechat_voice',
|
|
||||||
sender: 'xiaomei', senderName: '小美',
|
sender: 'xiaomei', senderName: '小美',
|
||||||
audio: [
|
audio: [
|
||||||
`${TDIR}/lv11_2115_xiaomei_voice_01_wav_v1.mp3`,
|
`${TDIR}/lv11_2115_xiaomei_voice_01_wav_v1.mp3`,
|
||||||
`${TDIR}/lv11_2115_xiaomei_voice_02_wav_v1.mp3`,
|
`${TDIR}/lv11_2115_xiaomei_voice_02_wav_v1.mp3`,
|
||||||
`${TDIR}/lv11_2115_xiaomei_voice_03_wav_v1.mp3`,
|
`${TDIR}/lv11_2115_xiaomei_voice_03_wav_v1.mp3`,
|
||||||
],
|
],
|
||||||
note: '你被甩了我都听说了,明天喝酒',
|
choices: {
|
||||||
isTutorial: true,
|
tap: { label: '好啊。', profile: { engagement: 1 } },
|
||||||
detailReplies: [
|
right: { label: '好啊,明天不见不散。', profile: { engagement: 2 } },
|
||||||
{ label: '好啊,明天不见不散。', dir: 'left', profile: { engagement: 1 } },
|
up: { label: '我不太想出门……', profile: { nostalgia: 1 } },
|
||||||
{ label: '我不太想出门……', dir: 'right', profile: { nostalgia: 1 } },
|
left: { label: '你先去,我再说。', profile: { avoidance: 1 } },
|
||||||
{ label: '你先去,我再说。', dir: 'up', profile: { avoidance: 1 } },
|
down: { label: '(沉默)', isSilence: true, profile: { avoidance: 2 } },
|
||||||
{ label: '(沉默)', dir: 'down', isSilence: true, profile: { avoidance: 2 } },
|
},
|
||||||
],
|
|
||||||
standardReply: '好啊。',
|
|
||||||
profileOnRead: { engagement: 1 },
|
|
||||||
profileOnAvoid: { avoidance: 1 },
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── 03 · 21:20 · 外卖小哥 · 电话 ─────────────────────────────
|
// ── 03 · 外卖小哥 · 电话 ──
|
||||||
{
|
{
|
||||||
id: 3, gameTime: '21:20', triggerAt: gt(21, 20),
|
id: 3, type: 'call',
|
||||||
type: 'call',
|
|
||||||
sender: 'delivery', senderName: '外卖小哥',
|
sender: 'delivery', senderName: '外卖小哥',
|
||||||
ringtone: `${MUS}/lv11_ring_delivery_v1.mp3`,
|
ringtone: `${MUS}/lv11_ring_delivery_v1.mp3`,
|
||||||
ringDuration: 20,
|
|
||||||
audio: [`${TDIR}/lv11_2120_delivery_call_01_wav_v1.mp3`],
|
audio: [`${TDIR}/lv11_2120_delivery_call_01_wav_v1.mp3`],
|
||||||
note: '你好,你的麻辣烫到了',
|
profileOnMiss: { avoidance: 1 },
|
||||||
profileOnMiss: { avoidance: 1 },
|
profileOnAnswer: { engagement: 1 },
|
||||||
profileOnAnswer:{ engagement: 1 },
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── 04 · 21:30 · 阿哲 · 微信语音(第一刀)────────────────────
|
// ── 04 · 阿哲 · 微信语音 ──
|
||||||
{
|
{
|
||||||
id: 4, gameTime: '21:30', triggerAt: gt(21, 30),
|
id: 4, type: 'wechat_voice',
|
||||||
type: 'wechat_voice',
|
|
||||||
sender: 'azhe', senderName: '阿哲',
|
sender: 'azhe', senderName: '阿哲',
|
||||||
audio: [`${TDIR}/lv11_2130_azhe_voice_01_wav_v1.mp3`],
|
audio: [`${TDIR}/lv11_2130_azhe_voice_01_wav_v1.mp3`],
|
||||||
note: 'AirPods落你那了,方便寄一下吗',
|
choices: {
|
||||||
detailReplies: [
|
tap: { label: '好,明天寄。', state: { HE_BACK: false }, profile: { engagement: 1 } },
|
||||||
{ label: '好,明天寄。', dir: 'left', state: { HE_BACK: false }, profile: { engagement: 1 } },
|
right: { label: '你那边……都好吗。', profile: { engagement: 2 } },
|
||||||
{ label: '你怎么不自己来拿。', dir: 'right', state: {}, profile: { engagement: 2 } },
|
up: { label: '你怎么不自己来拿。', profile: { engagement: 2 } },
|
||||||
{ label: '你那边……都好吗。', dir: 'up', state: {}, profile: { engagement: 2 } },
|
left: { label: '(不想理)', profile: { avoidance: 1 } },
|
||||||
{ label: '(什么都不说)', dir: 'down', isSilence: true, state: { HE_BACK: null }, profile: { avoidance: 2 } },
|
down: { label: '(什么都不说)', isSilence: true, state: { HE_BACK: null }, profile: { avoidance: 2 } },
|
||||||
],
|
},
|
||||||
standardReply: '好,明天寄给你。',
|
|
||||||
stateOnStdReply: { HE_BACK: false },
|
|
||||||
// 10分钟后阿哲撤回消息(如果玩家未回复)
|
|
||||||
followUp: { delayMin: 10, action: 'azhe_recall' },
|
|
||||||
profileOnRead: { engagement: 0 },
|
|
||||||
profileOnAvoid: { avoidance: 1 },
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── 05 · 21:45 · HR王姐 · 微信语音 ───────────────────────────
|
// ── 05 · HR王姐 · 微信语音 ──
|
||||||
{
|
{
|
||||||
id: 5, gameTime: '21:45', triggerAt: gt(21, 45),
|
id: 5, type: 'wechat_voice',
|
||||||
type: 'wechat_voice',
|
|
||||||
sender: 'hr', senderName: 'HR王姐',
|
sender: 'hr', senderName: 'HR王姐',
|
||||||
audio: [`${TDIR}/lv11_2145_hr_voice_01_wav_v1.mp3`],
|
audio: [`${TDIR}/lv11_2145_hr_voice_01_wav_v1.mp3`],
|
||||||
note: '离职流程周三前办完哈',
|
choices: {
|
||||||
detailReplies: [
|
tap: { label: '好的,我知道了。', profile: { engagement: 1 } },
|
||||||
{ label: '好的,我知道了。谢谢王姐。', dir: 'left', profile: { engagement: 1 } },
|
right: { label: '谢谢王姐。', profile: { engagement: 1 } },
|
||||||
{ label: '王姐,有什么建议吗?', dir: 'right', profile: { engagement: 2 } },
|
up: { label: '王姐,有什么建议吗?', profile: { engagement: 2 } },
|
||||||
{ label: '……谢谢你。', dir: 'up', profile: { nostalgia: 1 } },
|
left: { label: '……谢谢你。', profile: { nostalgia: 1 } },
|
||||||
{ label: '(不回复)', dir: 'down', isSilence: true, profile: { avoidance: 1 } },
|
down: { label: '(不回复)', isSilence: true, profile: { avoidance: 1 } },
|
||||||
],
|
},
|
||||||
standardReply: '好的,谢谢王姐。',
|
|
||||||
profileOnRead: { engagement: 1 },
|
|
||||||
profileOnAvoid: { avoidance: 1 },
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── 06 · 22:00 · 安安 · 微信语音(酒吧背景)─────────────────
|
// ── 06 · 安安 · 微信语音 ──
|
||||||
{
|
{
|
||||||
id: 6, gameTime: '22:00', triggerAt: gt(22, 0),
|
id: 6, type: 'wechat_voice',
|
||||||
type: 'wechat_voice',
|
|
||||||
sender: 'anan', senderName: '安安',
|
sender: 'anan', senderName: '安安',
|
||||||
audio: [`${TDIR}/lv11_2200_anan_voice_01_wav_v1.mp3`],
|
audio: [`${TDIR}/lv11_2200_anan_voice_01_wav_v1.mp3`],
|
||||||
note: '我在helens!要不要过来!',
|
choices: {
|
||||||
bgUnder: `${AMB}/amb_bar_loud_01_v1.mp3`, // 酒吧底噪混入
|
tap: { label: '不了,你们玩好~', profile: {} },
|
||||||
detailReplies: [
|
right: { label: '在哪儿啊?我……看看吧。', profile: { engagement: 1 } },
|
||||||
{ label: '我今晚不方便,你们玩好啊。', dir: 'left', profile: { avoidance: 1 } },
|
up: { label: '你喝了多少了哈哈。', profile: { engagement: 1 } },
|
||||||
{ label: '在哪儿啊?我……看看吧。', dir: 'right', profile: { engagement: 1 } },
|
left: { label: '我今晚不方便。', profile: { avoidance: 1 } },
|
||||||
{ label: '你喝了多少了哈哈。', dir: 'up', profile: { engagement: 1 } },
|
down: { label: '(不回复)', isSilence: true, profile: { avoidance: 2 } },
|
||||||
{ label: '(不回复)', dir: 'down', isSilence: true, profile: { avoidance: 2 } },
|
},
|
||||||
],
|
|
||||||
standardReply: '不了,你们玩好~',
|
|
||||||
profileOnRead: { engagement: 0 },
|
|
||||||
profileOnAvoid: { avoidance: 1 },
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── 07 · 22:15 · 大学寝室群 · 5条语音 ────────────────────────
|
// ── 07 · 大学寝室群 · 5条语音 ──
|
||||||
{
|
{
|
||||||
id: 7, gameTime: '22:15', triggerAt: gt(22, 15),
|
id: 7, type: 'wechat_voice',
|
||||||
type: 'wechat_voice',
|
sender: 'dorm', senderName: '寝室群',
|
||||||
sender: 'dorm', senderName: '我们寝室的(4人群)',
|
|
||||||
audio: [
|
audio: [
|
||||||
`${TDIR}/lv11_2215_dorm_a_voice_01_wav_v1.mp3`,
|
`${TDIR}/lv11_2215_dorm_a_voice_01_wav_v1.mp3`,
|
||||||
`${TDIR}/lv11_2215_dorm_a_voice_02_wav_v1.mp3`,
|
`${TDIR}/lv11_2215_dorm_a_voice_02_wav_v1.mp3`,
|
||||||
@@ -136,40 +108,30 @@ const MESSAGES = [
|
|||||||
`${TDIR}/lv11_2215_dorm_b_voice_02_wav_v1.mp3`,
|
`${TDIR}/lv11_2215_dorm_b_voice_02_wav_v1.mp3`,
|
||||||
`${TDIR}/lv11_2215_dorm_c_voice_01_wav_v1.mp3`,
|
`${TDIR}/lv11_2215_dorm_c_voice_01_wav_v1.mp3`,
|
||||||
],
|
],
|
||||||
note: '室友A升职了,最后她@你',
|
choices: {
|
||||||
isGroup: true,
|
tap: { label: '恭喜!', state: { GROUP_REPLY: true }, profile: { engagement: 1 } },
|
||||||
detailReplies: [
|
right: { label: '哇,快讲讲!', state: { GROUP_REPLY: true }, profile: { engagement: 2 } },
|
||||||
{ label: '恭喜恭喜!', dir: 'left', state: { GROUP_REPLY: true }, profile: { engagement: 1 } },
|
up: { label: '最近有点忙,你们聊~', profile: { avoidance: 1 } },
|
||||||
{ label: '哇,快讲讲!', dir: 'right', state: { GROUP_REPLY: true }, profile: { engagement: 2 } },
|
left: { label: '(不想参与)', profile: { avoidance: 1 } },
|
||||||
{ label: '最近有点忙,你们聊~', dir: 'up', profile: { avoidance: 1 } },
|
down: { label: '(沉默)', isSilence: true, profile: { avoidance: 2 } },
|
||||||
{ label: '(沉默)', dir: 'down', isSilence: true, profile: { avoidance: 2 } },
|
},
|
||||||
],
|
|
||||||
standardReply: '恭喜!',
|
|
||||||
stateOnStdReply: { GROUP_REPLY: true },
|
|
||||||
profileOnRepeat: { nostalgia: 2 },
|
|
||||||
profileOnRead: { engagement: 0 },
|
|
||||||
profileOnAvoid: { avoidance: 1 },
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── 08 · 22:30 · 未知号码 · 电话(悬疑钩子)─────────────────
|
// ── 08 · 未知号码 · 电话(悬疑)──
|
||||||
{
|
{
|
||||||
id: 8, gameTime: '22:30', triggerAt: gt(22, 30),
|
id: 8, type: 'call',
|
||||||
type: 'call',
|
|
||||||
sender: 'unknown', senderName: '未知号码',
|
sender: 'unknown', senderName: '未知号码',
|
||||||
ringtone: `${MUS}/lv11_ring_unknown_v1.mp3`,
|
ringtone: `${MUS}/lv11_ring_unknown_v1.mp3`,
|
||||||
ringDuration: 20,
|
|
||||||
audio: [`${SFX}/sfx_breathing_unknown_v1.mp3`],
|
audio: [`${SFX}/sfx_breathing_unknown_v1.mp3`],
|
||||||
note: '接通后只有呼吸声,6秒挂断',
|
autoHangup: true,
|
||||||
autoHangup: 6, // 接通后自动挂断秒数
|
|
||||||
sfxAfter: `${SFX}/sfx_heartbeat_fast_v1.mp3`,
|
sfxAfter: `${SFX}/sfx_heartbeat_fast_v1.mp3`,
|
||||||
profileOnMiss: { avoidance: 1 },
|
profileOnMiss: { avoidance: 1 },
|
||||||
profileOnAnswer:{ nostalgia: 1 },
|
profileOnAnswer: { nostalgia: 1 },
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── 09 · 22:45 · 小美醉语音 · 4段(秘密揭晓)───────────────
|
// ── 09 · 小美(醉)· 4段语音 ──
|
||||||
{
|
{
|
||||||
id: 9, gameTime: '22:45', triggerAt: gt(22, 45),
|
id: 9, type: 'wechat_voice',
|
||||||
type: 'wechat_voice',
|
|
||||||
sender: 'xiaomei', senderName: '小美',
|
sender: 'xiaomei', senderName: '小美',
|
||||||
audio: [
|
audio: [
|
||||||
`${TDIR}/lv11_2245_xiaomei_voice_01_seg1_wav_v1.mp3`,
|
`${TDIR}/lv11_2245_xiaomei_voice_01_seg1_wav_v1.mp3`,
|
||||||
@@ -177,83 +139,63 @@ const MESSAGES = [
|
|||||||
`${TDIR}/lv11_2245_xiaomei_voice_01_seg3_wav_v1.mp3`,
|
`${TDIR}/lv11_2245_xiaomei_voice_01_seg3_wav_v1.mp3`,
|
||||||
`${TDIR}/lv11_2245_xiaomei_voice_01_seg4_wav_v1.mp3`,
|
`${TDIR}/lv11_2245_xiaomei_voice_01_seg4_wav_v1.mp3`,
|
||||||
],
|
],
|
||||||
note: '阿哲大三追过我,我没答应他',
|
bgm: `${MUS}/lv11_bgm_m2_suspense_v1.mp3`,
|
||||||
isDrunk: true,
|
choices: {
|
||||||
bgm: `${MUS}/lv11_bgm_m2_suspense_v1.mp3`, // 悬疑BGM
|
tap: { label: '我知道了。', profile: { avoidance: 1 } },
|
||||||
detailReplies: [
|
right: { label: '小美,你还好吗。', profile: { engagement: 2 } },
|
||||||
{ label: '我知道了。', dir: 'left', profile: { avoidance: 1 } },
|
up: { label: '你……为什么现在才说。', profile: { engagement: 2 } },
|
||||||
{ label: '你……为什么现在才说。', dir: 'right', profile: { engagement: 2 } },
|
left: { label: '(不想理她)', profile: { avoidance: 1 } },
|
||||||
{ label: '小美,你还好吗。', dir: 'up', profile: { engagement: 2 } },
|
down: { label: '(什么都不说)', isSilence: true, profile: { avoidance: 2, nostalgia: 1 } },
|
||||||
{ label: '(什么都不说)', dir: 'down', isSilence: true, profile: { avoidance: 2, nostalgia: 1 } },
|
},
|
||||||
],
|
|
||||||
standardReply: '我知道了。',
|
|
||||||
// 5秒后消息被撤回
|
|
||||||
followUp: { delaySec: 5, action: 'xiaomei_recall' },
|
|
||||||
profileOnRepeat: { nostalgia: 2 },
|
|
||||||
profileOnAvoid: { avoidance: 1, nostalgia: 1 },
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── 10 · 23:00 · 妈妈 · 微信文字 ─────────────────────────────
|
// ── 10 · 妈妈 · 文字消息 ──
|
||||||
{
|
{
|
||||||
id: 10, gameTime: '23:00', triggerAt: gt(23, 0),
|
id: 10, type: 'wechat_text',
|
||||||
type: 'wechat_text',
|
|
||||||
sender: 'mom', senderName: '妈妈',
|
sender: 'mom', senderName: '妈妈',
|
||||||
text: '睡了吗?',
|
text: '睡了吗?',
|
||||||
note: '三个字,最痛',
|
choices: {
|
||||||
detailReplies: [
|
tap: { label: '嗯。', state: { MOM_LINK: true } },
|
||||||
{ label: '嗯,要睡了。', dir: 'left', state: { MOM_LINK: true }, profile: { avoidance: 1 } },
|
right: { label: '没有,怎么了。', state: { MOM_LINK: true }, profile: { engagement: 1 } },
|
||||||
{ label: '没有,怎么了。', dir: 'right', state: { MOM_LINK: true }, profile: { engagement: 1 } },
|
up: { label: '妈,我想打电话。', state: { MOM_LINK: true }, profile: { engagement: 2 } },
|
||||||
{ label: '妈,你在吗,我想打电话。', dir: 'up', state: { MOM_LINK: true }, profile: { engagement: 2 } },
|
left: { label: '嗯,要睡了。', state: { MOM_LINK: true }, profile: { avoidance: 1 } },
|
||||||
{ label: '(不回复)', dir: 'down', isSilence: true, profile: { avoidance: 2 } },
|
down: { label: '(不回复)', isSilence: true, profile: { avoidance: 2 } },
|
||||||
],
|
},
|
||||||
standardReply: '嗯。',
|
|
||||||
stateOnStdReply: { MOM_LINK: true },
|
|
||||||
profileOnRead: { nostalgia: 1 },
|
|
||||||
profileOnAvoid: { avoidance: 1 },
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── 11 · 23:15 · 阿哲 · 电话(关键分支)─────────────────────
|
// ── 11 · 阿哲 · 电话(关键分支)──
|
||||||
{
|
{
|
||||||
id: 11, gameTime: '23:15', triggerAt: gt(23, 15),
|
id: 11, type: 'call',
|
||||||
type: 'call',
|
|
||||||
sender: 'azhe', senderName: '阿哲',
|
sender: 'azhe', senderName: '阿哲',
|
||||||
ringtone: `${MUS}/lv11_ring_azhe_v1.mp3`,
|
ringtone: `${MUS}/lv11_ring_azhe_v1.mp3`,
|
||||||
ringDuration: 30,
|
|
||||||
audio: [`${TDIR}/lv11_2315_azhe_call_01_wav_v1.mp3`],
|
audio: [`${TDIR}/lv11_2315_azhe_call_01_wav_v1.mp3`],
|
||||||
note: '我能过去拿吗?就5分钟',
|
profileOnMiss: { avoidance: 2 },
|
||||||
// 通话中的选择(接通后听完再做)
|
profileOnAnswer: { engagement: 1 },
|
||||||
inCallChoices: [
|
choices: {
|
||||||
{ label: '不方便,明天吧。', dir: 'left', state: { HE_BACK: false } },
|
tap: { label: '不方便,明天吧。', state: { HE_BACK: false } },
|
||||||
{ label: '可以,但只有5分钟。', dir: 'right', state: { HE_BACK: true } },
|
right: { label: '可以,但只有5分钟。', state: { HE_BACK: true } },
|
||||||
{ label: '(不说话,挂断)', dir: 'down', isSilence: true, state: { HE_BACK: false } },
|
left: { label: '(挂断)', state: { HE_BACK: false } },
|
||||||
],
|
down: { label: '(沉默挂断)', isSilence: true, state: { HE_BACK: false } },
|
||||||
profileOnMiss: { avoidance: 2 },
|
},
|
||||||
profileOnAnswer:{ engagement: 1 },
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── 12 · 23:30 · 周南 · 微信语音(悬疑揭晓)────────────────
|
// ── 12 · 周南 · 微信语音 ──
|
||||||
{
|
{
|
||||||
id: 12, gameTime: '23:30', triggerAt: gt(23, 30),
|
id: 12, type: 'wechat_voice',
|
||||||
type: 'wechat_voice',
|
|
||||||
sender: 'zhounan', senderName: '周南',
|
sender: 'zhounan', senderName: '周南',
|
||||||
audio: [`${TDIR}/lv11_2330_zhounan_voice_01_wav_v1.mp3`],
|
audio: [`${TDIR}/lv11_2330_zhounan_voice_01_wav_v1.mp3`],
|
||||||
note: '林夏,是我,周南。初三坐你后桌那个',
|
choices: {
|
||||||
detailReplies: [
|
tap: { label: '嗯……你好。', state: { ZHOUNAN_DEPTH: 1 }, profile: { nostalgia: 1 } },
|
||||||
{ label: '嗯……你好。', dir: 'left', state: { ZHOUNAN_DEPTH: 1 }, profile: { nostalgia: 1 } },
|
right: { label: '周南!你还记得我啊!', state: { ZHOUNAN_DEPTH: 1 }, profile: { engagement: 2, nostalgia: 1 } },
|
||||||
{ label: '周南!你还记得我啊!', dir: 'right', state: { ZHOUNAN_DEPTH: 1 }, profile: { engagement: 2, nostalgia: 1 } },
|
up: { label: '你是……初三那个……?', state: { ZHOUNAN_DEPTH: 1 }, profile: { nostalgia: 2 } },
|
||||||
{ label: '你是……初三坐我后桌那个……?', dir: 'up', state: { ZHOUNAN_DEPTH: 1 }, profile: { nostalgia: 2 } },
|
left: { label: '(冷淡)', profile: { avoidance: 1 } },
|
||||||
{ label: '(不回复)', dir: 'down', isSilence: true },
|
down: { label: '(不回复)', isSilence: true },
|
||||||
],
|
},
|
||||||
standardReply: '嗯……好久不见。',
|
|
||||||
stateOnStdReply: { ZHOUNAN_DEPTH: 1 },
|
|
||||||
profileOnRead: { nostalgia: 1 },
|
|
||||||
profileOnAvoid: { avoidance: 1 },
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── 13 · 23:45 · 周南 · 4条语音(十年故事)─────────────────
|
// ── 13 · 周南 · 4条长语音 ──
|
||||||
{
|
{
|
||||||
id: 13, gameTime: '23:45', triggerAt: gt(23, 45),
|
id: 13, type: 'wechat_voice',
|
||||||
type: 'wechat_voice',
|
|
||||||
sender: 'zhounan', senderName: '周南',
|
sender: 'zhounan', senderName: '周南',
|
||||||
audio: [
|
audio: [
|
||||||
`${TDIR}/lv11_2345_zhounan_voice_01_wav_v1.mp3`,
|
`${TDIR}/lv11_2345_zhounan_voice_01_wav_v1.mp3`,
|
||||||
@@ -261,56 +203,45 @@ const MESSAGES = [
|
|||||||
`${TDIR}/lv11_2345_zhounan_voice_03_wav_v1.mp3`,
|
`${TDIR}/lv11_2345_zhounan_voice_03_wav_v1.mp3`,
|
||||||
`${TDIR}/lv11_2345_zhounan_voice_04_wav_v1.mp3`,
|
`${TDIR}/lv11_2345_zhounan_voice_04_wav_v1.mp3`,
|
||||||
],
|
],
|
||||||
note: '高考、复读、二本、回老家……今天我生日',
|
|
||||||
bgm: `${MUS}/lv11_bgm_m3_zhounan_v1.mp3`,
|
bgm: `${MUS}/lv11_bgm_m3_zhounan_v1.mp3`,
|
||||||
detailReplies: [
|
choices: {
|
||||||
{ label: '你讲了好多。我都听了。', dir: 'left', state: { ZHOUNAN_DEPTH_ADD: 1 }, profile: { engagement: 2, nostalgia: 1 } },
|
tap: { label: '谢谢你告诉我。', state: { ZHOUNAN_DEPTH_ADD: 1 } },
|
||||||
{ label: '你一个人在北京,还好吗?', dir: 'right', state: { ZHOUNAN_DEPTH_ADD: 1 }, profile: { engagement: 2 } },
|
right: { label: '你在北京还好吗?', state: { ZHOUNAN_DEPTH_ADD: 1 }, profile: { engagement: 2 } },
|
||||||
{ label: '……生日快乐。', dir: 'up', state: { ZHOUNAN_DEPTH_ADD: 1, ZHOUNAN_SHARE: true }, profile: { nostalgia: 2, engagement: 1 } },
|
up: { label: '……生日快乐。', state: { ZHOUNAN_DEPTH_ADD: 1, ZHOUNAN_SHARE: true }, profile: { nostalgia: 2, engagement: 1 } },
|
||||||
{ label: '(沉默)', dir: 'down', isSilence: true, profile: { nostalgia: 1, avoidance: 1 } },
|
left: { label: '你讲了好多。', state: { ZHOUNAN_DEPTH_ADD: 1 }, profile: { engagement: 2, nostalgia: 1 } },
|
||||||
],
|
down: { label: '(沉默)', isSilence: true, profile: { nostalgia: 1, avoidance: 1 } },
|
||||||
standardReply: '……谢谢你告诉我。',
|
},
|
||||||
stateOnStdReply: { ZHOUNAN_DEPTH_ADD: 1 },
|
|
||||||
profileOnRepeat: { nostalgia: 2 },
|
|
||||||
profileOnAvoid: { nostalgia: 1, avoidance: 1 },
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── 14 · 00:15 · 妈妈 · 电话(第二次)─────────────────────
|
// ── 14 · 妈妈 · 第二次电话 ──
|
||||||
{
|
{
|
||||||
id: 14, gameTime: '00:15', triggerAt: gt(0, 15),
|
id: 14, type: 'call',
|
||||||
type: 'call',
|
|
||||||
sender: 'mom', senderName: '妈妈',
|
sender: 'mom', senderName: '妈妈',
|
||||||
ringtone: `${MUS}/lv11_ring_mom_v1.mp3`,
|
ringtone: `${MUS}/lv11_ring_mom_v1.mp3`,
|
||||||
ringDuration: 30,
|
|
||||||
audio: [`${TDIR}/lv11_0015_mom_call_01_wav_v1.mp3`],
|
audio: [`${TDIR}/lv11_0015_mom_call_01_wav_v1.mp3`],
|
||||||
note: '夏夏,你不接电话我有点担心',
|
|
||||||
stateOnAnswer: { MOM_LINK: true },
|
stateOnAnswer: { MOM_LINK: true },
|
||||||
profileOnMiss: { avoidance: 2 },
|
profileOnMiss: { avoidance: 2 },
|
||||||
profileOnAnswer:{ engagement: 2 },
|
profileOnAnswer: { engagement: 2 },
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── 15 · 00:30 · 自己 · 时光胶囊(系统通知)────────────────
|
// ── 15 · 时光胶囊 · 系统通知 ──
|
||||||
{
|
{
|
||||||
id: 15, gameTime: '00:30', triggerAt: gt(0, 30),
|
id: 15, type: 'system_notification',
|
||||||
type: 'system_notification',
|
|
||||||
sender: 'self', senderName: '时光胶囊',
|
sender: 'self', senderName: '时光胶囊',
|
||||||
text: '3年前的今晚,你给自己录过一段备忘录。是否播放?',
|
|
||||||
systemAudio: `${TDIR}/lv11_sys_memo_01_wav_v1.mp3`,
|
systemAudio: `${TDIR}/lv11_sys_memo_01_wav_v1.mp3`,
|
||||||
audioByProfile: {
|
audioByProfile: {
|
||||||
avoidance: `${TDIR}/lv11_0030_self3y_voice_02_wav_v1.mp3`,
|
avoidance: `${TDIR}/lv11_0030_self3y_voice_02_wav_v1.mp3`,
|
||||||
engagement: `${TDIR}/lv11_0030_self3y_voice_01_wav_v1.mp3`,
|
engagement: `${TDIR}/lv11_0030_self3y_voice_01_wav_v1.mp3`,
|
||||||
nostalgia: `${TDIR}/lv11_0030_self3y_voice_03_wav_v1.mp3`,
|
nostalgia: `${TDIR}/lv11_0030_self3y_voice_03_wav_v1.mp3`,
|
||||||
},
|
},
|
||||||
note: 'self-3y,由PROFILE决定版本',
|
stateOnPlay: { SELF_RECORD: true },
|
||||||
stateOnPlay: { SELF_RECORD: true },
|
|
||||||
profileOnPlay: { nostalgia: 2 },
|
profileOnPlay: { nostalgia: 2 },
|
||||||
profileOnSkip: { avoidance: 2 },
|
profileOnSkip: { avoidance: 2 },
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── 16 · 02:00 · 妈妈 · 长语音4分17秒(结局)──────────────
|
// ── 16 · 妈妈 · 长语音(结局前奏)──
|
||||||
{
|
{
|
||||||
id: 16, gameTime: '02:00', triggerAt: gt(2, 0),
|
id: 16, type: 'wechat_voice',
|
||||||
type: 'wechat_voice',
|
|
||||||
sender: 'mom', senderName: '妈妈',
|
sender: 'mom', senderName: '妈妈',
|
||||||
audio: [
|
audio: [
|
||||||
`${TDIR}/lv11_0200_mom_voice_01_seg1_wav_v1.mp3`,
|
`${TDIR}/lv11_0200_mom_voice_01_seg1_wav_v1.mp3`,
|
||||||
@@ -322,37 +253,24 @@ const MESSAGES = [
|
|||||||
`${TDIR}/lv11_0200_mom_voice_01_seg7_wav_v1.mp3`,
|
`${TDIR}/lv11_0200_mom_voice_01_seg7_wav_v1.mp3`,
|
||||||
`${TDIR}/lv11_0200_mom_voice_01_seg8_wav_v1.mp3`,
|
`${TDIR}/lv11_0200_mom_voice_01_seg8_wav_v1.mp3`,
|
||||||
],
|
],
|
||||||
note: '妈妈讲她24岁那年,结局起点',
|
|
||||||
bgm: `${MUS}/lv11_bgm_m4_ending_v1.mp3`,
|
bgm: `${MUS}/lv11_bgm_m4_ending_v1.mp3`,
|
||||||
sfxAfter: [
|
|
||||||
`${SFX}/sfx_night_bus_v1.mp3`,
|
|
||||||
`${SFX}/sfx_cat_yowl_v1.mp3`,
|
|
||||||
],
|
|
||||||
isEnding: true,
|
isEnding: true,
|
||||||
requiresMomLink: true, // 必须 MOM_LINK=true 才自动播放
|
requiresMomLink: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 角色→铃声映射(用于收件箱读出)
|
// 角色信息
|
||||||
const SENDERS = {
|
const SENDERS = {
|
||||||
mom: { name: '妈妈', ringtone: `${MUS}/lv11_ring_mom_v1.mp3` },
|
mom: { name: '妈妈' },
|
||||||
azhe: { name: '阿哲', ringtone: `${MUS}/lv11_ring_azhe_v1.mp3` },
|
azhe: { name: '阿哲' },
|
||||||
xiaomei: { name: '小美', ringtone: `${MUS}/lv11_ring_xiaomei_v1.mp3` },
|
xiaomei: { name: '小美' },
|
||||||
delivery: { name: '外卖小哥', ringtone: `${MUS}/lv11_ring_delivery_v1.mp3` },
|
delivery: { name: '外卖小哥' },
|
||||||
hr: { name: 'HR王姐', ringtone: `${MUS}/lv11_ring_hr_v1.mp3` },
|
hr: { name: 'HR王姐' },
|
||||||
anan: { name: '安安', ringtone: `${MUS}/lv11_ring_anan_v1.mp3` },
|
anan: { name: '安安' },
|
||||||
dorm: { name: '寝室群', ringtone: `${MUS}/lv11_ring_dorm_group_v1.mp3` },
|
dorm: { name: '寝室群' },
|
||||||
unknown: { name: '未知号码', ringtone: `${MUS}/lv11_ring_unknown_v1.mp3` },
|
unknown: { name: '未知号码' },
|
||||||
zhounan: { name: '周南', ringtone: `${MUS}/lv11_ring_zhounan_v1.mp3` },
|
zhounan: { name: '周南' },
|
||||||
self: { name: '时光胶囊', ringtone: null },
|
self: { name: '时光胶囊' },
|
||||||
};
|
|
||||||
|
|
||||||
const SFX_UI = {
|
|
||||||
wechat_msg: `${SFX}/sfx_deep_breath_v1.mp3`, // 微信消息提示(用sfx模拟)
|
|
||||||
keyboard: `${SFX}/sfx_keyboard_slow_v1.mp3`,
|
|
||||||
chime: `${SFX}/sfx_ending_chime_v1.mp3`,
|
|
||||||
transition: `${SFX}/sfx_transition_chapter_v1.mp3`,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const AMBIENCE_DEFAULT = `${AMB}/amb_apartment_night_01_v1.mp3`;
|
const AMBIENCE_DEFAULT = `${AMB}/amb_apartment_night_01_v1.mp3`;
|
||||||
const AMBIENCE_RAIN = `${AMB}/amb_rain_window_01_v1.mp3`;
|
|
||||||
|
|||||||
+11
-26
@@ -1,4 +1,6 @@
|
|||||||
// utils.js — 轻量工具函数
|
// utils.js — 工具函数
|
||||||
|
|
||||||
|
const DEV = new URLSearchParams(location.search).has('dev');
|
||||||
|
|
||||||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||||
|
|
||||||
@@ -7,47 +9,30 @@ function dbg(msg) {
|
|||||||
if (el && el.style.display !== 'none') el.textContent = msg;
|
if (el && el.style.display !== 'none') el.textContent = msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 语音合成(选项朗读用)
|
// TTS — 仅用于朗读文字消息内容,不用于任何 UI 反馈
|
||||||
const TTS = {
|
const TTS = {
|
||||||
_voices: [],
|
_voices: [],
|
||||||
_ready: false,
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
if (!window.speechSynthesis) return;
|
if (!window.speechSynthesis) return;
|
||||||
const load = () => {
|
const load = () => { this._voices = speechSynthesis.getVoices(); };
|
||||||
this._voices = speechSynthesis.getVoices();
|
|
||||||
this._ready = true;
|
|
||||||
};
|
|
||||||
load();
|
load();
|
||||||
speechSynthesis.onvoiceschanged = load;
|
speechSynthesis.onvoiceschanged = load;
|
||||||
},
|
},
|
||||||
|
|
||||||
_pick() {
|
_pick() {
|
||||||
// 优先选中文语音
|
return this._voices.find(v => v.lang.startsWith('zh') || v.name.includes('Chinese'))
|
||||||
const zh = this._voices.find(v =>
|
|| this._voices[0] || null;
|
||||||
v.lang.startsWith('zh') || v.name.includes('Chinese') || v.name.includes('中')
|
|
||||||
);
|
|
||||||
return zh || this._voices[0] || null;
|
|
||||||
},
|
},
|
||||||
|
speak(text, { rate = 0.85, volume = 0.8 } = {}) {
|
||||||
speak(text, { rate = 0.85, pitch = 1, volume = 0.85 } = {}) {
|
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
if (!window.speechSynthesis) { resolve(); return; }
|
if (!window.speechSynthesis) { resolve(); return; }
|
||||||
speechSynthesis.cancel();
|
speechSynthesis.cancel();
|
||||||
const u = new SpeechSynthesisUtterance(text);
|
const u = new SpeechSynthesisUtterance(text);
|
||||||
u.lang = 'zh-CN';
|
u.lang = 'zh-CN'; u.rate = rate; u.volume = volume;
|
||||||
u.rate = rate;
|
|
||||||
u.pitch = pitch;
|
|
||||||
u.volume = volume;
|
|
||||||
const v = this._pick();
|
const v = this._pick();
|
||||||
if (v) u.voice = v;
|
if (v) u.voice = v;
|
||||||
u.onend = resolve;
|
u.onend = resolve; u.onerror = resolve;
|
||||||
u.onerror = resolve;
|
|
||||||
speechSynthesis.speak(u);
|
speechSynthesis.speak(u);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
stop() { if (window.speechSynthesis) speechSynthesis.cancel(); },
|
||||||
stop() {
|
|
||||||
if (window.speechSynthesis) speechSynthesis.cancel();
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user