From b3384499e385e528256191a65bce7648d3f343a7 Mon Sep 17 00:00:00 2001 From: xsl Date: Sun, 24 May 2026 12:35:35 +0800 Subject: [PATCH] save code --- game/js/engine.js | 31 +++++-- game/js/haptics.js | 3 - game/js/main.js | 27 +++--- game/js/profile.js | 21 +---- game/js/utils.js | 6 +- game/操作说明.md | 200 +++++++++++++++++---------------------------- 6 files changed, 121 insertions(+), 167 deletions(-) diff --git a/game/js/engine.js b/game/js/engine.js index 6915907..a3c3f1f 100644 --- a/game/js/engine.js +++ b/game/js/engine.js @@ -52,10 +52,10 @@ class GameEngine { if (!el) return; const H = { idle: '', - notification: '单击 播放 · 双击 跳过', - ringing: '单击 接听 · 左滑 拒接', + notification: '单击 播放 · 双击/左滑 跳过', + ringing: '单击/右滑 接听 · 左滑/双击 拒接', playing: '双击 跳过 · 长按 重播', - choosing: '→温暖 ←冷淡 ↑追问 ↓沉默', + choosing: '单击回应 · →温暖 ←冷淡 ↑追问 ↓沉默 · 双击跳过', responding: '双击 跳过', reacting: '双击 跳过', paused: '单击 继续 · 三指点击 退出', @@ -286,9 +286,13 @@ class GameEngine { this._skipRequested = false; - // 文字消息(TTS 朗读) + // 文字消息(TTS 朗读,不可用时播放提示音) if (msg.type === 'wechat_text' && msg.text) { - await TTS.speak(msg.text); + const ok = await TTS.speak(msg.text); + if (!ok) { + SFX_UI.notify(); + await sleep(600); + } } // 语音消息 else if (msg.audio?.length > 0) { @@ -546,6 +550,9 @@ class GameEngine { this._playMessage(this.currentMsg); } else if (s === 'choosing') { this._handleChoice('tap'); + } else if (s === 'idle') { + SFX_UI.error(); + Haptic.error(); } } @@ -598,6 +605,11 @@ class GameEngine { this._skipChoice(); return; } + + if (s === 'idle') { + SFX_UI.error(); + Haptic.error(); + } } _onSwipe(dir) { @@ -626,6 +638,11 @@ class GameEngine { this._handleChoice(dir); return; } + + if (s === 'idle') { + SFX_UI.error(); + Haptic.error(); + } } // ── 长按:重播 / 状态广播 ── @@ -642,13 +659,13 @@ class GameEngine { 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); + TTS.speak(this.currentMsg.text).then(ok => { if (!ok) SFX_UI.notify(); }); } return; } // idle/notification → 广播未读数 - TTS.speak(`当前未读${this.vars.UNREAD}条消息`); + TTS.speak(`当前未读${this.vars.UNREAD}条消息`).then(ok => { if (!ok) SFX_UI.error(); }); } // ── 暂停 ── diff --git a/game/js/haptics.js b/game/js/haptics.js index 02bbbce..61fd2e0 100644 --- a/game/js/haptics.js +++ b/game/js/haptics.js @@ -19,9 +19,6 @@ const Haptic = { // 拒绝 / 挂断 reject() { this._v([100, 50, 100, 50, 100]); }, - // 进入细回复模式 - detailMode() { this._v([30, 30, 60]); }, - // 选项选中 select() { this._v(80); }, diff --git a/game/js/main.js b/game/js/main.js index fbd60bc..7498d4d 100644 --- a/game/js/main.js +++ b/game/js/main.js @@ -31,23 +31,26 @@ async function main() { } } +// 防止双重启动 +let _started = false; +function startGame(el) { + if (_started) return; + _started = true; + el.style.transition = 'opacity 0.8s'; + el.style.opacity = '0'; + setTimeout(() => { el.style.display = 'none'; }, 800); + main(); +} + // 触摸启动(手机) -document.getElementById('start').addEventListener('touchend', async function onStart(e) { +document.getElementById('start').addEventListener('touchend', function(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(); + startGame(this); }, { 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(); +document.getElementById('start').addEventListener('click', function() { + startGame(this); }, { once: true }); // 防止页面滚动/缩放 diff --git a/game/js/profile.js b/game/js/profile.js index c5d6024..803b276 100644 --- a/game/js/profile.js +++ b/game/js/profile.js @@ -12,28 +12,11 @@ class ProfileTracker { } } - // 漏接电话 - 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'; + if (s.engagement > s.avoidance && s.engagement >= s.nostalgia) return 'engagement'; + if (s.nostalgia > s.avoidance) return 'nostalgia'; return 'avoidance'; } } diff --git a/game/js/utils.js b/game/js/utils.js index 29a1f93..563a3ec 100644 --- a/game/js/utils.js +++ b/game/js/utils.js @@ -22,15 +22,17 @@ const TTS = { return this._voices.find(v => v.lang.startsWith('zh') || v.name.includes('Chinese')) || this._voices[0] || null; }, + get available() { return !!window.speechSynthesis; }, speak(text, { rate = 0.85, volume = 0.8 } = {}) { return new Promise(resolve => { - if (!window.speechSynthesis) { resolve(); return; } + if (!window.speechSynthesis) { resolve(false); 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; + u.onend = () => resolve(true); + u.onerror = () => resolve(false); speechSynthesis.speak(u); }); }, diff --git a/game/操作说明.md b/game/操作说明.md index 9601460..2345c39 100644 --- a/game/操作说明.md +++ b/game/操作说明.md @@ -19,7 +19,7 @@ 2. 打开手机浏览器,输入地址: ``` - http://172.28.107.97:8765/game/ + http://<服务器IP>:8765/game/ ``` 3. **关灯,戴上耳机** @@ -30,167 +30,119 @@ --- -## 三、消息类型与接收方式 +## 三、操作总览 -游戏中有三种消息形态: - -### 来电 -- 触发:手机长震 + 角色专属铃声 -- 响 **20–30 秒**后自动漏接 +### 来电时 | 操作 | 效果 | |------|------| -| **单点** | 接听 | -| **左滑** | 拒接 | -| **不动** | 漏接(对方挂断) | +| **单击 / 右滑** | 接听 | +| **左滑 / 双击** | 拒接 | +| **不动** | 铃声持续(不会自动挂断) | -### 微信语音 / 微信文字 -- 触发:两短震 + 系统提示音(TTS 报出发件人) +### 收到消息时 | 操作 | 效果 | |------|------| -| **单点** | 播放语音 / 念出文字 | -| **左滑** | 忽略(保持未读) | -| **不动** | 保持未读,晚些再听 | +| **单击** | 播放消息内容 | +| **双击 / 左滑** | 跳过这条消息 | ---- +### 播放中 -## 四、回复消息 +| 操作 | 效果 | +|------|------| +| **双击** | 跳过当前播放 | +| **长按** | 重播当前消息 | -**听完语音 / 文字念完后**,进入等待回复状态: +### 选择回复(听完消息后) -| 操作 | 发送内容 | +系统会播放提示音,进入回复选择状态: + +| 操作 | 情感方向 | |------|----------| -| **单点** | 发"嗯。"(最简单的回执) | -| **双点** | 发标准回复(针对该消息的一句话) | -| **长按 1 秒** | 进入**细回复**模式 | -| **左滑** | 已读不回 | -| **不动** | 维持"已读未回"状态 | +| **单击** | 简单回应 | +| **右滑** | 温暖 / 接受 | +| **左滑** | 冷淡 / 拒绝 | +| **上滑** | 好奇 / 追问 | +| **下滑** | 沉默 / 退缩 | +| **双击** | 不回复(跳过) | + +> 部分消息的回复会触发**多轮对话**——NPC 回应后,再次进入选择状态。 + +### 随时可用 + +| 操作 | 效果 | +|------|------| +| **两指长按** | 暂停游戏 | +| **单击**(暂停中) | 继续游戏 | +| **三指点击**(暂停中) | 退出游戏 | +| **长按**(空闲/通知时) | 语音播报未读消息数 | --- -## 五、细回复(核心机制) +## 四、消息类型 -长按 1 秒后,震动确认进入细回复。系统会依次念出 **4 个候选回复**: +游戏中有四种消息形态: -``` -一…… "好,明天寄。" -二…… "你怎么不自己来拿。" -三…… "你那边……都好吗。" -四…… (什么都不说) -``` +### 来电(call) +手机长震 + 角色专属铃声。单击/右滑接听,左滑/双击拒接。 -**在听到目标选项时,朝对应方向滑动:** +### 微信语音(wechat_voice) +两短震 + 系统提示音。单击播放语音内容。 -| 滑动方向 | 对应选项 | -|----------|----------| -| **左滑** | 一 | -| **右滑** | 二 | -| **上滑** | 三 | -| **下滑** | 四 | +### 微信文字(wechat_text) +两短震 + 系统提示音。单击后由系统朗读文字内容。 -> **没听清?** 单点屏幕可重听所有选项,或长按 1 秒重新播放。 -> 选错了也没关系——每条消息只影响行为画像,不会"game over"。 +### 系统通知(system_notification) +特殊消息,播放系统提示后自动播放内容。 --- -## 六、查看当前状态 - -**随时下滑**,系统会用语音告诉你当前在哪个页面,以及可以做什么操作。 - -例如: -- 主屏幕时下滑 → "主屏幕,3条未读。上滑查看收件箱。" -- 来电时下滑 → "妈妈来电中。单点接听,左滑拒接。" -- 听完消息后下滑 → "阿哲的消息已播完。单点回嗯,双点标准回复,长按细回复,左滑已读不回。" - -> 不确定自己在哪?下滑就对了。 - ---- - -## 七、查看未读列表(收件箱) - -在主屏幕**上滑**,进入收件箱模式。 - -系统会依次念出所有未读消息: -``` -"收件箱:4条未读。妈妈,来电。阿哲,语音。周南,语音。……" -``` - -- **单点** → 跳转至列表第一条 -- **左滑** → 返回主屏幕 -- **等待 8 秒** → 自动关闭收件箱,返回游戏 - ---- - -## 八、游戏内时间 - -游戏将现实的 21:00–02:00(5 小时)压缩为 **60 分钟**真实时间。 - -| 游戏时间 | 真实等待 | -|----------|----------| -| 21:03 | 约 36 秒 | -| 21:30 | 约 6 分钟 | -| 22:30 | 约 18 分钟 | -| 23:45 | 约 33 分钟 | -| 02:00 | 约 60 分钟 | - -消息到来时间固定,**漏接 / 不听是合法选择**,但漏掉的内容不会重发。 - ---- - -## 九、结局说明(不含剧透) - -共 **5 种结局**,由以下 5 个变量决定: - -| 变量 | 触发条件 | -|------|----------| -| 与妈妈的连接 | 是否接了妈妈的电话 / 回了文字 | -| 阿哲是否回来 | 23:15 那通电话的选择 | -| 群里是否发言 | 寝室群消息的回应 | -| 与周南的深度 | 回应周南的次数与方式 | -| 是否听自己 | 00:30 时光胶囊是否播放 | - -另有一个**隐藏结局 E**,需要与某位角色深度互动才能触发。 - ---- - -## 十、行为画像 +## 五、行为画像 游戏全程追踪三个维度(玩家不可见): | 维度 | 含义 | 触发行为 | |------|------|----------| -| **avoidance**(回避) | 林夏今晚想一个人待着 | 漏接、不回、已读不回 | -| **engagement**(投入) | 林夏在认真面对每一个人 | 细回复、主动追问 | -| **nostalgia**(怀旧) | 林夏在这一夜反复回望 | 重听同一条消息、分享自己 | +| **avoidance**(回避) | 林夏今晚想一个人待着 | 跳过消息、不回复、沉默 | +| **engagement**(投入) | 林夏在认真面对每一个人 | 温暖回应、好奇追问 | +| **nostalgia**(怀旧) | 林夏在这一夜反复回望 | 与老同学深聊、分享自己 | -画像决定 **00:30 时光胶囊**播放哪个版本(明朗 / 迷茫 / 温柔)。 +画像影响**时光胶囊**(消息15)播放哪个版本。 --- -## 十一、彩蛋 +## 六、结局说明(不含剧透) -- **夜间启动**(现实时间 22:00–04:00 之间打开游戏):开场多一句话 -- **消息09**:小美醉语音结束后会有一个撤回事件 -- **消息04**:如果一直不回阿哲,会听到一个小细节 +共 **5 种结局**,由以下变量决定: + +| 变量 | 触发条件 | +|------|----------| +| 与妈妈的连接 | 是否接了妈妈的电话 / 回了消息 | +| 阿哲是否回来 | 23:15 那通电话的选择 | +| 与周南的深度 | 回应周南的次数与方式 | +| 是否听自己 | 00:30 时光胶囊是否播放 | +| 行为画像 | 整体回应风格 | + +另有一个**隐藏结局 E**,需要与某位角色深度互动才能触发。 --- -## 十二、开发者 / 测试模式 +## 七、开发者 / 测试模式 -在 URL 末尾加 `?dev` 可开启**10倍速**模式: +在 URL 末尾加 `?dev` 可开启开发模式: ``` -http://172.28.107.97:8765/game/?dev +http://<服务器IP>:8765/game/?dev ``` -- 所有消息的触发时间缩短为原来的 1/10 -- 全程约 **6 分钟**可打通 -- 适合测试结局分支 +- 消息之间的等待间隔从 2 秒缩短为 0.2 秒 +- 屏幕底部显示调试信息(当前状态) +- 适合快速测试分支和结局 --- -## 十三、启动服务器 +## 八、启动服务器 关机重启后需要重新启动 HTTP 服务器: @@ -207,7 +159,7 @@ hostname -I | awk '{print $1}' --- -## 十四、文件结构 +## 九、文件结构 ``` /home/xsl/blind/ @@ -218,18 +170,18 @@ hostname -I | awk '{print $1}' │ └── js/ │ ├── utils.js 工具函数 + 浏览器 TTS │ ├── haptics.js 震动反馈 -│ ├── audio.js 音频管理器 +│ ├── audio.js 音频管理器 + 合成 UI 音效 │ ├── gestures.js 单指手势检测 │ ├── profile.js 行为画像评分 │ ├── story.js 16条消息数据 │ ├── engine.js 游戏状态机 │ └── main.js 入口 └── audio/ - └── 00_raw/ - ├── tts/ 角色语音(68条) - ├── music/ BGM + 铃声 - ├── sfx/ 音效 - └── ambience/ 环境底噪 + └── mp3/ + ├── tts/ 角色语音(169条) + ├── music/ BGM + 铃声(8条) + ├── sfx/ 音效(4条) + └── ambience/ 环境底噪(3条) ``` ---