feat: add dev diary and ending thumbnails, update chapter endings display

This commit is contained in:
2026-06-14 11:51:32 +08:00
parent d373cb8fc0
commit 92966331d3
3 changed files with 76 additions and 14 deletions

View File

@@ -123,6 +123,27 @@ QTE 成功 → effects: enemy_hp -= 25
- [x] `public/scenes/demo.json` — 新增 `combat_router` 条件路由示例 - [x] `public/scenes/demo.json` — 新增 `combat_router` 条件路由示例
- [x] 验证TypeScript + Vite build 通过 - [x] 验证TypeScript + Vite build 通过
### P26 关键节点过滤 — StoryGallery 只展示剧情分叉点 ✅ 已完成 2026-06-12
目标StoryGallery 不再展示所有场景,只展示剧情关键节点(章节起始、选择分支点、结局点)。
QTE 场景和过渡/路由场景被过滤,子节点上浮一级。
**三层判断:**
| 优先级 | 来源 | 说明 |
|:--:|------|------|
| 1 | `SceneNode.keyMoment` | 手动覆盖。`true`=强制展示,`false`=强制隐藏 |
| 2 | `endings[].sceneId` | 结局节点 |
| 3 | 自动判断 | 章节起点 / 有 `choices`(分支点)→ 关键节点。QTE 不算 |
**扁平化:** 非关键节点不渲染,子节点上浮到父节点层级,路径语义不变。
**实现清单:**
- [x] `engine/types.ts``SceneNode.keyMoment?: boolean`
- [x] `src/components/StoryGallery.vue``isKeyMoment()` 三层逻辑 + `collectKeyTargets()` 扁平化非关键节点
- [x] 验证TypeScript + Vite build 通过
## 已完成 ## 已完成
P0~P23 全部实现(除 P18。详见 [CHANGELOG.md](CHANGELOG.md)。 P0~P23 全部实现(除 P18。详见 [CHANGELOG.md](CHANGELOG.md)。

View File

@@ -22,6 +22,7 @@ export interface SceneNode {
videoMuted?: boolean videoMuted?: boolean
skippable?: boolean skippable?: boolean
streamingUrl?: Record<string, string> streamingUrl?: Record<string, string>
keyMoment?: boolean
} }
export interface Choice { export interface Choice {

View File

@@ -117,6 +117,43 @@ function endingStatus(endingId: string) {
return props.visitedIds.has(props.endings.find(e => e.id === endingId)?.sceneId ?? '') return props.visitedIds.has(props.endings.find(e => e.id === endingId)?.sceneId ?? '')
} }
function isKeyMoment(sceneId: string): boolean {
const scene = props.scenes[sceneId]
if (!scene) return false
if (scene.keyMoment !== undefined) return scene.keyMoment
if (props.chapters.some(c => c.startScene === sceneId)) return true
if (scene.choices && scene.choices.length > 0) return true
if (props.endings.some(e => e.sceneId === sceneId)) return true
return false
}
function collectKeyTargets(sceneId: string, depth: number, pathSet: Set<string>): string[] {
if (depth > 10 || pathSet.has(sceneId)) return []
pathSet.add(sceneId)
const scene = props.scenes[sceneId]
if (!scene) return []
if (isKeyMoment(sceneId)) return [sceneId]
const results: string[] = []
function pushTarget(target: string | undefined) {
if (!target) return
results.push(...collectKeyTargets(target, depth + 1, pathSet))
}
if (scene.choices) for (const c of scene.choices) pushTarget(c.targetScene)
if (scene.nextScene) {
if (Array.isArray(scene.nextScene)) {
for (const r of scene.nextScene) pushTarget(r.targetScene)
} else {
pushTarget(scene.nextScene)
}
}
if (scene.qte) {
pushTarget(scene.qte.successScene)
pushTarget(scene.qte.failScene)
}
if (scene.hotspots) for (const h of scene.hotspots) pushTarget(h.targetScene)
return results
}
function resolveInitialChapter(): string { function resolveInitialChapter(): string {
const saved = localStorage.getItem('story_chapter') const saved = localStorage.getItem('story_chapter')
if (saved && props.chapters.some(c => c.id === saved)) return saved if (saved && props.chapters.some(c => c.id === saved)) return saved
@@ -146,21 +183,24 @@ function buildPlayerTree(sceneId: string, chapterId: string, depth: number, path
if (scene) { if (scene) {
function pushChild(target: string | undefined) { function pushChild(target: string | undefined) {
if (!target) return if (!target) return
if (isOtherChapterStart(target, chapterId)) { const keyIds = collectKeyTargets(target, 0, new Set())
const gatewayCh = props.chapters.find(c => c.startScene === target) for (const keyId of keyIds) {
children.push({ if (isOtherChapterStart(keyId, chapterId)) {
sceneId: '', const gatewayCh = props.chapters.find(c => c.startScene === keyId)
label: gatewayCh ? (t(gatewayCh.labelKey || gatewayCh.label)) : target, children.push({
visited: false, sceneId: '',
locked: !props.unlockedChapterIds.has(gatewayCh?.id ?? ''), label: gatewayCh ? (t(gatewayCh.labelKey || gatewayCh.label)) : keyId,
children: [], visited: false,
isGateway: true, locked: !props.unlockedChapterIds.has(gatewayCh?.id ?? ''),
gatewayChapterId: gatewayCh?.id, children: [],
}) isGateway: true,
return gatewayChapterId: gatewayCh?.id,
})
} else {
const child = buildPlayerTree(keyId, chapterId, depth + 1, pathSet)
if (child) children.push(child)
}
} }
const child = buildPlayerTree(target, chapterId, depth + 1, pathSet)
if (child) children.push(child)
} }
if (scene.choices) { if (scene.choices) {
for (const c of scene.choices) pushChild(c.targetScene) for (const c of scene.choices) pushChild(c.targetScene)