From 035a115c294a3fb6d8b105ce5fed3aed47938c42 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=BC=A0=E6=96=8C?= <259278618@qq.com>
Date: Thu, 13 Aug 2026 16:59:49 +0800
Subject: [PATCH] =?UTF-8?q?docs:=20v2.1=20=E5=85=A8=E8=87=AA=E5=8A=A8?=
=?UTF-8?q?=E6=BC=94=E5=87=BA=E8=AE=BE=E8=AE=A1=20+=20=E8=AE=A1=E5=88=92?=
=?UTF-8?q?=EF=BC=88=E5=BC=80=E5=9C=BA=E8=87=AA=E5=8A=A8/=E5=9B=9E?=
=?UTF-8?q?=E5=A4=8D=E8=87=AA=E5=8A=A8=E7=BB=A7=E7=BB=AD/=E7=BB=88?=
=?UTF-8?q?=E5=B1=80=E6=BC=94=E7=BB=8E/=E5=B7=B2=E6=92=AD=E8=A1=8C?=
=?UTF-8?q?=E6=8A=98=E5=8F=A0=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../plans/2026-08-13-story-theater-v2.md | 257 +++++++++++++++++-
.../specs/2026-08-13-story-theater-design.md | 39 +++
2 files changed, 295 insertions(+), 1 deletion(-)
diff --git a/docs/superpowers/plans/2026-08-13-story-theater-v2.md b/docs/superpowers/plans/2026-08-13-story-theater-v2.md
index 847e6fc..5bd86f4 100644
--- a/docs/superpowers/plans/2026-08-13-story-theater-v2.md
+++ b/docs/superpowers/plans/2026-08-13-story-theater-v2.md
@@ -1126,7 +1126,262 @@ git commit -m "docs: 剧情演绎文档补充 + 全量剧本生成导入"
---
-## 验收对照(spec v2)
+### Task 8: 后端 FinalSettle.final_node(v2.1 终局演绎数据层)
+
+> v2.1:choose 到终局时,结局台词目前从未播放(前端直接跳结算页)。本任务让终局节点(含 script)随 FinalSettle 一并返回,前端 Task 9 先演绎再跳结算。
+
+**Files:**
+- Modify: `biz/service/level_play.go:32-43`(FinalSettle struct)
+- Modify: `biz/service/level_play.go:149-166`(Choose 终局分支)
+- Modify: `biz/model/dto/level_play.go:18-28`
+- Modify: `biz/controller/level_play.go:25-37`
+
+- [ ] **Step 1: service FinalSettle 加 FinalNode**
+
+`biz/service/level_play.go` FinalSettle struct 末尾加:
+
+```go
+ FinalNode *Node // 终局节点(含 script,v2.1 前端演绎结局台词后跳结算)
+```
+
+- [ ] **Step 2: Choose 终局分支返回终局 Node**
+
+`biz/service/level_play.go` Choose 中 `WithLock` 调用之后、`return nil, settle, nil` 之前插入:
+
+```go
+ // v2.1:终局节点剧本随结算返回(前端先演绎结局台词再跳结算页)
+ finalNode, err := nodeOf(ctx, levelRec, nextRec)
+ if err != nil {
+ return nil, nil, err
+ }
+ settle.FinalNode = finalNode
+```
+
+(`nodeOf` 与决策分支同函数、同签名,构建选项 + 元素 + buildNode;只读查询,锁外执行安全。)
+
+- [ ] **Step 3: dto FinalSettle 加 FinalNode**
+
+`biz/model/dto/level_play.go` FinalSettle 末尾加:
+
+```go
+ FinalNode *NodeVO `json:"final_node"`
+```
+
+- [ ] **Step 4: controller 映射**
+
+`biz/controller/level_play.go` Choose 的 `res.Final = &dto.FinalSettle{...}` 之后加:
+
+```go
+ if settle.FinalNode != nil {
+ vo := nodeVO(settle.FinalNode)
+ res.Final.FinalNode = &vo
+ }
+```
+
+- [ ] **Step 5: 编译 + 单测**
+
+Run: `go build ./... && go test ./biz/service/ ./cmd/genasset/`
+Expected: 全部 PASS(SettleFinal 纯函数不受影响)
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add biz/service/level_play.go biz/model/dto/level_play.go biz/controller/level_play.go
+git commit -m "feat: FinalSettle 增加 final_node——终局节点剧本透出(v2.1 终局演绎数据层)"
+```
+
+---
+
+### Task 9: 前端全自动演出(v2.1)
+
+> 用户走查反馈:情景执行依赖点按钮、文字太多。本任务:开场自动进入、回应气泡自动继续、终局台词先演绎再跳结算、StoryPlayer 已播行折叠。
+
+**Files:**
+- Modify: `ui-src/src/components/SceneTheater.vue`
+- Modify: `ui-src/src/components/StoryPlayer.vue`
+- Modify: `ui-src/src/pages/play/play.vue`
+
+- [ ] **Step 1: SceneTheater 移除「开始」按钮,朗读结束自动 done**
+
+`SceneTheater.vue` 三处修改:
+
+a) 模板删除 `theater-actions` 整块:
+
+```html
+
+ 开始 ›
+
+```
+
+b) mounted 改为朗读结束自动 emit done:
+
+```js
+ mounted() {
+ this.speaking = true
+ speak(this.content, this.audio)
+ // v2.1:朗读结束自动进入决策幕(「跳过」仍可手动跳过)
+ setTimeout(() => { this.speaking = false; this.$emit('done') }, Math.min(3000, this.content.length * 300))
+ },
+```
+
+c) 删除样式(不再有按钮):
+
+```css
+.theater-actions { display: flex; justify-content: center; margin-top: 32rpx; position: relative; z-index: 2; }
+.theater-go { font-size: 34rpx; min-width: 240rpx; animation: t-pop 0.5s ease 1.2s backwards; }
+```
+
+- [ ] **Step 2: StoryPlayer 已播行折叠**
+
+`StoryPlayer.vue` 模板 `story-lines` 循环改为只保留当前行 + 上一行(v-show 保持 DOM 存活,fading 淡出):
+
+```html
+
+
+```
+
+CSS `.story-line.done` 替换为:
+
+```css
+.story-line.fading { opacity: 0.35; }
+```
+
+(`done` 类不再被引用,一并删除;`active` 保持现状。)
+
+- [ ] **Step 3: play.vue 终局演绎 + 回复自动继续**
+
+`play.vue` 六处修改:
+
+a) data 加:
+
+```js
+ replyTimer: null,
+ playingFinal: false,
+ finalNode: null,
+```
+
+b) onUnload 清回复计时器:
+
+```js
+ onUnload() {
+ if (this.branchTimer) clearTimeout(this.branchTimer)
+ if (this.replyTimer) clearTimeout(this.replyTimer)
+ },
+```
+
+c) computed 加:
+
+```js
+ finalScript() {
+ return this.finalNode ? parseScript(this.finalNode.script) : null
+ }
+```
+
+d) 模板在 `v-if="curNode && isStoryNode"` 之前插入终局演绎分支(v-if 优先于剧情流,避免终局节点 script 非空时走错分支):
+
+```html
+
+
+
+
+```
+
+e) submit 的 `else if (data.final)` 分支替换为:
+
+```js
+ } else if (data.final) {
+ uni.setStorageSync(RESULT_KEY, {
+ level_id: this.levelId,
+ title: this.detail.title,
+ scene_name: this.detail.scene ? this.detail.scene.name : '',
+ route_steps: this.routeSteps,
+ final: data.final
+ })
+ this.choosing = false
+ // v2.1:终局节点有剧本 → 先演绎结局台词再跳结算;无剧本直接跳(回退)
+ if (data.final.final_node && parseScript(data.final.final_node.script)) {
+ this.finalNode = data.final.final_node
+ this.playingFinal = true
+ } else {
+ playSound('finish')
+ uni.redirectTo({ url: '/pages/result/result' })
+ }
+ }
+```
+
+f) branchTimer 内剧情流分支(`this.isStoryNode`)加 3 秒自动继续:
+
+```js
+ if (this.isStoryNode) {
+ this.reply = fb
+ this.showOptions = false // 提交后隐藏选项区,防重复点击
+ // v2.1:回应气泡约 3 秒自动继续(点击「继续」立即继续)
+ this.replyTimer = setTimeout(() => this.autoContinue(), 3000)
+ } else {
+ this.feedback = fb
+ }
+```
+
+g) methods 加 `autoContinue` / `gotoResult`,`continuePlay` 开头清计时器:
+
+```js
+ autoContinue() {
+ if (this.reply && !this.choosing) this.continuePlay()
+ },
+ gotoResult() {
+ playSound('finish')
+ uni.redirectTo({ url: '/pages/result/result' })
+ },
+ continuePlay() {
+ if (this.replyTimer) { clearTimeout(this.replyTimer); this.replyTimer = null }
+ if (this.isStoryNode) {
+ this.curNode = this.reply.next
+ this.reply = null
+ this.showOptions = false
+ } else {
+ this.curNode = this.feedback.next
+ this.feedback = null
+ }
+ this.mood = '🤔'
+ }
+```
+
+- [ ] **Step 4: 构建验证**
+
+Run: `cd ui-src && npx vite build --mode h5 2>&1 | tail -3`
+Expected: 构建成功无报错
+
+- [ ] **Step 5: H5 端到端走查**
+
+后端与 H5 dev server 运行中,走查 `http://localhost:5173/#/pages/play/play?level_id=<第1计关卡>`(第 1 计剧本已导入):
+1. 开场朗读结束自动进入剧情流(无「开始」按钮);「跳过 ›」立即进入
+2. 剧情流只显示当前行 + 上一行淡出,无 3-6 行堆叠
+3. 选择后回应气泡约 3 秒自动进入下一节点;点击「继续 ›」立即进入;连点不双跳
+4. 走到终局:结局台词先演绎(跳过可直达结算),播完自动跳结算页;结算数据正常(final 已在 RESULT_KEY)
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add ui-src/src/components/SceneTheater.vue ui-src/src/components/StoryPlayer.vue ui-src/src/pages/play/play.vue
+git commit -m "feat: 全自动演出——开场自动进入/回复自动继续/终局台词演绎/已播行折叠(v2.1)"
+```
+
+---
+
+## 验收对照(spec v2 + v2.1)
+
+| 验收标准 | 任务 |
+|---|---|
+| 决策节点台词流演绎(气泡/朗读/逐字高亮/表情,末句提问) | Task 3 + Task 6 |
+| ActionCard 嵌入剧情流(触控 5 种同) | Task 4 |
+| 角色回应气泡融入剧情流,无弹窗 | Task 4 Step 3 |
+| script 空回退 v1 呈现 | Task 4 Step 2(v-else 分支) |
+| 管线生成→精修→导入→前端生效;校验拒绝不合规草稿 | Task 5 + Task 6 |
+| level/choose 判分与完美机制不变 | Task 1(仅加列透出);Task 6 Step 5 走查确认 |
+| 开场自动进入决策幕(无「开始」按钮),跳过可用 | Task 9 Step 1 |
+| 回应气泡约 3 秒自动继续,点击立即继续,无重复推进 | Task 9 Step 3 |
+| 终局台词先演绎再自动跳结算;final_node.script 空直接跳 | Task 8 + Task 9 Step 3 |
+| StoryPlayer 已播行折叠(当前行 + 上一行淡出) | Task 9 Step 2 |
| 验收标准 | 任务 |
|---|---|
diff --git a/docs/superpowers/specs/2026-08-13-story-theater-design.md b/docs/superpowers/specs/2026-08-13-story-theater-design.md
index a4124c6..3af2648 100644
--- a/docs/superpowers/specs/2026-08-13-story-theater-design.md
+++ b/docs/superpowers/specs/2026-08-13-story-theater-design.md
@@ -85,6 +85,45 @@
- **阶段 B(生成管线)**:genasset `--only=scripts` / `--import-scripts` + 第 1 计试点生成并精修 1 关
- **阶段 C(全量)**:36 关剧本后台生成 + 人工精修流程 + 走查验收
+## v2.1 修订(2026-08-13):全自动演出 + 文字精简
+
+> 用户走查反馈(v2 落地后):**「现在的情景执行依赖于点击按钮,这是不对的,应该自动的,而且也不需要显示这么多文字,文字多了,孩子更不喜欢」**。v2 解决「演什么」,v2.1 解决「怎么演得像动画」——消除按钮依赖、削减屏幕文字量。数据模型零改动;接口仅 `FinalSettle` 多返回 `final_node`(复用既有 NodeVO 链路,含 script)。
+
+### 核心转变
+
+| 维度 | v2(现状) | v2.1(自动演出) |
+|---|---|---|
+| 开场 | SceneTheater 朗读后需点「开始 ›」 | 朗读完成后自动进入决策幕(保留「跳过 ›」) |
+| 反馈 | 回应气泡需点「继续 ›」 | 气泡出现约 3 秒后自动继续(点击可立即继续,计时器防重复) |
+| 终局 | choose 到终局直接跳结算页,结局台词从未播放 | 先由 StoryPlayer 演绎终局节点台词(`final_node.script`),播完自动跳结算 |
+| 文字量 | StoryPlayer 3-6 行完整堆叠 | 已播行折叠:只保留当前行 + 上一行淡出 |
+
+### 数据与接口(最小改动)
+
+- `FinalSettle` 增加 `final_node`(NodeVO,含 script):service Choose 终局分支把终局节点记录(既有 `nextRec`)经 `nodeOf` 构建为 Node 一并返回;controller 映射到 dto
+- play.vue `data.final` 分支:先存 RESULT_KEY(同现状),若 `final_node.script` 非空 → 进入终局演绎态(StoryPlayer 播 `final_node` 剧本)→ `@done` 后 `redirectTo` 结算页;script 为空 → 直接跳结算(行为同现状)
+
+### 自动节奏
+
+- SceneTheater:移除「开始 ›」按钮;朗读结束(mounted 的 setTimeout,同现状)自动 emit done;「跳过 ›」保留
+- 回应气泡(剧情流 reply):出现后 3 秒自动 `continuePlay()`;点击「继续 ›」立即继续;自动计时触发前点击需 clearTimeout 防重复推进;v1 弹窗反馈(feedback)不加自动继续(保持人工节奏)
+- 终局演绎:StoryPlayer 播完自动跳结算(3-6 句 × 8-30 字,约 10-25 秒);跳过按钮同样可跳过终局台词直接跳结算
+
+### 前端回退与渐进升级
+
+- `final_node.script` 为空 → 直接跳结算(现状不变)
+- 终局演绎复用 StoryPlayer 与剧情流形态(`:key` 强制重建、done → 跳转),不新增组件
+
+### 验收标准(v2.1)
+
+1. 开场自动进入决策幕(无「开始」按钮),「跳过」仍可用
+2. 回应气泡约 3 秒自动继续;点击可立即继续;无重复推进(连点/自动+点击不双跳)
+3. 终局节点台词先演绎再自动跳结算;`final_node.script` 为空直接跳结算(回退)
+4. StoryPlayer 已播行折叠:当前行 + 上一行淡出,无 3-6 行完整堆叠
+5. 判分与完美机制行为不变(level_play 单测全过)
+
+---
+
## 设计总览
每关 = 一部 2-3 分钟儿童小剧场,三幕结构: