feat: add dev diary and ending thumbnails, update chapter endings display
This commit is contained in:
21
ROADMAP.md
21
ROADMAP.md
@@ -123,6 +123,27 @@ QTE 成功 → effects: enemy_hp -= 25
|
||||
- [x] `public/scenes/demo.json` — 新增 `combat_router` 条件路由示例
|
||||
- [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)。
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface SceneNode {
|
||||
videoMuted?: boolean
|
||||
skippable?: boolean
|
||||
streamingUrl?: Record<string, string>
|
||||
keyMoment?: boolean
|
||||
}
|
||||
|
||||
export interface Choice {
|
||||
|
||||
@@ -117,6 +117,43 @@ function endingStatus(endingId: string) {
|
||||
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 {
|
||||
const saved = localStorage.getItem('story_chapter')
|
||||
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) {
|
||||
function pushChild(target: string | undefined) {
|
||||
if (!target) return
|
||||
if (isOtherChapterStart(target, chapterId)) {
|
||||
const gatewayCh = props.chapters.find(c => c.startScene === target)
|
||||
children.push({
|
||||
sceneId: '',
|
||||
label: gatewayCh ? (t(gatewayCh.labelKey || gatewayCh.label)) : target,
|
||||
visited: false,
|
||||
locked: !props.unlockedChapterIds.has(gatewayCh?.id ?? ''),
|
||||
children: [],
|
||||
isGateway: true,
|
||||
gatewayChapterId: gatewayCh?.id,
|
||||
})
|
||||
return
|
||||
const keyIds = collectKeyTargets(target, 0, new Set())
|
||||
for (const keyId of keyIds) {
|
||||
if (isOtherChapterStart(keyId, chapterId)) {
|
||||
const gatewayCh = props.chapters.find(c => c.startScene === keyId)
|
||||
children.push({
|
||||
sceneId: '',
|
||||
label: gatewayCh ? (t(gatewayCh.labelKey || gatewayCh.label)) : keyId,
|
||||
visited: false,
|
||||
locked: !props.unlockedChapterIds.has(gatewayCh?.id ?? ''),
|
||||
children: [],
|
||||
isGateway: true,
|
||||
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) {
|
||||
for (const c of scene.choices) pushChild(c.targetScene)
|
||||
|
||||
Reference in New Issue
Block a user