save code

This commit is contained in:
xsl
2026-05-21 00:13:41 +08:00
parent e5d3d44f9d
commit bb79a69bd2
226 changed files with 2621 additions and 0 deletions
+160
View File
@@ -0,0 +1,160 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="theme-color" content="#000000">
<title>林夏</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html,body{
width:100%;height:100%;background:#000;
overflow:hidden;touch-action:none;
-webkit-tap-highlight-color:transparent;
user-select:none;-webkit-user-select:none;
font-family:-apple-system,'PingFang SC','Hiragino Sans GB',sans-serif;
}
#game{position:fixed;inset:0;background:#000;}
/* ── 开始界面 ── */
#start{
position:fixed;inset:0;background:#000;z-index:80;
display:flex;flex-direction:column;align-items:center;
color:#fff;text-align:center;padding:40px 32px 48px;
overflow-y:auto;-webkit-overflow-scrolling:touch;
}
#start h1{font-size:38px;font-weight:200;letter-spacing:14px;margin-top:48px;margin-bottom:28px}
#start .sub{font-size:14px;color:#777;line-height:2.4}
#start .hint{margin-top:40px;font-size:12px;color:#444;animation:pulse 2.5s ease-in-out infinite}
#start .tip{margin-top:16px;font-size:11px;color:#2a2a2a;line-height:1.9}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.2}}
/* ── 操作说明 ── */
#controls{
margin-top:44px;width:100%;max-width:320px;text-align:left;
}
#controls .ctrl-title{
font-size:10px;letter-spacing:4px;color:#333;text-align:center;margin-bottom:20px;
}
#controls .ctrl-group{
margin-bottom:20px;
}
#controls .ctrl-scene{
font-size:10px;letter-spacing:2px;color:#3a3a3a;margin-bottom:8px;padding-bottom:6px;
border-bottom:1px solid #1a1a1a;
}
#controls .ctrl-row{
display:flex;justify-content:space-between;align-items:baseline;
padding:5px 0;border-bottom:1px solid #111;
}
#controls .ctrl-row:last-child{border-bottom:none;}
#controls .ctrl-gesture{font-size:11px;color:#555;white-space:nowrap;flex-shrink:0;}
#controls .ctrl-action{font-size:11px;color:#2e2e2e;text-align:right;margin-left:12px;}
/* ── 字幕层(结局文字)── */
#caption{
position:fixed;inset:0;z-index:60;
display:none;align-items:center;justify-content:center;
color:#fff;font-size:18px;font-weight:300;letter-spacing:4px;
text-align:center;line-height:2;padding:40px;
animation:fadein .8s ease-out;
}
@keyframes fadein{from{opacity:0}to{opacity:1}}
/* ── 操作提示栏 ── */
#hint{
position:fixed;bottom:0;left:0;right:0;z-index:70;
padding:12px 24px 20px;
text-align:center;
font-size:11px;letter-spacing:1.5px;color:#444;
background:linear-gradient(transparent,rgba(0,0,0,0.85));
pointer-events:none;
min-height:52px;
display:flex;align-items:flex-end;justify-content:center;
transition:opacity 0.4s;
}
/* ── 调试条 ── */
#dbg{
display:none;
position:fixed;bottom:30px;left:0;right:0;text-align:center;
font-size:11px;color:#333;z-index:200;pointer-events:none;font-family:monospace;
}
</style>
</head>
<body>
<div id="game"></div>
<div id="start">
<h1>林夏</h1>
<div class="sub">
今晚 16 条消息<br>
你接谁,不接谁<br>
决定明天的自己
</div>
<div id="controls">
<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-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-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>
<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-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 class="hint">· 轻触屏幕开始 ·</div>
<div class="tip">
建议关灯 · 戴上耳机<br>
按你的节奏来
</div>
<div class="tip" style="margin-top:20px;color:#333;">
本游戏涉及孤独、家庭关系等情感话题<br>
请在舒适的状态下体验
</div>
</div>
<div id="caption"></div>
<div id="hint"></div>
<div id="dbg"></div>
<script src="js/utils.js"></script>
<script src="js/haptics.js"></script>
<script src="js/audio.js"></script>
<script src="js/gestures.js"></script>
<script src="js/profile.js"></script>
<script src="js/story.js"></script>
<script src="js/engine.js"></script>
<script src="js/main.js"></script>
</body>
</html>
+283
View File
@@ -0,0 +1,283 @@
// audio.js — 音频管理器 + 合成 UI 音效
class AudioManager {
constructor() {
this._ctx = null;
this._master = null;
this._voiceQueue = [];
this._voiceEl = null;
this._ringEl = null;
this._bgmEl = null;
this._ambEl = null;
this._preloaded = {};
}
// ── 初始化(必须在用户手势后调用)──
async init() {
this._ctx = new (window.AudioContext || window.webkitAudioContext)();
if (this._ctx.state === 'suspended') await this._ctx.resume();
this._master = this._ctx.createGain();
this._master.gain.value = 1.0;
this._master.connect(this._ctx.destination);
}
// ── 合成短音(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) {
if (this._preloaded[key]) return;
try {
const res = await fetch(url);
const buf = await res.arrayBuffer();
this._preloaded[key] = await this._ctx.decodeAudioData(buf);
} catch (e) {
console.warn('[Audio] preload failed:', key, e);
}
}
// ── 播放预加载音效 ──
playFX(key, { vol = 1.0, loop = false } = {}) {
const buf = this._preloaded[key];
if (!buf) return null;
const src = this._ctx.createBufferSource();
src.buffer = buf;
src.loop = loop;
const gain = this._ctx.createGain();
gain.gain.value = vol;
src.connect(gain);
gain.connect(this._master);
src.start();
return { src, gain, stop: () => { try { src.stop(); } catch (_) {} } };
}
// ── 铃声(循环)──
async startRingtone(url) {
this.stopRingtone();
this._ringEl = new Audio(url);
this._ringEl.loop = true;
this._ringEl.volume = 1.0;
try { await this._ringEl.play(); } catch (e) { console.warn('[Audio] ring error', e); }
}
stopRingtone() {
if (this._ringEl) {
this._ringEl.pause();
this._ringEl.src = '';
this._ringEl = null;
}
}
// ── 语音播放(多段顺序)──
playVoice(urls, { vol = 1.0, onEnd = null } = {}) {
this.stopVoice();
this._voiceQueue = [...urls];
this._playNext(vol, onEnd);
}
_playNext(vol, onEnd) {
if (this._voiceQueue.length === 0) {
this._voiceEl = null;
if (onEnd) onEnd();
return;
}
const url = this._voiceQueue.shift();
const el = new Audio(url);
el.volume = vol;
this._voiceEl = el;
el.onended = () => this._playNext(vol, onEnd);
el.onerror = () => {
console.warn('[Audio] voice error:', url);
this._playNext(vol, onEnd);
};
el.play().catch(e => console.warn('[Audio] play error', e));
}
stopVoice() {
this._voiceQueue = [];
if (this._voiceEl) {
this._voiceEl.onended = null;
this._voiceEl.pause();
this._voiceEl = null;
}
}
isVoicePlaying() {
return !!(this._voiceEl && !this._voiceEl.paused);
}
// ── BGM(淡入淡出切换)──
async startBGM(url, { vol = 0.35, fade = 2000 } = {}) {
if (this._bgmEl) {
const old = this._bgmEl;
this._fadeOut(old, fade / 2).then(() => { old.pause(); old.src = ''; });
}
const el = new Audio(url);
el.loop = true;
el.volume = 0;
this._bgmEl = el;
try {
await el.play();
this._fadeTo(el, vol, fade);
} catch (e) { console.warn('[Audio] BGM error', e); }
}
stopBGM(fade = 2000) {
if (this._bgmEl) {
const el = this._bgmEl;
this._bgmEl = null;
this._fadeOut(el, fade).then(() => { el.pause(); el.src = ''; });
}
}
// ── 环境音 ──
async startAmbience(url, { vol = 0.25 } = {}) {
if (this._ambEl) { this._ambEl.pause(); this._ambEl.src = ''; }
const el = new Audio(url);
el.loop = true;
el.volume = vol;
this._ambEl = el;
try { await el.play(); } catch (e) { console.warn('[Audio] ambience error', e); }
}
stopAmbience() {
if (this._ambEl) { this._ambEl.pause(); this._ambEl.src = ''; this._ambEl = null; }
}
// ── 音量渐变 ──
_fadeTo(el, target, duration) {
const start = el.volume;
const steps = 30;
const dt = duration / steps;
const dv = (target - start) / steps;
let i = 0;
const t = setInterval(() => {
el.volume = Math.max(0, Math.min(1, start + dv * i));
if (++i >= steps) clearInterval(t);
}, dt);
}
_fadeOut(el, duration) {
return new Promise(resolve => {
const start = el.volume;
const steps = 20;
const dt = duration / steps;
const dv = start / steps;
let i = 0;
const t = setInterval(() => {
el.volume = Math.max(0, start - dv * i);
if (++i >= steps) { clearInterval(t); resolve(); }
}, dt);
});
}
}
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);
},
// 打字/录音音效(~800ms
typing() {
for (let i = 0; i < 6; i++) {
setTimeout(() => {
Snd.synth({ freq: 600 + Math.random() * 200, dur: 30, type: 'sine', vol: 0.1 });
}, i * 120);
}
},
// 发送音效
send() {
Snd.synth({ freq: 400, freq2: 900, dur: 120, type: 'sine', vol: 0.18 });
},
// 结局音效
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);
},
};
+825
View File
@@ -0,0 +1,825 @@
// engine.js — 游戏引擎 v2(多轮对话 + 林夏语音 + NPC反应)
//
// 状态流:idle → notification/ringing → playing → choosing
// → responding → reacting → [choosing | idle] → …… → ending
//
// 新增状态:
// responding — 林夏说话中(打字音效 + 语音播放)
// reacting — NPC反应播放中
// paused — 游戏暂停(两指长按触发)
class GameEngine {
constructor() {
this._state = 'idle';
// 剧情变量
this.vars = {
MOM_LINK: false,
HE_BACK: null,
GROUP_REPLY: false,
ZHOUNAN_DEPTH: 0,
ZHOUNAN_SHARE: false,
SELF_RECORD: false,
UNREAD: 0,
REPLY_COUNT: 0,
};
this.profile = new ProfileTracker();
this.msgState = {};
this.currentMsg = null;
this._queue = [];
this._endingTriggered = false;
this._reminderTimer = null;
// v2: 对话系统
this._currentChoices = null; // 当前可用的选项集
this._pendingResolve = null; // 用于中断语音播放的 resolve
this._skipRequested = false; // 跳过标志
// 暂停系统
this._pausedState = null; // 暂停前的状态
this.gest = null;
}
// ── 状态 getter/setter ──
get state() { return this._state; }
set state(s) {
this._state = s;
dbg(`state: ${s}`);
const el = document.getElementById('hint');
if (!el) return;
const H = {
idle: '',
notification: '单击 播放 · 双击 跳过',
ringing: '单击 接听 · 左滑 拒接',
playing: '双击 跳过 · 长按 重播',
choosing: '→温暖 ←冷淡 ↑追问 ↓沉默',
responding: '双击 跳过',
reacting: '双击 跳过',
paused: '单击 继续 · 三指点击 退出',
ending: '',
};
el.textContent = H[s] || '';
}
// ══════════════════════════════════════════
// 初始化 & 启动
// ══════════════════════════════════════════
init(gestureDetector) {
this.gest = gestureDetector;
this.gest
.on('singletap', () => this._onTap())
.on('doubletap', () => this._onDoubleTap())
.on('swipe_left', () => this._onSwipe('left'))
.on('swipe_right', () => this._onSwipe('right'))
.on('swipe_up', () => this._onSwipe('up'))
.on('swipe_down', () => this._onSwipe('down'))
.on('longpress', () => this._onLongPress())
.on('twofinger_longpress', () => this._onPause())
.on('threefinger_tap', () => this._onExit());
}
async start() {
this._queue = [...MESSAGES];
// 内容预警
await this._playVoiceAsync([`${T}/v2_sys_content_warning_wav_v1.mp3`]);
await sleep(1000);
await Snd.startAmbience(AMBIENCE_DEFAULT, { vol: 0.15 });
await sleep(1500);
Snd.startBGM(`${MUS}/lv11_bgm_m1_main_loop_v1.mp3`, { vol: 0.1 });
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);
}
// ══════════════════════════════════════════
// 消息队列推进
// ══════════════════════════════════════════
async _next() {
if (this._endingTriggered) return;
const msg = this._queue.shift();
if (!msg) {
this.triggerFinalEnding();
return;
}
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++;
if (msg.type === 'call') {
this._startRinging(msg);
} else {
this._notifyMessage(msg);
}
}
_notifyMessage(msg) {
this.state = 'notification';
Haptic.message();
SFX_UI.notify();
this._startReminder();
}
async _startRinging(msg) {
this.state = 'ringing';
Haptic.ring();
await Snd.startRingtone(msg.ringtone);
this._startReminder();
}
// ══════════════════════════════════════════
// 通话流程(异步)
// ══════════════════════════════════════════
async _answerCall(msg) {
Snd.stopRingtone();
Haptic.stop();
Haptic.confirm();
SFX_UI.tap();
this._clearReminder();
this.state = 'playing';
this.msgState[msg.id] = 'read';
this.vars.UNREAD = Math.max(0, this.vars.UNREAD - 1);
if (msg.stateOnAnswer) this._applyState(msg.stateOnAnswer);
if (msg.profileOnAnswer) this.profile.add(msg.profileOnAnswer);
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) {
this.state = 'reacting';
// 前导语(如"真的没事?")
if (msg.autoReactPreamble) {
await sleep(500);
if (!this._skipRequested) {
await this._playVoiceAsync([msg.autoReactPreamble]);
}
}
// 条件分支反应
if (!this._skipRequested) {
const val = this.vars[msg.autoReact.condition];
const reactUrl = val ? msg.autoReact['true'] : msg.autoReact['false'];
if (reactUrl) {
await sleep(400);
if (!this._skipRequested) {
await this._playVoiceAsync([reactUrl]);
}
}
}
}
this._proceedToNext();
return;
}
// 通话后有选择(MSG-11 阿哲)
if (msg.choices) {
this._enterChoosing(msg.choices);
return;
}
this._proceedToNext();
}
_rejectCall(msg) {
Snd.stopRingtone();
Haptic.reject();
SFX_UI.skip();
this._clearReminder();
this.msgState[msg.id] = 'missed';
if (msg.profileOnMiss) this.profile.add(msg.profileOnMiss);
this._proceedToNext();
}
// ══════════════════════════════════════════
// 消息播放(异步)
// ══════════════════════════════════════════
async _playMessage(msg) {
this._clearReminder();
this.state = 'playing';
this.msgState[msg.id] = 'read';
this.vars.UNREAD = Math.max(0, this.vars.UNREAD - 1);
if (msg.bgm) Snd.startBGM(msg.bgm, { vol: 0.1 });
// 系统通知(时光胶囊)
if (msg.type === 'system_notification') {
this._playSystemNotification(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 (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);
if (msg.profileOnPlay) this.profile.add(msg.profileOnPlay);
setTimeout(() => {
if (this.state !== 'playing') return;
Snd.playVoice([audio], {
onEnd: () => this._proceedToNext(),
});
}, 800);
} else {
this._proceedToNext();
}
},
});
}
_afterPlayed(msg) {
if (this.state !== 'playing') return;
if (msg.isEnding) {
this.triggerFinalEnding();
return;
}
if (msg.choices) {
this._enterChoosing(msg.choices);
return;
}
this._proceedToNext();
}
// ══════════════════════════════════════════
// 选择系统(v2 多轮对话核心)
// ══════════════════════════════════════════
_enterChoosing(choices) {
this._currentChoices = choices;
this.state = 'choosing';
SFX_UI.choiceReady();
Haptic.confirm();
Snd.playVoice([`${T}/v2_sys_wait_reply_wav_v1.mp3`]);
this._startReminder();
}
async _handleChoice(dir) {
const choices = this._currentChoices;
if (!choices) {
SFX_UI.error();
Haptic.error();
return;
}
const choice = choices[dir];
if (!choice) {
SFX_UI.error();
Haptic.error();
return;
}
this._clearReminder();
this._skipRequested = false;
// 1. 播放方向对应的情感音效
const sfxMap = {
tap: 'tap',
right: 'choiceWarm',
left: 'choiceCold',
up: 'choiceCurious',
down: 'choiceSilent',
};
if (sfxMap[dir]) SFX_UI[sfxMap[dir]]();
Haptic.select();
// 2. 应用状态和画像
if (!choice.isSilence) {
this.msgState[this.currentMsg.id] = 'replied';
this.vars.REPLY_COUNT++;
}
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();
}
_skipChoice() {
this._clearReminder();
SFX_UI.skip();
Haptic.confirm();
this.profile.add({ avoidance: 1 });
this._proceedToNext();
}
// ══════════════════════════════════════════
// 推进到下一条
// ══════════════════════════════════════════
async _proceedToNext() {
const prev = this.currentMsg;
this.currentMsg = null;
this._currentChoices = null;
this.state = 'idle';
this._clearReminder();
if (this._endingTriggered) return;
// 特殊事件:阿哲来了(门铃)
if (prev?.id === 11 && this.vars.HE_BACK === true) {
await sleep(1500);
Snd.playVoice([`${SFX}/sfx_door_knock_gentle_v1.mp3`]);
Haptic.ring();
await sleep(3000);
}
// 检查结局D:未读过多
const triggered = MESSAGES.length - this._queue.length;
if (this.vars.UNREAD >= 8 && triggered >= 14) {
this._triggerEnding('D');
return;
}
// 短暂停顿后推进
SFX_UI.next();
await sleep(DEV ? 200 : 2000);
this._next();
}
// ══════════════════════════════════════════
// 提醒定时器
// ══════════════════════════════════════════
_startReminder() {
this._clearReminder();
this._reminderTimer = setInterval(() => {
if (this.state === 'notification') {
SFX_UI.notify();
Haptic.message();
} else if (this.state === 'choosing') {
SFX_UI.choiceReady();
Haptic.confirm();
} else if (this.state === 'ringing') {
Haptic.ring();
} else {
this._clearReminder();
}
}, 8000);
}
_clearReminder() {
if (this._reminderTimer) {
clearInterval(this._reminderTimer);
this._reminderTimer = null;
}
}
// ══════════════════════════════════════════
// 状态变量
// ══════════════════════════════════════════
_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;
}
}
}
// ══════════════════════════════════════════
// 手势处理
// ══════════════════════════════════════════
_onTap() {
const s = this.state;
// 暂停中 → 恢复
if (s === 'paused') {
this._onResume();
return;
}
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');
}
}
_onDoubleTap() {
const s = this.state;
if (s === 'paused') return;
if (s === 'ringing') {
this._rejectCall(this.currentMsg);
return;
}
if (s === 'notification') {
this._clearReminder();
SFX_UI.skip();
Haptic.confirm();
const msg = this.currentMsg;
if (msg?.profileOnSkip) this.profile.add(msg.profileOnSkip);
else this.profile.add({ avoidance: 1 });
this._proceedToNext();
return;
}
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._enterChoosing(msg.choices);
} else {
this._proceedToNext();
}
return;
}
if (s === 'responding' || s === 'reacting') {
this._skipRequested = true;
this._interruptVoice();
SFX_UI.skip();
Haptic.confirm();
return;
}
if (s === 'choosing') {
this._skipChoice();
return;
}
}
_onSwipe(dir) {
const s = this.state;
if (s === 'paused') return;
if (s === 'ringing') {
if (dir === 'left') this._rejectCall(this.currentMsg);
else if (dir === 'right') this._answerCall(this.currentMsg);
return;
}
if (s === 'notification') {
if (dir === 'left') {
this._clearReminder();
SFX_UI.skip();
Haptic.confirm();
this.profile.add({ avoidance: 1 });
this._proceedToNext();
}
return;
}
if (s === 'choosing') {
this._handleChoice(dir);
return;
}
}
// ── 长按:重播 / 状态广播 ──
_onLongPress() {
const s = this.state;
if (s === 'paused' || s === 'ending') return;
Haptic.confirm();
// playing/responding/reacting/choosing → 重播当前消息音频
if ((s === 'playing' || s === 'responding' || s === 'reacting' || s === 'choosing') && this.currentMsg) {
this._interruptVoice();
if (this.currentMsg.audio?.length > 0) {
this._playVoiceAsync(this.currentMsg.audio);
} else if (this.currentMsg.type === 'wechat_text' && this.currentMsg.text) {
TTS.speak(this.currentMsg.text);
}
return;
}
// idle/notification → 广播未读数
TTS.speak(`当前未读${this.vars.UNREAD}条消息`);
}
// ── 暂停 ──
_onPause() {
const s = this.state;
if (s === 'ending' || s === 'paused') return;
this._pausedState = s;
this._interruptVoice();
this._clearReminder();
Haptic.stop();
this.state = 'paused';
Snd.playVoice([`${T}/v2_sys_paused_wav_v1.mp3`]);
}
// ── 恢复 ──
_onResume() {
Snd.stopVoice();
const prev = this._pausedState || 'idle';
this._pausedState = null;
// 语音无法断点续播,根据暂停前状态决定恢复行为
if (prev === 'choosing' && this._currentChoices) {
this.state = 'choosing';
SFX_UI.choiceReady();
Haptic.confirm();
this._startReminder();
} else if (prev === 'notification' || prev === 'ringing') {
this.state = prev;
this._startReminder();
} else {
// playing/responding/reacting → 无法续播,推进到下一条或回到选择
if (this._currentChoices) {
this._enterChoosing(this._currentChoices);
} else {
this._proceedToNext();
}
}
}
// ── 退出 ──
_onExit() {
if (this.state !== 'paused') return;
this._endingTriggered = true;
this._interruptVoice();
// 退出不需要特殊音频,直接返回开始页
setTimeout(() => {
const startEl = document.getElementById('start');
if (startEl) {
startEl.style.opacity = '1';
startEl.style.display = 'flex';
}
}, 2000);
}
// ══════════════════════════════════════════
// 结局系统
// ══════════════════════════════════════════
async triggerFinalEnding() {
if (this._endingTriggered) return;
const v = this.vars;
// 结局E(隐藏)— 优先级 1
if (v.ZHOUNAN_DEPTH >= 2 && v.ZHOUNAN_SHARE &&
(this.profile.label === 'engagement' || this.profile.label === 'nostalgia')) {
return this._triggerEnding('E');
}
// 结局A:妈妈连接 + 自己录音 — 优先级 2
if (v.MOM_LINK && v.SELF_RECORD) {
return this._triggerEnding('A');
}
// 结局B:拒了阿哲 + 回了周南 — 优先级 3
if (v.HE_BACK === false && v.ZHOUNAN_DEPTH >= 1) {
return this._triggerEnding('B');
}
// 结局C:全部已读但极少回复 — 优先级 4
const read = Object.values(this.msgState).filter(s => s !== 'unread').length;
if (read >= MESSAGES.length - 2 && v.REPLY_COUNT <= 2) {
return this._triggerEnding('C');
}
// 结局D(兜底)— 优先级 5
this._triggerEnding('D');
}
async _triggerEnding(type) {
if (this._endingTriggered) return;
this._endingTriggered = true;
this.state = 'ending';
this._clearReminder();
this._interruptVoice();
Snd.stopRingtone();
Snd.stopBGM(3000);
await sleep(2000);
dbg(`ending: ${type}`);
const fn = {
A: () => this._endingA(),
B: () => this._endingB(),
C: () => this._endingC(),
D: () => this._endingD(),
E: () => this._endingE(),
}[type];
if (fn) await fn();
// 结局后播报心理支持热线
await sleep(3000);
await this._playVoiceAsync([`${T}/v2_sys_hotline_wav_v1.mp3`]);
}
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);
SFX_UI.ending();
this._showCaption('明天再说。');
await this._playVoiceAsync([`${T}/v2_ending_a_linxia_wav_v1.mp3`]);
}
async _endingB() {
await sleep(2000);
SFX_UI.ending();
await sleep(1000);
this._showCaption('寄出去了。');
await this._playVoiceAsync([`${T}/v2_ending_b_linxia_wav_v1.mp3`]);
}
async _endingC() {
await sleep(3000);
SFX_UI.ending();
await sleep(1000);
this._showCaption('今晚就这样。');
await this._playVoiceAsync([`${T}/v2_ending_c_linxia_wav_v1.mp3`]);
}
async _endingD() {
Haptic.system();
await sleep(2000);
Haptic.ring();
await sleep(4000);
Snd.startAmbience(`${AMB}/amb_dawn_birds_v1.mp3`, { vol: 0.4 });
await sleep(4000);
this._showCaption('早上好。');
await this._playVoiceAsync([`${T}/v2_ending_d_linxia_wav_v1.mp3`]);
}
async _endingE() {
Haptic.message();
SFX_UI.notify();
await sleep(1500);
// 周南秒回:"那我下次出差来北京,请你吃饭好不好。"
await this._playVoiceAsync([`${T}/v2_ending_e_zhounan_wav_v1.mp3`]);
await sleep(2000);
SFX_UI.ending();
await sleep(1000);
this._showCaption('好。');
await this._playVoiceAsync([`${T}/v2_ending_e_linxia_wav_v1.mp3`]);
}
_showCaption(text) {
const el = document.getElementById('caption');
el.textContent = text;
el.style.display = 'flex';
}
}
+262
View File
@@ -0,0 +1,262 @@
// gestures.js — 触控手势检测器
//
// 支持 9 种手势:
// 单击 · 双击 · 长按(1s) · 左滑 · 右滑 · 上滑 · 下滑
// 两指长按(1s) · 三指点击
class GestureDetector {
constructor(el) {
this._el = el;
this._handlers = {};
this._startX = 0;
this._startY = 0;
this._startTime = 0;
this._tapTimer = null;
this._tapCount = 0;
// 长按检测
this._longPressTimer = null;
this._longPressTriggered = false;
// 多指检测
this._maxTouches = 0; // 本次触摸中同时出现的最大手指数
this._multiTouchLPTimer = null; // 两指长按计时器
this._multiTouchHandled = false;
this._bindTouch();
this._bindMouse();
this._bindKeyboard();
}
on(evt, fn) { this._handlers[evt] = fn; return this; }
_emit(evt) {
dbg(evt);
const fn = this._handlers[evt];
if (fn) fn();
}
// ── 触摸事件 ──
_bindTouch() {
const el = this._el;
el.addEventListener('touchstart', e => {
e.preventDefault();
const count = e.touches.length;
// 记录本次触摸出现过的最大手指数
if (count > this._maxTouches) this._maxTouches = count;
if (count === 1) {
// 单指:记录起点 + 启动长按计时
const t = e.touches[0];
this._startX = t.clientX;
this._startY = t.clientY;
this._startTime = Date.now();
this._longPressTriggered = false;
this._longPressTimer = setTimeout(() => {
this._longPressTriggered = true;
this._longPressTimer = null;
this._emit('longpress');
}, 1000);
} else if (count === 2) {
// 两指:取消单指长按,启动两指长按计时
this._cancelLongPress();
this._multiTouchLPTimer = setTimeout(() => {
this._multiTouchHandled = true;
this._multiTouchLPTimer = null;
this._emit('twofinger_longpress');
}, 1000);
} else if (count >= 3) {
// 三指+:取消所有计时
this._cancelLongPress();
this._cancelMultiTouchLP();
}
}, { passive: false });
el.addEventListener('touchmove', e => {
e.preventDefault();
// 移动超 15px 取消长按
if (this._longPressTimer && e.touches.length === 1) {
const t = e.touches[0];
const dx = t.clientX - this._startX;
const dy = t.clientY - this._startY;
if (Math.sqrt(dx * dx + dy * dy) > 15) {
this._cancelLongPress();
}
}
// 两指移动也取消两指长按
if (this._multiTouchLPTimer) {
this._cancelMultiTouchLP();
}
}, { passive: false });
el.addEventListener('touchend', e => {
e.preventDefault();
// 还有手指在屏幕上,等全部抬起再处理
if (e.touches.length > 0) return;
// 全部手指抬起 — 根据 maxTouches 判定手势类型
const max = this._maxTouches;
// 清理计时器
this._cancelLongPress();
this._cancelMultiTouchLP();
// 已在计时器中处理过(长按 / 两指长按)
if (this._longPressTriggered || this._multiTouchHandled) {
this._resetMultiTouch();
return;
}
// 三指点击
if (max >= 3) {
this._resetMultiTouch();
this._emit('threefinger_tap');
return;
}
// 两指快速触摸(< 1s)→ 忽略
if (max === 2) {
this._resetMultiTouch();
return;
}
// 单指:正常处理(滑动 / 点击)
this._resetMultiTouch();
const t = e.changedTouches[0];
this._process(t.clientX, t.clientY);
}, { passive: false });
el.addEventListener('touchcancel', () => {
this._cancelLongPress();
this._cancelMultiTouchLP();
this._resetMultiTouch();
this._reset();
}, { passive: false });
}
// ── 鼠标事件(桌面调试)──
_bindMouse() {
this._el.addEventListener('mousedown', e => {
this._startX = e.clientX;
this._startY = e.clientY;
this._startTime = Date.now();
this._longPressTriggered = false;
this._longPressTimer = setTimeout(() => {
this._longPressTriggered = true;
this._longPressTimer = null;
this._emit('longpress');
}, 1000);
});
this._el.addEventListener('mouseup', e => {
this._cancelLongPress();
if (this._longPressTriggered) {
this._longPressTriggered = false;
return;
}
this._process(e.clientX, e.clientY);
});
this._el.addEventListener('mousemove', e => {
if (this._longPressTimer) {
const dx = e.clientX - this._startX;
const dy = e.clientY - this._startY;
if (Math.sqrt(dx * dx + dy * dy) > 15) {
this._cancelLongPress();
}
}
});
}
// ── 键盘事件(桌面调试)──
_bindKeyboard() {
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;
case 'KeyL': e.preventDefault(); this._emit('longpress'); break;
case 'KeyP': e.preventDefault(); this._emit('twofinger_longpress'); break;
case 'KeyQ': e.preventDefault(); this._emit('threefinger_tap'); break;
}
});
}
// ── 手势判定(单指) ──
_process(endX, endY) {
const dx = endX - this._startX;
const dy = endY - this._startY;
const dist = Math.sqrt(dx * dx + dy * dy);
const dt = Date.now() - this._startTime;
// 滑动:距离 > 35px
if (dist > 35) {
const angle = Math.abs(Math.atan2(dy, dx) * 180 / Math.PI);
let dir = null;
if (angle < 45) dir = 'right';
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 = 'down';
if (dir) {
this._tapCount = 0;
clearTimeout(this._tapTimer);
this._emit(`swipe_${dir}`);
return;
}
}
// 点击:距离 < 20px,时间 < 500ms
if (dist < 20 && dt < 500) {
this._tapCount++;
if (this._tapCount === 1) {
this._tapTimer = setTimeout(() => {
this._tapCount = 0;
this._emit('singletap');
}, 250);
} else if (this._tapCount >= 2) {
clearTimeout(this._tapTimer);
this._tapCount = 0;
this._emit('doubletap');
}
}
}
// ── 工具方法 ──
_cancelLongPress() {
if (this._longPressTimer) {
clearTimeout(this._longPressTimer);
this._longPressTimer = null;
}
}
_cancelMultiTouchLP() {
if (this._multiTouchLPTimer) {
clearTimeout(this._multiTouchLPTimer);
this._multiTouchLPTimer = null;
}
}
_resetMultiTouch() {
this._maxTouches = 0;
this._multiTouchHandled = false;
}
_reset() {
clearTimeout(this._tapTimer);
this._tapCount = 0;
this._longPressTriggered = false;
this._resetMultiTouch();
}
}
+35
View File
@@ -0,0 +1,35 @@
// haptics.js — 震动反馈(Android Chrome 支持,iOS 降级为无震动)
const Haptic = {
_ok: typeof navigator !== 'undefined' && !!navigator.vibrate,
_v(pattern) {
if (this._ok) navigator.vibrate(pattern);
},
// 来电震动(长震 + 停顿,循环由来电循环调用)
ring() { this._v([400, 200, 400]); },
// 微信消息(两短震)
message() { this._v([80, 60, 80]); },
// 确认操作
confirm() { this._v(60); },
// 拒绝 / 挂断
reject() { this._v([100, 50, 100, 50, 100]); },
// 进入细回复模式
detailMode() { this._v([30, 30, 60]); },
// 选项选中
select() { this._v(80); },
// 系统通知
system() { this._v([200, 100, 100, 100, 200]); },
// 错误 / 无效手势
error() { this._v([50, 30, 50]); },
stop() { if (this._ok) navigator.vibrate(0); },
};
+55
View File
@@ -0,0 +1,55 @@
// main.js — 游戏入口
let engine = null;
let gestures = null;
async function main() {
TTS.init();
// 初始化音频(必须在用户手势后)
await Snd.init();
// 请求屏幕常亮
try {
if ('wakeLock' in navigator) {
window._wakeLock = await navigator.wakeLock.request('screen');
}
} catch (_) {}
// 绑定手势
const gameEl = document.getElementById('game');
gestures = new GestureDetector(gameEl);
// 创建并启动引擎
engine = new GameEngine();
engine.init(gestures);
await engine.start();
if (DEV) {
document.getElementById('dbg').style.display = 'block';
document.title = '林夏 [DEV]';
}
}
// 触摸启动(手机)
document.getElementById('start').addEventListener('touchend', async function onStart(e) {
e.preventDefault();
this.removeEventListener('touchend', onStart);
this.style.transition = 'opacity 0.8s';
this.style.opacity = '0';
setTimeout(() => { this.style.display = 'none'; }, 800);
await main();
}, { once: true });
// 鼠标点击(电脑调试)
document.getElementById('start').addEventListener('click', async function onClick() {
this.removeEventListener('click', onClick);
this.style.transition = 'opacity 0.8s';
this.style.opacity = '0';
setTimeout(() => { this.style.display = 'none'; }, 800);
await main();
}, { once: true });
// 防止页面滚动/缩放
document.addEventListener('touchmove', e => e.preventDefault(), { passive: false });
document.addEventListener('gesturestart', e => e.preventDefault());
+39
View File
@@ -0,0 +1,39 @@
// profile.js — 行为画像追踪器(avoidance / engagement / nostalgia
class ProfileTracker {
constructor() {
this.scores = { avoidance: 0, engagement: 0, nostalgia: 0 };
}
add(effects) {
if (!effects) return;
for (const k of ['avoidance', 'engagement', 'nostalgia']) {
if (effects[k]) this.scores[k] += effects[k];
}
}
// 漏接电话
onMissedCall() { this.add({ avoidance: 2 }); }
// 单点回"嗯"
onSimpleReply() { this.add({ avoidance: 1 }); }
// 双指上滑已读不回
onReadNoReply() { this.add({ avoidance: 2 }); }
// 进入细回复
onDetailReply() { this.add({ engagement: 2 }); }
// 细回复选"主动追问"engagement 方向)
onEngage() { this.add({ engagement: 2 }); }
// 细回复选"分享自己"
onShare() { this.add({ engagement: 1, nostalgia: 1 }); }
// 反复重听同一条
onReplay() { this.add({ nostalgia: 2 }); }
// 听完长语音未回应
onListenNoReact(){ this.add({ avoidance: 1, nostalgia: 1 }); }
// 获取最终画像标签
get label() {
const s = this.scores;
if (s.engagement >= s.avoidance && s.engagement >= s.nostalgia) return 'engagement';
if (s.nostalgia >= s.avoidance) return 'nostalgia';
return 'avoidance';
}
}
+674
View File
@@ -0,0 +1,674 @@
// story.js — 消息数据 v2CosyVoice2 语音 + 多轮对话)
//
// 选择方向的情感含义(全局一致):
// 单击(tap) → 简单回应
// 双击 → 不回复 / 跳过
// 右滑(right) → 温暖 / 接受 / 开放
// 左滑(left) → 冷淡 / 拒绝 / 疏远
// 上滑(up) → 好奇 / 主动 / 追问
// 下滑(down) → 沉默 / 退缩
//
// 每个 choice 结构:
// linxia: 林夏回应的音频路径(null/缺省 = 沉默)
// npcReact: NPC反应音频数组(缺省 = 无反应)
// isSilence: 沉默选项标记
// state: 剧情状态变更
// profile: 画像分数变更
// followUp: 可选的下一轮对话 { choices: { ... } }
const A = 'mp3';
const T = `${A}/tts`; // CosyVoice2 生成的所有语音
const MUS = `${A}/music`;
const SFX = `${A}/sfx`;
const AMB = `${A}/ambience`;
// ── 快捷函数 ──
const npc = f => `${T}/${f}`; // NPC Round 1 台词
const lx = f => `${T}/${f}`; // 林夏回应
const rx = f => `${T}/${f}`; // NPC 反应
const MESSAGES = [
// ══════════════════════════════════════════
// 01 · 妈妈 · 电话
// ══════════════════════════════════════════
{
id: 1, type: 'call',
sender: 'mom', senderName: '妈妈',
ringtone: `${MUS}/lv11_ring_mom_v1.mp3`,
audio: [npc('lv11_2103_mom_call_01_wav_v1.mp3')],
autoReply: lx('v2_msg01_linxia_auto_wav_v1.mp3'),
stateOnAnswer: { MOM_LINK: true },
profileOnMiss: { avoidance: 2 },
profileOnAnswer: { engagement: 1 },
},
// ══════════════════════════════════════════
// 02 · 小美 · 语音 ×3(教学关)
// ══════════════════════════════════════════
{
id: 2, type: 'wechat_voice',
sender: 'xiaomei', senderName: '小美',
audio: [
npc('lv11_2115_xiaomei_voice_01_wav_v1.mp3'),
npc('lv11_2115_xiaomei_voice_02_wav_v1.mp3'),
npc('lv11_2115_xiaomei_voice_03_wav_v1.mp3'),
],
choices: {
right: {
linxia: lx('v2_msg02_linxia_warm_wav_v1.mp3'),
npcReact: [rx('v2_msg02_react_warm_wav_v1.mp3')],
profile: { engagement: 2 },
},
left: {
linxia: lx('v2_msg02_linxia_cold_wav_v1.mp3'),
npcReact: [rx('v2_msg02_react_cold_wav_v1.mp3')],
profile: { avoidance: 1 },
},
up: {
linxia: lx('v2_msg02_linxia_curious_wav_v1.mp3'),
npcReact: [rx('v2_msg02_react_curious_wav_v1.mp3')],
profile: { engagement: 1 },
followUp: {
choices: {
right: {
linxia: lx('v2_msg02_r3_linxia_curious_wav_v1.mp3'),
npcReact: [rx('v2_msg02_r3_react_wav_v1.mp3')],
profile: { nostalgia: 1 },
},
},
},
},
down: {
isSilence: true,
npcReact: [rx('v2_msg02_react_silent_wav_v1.mp3')],
profile: { avoidance: 2 },
},
tap: {
linxia: lx('v2_msg02_linxia_tap_wav_v1.mp3'),
npcReact: [rx('v2_msg02_react_tap_wav_v1.mp3')],
},
},
},
// ══════════════════════════════════════════
// 03 · 外卖小哥 · 电话
// ══════════════════════════════════════════
{
id: 3, type: 'call',
sender: 'delivery', senderName: '外卖小哥',
ringtone: `${MUS}/lv11_ring_delivery_v1.mp3`,
audio: [npc('lv11_2120_delivery_call_01_wav_v1.mp3')],
autoReply: lx('v2_msg03_linxia_auto_wav_v1.mp3'),
profileOnMiss: { avoidance: 1 },
profileOnAnswer: { engagement: 1 },
},
// ══════════════════════════════════════════
// 04 · 阿哲 · 语音(前男友要东西)
// ══════════════════════════════════════════
{
id: 4, type: 'wechat_voice',
sender: 'azhe', senderName: '阿哲',
audio: [npc('lv11_2130_azhe_voice_01_wav_v1.mp3')],
choices: {
right: {
linxia: lx('v2_msg04_linxia_warm_wav_v1.mp3'),
npcReact: [rx('v2_msg04_react_warm_wav_v1.mp3')],
profile: { engagement: 2 },
followUp: {
// 阿哲问"你呢?"后
choices: {
right: {
linxia: lx('v2_msg04_r3w_linxia_warm_wav_v1.mp3'),
npcReact: [rx('v2_msg04_r3w_react_warm_wav_v1.mp3')],
},
left: {
linxia: lx('v2_msg04_r3w_linxia_cold_wav_v1.mp3'),
npcReact: [rx('v2_msg04_r3w_react_cold_wav_v1.mp3')],
},
down: {
isSilence: true,
npcReact: [rx('v2_msg04_r3w_react_silent_wav_v1.mp3')],
},
},
},
},
left: {
linxia: lx('v2_msg04_linxia_cold_wav_v1.mp3'),
npcReact: [rx('v2_msg04_react_cold_wav_v1.mp3')],
state: { HE_BACK: false },
profile: { avoidance: 1 },
},
up: {
linxia: lx('v2_msg04_linxia_curious_wav_v1.mp3'),
npcReact: [rx('v2_msg04_react_curious_wav_v1.mp3')],
profile: { engagement: 2 },
followUp: {
// 阿哲说"不太方便"后
choices: {
up: {
linxia: lx('v2_msg04_r3c_linxia_curious_wav_v1.mp3'),
npcReact: [rx('v2_msg04_r3c_react_curious_wav_v1.mp3')],
state: { HE_BACK: false },
},
right: {
linxia: lx('v2_msg04_r3c_linxia_warm_wav_v1.mp3'),
},
},
},
},
down: {
isSilence: true,
npcReact: [rx('v2_msg04_react_silent_wav_v1.mp3')],
state: { HE_BACK: null },
profile: { avoidance: 2 },
},
tap: {
linxia: lx('v2_msg04_linxia_tap_wav_v1.mp3'),
npcReact: [rx('v2_msg04_react_tap_wav_v1.mp3')],
state: { HE_BACK: false },
profile: { engagement: 1 },
},
},
},
// ══════════════════════════════════════════
// 05 · HR王姐 · 语音
// ══════════════════════════════════════════
{
id: 5, type: 'wechat_voice',
sender: 'hr', senderName: 'HR王姐',
audio: [npc('lv11_2145_hr_voice_01_wav_v1.mp3')],
choices: {
right: {
linxia: lx('v2_msg05_linxia_warm_wav_v1.mp3'),
npcReact: [rx('v2_msg05_react_warm_wav_v1.mp3')],
profile: { engagement: 1 },
},
left: {
linxia: lx('v2_msg05_linxia_cold_wav_v1.mp3'),
},
up: {
linxia: lx('v2_msg05_linxia_curious_wav_v1.mp3'),
npcReact: [rx('v2_msg05_react_curious_wav_v1.mp3')],
profile: { engagement: 2 },
},
down: {
isSilence: true,
profile: { avoidance: 1 },
},
tap: {
linxia: lx('v2_msg05_linxia_tap_wav_v1.mp3'),
},
},
},
// ══════════════════════════════════════════
// 06 · 安安 · 语音(酒吧邀约)
// ══════════════════════════════════════════
{
id: 6, type: 'wechat_voice',
sender: 'anan', senderName: '安安',
audio: [npc('lv11_2200_anan_voice_01_wav_v1.mp3')],
choices: {
right: {
linxia: lx('v2_msg06_linxia_warm_wav_v1.mp3'),
npcReact: [rx('v2_msg06_react_warm_wav_v1.mp3')],
profile: { engagement: 2 },
},
left: {
linxia: lx('v2_msg06_linxia_cold_wav_v1.mp3'),
npcReact: [rx('v2_msg06_react_cold_wav_v1.mp3')],
},
up: {
linxia: lx('v2_msg06_linxia_curious_wav_v1.mp3'),
npcReact: [rx('v2_msg06_react_curious_wav_v1.mp3')],
profile: { engagement: 1 },
},
down: {
isSilence: true,
npcReact: [rx('v2_msg06_react_silent_wav_v1.mp3')],
profile: { avoidance: 1 },
},
tap: {
linxia: lx('v2_msg06_linxia_tap_wav_v1.mp3'),
npcReact: [rx('v2_msg06_react_tap_wav_v1.mp3')],
},
},
},
// ══════════════════════════════════════════
// 07 · 寝室群 · 5条语音
// ══════════════════════════════════════════
{
id: 7, type: 'wechat_voice',
sender: 'dorm', senderName: '寝室群',
audio: [
npc('lv11_2215_dorm_a_voice_01_wav_v1.mp3'),
npc('lv11_2215_dorm_b_voice_01_wav_v1.mp3'),
npc('lv11_2215_dorm_c_voice_01_wav_v1.mp3'),
npc('lv11_2215_dorm_a_voice_02_wav_v1.mp3'),
npc('lv11_2215_dorm_b_voice_02_wav_v1.mp3'),
],
choices: {
right: {
linxia: lx('v2_msg07_linxia_warm_wav_v1.mp3'),
npcReact: [rx('v2_msg07_react_warm_wav_v1.mp3')],
state: { GROUP_REPLY: true },
profile: { engagement: 2 },
followUp: {
// 室友A问"你最近怎么样"后
choices: {
right: {
linxia: lx('v2_msg07_r3_linxia_warm_wav_v1.mp3'),
profile: { engagement: 1 },
},
left: {
linxia: lx('v2_msg07_r3_linxia_cold_wav_v1.mp3'),
npcReact: [rx('v2_msg07_r3_react_cold_wav_v1.mp3')],
},
},
},
},
left: {
linxia: lx('v2_msg07_linxia_cold_wav_v1.mp3'),
profile: { avoidance: 1 },
},
up: {
linxia: lx('v2_msg07_linxia_curious_wav_v1.mp3'),
npcReact: [rx('v2_msg07_react_curious_wav_v1.mp3')],
state: { GROUP_REPLY: true },
profile: { engagement: 2 },
},
down: {
isSilence: true,
profile: { avoidance: 2 },
},
tap: {
linxia: lx('v2_linxia_generic_hmm_wav_v1.mp3'),
npcReact: [rx('v2_msg07_react_tap_wav_v1.mp3')],
state: { GROUP_REPLY: true },
},
},
},
// ══════════════════════════════════════════
// 08 · 未知号码 · 电话(悬疑钩子)
// ══════════════════════════════════════════
{
id: 8, type: 'call',
sender: 'unknown', senderName: '未知号码',
ringtone: `${MUS}/lv11_ring_unknown_v1.mp3`,
audio: [`${SFX}/sfx_breathing_unknown_v1.mp3`],
autoHangup: true,
sfxAfter: `${SFX}/sfx_heartbeat_fast_v1.mp3`,
profileOnMiss: { avoidance: 1 },
profileOnAnswer: { nostalgia: 1 },
},
// ══════════════════════════════════════════
// 09 · 小美(醉)· 4段语音
// ══════════════════════════════════════════
{
id: 9, type: 'wechat_voice',
sender: 'xiaomei', senderName: '小美',
audio: [
npc('lv11_2245_xiaomei_voice_01_seg1_wav_v1.mp3'),
npc('lv11_2245_xiaomei_voice_01_seg2_wav_v1.mp3'),
npc('lv11_2245_xiaomei_voice_01_seg3_wav_v1.mp3'),
npc('lv11_2245_xiaomei_voice_01_seg4_wav_v1.mp3'),
],
bgm: `${MUS}/lv11_bgm_m2_suspense_v1.mp3`,
choices: {
right: {
linxia: lx('v2_msg09_linxia_warm_wav_v1.mp3'),
npcReact: [rx('v2_msg09_react_warm_wav_v1.mp3')],
profile: { engagement: 2 },
},
left: {
linxia: lx('v2_msg09_linxia_cold_wav_v1.mp3'),
profile: { avoidance: 1 },
},
up: {
linxia: lx('v2_msg09_linxia_curious_wav_v1.mp3'),
npcReact: [rx('v2_msg09_react_curious_wav_v1.mp3')],
profile: { engagement: 2 },
followUp: {
// 小美说"怕你恨我"后
choices: {
right: {
linxia: lx('v2_msg09_r3_linxia_warm_wav_v1.mp3'),
npcReact: [rx('v2_msg09_r3_react_warm_wav_v1.mp3')],
},
left: {
linxia: lx('v2_msg09_r3_linxia_cold_wav_v1.mp3'),
npcReact: [rx('v2_msg09_r3_react_cold_wav_v1.mp3')],
},
down: {
isSilence: true,
npcReact: [rx('v2_msg09_r3_react_silent_q_wav_v1.mp3')],
},
},
},
},
down: {
isSilence: true,
profile: { avoidance: 2, nostalgia: 1 },
},
tap: {
linxia: lx('v2_msg09_linxia_tap_wav_v1.mp3'),
npcReact: [rx('v2_msg09_react_tap_wav_v1.mp3')],
profile: { nostalgia: 1 },
},
},
},
// ══════════════════════════════════════════
// 10 · 妈妈 · 文字消息(多层嵌套对话)
// ══════════════════════════════════════════
{
id: 10, type: 'wechat_text',
sender: 'mom', senderName: '妈妈',
text: '睡了吗?',
choices: {
right: {
linxia: lx('v2_msg10_linxia_warm_wav_v1.mp3'),
npcReact: [rx('v2_msg10_react_warm_wav_v1.mp3')],
state: { MOM_LINK: true },
profile: { engagement: 2 },
followUp: {
// 妈妈问"今天怎么样"后
choices: {
right: {
linxia: lx('v2_msg10_rw_linxia_warm_wav_v1.mp3'),
npcReact: [rx('v2_msg10_rw_react_warm_wav_v1.mp3')],
},
up: {
linxia: lx('v2_msg10_rw_linxia_curious_wav_v1.mp3'),
npcReact: [rx('v2_msg10_rw_react_curious_wav_v1.mp3')],
},
down: {
isSilence: true,
npcReact: [rx('v2_msg10_rw_react_silent_wav_v1.mp3')],
profile: { avoidance: 1 },
},
},
},
},
left: {
linxia: lx('v2_msg10_linxia_cold_wav_v1.mp3'),
npcReact: [rx('v2_msg10_react_cold_wav_v1.mp3')],
state: { MOM_LINK: true },
profile: { avoidance: 1 },
},
up: {
linxia: lx('v2_msg10_linxia_curious_wav_v1.mp3'),
npcReact: [rx('v2_msg10_call_mom_wav_v1.mp3')],
state: { MOM_LINK: true },
profile: { engagement: 3 },
followUp: {
// 妈妈打来电话说"怎么了夏夏?"后
choices: {
right: {
linxia: lx('v2_msg10_call_linxia_warm_wav_v1.mp3'),
npcReact: [rx('v2_msg10_call_react_warm_wav_v1.mp3')],
},
up: {
linxia: lx('v2_msg10_call_linxia_curious_wav_v1.mp3'),
npcReact: [rx('v2_msg10_call_react_curious_wav_v1.mp3')],
},
down: {
isSilence: true,
npcReact: [rx('v2_msg10_call_react_down_wav_v1.mp3')],
profile: { avoidance: 1 },
},
},
},
},
down: {
isSilence: true,
npcReact: [rx('v2_msg10_react_silent_wav_v1.mp3')],
profile: { avoidance: 2 },
},
tap: {
linxia: lx('v2_msg10_linxia_tap_wav_v1.mp3'),
npcReact: [rx('v2_msg10_react_tap_wav_v1.mp3')],
state: { MOM_LINK: true },
},
},
},
// ══════════════════════════════════════════
// 11 · 阿哲 · 电话(关键分支:来不来)
// ══════════════════════════════════════════
{
id: 11, type: 'call',
sender: 'azhe', senderName: '阿哲',
ringtone: `${MUS}/lv11_ring_azhe_v1.mp3`,
audio: [npc('lv11_2315_azhe_call_01_wav_v1.mp3')],
profileOnMiss: { avoidance: 2 },
profileOnAnswer: { engagement: 1 },
choices: {
right: {
linxia: lx('v2_msg11_linxia_warm_wav_v1.mp3'),
npcReact: [rx('v2_msg11_react_warm_wav_v1.mp3')],
state: { HE_BACK: true },
},
left: {
linxia: lx('v2_msg11_linxia_cold_wav_v1.mp3'),
npcReact: [rx('v2_msg11_react_cold_wav_v1.mp3')],
state: { HE_BACK: false },
},
up: {
linxia: lx('v2_msg11_linxia_curious_wav_v1.mp3'),
npcReact: [rx('v2_msg11_react_curious_wav_v1.mp3')],
followUp: {
choices: {
right: {
linxia: lx('v2_msg11_linxia_warm_wav_v1.mp3'),
npcReact: [rx('v2_msg11_react_warm_wav_v1.mp3')],
state: { HE_BACK: true },
},
left: {
linxia: lx('v2_msg11_linxia_cold_wav_v1.mp3'),
npcReact: [rx('v2_msg11_react_cold_wav_v1.mp3')],
state: { HE_BACK: false },
},
down: {
isSilence: true,
npcReact: [rx('v2_msg11_r3_react_silent_wav_v1.mp3')],
state: { HE_BACK: false },
profile: { avoidance: 1 },
},
},
},
},
down: {
isSilence: true,
state: { HE_BACK: false },
},
tap: {
linxia: lx('v2_msg11_linxia_tap_wav_v1.mp3'),
npcReact: [rx('v2_msg11_react_tap_wav_v1.mp3')],
state: { HE_BACK: false },
},
},
},
// ══════════════════════════════════════════
// 12 · 周南 · 语音(老同学)
// ══════════════════════════════════════════
{
id: 12, type: 'wechat_voice',
sender: 'zhounan', senderName: '周南',
audio: [npc('lv11_2330_zhounan_voice_01_wav_v1.mp3')],
choices: {
right: {
linxia: lx('v2_msg12_linxia_warm_wav_v1.mp3'),
npcReact: [rx('v2_msg12_react_warm_wav_v1.mp3')],
state: { ZHOUNAN_DEPTH: 1 },
profile: { engagement: 2, nostalgia: 1 },
},
left: {
linxia: lx('v2_msg12_linxia_cold_wav_v1.mp3'),
npcReact: [rx('v2_msg12_react_cold_wav_v1.mp3')],
state: { ZHOUNAN_DEPTH: 1 },
profile: { nostalgia: 1 },
},
up: {
linxia: lx('v2_msg12_linxia_curious_wav_v1.mp3'),
npcReact: [rx('v2_msg12_react_curious_wav_v1.mp3')],
state: { ZHOUNAN_DEPTH: 1 },
profile: { nostalgia: 2 },
},
down: {
isSilence: true,
npcReact: [rx('v2_msg12_react_silent_wav_v1.mp3')],
profile: { nostalgia: 1 },
},
tap: {
linxia: lx('v2_msg12_linxia_tap_wav_v1.mp3'),
npcReact: [rx('v2_msg12_react_tap_wav_v1.mp3')],
state: { ZHOUNAN_DEPTH: 1 },
},
},
},
// ══════════════════════════════════════════
// 13 · 周南 · 4条长语音(他的十年)
// ══════════════════════════════════════════
{
id: 13, type: 'wechat_voice',
sender: 'zhounan', senderName: '周南',
audio: [
npc('lv11_2345_zhounan_voice_01_wav_v1.mp3'),
npc('lv11_2345_zhounan_voice_02_wav_v1.mp3'),
npc('lv11_2345_zhounan_voice_03_wav_v1.mp3'),
npc('lv11_2345_zhounan_voice_04_wav_v1.mp3'),
],
bgm: `${MUS}/lv11_bgm_m3_zhounan_v1.mp3`,
choices: {
right: {
linxia: lx('v2_msg13_linxia_warm_wav_v1.mp3'),
npcReact: [rx('v2_msg13_react_warm_wav_v1.mp3')],
state: { ZHOUNAN_DEPTH_ADD: 1 },
profile: { engagement: 2 },
followUp: {
// 周南问"你在北京过得好吗"后
choices: {
right: {
linxia: lx('v2_msg13_r3_linxia_warm_wav_v1.mp3'),
npcReact: [rx('v2_msg13_r3_react_warm_wav_v1.mp3')],
state: { ZHOUNAN_SHARE: true },
},
left: {
linxia: lx('v2_msg13_r3_linxia_cold_wav_v1.mp3'),
npcReact: [rx('v2_msg13_r3_react_cold_wav_v1.mp3')],
},
up: {
linxia: lx('v2_msg13_r3_linxia_curious_wav_v1.mp3'),
npcReact: [rx('v2_msg13_r3_react_curious_wav_v1.mp3')],
},
},
},
},
left: {
linxia: lx('v2_msg13_linxia_cold_wav_v1.mp3'),
npcReact: [rx('v2_msg13_react_cold_wav_v1.mp3')],
state: { ZHOUNAN_DEPTH_ADD: 1 },
profile: { nostalgia: 1 },
},
up: {
linxia: lx('v2_msg13_linxia_curious_wav_v1.mp3'),
npcReact: [rx('v2_msg13_react_curious_wav_v1.mp3')],
state: { ZHOUNAN_DEPTH_ADD: 1, ZHOUNAN_SHARE: true },
profile: { nostalgia: 2, engagement: 1 },
},
down: {
isSilence: true,
npcReact: [rx('v2_msg13_react_silent_wav_v1.mp3')],
profile: { avoidance: 1 },
},
tap: {
linxia: lx('v2_msg13_linxia_tap_wav_v1.mp3'),
npcReact: [rx('v2_msg13_react_tap_wav_v1.mp3')],
state: { ZHOUNAN_DEPTH_ADD: 1 },
},
},
},
// ══════════════════════════════════════════
// 14 · 妈妈 · 第二次电话
// ══════════════════════════════════════════
{
id: 14, type: 'call',
sender: 'mom', senderName: '妈妈',
ringtone: `${MUS}/lv11_ring_mom_v1.mp3`,
audio: [npc('lv11_0015_mom_call_01_wav_v1.mp3')],
autoReply: lx('v2_msg14_linxia_auto_wav_v1.mp3'),
autoReact: {
condition: 'MOM_LINK',
true: rx('v2_msg14_react_linked_wav_v1.mp3'),
false: rx('v2_msg14_react_unlinked_wav_v1.mp3'),
},
autoReactPreamble: rx('v2_msg14_mom_ask_wav_v1.mp3'),
stateOnAnswer: { MOM_LINK: true },
profileOnMiss: { avoidance: 2 },
profileOnAnswer: { engagement: 2 },
},
// ══════════════════════════════════════════
// 15 · 时光胶囊 · 系统通知
// ══════════════════════════════════════════
{
id: 15, type: 'system_notification',
sender: 'self', senderName: '时光胶囊',
systemAudio: npc('lv11_sys_memo_01_wav_v1.mp3'),
audioByProfile: {
avoidance: npc('lv11_0030_self3y_voice_02_wav_v1.mp3'),
engagement: npc('lv11_0030_self3y_voice_01_wav_v1.mp3'),
nostalgia: npc('lv11_0030_self3y_voice_03_wav_v1.mp3'),
},
stateOnPlay: { SELF_RECORD: true },
profileOnPlay: { nostalgia: 2 },
profileOnSkip: { avoidance: 2 },
},
// ══════════════════════════════════════════
// 16 · 妈妈 · 长语音 ×8(结局前奏)
// ══════════════════════════════════════════
{
id: 16, type: 'wechat_voice',
sender: 'mom', senderName: '妈妈',
audio: [
npc('lv11_0200_mom_voice_01_seg1_wav_v1.mp3'),
npc('lv11_0200_mom_voice_01_seg2_wav_v1.mp3'),
npc('lv11_0200_mom_voice_01_seg3_wav_v1.mp3'),
npc('lv11_0200_mom_voice_01_seg4_wav_v1.mp3'),
npc('lv11_0200_mom_voice_01_seg5_wav_v1.mp3'),
npc('lv11_0200_mom_voice_01_seg6_wav_v1.mp3'),
npc('lv11_0200_mom_voice_01_seg7_wav_v1.mp3'),
npc('lv11_0200_mom_voice_01_seg8_wav_v1.mp3'),
],
bgm: `${MUS}/lv11_bgm_m4_ending_v1.mp3`,
isEnding: true,
requiresMomLink: true,
},
];
// 角色信息
const SENDERS = {
mom: { name: '妈妈' },
azhe: { name: '阿哲' },
xiaomei: { name: '小美' },
delivery: { name: '外卖小哥' },
hr: { name: 'HR王姐' },
anan: { name: '安安' },
dorm: { name: '寝室群' },
unknown: { name: '未知号码' },
zhounan: { name: '周南' },
self: { name: '时光胶囊' },
};
const AMBIENCE_DEFAULT = `${AMB}/amb_apartment_night_01_v1.mp3`;
+38
View File
@@ -0,0 +1,38 @@
// utils.js — 工具函数
const DEV = new URLSearchParams(location.search).has('dev');
const sleep = ms => new Promise(r => setTimeout(r, ms));
function dbg(msg) {
const el = document.getElementById('dbg');
if (el && el.style.display !== 'none') el.textContent = msg;
}
// TTS — 仅用于朗读文字消息内容,不用于任何 UI 反馈
const TTS = {
_voices: [],
init() {
if (!window.speechSynthesis) return;
const load = () => { this._voices = speechSynthesis.getVoices(); };
load();
speechSynthesis.onvoiceschanged = load;
},
_pick() {
return this._voices.find(v => v.lang.startsWith('zh') || v.name.includes('Chinese'))
|| this._voices[0] || null;
},
speak(text, { rate = 0.85, volume = 0.8 } = {}) {
return new Promise(resolve => {
if (!window.speechSynthesis) { resolve(); return; }
speechSynthesis.cancel();
const u = new SpeechSynthesisUtterance(text);
u.lang = 'zh-CN'; u.rate = rate; u.volume = volume;
const v = this._pick();
if (v) u.voice = v;
u.onend = resolve; u.onerror = resolve;
speechSynthesis.speak(u);
});
},
stop() { if (window.speechSynthesis) speechSynthesis.cancel(); },
};
+13
View File
@@ -0,0 +1,13 @@
{
"name": "林夏",
"short_name": "林夏",
"description": "一款全黑屏的情感叙事游戏。今晚16条消息,你接谁不接谁,决定明天的自己。",
"start_url": "/game/",
"display": "fullscreen",
"orientation": "portrait",
"background_color": "#000000",
"theme_color": "#000000",
"icons": [
{ "src": "icon.png", "sizes": "192x192", "type": "image/png" }
]
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More