feat: add scene thumbnails to TreeFlow nodes with auto-generated demo thumbs
@@ -3,6 +3,7 @@ export interface SceneNode {
|
|||||||
type?: 'video' | 'image'
|
type?: 'video' | 'image'
|
||||||
videoUrl: string
|
videoUrl: string
|
||||||
imageUrl?: string
|
imageUrl?: string
|
||||||
|
thumbnail?: string
|
||||||
contentSize?: { w: number; h: number }
|
contentSize?: { w: number; h: number }
|
||||||
subtitleUrl?: string
|
subtitleUrl?: string
|
||||||
subtitles?: Record<string, string>
|
subtitles?: Record<string, string>
|
||||||
|
|||||||
BIN
public/demo/alone_ending/thumb.jpg
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/demo/corridor/thumb.jpg
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/demo/intro/thumb.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/demo/left_door/thumb.jpg
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/demo/qte_fail/thumb.jpg
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
public/demo/qte_success/thumb.jpg
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
public/demo/right_door/thumb.jpg
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/demo/shared/thumb.jpg
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/demo/stay/thumb.jpg
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/demo/trust_ending/thumb.jpg
Normal file
|
After Width: | Height: | Size: 10 KiB |
@@ -2,7 +2,11 @@
|
|||||||
"assetBase": "demo/",
|
"assetBase": "demo/",
|
||||||
"locales": {
|
"locales": {
|
||||||
"path": "locales/",
|
"path": "locales/",
|
||||||
"languages": ["zh", "en", "ja"]
|
"languages": [
|
||||||
|
"zh",
|
||||||
|
"en",
|
||||||
|
"ja"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"startScene": "intro",
|
"startScene": "intro",
|
||||||
"variables": {
|
"variables": {
|
||||||
@@ -21,7 +25,11 @@
|
|||||||
"descKey": "achievement.qte_master.desc",
|
"descKey": "achievement.qte_master.desc",
|
||||||
"icon": "",
|
"icon": "",
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"condition": { "variable": "qte_succeeded", "op": ">=", "value": 1 }
|
"condition": {
|
||||||
|
"variable": "qte_succeeded",
|
||||||
|
"op": ">=",
|
||||||
|
"value": 1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "explorer",
|
"id": "explorer",
|
||||||
@@ -31,7 +39,11 @@
|
|||||||
"descKey": "achievement.explorer.desc",
|
"descKey": "achievement.explorer.desc",
|
||||||
"icon": "",
|
"icon": "",
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"condition": { "variable": "investigation", "op": ">=", "value": 2 }
|
"condition": {
|
||||||
|
"variable": "investigation",
|
||||||
|
"op": ">=",
|
||||||
|
"value": 2
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "game_finished",
|
"id": "game_finished",
|
||||||
@@ -41,13 +53,38 @@
|
|||||||
"descKey": "achievement.game_finished.desc",
|
"descKey": "achievement.game_finished.desc",
|
||||||
"icon": "",
|
"icon": "",
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"condition": { "variable": "completed_game", "op": ">=", "value": 1 }
|
"condition": {
|
||||||
|
"variable": "completed_game",
|
||||||
|
"op": ">=",
|
||||||
|
"value": 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"endings": [
|
"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": "trust_end",
|
||||||
{ "id": "continue_end", "label": "继续前行", "labelKey": "ending.continue_end", "sceneId": "continue_ending", "chapterId": "ch3", "thumbnail": "ui/images/end_continue.jpg" }
|
"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": [
|
"chapters": [
|
||||||
{
|
{
|
||||||
@@ -56,7 +93,11 @@
|
|||||||
"labelKey": "chapter.ch1",
|
"labelKey": "chapter.ch1",
|
||||||
"startScene": "intro",
|
"startScene": "intro",
|
||||||
"thumbnail": "ui/images/ch1.jpg",
|
"thumbnail": "ui/images/ch1.jpg",
|
||||||
"defaultVariables": { "trust": 50, "courage": 0, "investigation": 0 }
|
"defaultVariables": {
|
||||||
|
"trust": 50,
|
||||||
|
"courage": 0,
|
||||||
|
"investigation": 0
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ch2",
|
"id": "ch2",
|
||||||
@@ -64,7 +105,11 @@
|
|||||||
"labelKey": "chapter.ch2",
|
"labelKey": "chapter.ch2",
|
||||||
"startScene": "desk_detail",
|
"startScene": "desk_detail",
|
||||||
"thumbnail": "ui/images/ch2.jpg",
|
"thumbnail": "ui/images/ch2.jpg",
|
||||||
"defaultVariables": { "trust": 60, "courage": 10, "investigation": 1 }
|
"defaultVariables": {
|
||||||
|
"trust": 60,
|
||||||
|
"courage": 10,
|
||||||
|
"investigation": 1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ch3",
|
"id": "ch3",
|
||||||
@@ -72,7 +117,11 @@
|
|||||||
"labelKey": "chapter.ch3",
|
"labelKey": "chapter.ch3",
|
||||||
"startScene": "qte_success",
|
"startScene": "qte_success",
|
||||||
"thumbnail": "ui/images/ch3.jpg",
|
"thumbnail": "ui/images/ch3.jpg",
|
||||||
"defaultVariables": { "trust": 70, "courage": 20, "investigation": 2 }
|
"defaultVariables": {
|
||||||
|
"trust": 70,
|
||||||
|
"courage": 20,
|
||||||
|
"investigation": 2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scenes": {
|
"scenes": {
|
||||||
@@ -95,7 +144,11 @@
|
|||||||
"textKey": "intro.choice.left_door",
|
"textKey": "intro.choice.left_door",
|
||||||
"targetScene": "left_door",
|
"targetScene": "left_door",
|
||||||
"effects": [
|
"effects": [
|
||||||
{ "type": "add", "target": "courage", "value": 10 }
|
{
|
||||||
|
"type": "add",
|
||||||
|
"target": "courage",
|
||||||
|
"value": 10
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -103,7 +156,11 @@
|
|||||||
"textKey": "intro.choice.right_door",
|
"textKey": "intro.choice.right_door",
|
||||||
"targetScene": "right_door",
|
"targetScene": "right_door",
|
||||||
"effects": [
|
"effects": [
|
||||||
{ "type": "add", "target": "courage", "value": -5 }
|
{
|
||||||
|
"type": "add",
|
||||||
|
"target": "courage",
|
||||||
|
"value": -5
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -116,14 +173,18 @@
|
|||||||
"textKey": "intro.choice.stay",
|
"textKey": "intro.choice.stay",
|
||||||
"targetScene": "stay"
|
"targetScene": "stay"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"thumbnail": "intro/thumb.jpg"
|
||||||
},
|
},
|
||||||
"investigation_site": {
|
"investigation_site": {
|
||||||
"id": "investigation_site",
|
"id": "investigation_site",
|
||||||
"type": "image",
|
"type": "image",
|
||||||
"videoUrl": "",
|
"videoUrl": "",
|
||||||
"imageUrl": "investigation_site/investigation_scene.jpg",
|
"imageUrl": "investigation_site/investigation_scene.jpg",
|
||||||
"contentSize": { "w": 1280, "h": 720 },
|
"contentSize": {
|
||||||
|
"w": 1280,
|
||||||
|
"h": 720
|
||||||
|
},
|
||||||
"subtitleUrl": "investigation_site/investigation.vtt",
|
"subtitleUrl": "investigation_site/investigation.vtt",
|
||||||
"subtitles": {
|
"subtitles": {
|
||||||
"zh": "investigation_site/investigation.vtt",
|
"zh": "investigation_site/investigation.vtt",
|
||||||
@@ -136,10 +197,20 @@
|
|||||||
"label": "查看书桌",
|
"label": "查看书桌",
|
||||||
"labelKey": "investigation_site.hotspot.desk",
|
"labelKey": "investigation_site.hotspot.desk",
|
||||||
"targetScene": "desk_detail",
|
"targetScene": "desk_detail",
|
||||||
"x": 154, "y": 144, "width": 230, "height": 101,
|
"x": 154,
|
||||||
|
"y": 144,
|
||||||
|
"width": 230,
|
||||||
|
"height": 101,
|
||||||
"effects": [
|
"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": "查看窗户",
|
"label": "查看窗户",
|
||||||
"labelKey": "investigation_site.hotspot.window",
|
"labelKey": "investigation_site.hotspot.window",
|
||||||
"targetScene": "corridor",
|
"targetScene": "corridor",
|
||||||
"x": 602, "y": 43, "width": 192, "height": 202
|
"x": 602,
|
||||||
|
"y": 43,
|
||||||
|
"width": 192,
|
||||||
|
"height": 202
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "hs_closet",
|
"id": "hs_closet",
|
||||||
"label": "检查衣柜",
|
"label": "检查衣柜",
|
||||||
"labelKey": "investigation_site.hotspot.closet",
|
"labelKey": "investigation_site.hotspot.closet",
|
||||||
"targetScene": "desk_detail",
|
"targetScene": "desk_detail",
|
||||||
"x": 422, "y": 346, "width": 128, "height": 187,
|
"x": 422,
|
||||||
|
"y": 346,
|
||||||
|
"width": 128,
|
||||||
|
"height": 187,
|
||||||
"conditions": [
|
"conditions": [
|
||||||
{ "variable": "investigation", "op": ">=", "value": 1 }
|
{
|
||||||
|
"variable": "investigation",
|
||||||
|
"op": ">=",
|
||||||
|
"value": 1
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"effects": [
|
"effects": [
|
||||||
{ "type": "add", "target": "investigation", "value": 1 }
|
{
|
||||||
|
"type": "add",
|
||||||
|
"target": "investigation",
|
||||||
|
"value": 1
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"choices": [
|
"choices": []
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"corridor": {
|
"corridor": {
|
||||||
"id": "corridor",
|
"id": "corridor",
|
||||||
"videoUrl": "corridor/corridor.mp4",
|
"videoUrl": "corridor/corridor.mp4",
|
||||||
"contentSize": { "w": 1280, "h": 720 },
|
"contentSize": {
|
||||||
|
"w": 1280,
|
||||||
|
"h": 720
|
||||||
|
},
|
||||||
"skippable": false,
|
"skippable": false,
|
||||||
"hotspots": [
|
"hotspots": [
|
||||||
{
|
{
|
||||||
"id": "hs_left",
|
"id": "hs_left",
|
||||||
"label": "走向左边通道",
|
"label": "走向左边通道",
|
||||||
"targetScene": "left_door",
|
"targetScene": "left_door",
|
||||||
"x": 26, "y": 216, "width": 384, "height": 324,
|
"x": 26,
|
||||||
|
"y": 216,
|
||||||
|
"width": 384,
|
||||||
|
"height": 324,
|
||||||
"showAt": 1.5,
|
"showAt": 1.5,
|
||||||
"effects": [
|
"effects": [
|
||||||
{ "type": "add", "target": "courage", "value": 5 }
|
{
|
||||||
|
"type": "add",
|
||||||
|
"target": "courage",
|
||||||
|
"value": 5
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "hs_center",
|
"id": "hs_center",
|
||||||
"label": "走向中间通道",
|
"label": "走向中间通道",
|
||||||
"targetScene": "trust_ending",
|
"targetScene": "trust_ending",
|
||||||
"x": 422, "y": 180, "width": 435, "height": 396,
|
"x": 422,
|
||||||
|
"y": 180,
|
||||||
|
"width": 435,
|
||||||
|
"height": 396,
|
||||||
"showAt": 3.0
|
"showAt": 3.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "hs_right",
|
"id": "hs_right",
|
||||||
"label": "走向右边通道",
|
"label": "走向右边通道",
|
||||||
"targetScene": "alone_ending",
|
"targetScene": "alone_ending",
|
||||||
"x": 870, "y": 216, "width": 384, "height": 324,
|
"x": 870,
|
||||||
|
"y": 216,
|
||||||
|
"width": 384,
|
||||||
|
"height": 324,
|
||||||
"showAt": 5.0
|
"showAt": 5.0
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"thumbnail": "corridor/thumb.jpg"
|
||||||
},
|
},
|
||||||
"left_door": {
|
"left_door": {
|
||||||
"id": "left_door",
|
"id": "left_door",
|
||||||
@@ -215,7 +316,11 @@
|
|||||||
"promptKey": "left_door.prompt.handshake",
|
"promptKey": "left_door.prompt.handshake",
|
||||||
"targetScene": "trust_ending",
|
"targetScene": "trust_ending",
|
||||||
"effects": [
|
"effects": [
|
||||||
{ "type": "add", "target": "trust", "value": 30 }
|
{
|
||||||
|
"type": "add",
|
||||||
|
"target": "trust",
|
||||||
|
"value": 30
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -223,7 +328,8 @@
|
|||||||
"textKey": "left_door.choice.reject",
|
"textKey": "left_door.choice.reject",
|
||||||
"targetScene": "alone_ending"
|
"targetScene": "alone_ending"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"thumbnail": "left_door/thumb.jpg"
|
||||||
},
|
},
|
||||||
"right_door": {
|
"right_door": {
|
||||||
"id": "right_door",
|
"id": "right_door",
|
||||||
@@ -237,19 +343,39 @@
|
|||||||
"triggerTime": 1.0,
|
"triggerTime": 1.0,
|
||||||
"prompt": "躲避飞来的石块!",
|
"prompt": "躲避飞来的石块!",
|
||||||
"promptKey": "right_door.qte.dodge",
|
"promptKey": "right_door.qte.dodge",
|
||||||
"keys": ["ArrowLeft", "ArrowRight", "a", "d"],
|
"keys": [
|
||||||
|
"ArrowLeft",
|
||||||
|
"ArrowRight",
|
||||||
|
"a",
|
||||||
|
"d"
|
||||||
|
],
|
||||||
"timeLimit": 3.0,
|
"timeLimit": 3.0,
|
||||||
"successScene": "qte_success",
|
"successScene": "qte_success",
|
||||||
"failScene": "qte_fail",
|
"failScene": "qte_fail",
|
||||||
"effects": {
|
"effects": {
|
||||||
"success": [
|
"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": {
|
"qte_success": {
|
||||||
"id": "qte_success",
|
"id": "qte_success",
|
||||||
"videoUrl": "qte_success/qte_success.mp4",
|
"videoUrl": "qte_success/qte_success.mp4",
|
||||||
@@ -264,7 +390,8 @@
|
|||||||
"textKey": "qte_success.choice.back",
|
"textKey": "qte_success.choice.back",
|
||||||
"targetScene": "intro"
|
"targetScene": "intro"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"thumbnail": "qte_success/thumb.jpg"
|
||||||
},
|
},
|
||||||
"qte_fail": {
|
"qte_fail": {
|
||||||
"id": "qte_fail",
|
"id": "qte_fail",
|
||||||
@@ -280,7 +407,8 @@
|
|||||||
"textKey": "qte_fail.choice.back",
|
"textKey": "qte_fail.choice.back",
|
||||||
"targetScene": "intro"
|
"targetScene": "intro"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"thumbnail": "qte_fail/thumb.jpg"
|
||||||
},
|
},
|
||||||
"desk_detail": {
|
"desk_detail": {
|
||||||
"id": "desk_detail",
|
"id": "desk_detail",
|
||||||
@@ -296,7 +424,8 @@
|
|||||||
"textKey": "desk_detail.choice.leave",
|
"textKey": "desk_detail.choice.leave",
|
||||||
"targetScene": "corridor"
|
"targetScene": "corridor"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"thumbnail": "shared/thumb.jpg"
|
||||||
},
|
},
|
||||||
"stay": {
|
"stay": {
|
||||||
"id": "stay",
|
"id": "stay",
|
||||||
@@ -313,8 +442,13 @@
|
|||||||
"loopStart": 3.0,
|
"loopStart": 3.0,
|
||||||
"loopEnd": 6.0,
|
"loopEnd": 6.0,
|
||||||
"choices": [
|
"choices": [
|
||||||
{ "text": "站起来离开", "textKey": "stay.choice.stand", "targetScene": "alone_ending" }
|
{
|
||||||
]
|
"text": "站起来离开",
|
||||||
|
"textKey": "stay.choice.stand",
|
||||||
|
"targetScene": "alone_ending"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thumbnail": "stay/thumb.jpg"
|
||||||
},
|
},
|
||||||
"trust_ending": {
|
"trust_ending": {
|
||||||
"id": "trust_ending",
|
"id": "trust_ending",
|
||||||
@@ -327,7 +461,11 @@
|
|||||||
"promptKey": "trust_ending.prompt.journey",
|
"promptKey": "trust_ending.prompt.journey",
|
||||||
"targetScene": "secret_ending",
|
"targetScene": "secret_ending",
|
||||||
"conditions": [
|
"conditions": [
|
||||||
{ "variable": "trust", "op": ">=", "value": 80 }
|
{
|
||||||
|
"variable": "trust",
|
||||||
|
"op": ">=",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -335,25 +473,33 @@
|
|||||||
"textKey": "trust_ending.choice.leave",
|
"textKey": "trust_ending.choice.leave",
|
||||||
"targetScene": "alone_ending"
|
"targetScene": "alone_ending"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"thumbnail": "trust_ending/thumb.jpg"
|
||||||
},
|
},
|
||||||
"secret_ending": {
|
"secret_ending": {
|
||||||
"id": "secret_ending",
|
"id": "secret_ending",
|
||||||
"videoUrl": "shared/continue_ending.mp4",
|
"videoUrl": "shared/continue_ending.mp4",
|
||||||
"choices": []
|
"choices": [],
|
||||||
|
"thumbnail": "shared/thumb.jpg"
|
||||||
},
|
},
|
||||||
"alone_ending": {
|
"alone_ending": {
|
||||||
"id": "alone_ending",
|
"id": "alone_ending",
|
||||||
"videoUrl": "alone_ending/alone_ending.mp4",
|
"videoUrl": "alone_ending/alone_ending.mp4",
|
||||||
"choices": [],
|
"choices": [],
|
||||||
"onEnter": [
|
"onEnter": [
|
||||||
{ "type": "set", "target": "completed_game", "value": 1 }
|
{
|
||||||
]
|
"type": "set",
|
||||||
|
"target": "completed_game",
|
||||||
|
"value": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thumbnail": "alone_ending/thumb.jpg"
|
||||||
},
|
},
|
||||||
"continue_ending": {
|
"continue_ending": {
|
||||||
"id": "continue_ending",
|
"id": "continue_ending",
|
||||||
"videoUrl": "shared/continue_ending.mp4",
|
"videoUrl": "shared/continue_ending.mp4",
|
||||||
"choices": []
|
"choices": [],
|
||||||
|
"thumbnail": "shared/thumb.jpg"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,6 +205,7 @@ const tree = computed(() => buildTreeForChapter(currentChapterId.value))
|
|||||||
<TreeFlow
|
<TreeFlow
|
||||||
v-if="tree"
|
v-if="tree"
|
||||||
:node="tree"
|
:node="tree"
|
||||||
|
:scenes="scenes"
|
||||||
:key="currentChapterId"
|
:key="currentChapterId"
|
||||||
@select-scene="onSelectScene"
|
@select-scene="onSelectScene"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import dagre from 'dagre'
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
node: PlayerTreeNode | null
|
node: PlayerTreeNode | null
|
||||||
|
scenes?: Record<string, { thumbnail?: string }>
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -15,6 +16,7 @@ interface FlowNode {
|
|||||||
id: string
|
id: string
|
||||||
sceneId: string
|
sceneId: string
|
||||||
label: string
|
label: string
|
||||||
|
thumbnail?: string
|
||||||
visited: boolean
|
visited: boolean
|
||||||
isMystery: boolean
|
isMystery: boolean
|
||||||
locked: boolean
|
locked: boolean
|
||||||
@@ -38,7 +40,7 @@ const containerW = ref(800)
|
|||||||
const containerH = ref(400)
|
const containerH = ref(400)
|
||||||
|
|
||||||
function buildFlow(root: PlayerTreeNode) {
|
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 }[] = []
|
const dagreEdges: { from: string; to: string; visited: boolean }[] = []
|
||||||
|
|
||||||
function walk(node: PlayerTreeNode, parentId: string | null) {
|
function walk(node: PlayerTreeNode, parentId: string | null) {
|
||||||
@@ -47,6 +49,7 @@ function buildFlow(root: PlayerTreeNode) {
|
|||||||
dagreNodes.push({
|
dagreNodes.push({
|
||||||
id: dagreId,
|
id: dagreId,
|
||||||
sceneId: node.sceneId,
|
sceneId: node.sceneId,
|
||||||
|
thumbnail: props.scenes?.[node.sceneId]?.thumbnail,
|
||||||
parent: parentId,
|
parent: parentId,
|
||||||
label: node.label,
|
label: node.label,
|
||||||
visited: true,
|
visited: true,
|
||||||
@@ -91,11 +94,13 @@ function buildFlow(root: PlayerTreeNode) {
|
|||||||
g.setGraph({ rankdir: 'LR', nodesep: 40, ranksep: 80, marginx: 24, marginy: 24 })
|
g.setGraph({ rankdir: 'LR', nodesep: 40, ranksep: 80, marginx: 24, marginy: 24 })
|
||||||
g.setDefaultEdgeLabel(() => ({}))
|
g.setDefaultEdgeLabel(() => ({}))
|
||||||
|
|
||||||
const nodeW = 120
|
const baseW = 128
|
||||||
const nodeH = 44
|
const baseH = 40
|
||||||
|
const thumbH = 78
|
||||||
|
|
||||||
for (const n of dagreNodes) {
|
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) {
|
for (const e of dagreEdges) {
|
||||||
g.setEdge(e.from, e.to)
|
g.setEdge(e.from, e.to)
|
||||||
@@ -105,18 +110,22 @@ function buildFlow(root: PlayerTreeNode) {
|
|||||||
|
|
||||||
const resultNodes: FlowNode[] = dagreNodes.map((n) => {
|
const resultNodes: FlowNode[] = dagreNodes.map((n) => {
|
||||||
const pos = g.node(n.id)
|
const pos = g.node(n.id)
|
||||||
|
const hasThumb = !!n.thumbnail
|
||||||
|
const nw = hasThumb ? baseW + 20 : baseW
|
||||||
|
const nh = hasThumb ? baseH + thumbH + 8 : baseH
|
||||||
return {
|
return {
|
||||||
id: n.id,
|
id: n.id,
|
||||||
sceneId: n.sceneId,
|
sceneId: n.sceneId,
|
||||||
|
thumbnail: n.thumbnail,
|
||||||
label: n.label,
|
label: n.label,
|
||||||
visited: n.visited,
|
visited: n.visited,
|
||||||
isMystery: n.isMystery,
|
isMystery: n.isMystery,
|
||||||
locked: n.locked,
|
locked: n.locked,
|
||||||
lockHint: n.lockHint,
|
lockHint: n.lockHint,
|
||||||
x: pos.x - nodeW / 2,
|
x: pos.x - nw / 2,
|
||||||
y: pos.y - nodeH / 2,
|
y: pos.y - nh / 2,
|
||||||
w: nodeW,
|
w: nw,
|
||||||
h: nodeH,
|
h: nh,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -234,6 +243,7 @@ const svgH = computed(() => containerH.value)
|
|||||||
:title="n.lockHint || ''"
|
:title="n.lockHint || ''"
|
||||||
@click="n.visited && n.sceneId && emit('selectScene', n.sceneId)"
|
@click="n.visited && n.sceneId && emit('selectScene', n.sceneId)"
|
||||||
>
|
>
|
||||||
|
<img v-if="n.thumbnail" :src="n.thumbnail" class="node-thumb" />
|
||||||
<span class="node-icon">{{ n.visited ? '✦' : n.isMystery ? '?' : '⬜' }}</span>
|
<span class="node-icon">{{ n.visited ? '✦' : n.isMystery ? '?' : '⬜' }}</span>
|
||||||
<span class="node-label">{{ n.label }}</span>
|
<span class="node-label">{{ n.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -264,15 +274,29 @@ const svgH = computed(() => containerH.value)
|
|||||||
.flow-node {
|
.flow-node {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
justify-content: center;
|
||||||
padding: 0 10px;
|
gap: 4px;
|
||||||
|
padding: 4px 8px 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
overflow: hidden;
|
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 {
|
.flow-node.clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|||||||