docs: v2.1 全自动演出设计 + 计划(开场自动/回复自动继续/终局演绎/已播行折叠)
This commit is contained in:
@@ -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
|
||||
<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 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 |
|
||||
|
||||
| 验收标准 | 任务 |
|
||||
|---|---|
|
||||
|
||||
@@ -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 分钟儿童小剧场,三幕结构:
|
||||
|
||||
Reference in New Issue
Block a user