docs: v2.1 全自动演出设计 + 计划(开场自动/回复自动继续/终局演绎/已播行折叠)

This commit is contained in:
2026-08-13 16:59:49 +08:00
parent 3dde5b2b42
commit 035a115c29
2 changed files with 295 additions and 1 deletions
@@ -1126,7 +1126,262 @@ git commit -m "docs: 剧情演绎文档补充 + 全量剧本生成导入"
---
## 验收对照(spec v2
### Task 8: 后端 FinalSettle.final_nodev2.1 终局演绎数据层
> v2.1choose 到终局时,结局台词目前从未播放(前端直接跳结算页)。本任务让终局节点(含 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: 全部 PASSSettleFinal 纯函数不受影响)
- [ ] **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
<view class="theater-actions">
<view class="btn-primary theater-go" @click="finish">开始 </view>
</view>
```
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
<view class="story-lines">
<view v-for="(line, i) in script" :key="i" v-show="i === lineIndex || i === lineIndex - 1" class="story-line" :class="{ active: i === lineIndex, fading: i === lineIndex - 1 }">
```
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
<!-- v2.1 终局演绎:结局台词播完自动跳结算 -->
<view v-if="playingFinal && finalNode" class="node story-node final-story">
<StoryPlayer :key="'final-' + finalNode.node_id" :script="finalScript" :character="finalNode.character" :show-pinyin="showPinyin" @done="gotoResult" />
</view>
```
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 2v-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 |
| 验收标准 | 任务 |
|---|---|
@@ -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 分钟儿童小剧场,三幕结构: