diff --git a/engine/types.ts b/engine/types.ts index cc0ff85..40e12bf 100644 --- a/engine/types.ts +++ b/engine/types.ts @@ -3,6 +3,7 @@ export interface SceneNode { type?: 'video' | 'image' videoUrl: string imageUrl?: string + thumbnail?: string contentSize?: { w: number; h: number } subtitleUrl?: string subtitles?: Record diff --git a/public/demo/alone_ending/thumb.jpg b/public/demo/alone_ending/thumb.jpg new file mode 100644 index 0000000..ab155d5 Binary files /dev/null and b/public/demo/alone_ending/thumb.jpg differ diff --git a/public/demo/corridor/thumb.jpg b/public/demo/corridor/thumb.jpg new file mode 100644 index 0000000..e658d8b Binary files /dev/null and b/public/demo/corridor/thumb.jpg differ diff --git a/public/demo/intro/thumb.jpg b/public/demo/intro/thumb.jpg new file mode 100644 index 0000000..57a5ec2 Binary files /dev/null and b/public/demo/intro/thumb.jpg differ diff --git a/public/demo/left_door/thumb.jpg b/public/demo/left_door/thumb.jpg new file mode 100644 index 0000000..28e4f6d Binary files /dev/null and b/public/demo/left_door/thumb.jpg differ diff --git a/public/demo/qte_fail/thumb.jpg b/public/demo/qte_fail/thumb.jpg new file mode 100644 index 0000000..3f9c775 Binary files /dev/null and b/public/demo/qte_fail/thumb.jpg differ diff --git a/public/demo/qte_success/thumb.jpg b/public/demo/qte_success/thumb.jpg new file mode 100644 index 0000000..633e73d Binary files /dev/null and b/public/demo/qte_success/thumb.jpg differ diff --git a/public/demo/right_door/thumb.jpg b/public/demo/right_door/thumb.jpg new file mode 100644 index 0000000..87ecdb2 Binary files /dev/null and b/public/demo/right_door/thumb.jpg differ diff --git a/public/demo/shared/thumb.jpg b/public/demo/shared/thumb.jpg new file mode 100644 index 0000000..121fc48 Binary files /dev/null and b/public/demo/shared/thumb.jpg differ diff --git a/public/demo/stay/thumb.jpg b/public/demo/stay/thumb.jpg new file mode 100644 index 0000000..8ca95b8 Binary files /dev/null and b/public/demo/stay/thumb.jpg differ diff --git a/public/demo/trust_ending/thumb.jpg b/public/demo/trust_ending/thumb.jpg new file mode 100644 index 0000000..ab85473 Binary files /dev/null and b/public/demo/trust_ending/thumb.jpg differ diff --git a/public/scenes/demo.json b/public/scenes/demo.json index 5dc82ad..af37459 100644 --- a/public/scenes/demo.json +++ b/public/scenes/demo.json @@ -2,7 +2,11 @@ "assetBase": "demo/", "locales": { "path": "locales/", - "languages": ["zh", "en", "ja"] + "languages": [ + "zh", + "en", + "ja" + ] }, "startScene": "intro", "variables": { @@ -21,7 +25,11 @@ "descKey": "achievement.qte_master.desc", "icon": "", "hidden": false, - "condition": { "variable": "qte_succeeded", "op": ">=", "value": 1 } + "condition": { + "variable": "qte_succeeded", + "op": ">=", + "value": 1 + } }, { "id": "explorer", @@ -31,7 +39,11 @@ "descKey": "achievement.explorer.desc", "icon": "", "hidden": false, - "condition": { "variable": "investigation", "op": ">=", "value": 2 } + "condition": { + "variable": "investigation", + "op": ">=", + "value": 2 + } }, { "id": "game_finished", @@ -41,13 +53,38 @@ "descKey": "achievement.game_finished.desc", "icon": "", "hidden": false, - "condition": { "variable": "completed_game", "op": ">=", "value": 1 } + "condition": { + "variable": "completed_game", + "op": ">=", + "value": 1 + } } ], "endings": [ - { "id": "trust_end", "label": "信任的伙伴", "labelKey": "ending.trust_end", "sceneId": "trust_ending", "chapterId": "ch1", "thumbnail": "ui/images/end_trust.jpg" }, - { "id": "alone_end", "label": "独行之路", "labelKey": "ending.alone_end", "sceneId": "alone_ending", "chapterId": "ch1", "thumbnail": "ui/images/end_alone.jpg" }, - { "id": "continue_end", "label": "继续前行", "labelKey": "ending.continue_end", "sceneId": "continue_ending", "chapterId": "ch3", "thumbnail": "ui/images/end_continue.jpg" } + { + "id": "trust_end", + "label": "信任的伙伴", + "labelKey": "ending.trust_end", + "sceneId": "trust_ending", + "chapterId": "ch1", + "thumbnail": "ui/images/end_trust.jpg" + }, + { + "id": "alone_end", + "label": "独行之路", + "labelKey": "ending.alone_end", + "sceneId": "alone_ending", + "chapterId": "ch1", + "thumbnail": "ui/images/end_alone.jpg" + }, + { + "id": "continue_end", + "label": "继续前行", + "labelKey": "ending.continue_end", + "sceneId": "continue_ending", + "chapterId": "ch3", + "thumbnail": "ui/images/end_continue.jpg" + } ], "chapters": [ { @@ -56,7 +93,11 @@ "labelKey": "chapter.ch1", "startScene": "intro", "thumbnail": "ui/images/ch1.jpg", - "defaultVariables": { "trust": 50, "courage": 0, "investigation": 0 } + "defaultVariables": { + "trust": 50, + "courage": 0, + "investigation": 0 + } }, { "id": "ch2", @@ -64,7 +105,11 @@ "labelKey": "chapter.ch2", "startScene": "desk_detail", "thumbnail": "ui/images/ch2.jpg", - "defaultVariables": { "trust": 60, "courage": 10, "investigation": 1 } + "defaultVariables": { + "trust": 60, + "courage": 10, + "investigation": 1 + } }, { "id": "ch3", @@ -72,7 +117,11 @@ "labelKey": "chapter.ch3", "startScene": "qte_success", "thumbnail": "ui/images/ch3.jpg", - "defaultVariables": { "trust": 70, "courage": 20, "investigation": 2 } + "defaultVariables": { + "trust": 70, + "courage": 20, + "investigation": 2 + } } ], "scenes": { @@ -95,7 +144,11 @@ "textKey": "intro.choice.left_door", "targetScene": "left_door", "effects": [ - { "type": "add", "target": "courage", "value": 10 } + { + "type": "add", + "target": "courage", + "value": 10 + } ] }, { @@ -103,7 +156,11 @@ "textKey": "intro.choice.right_door", "targetScene": "right_door", "effects": [ - { "type": "add", "target": "courage", "value": -5 } + { + "type": "add", + "target": "courage", + "value": -5 + } ] }, { @@ -116,14 +173,18 @@ "textKey": "intro.choice.stay", "targetScene": "stay" } - ] + ], + "thumbnail": "intro/thumb.jpg" }, "investigation_site": { "id": "investigation_site", "type": "image", "videoUrl": "", "imageUrl": "investigation_site/investigation_scene.jpg", - "contentSize": { "w": 1280, "h": 720 }, + "contentSize": { + "w": 1280, + "h": 720 + }, "subtitleUrl": "investigation_site/investigation.vtt", "subtitles": { "zh": "investigation_site/investigation.vtt", @@ -136,10 +197,20 @@ "label": "查看书桌", "labelKey": "investigation_site.hotspot.desk", "targetScene": "desk_detail", - "x": 154, "y": 144, "width": 230, "height": 101, + "x": 154, + "y": 144, + "width": 230, + "height": 101, "effects": [ - { "type": "add", "target": "investigation", "value": 1 }, - { "type": "toggleFlag", "target": "checked_desk" } + { + "type": "add", + "target": "investigation", + "value": 1 + }, + { + "type": "toggleFlag", + "target": "checked_desk" + } ] }, { @@ -147,56 +218,86 @@ "label": "查看窗户", "labelKey": "investigation_site.hotspot.window", "targetScene": "corridor", - "x": 602, "y": 43, "width": 192, "height": 202 + "x": 602, + "y": 43, + "width": 192, + "height": 202 }, { "id": "hs_closet", "label": "检查衣柜", "labelKey": "investigation_site.hotspot.closet", "targetScene": "desk_detail", - "x": 422, "y": 346, "width": 128, "height": 187, + "x": 422, + "y": 346, + "width": 128, + "height": 187, "conditions": [ - { "variable": "investigation", "op": ">=", "value": 1 } + { + "variable": "investigation", + "op": ">=", + "value": 1 + } ], "effects": [ - { "type": "add", "target": "investigation", "value": 1 } + { + "type": "add", + "target": "investigation", + "value": 1 + } ] } ], - "choices": [ - ] + "choices": [] }, "corridor": { "id": "corridor", "videoUrl": "corridor/corridor.mp4", - "contentSize": { "w": 1280, "h": 720 }, + "contentSize": { + "w": 1280, + "h": 720 + }, "skippable": false, "hotspots": [ { "id": "hs_left", "label": "走向左边通道", "targetScene": "left_door", - "x": 26, "y": 216, "width": 384, "height": 324, + "x": 26, + "y": 216, + "width": 384, + "height": 324, "showAt": 1.5, "effects": [ - { "type": "add", "target": "courage", "value": 5 } + { + "type": "add", + "target": "courage", + "value": 5 + } ] }, { "id": "hs_center", "label": "走向中间通道", "targetScene": "trust_ending", - "x": 422, "y": 180, "width": 435, "height": 396, + "x": 422, + "y": 180, + "width": 435, + "height": 396, "showAt": 3.0 }, { "id": "hs_right", "label": "走向右边通道", "targetScene": "alone_ending", - "x": 870, "y": 216, "width": 384, "height": 324, + "x": 870, + "y": 216, + "width": 384, + "height": 324, "showAt": 5.0 } - ] + ], + "thumbnail": "corridor/thumb.jpg" }, "left_door": { "id": "left_door", @@ -215,7 +316,11 @@ "promptKey": "left_door.prompt.handshake", "targetScene": "trust_ending", "effects": [ - { "type": "add", "target": "trust", "value": 30 } + { + "type": "add", + "target": "trust", + "value": 30 + } ] }, { @@ -223,7 +328,8 @@ "textKey": "left_door.choice.reject", "targetScene": "alone_ending" } - ] + ], + "thumbnail": "left_door/thumb.jpg" }, "right_door": { "id": "right_door", @@ -237,18 +343,38 @@ "triggerTime": 1.0, "prompt": "躲避飞来的石块!", "promptKey": "right_door.qte.dodge", - "keys": ["ArrowLeft", "ArrowRight", "a", "d"], + "keys": [ + "ArrowLeft", + "ArrowRight", + "a", + "d" + ], "timeLimit": 3.0, "successScene": "qte_success", "failScene": "qte_fail", "effects": { "success": [ - { "type": "add", "target": "courage", "value": 15 }, - { "type": "set", "target": "qte_succeeded", "value": 1 } + { + "type": "add", + "target": "courage", + "value": 15 + }, + { + "type": "set", + "target": "qte_succeeded", + "value": 1 + } ], - "fail": [{ "type": "add", "target": "trust", "value": -20 }] + "fail": [ + { + "type": "add", + "target": "trust", + "value": -20 + } + ] } - } + }, + "thumbnail": "right_door/thumb.jpg" }, "qte_success": { "id": "qte_success", @@ -264,7 +390,8 @@ "textKey": "qte_success.choice.back", "targetScene": "intro" } - ] + ], + "thumbnail": "qte_success/thumb.jpg" }, "qte_fail": { "id": "qte_fail", @@ -280,7 +407,8 @@ "textKey": "qte_fail.choice.back", "targetScene": "intro" } - ] + ], + "thumbnail": "qte_fail/thumb.jpg" }, "desk_detail": { "id": "desk_detail", @@ -296,7 +424,8 @@ "textKey": "desk_detail.choice.leave", "targetScene": "corridor" } - ] + ], + "thumbnail": "shared/thumb.jpg" }, "stay": { "id": "stay", @@ -313,8 +442,13 @@ "loopStart": 3.0, "loopEnd": 6.0, "choices": [ - { "text": "站起来离开", "textKey": "stay.choice.stand", "targetScene": "alone_ending" } - ] + { + "text": "站起来离开", + "textKey": "stay.choice.stand", + "targetScene": "alone_ending" + } + ], + "thumbnail": "stay/thumb.jpg" }, "trust_ending": { "id": "trust_ending", @@ -327,7 +461,11 @@ "promptKey": "trust_ending.prompt.journey", "targetScene": "secret_ending", "conditions": [ - { "variable": "trust", "op": ">=", "value": 80 } + { + "variable": "trust", + "op": ">=", + "value": 80 + } ] }, { @@ -335,25 +473,33 @@ "textKey": "trust_ending.choice.leave", "targetScene": "alone_ending" } - ] + ], + "thumbnail": "trust_ending/thumb.jpg" }, "secret_ending": { "id": "secret_ending", "videoUrl": "shared/continue_ending.mp4", - "choices": [] + "choices": [], + "thumbnail": "shared/thumb.jpg" }, "alone_ending": { "id": "alone_ending", "videoUrl": "alone_ending/alone_ending.mp4", "choices": [], "onEnter": [ - { "type": "set", "target": "completed_game", "value": 1 } - ] + { + "type": "set", + "target": "completed_game", + "value": 1 + } + ], + "thumbnail": "alone_ending/thumb.jpg" }, "continue_ending": { "id": "continue_ending", "videoUrl": "shared/continue_ending.mp4", - "choices": [] + "choices": [], + "thumbnail": "shared/thumb.jpg" } } -} +} \ No newline at end of file diff --git a/src/components/StoryGallery.vue b/src/components/StoryGallery.vue index f767672..0ca2a57 100644 --- a/src/components/StoryGallery.vue +++ b/src/components/StoryGallery.vue @@ -205,6 +205,7 @@ const tree = computed(() => buildTreeForChapter(currentChapterId.value)) diff --git a/src/components/TreeFlow.vue b/src/components/TreeFlow.vue index a8167c2..d428183 100644 --- a/src/components/TreeFlow.vue +++ b/src/components/TreeFlow.vue @@ -5,6 +5,7 @@ import dagre from 'dagre' const props = defineProps<{ node: PlayerTreeNode | null + scenes?: Record }>() const emit = defineEmits<{ @@ -15,6 +16,7 @@ interface FlowNode { id: string sceneId: string label: string + thumbnail?: string visited: boolean isMystery: boolean locked: boolean @@ -38,7 +40,7 @@ const containerW = ref(800) const containerH = ref(400) function buildFlow(root: PlayerTreeNode) { - const dagreNodes: { id: string; sceneId: string; parent: string | null; label: string; visited: boolean; isMystery: boolean; locked: boolean; lockHint?: string }[] = [] + const dagreNodes: { id: string; sceneId: string; thumbnail?: string; parent: string | null; label: string; visited: boolean; isMystery: boolean; locked: boolean; lockHint?: string }[] = [] const dagreEdges: { from: string; to: string; visited: boolean }[] = [] function walk(node: PlayerTreeNode, parentId: string | null) { @@ -47,6 +49,7 @@ function buildFlow(root: PlayerTreeNode) { dagreNodes.push({ id: dagreId, sceneId: node.sceneId, + thumbnail: props.scenes?.[node.sceneId]?.thumbnail, parent: parentId, label: node.label, visited: true, @@ -91,11 +94,13 @@ function buildFlow(root: PlayerTreeNode) { g.setGraph({ rankdir: 'LR', nodesep: 40, ranksep: 80, marginx: 24, marginy: 24 }) g.setDefaultEdgeLabel(() => ({})) - const nodeW = 120 - const nodeH = 44 + const baseW = 128 + const baseH = 40 + const thumbH = 78 for (const n of dagreNodes) { - g.setNode(n.id, { width: nodeW, height: nodeH }) + const hasThumb = !!n.thumbnail + g.setNode(n.id, { width: hasThumb ? baseW + 20 : baseW, height: hasThumb ? baseH + thumbH + 8 : baseH }) } for (const e of dagreEdges) { g.setEdge(e.from, e.to) @@ -105,18 +110,22 @@ function buildFlow(root: PlayerTreeNode) { const resultNodes: FlowNode[] = dagreNodes.map((n) => { const pos = g.node(n.id) + const hasThumb = !!n.thumbnail + const nw = hasThumb ? baseW + 20 : baseW + const nh = hasThumb ? baseH + thumbH + 8 : baseH return { id: n.id, sceneId: n.sceneId, + thumbnail: n.thumbnail, label: n.label, visited: n.visited, isMystery: n.isMystery, locked: n.locked, lockHint: n.lockHint, - x: pos.x - nodeW / 2, - y: pos.y - nodeH / 2, - w: nodeW, - h: nodeH, + x: pos.x - nw / 2, + y: pos.y - nh / 2, + w: nw, + h: nh, } }) @@ -234,6 +243,7 @@ const svgH = computed(() => containerH.value) :title="n.lockHint || ''" @click="n.visited && n.sceneId && emit('selectScene', n.sceneId)" > + {{ n.visited ? '✦' : n.isMystery ? '?' : '⬜' }} {{ n.label }} @@ -264,15 +274,29 @@ const svgH = computed(() => containerH.value) .flow-node { position: absolute; display: flex; + flex-direction: column; align-items: center; - gap: 6px; - padding: 0 10px; + justify-content: center; + gap: 4px; + padding: 4px 8px 6px; border-radius: 4px; font-size: 12px; transition: all 0.15s; overflow: hidden; } +.flow-node:has(.node-thumb) { + padding: 4px 4px 6px; +} + +.node-thumb { + width: 128px; + height: 72px; + object-fit: cover; + border-radius: 3px; + border: 1px solid rgba(255,255,255,0.06); +} + .flow-node.clickable { cursor: pointer; }