From 937e709dca7f39c321bc58b0e33a741280d455c2 Mon Sep 17 00:00:00 2001 From: cocos02 Date: Wed, 10 Jun 2026 11:01:21 +0800 Subject: [PATCH] feat: global assetBase for scene JSON, convert demo to relative paths --- engine/types.ts | 1 + public/scenes/demo.json | 57 ++++++++++++++++---------------- src/composables/useGameEngine.ts | 34 +++++++++++++++++++ 3 files changed, 64 insertions(+), 28 deletions(-) diff --git a/engine/types.ts b/engine/types.ts index d4e7f0e..66b74f7 100644 --- a/engine/types.ts +++ b/engine/types.ts @@ -100,6 +100,7 @@ export interface GameData { scenes: Record startScene: string variables: Record + assetBase?: string chapters?: ChapterInfo[] achievements?: AchievementDef[] endings?: EndingDef[] diff --git a/public/scenes/demo.json b/public/scenes/demo.json index 9e0b0e8..9fb72c3 100644 --- a/public/scenes/demo.json +++ b/public/scenes/demo.json @@ -1,4 +1,5 @@ { + "assetBase": "", "startScene": "intro", "variables": { "trust": 50, @@ -32,43 +33,43 @@ } ], "endings": [ - { "id": "trust_end", "label": "信任的伙伴", "sceneId": "trust_ending", "chapterId": "ch1", "thumbnail": "/images/end_trust.jpg" }, - { "id": "alone_end", "label": "独行之路", "sceneId": "alone_ending", "chapterId": "ch1", "thumbnail": "/images/end_alone.jpg" }, - { "id": "continue_end", "label": "继续前行", "sceneId": "continue_ending", "chapterId": "ch3", "thumbnail": "/images/end_continue.jpg" } + { "id": "trust_end", "label": "信任的伙伴", "sceneId": "trust_ending", "chapterId": "ch1", "thumbnail": "images/end_trust.jpg" }, + { "id": "alone_end", "label": "独行之路", "sceneId": "alone_ending", "chapterId": "ch1", "thumbnail": "images/end_alone.jpg" }, + { "id": "continue_end", "label": "继续前行", "sceneId": "continue_ending", "chapterId": "ch3", "thumbnail": "images/end_continue.jpg" } ], "chapters": [ { "id": "ch1", "label": "第一章:醒来", "startScene": "intro", - "thumbnail": "/images/ch1.jpg", + "thumbnail": "images/ch1.jpg", "defaultVariables": { "trust": 50, "courage": 0, "investigation": 0 } }, { "id": "ch2", "label": "第二章:调查", "startScene": "desk_detail", - "thumbnail": "/images/ch2.jpg", + "thumbnail": "images/ch2.jpg", "defaultVariables": { "trust": 60, "courage": 10, "investigation": 1 } }, { "id": "ch3", "label": "第三章:终局", "startScene": "qte_success", - "thumbnail": "/images/ch3.jpg", + "thumbnail": "images/ch3.jpg", "defaultVariables": { "trust": 70, "courage": 20, "investigation": 2 } } ], "scenes": { "intro": { "id": "intro", - "videoUrl": "/videos/intro.mp4", - "subtitleUrl": "/subtitles/intro.vtt", + "videoUrl": "videos/intro.mp4", + "subtitleUrl": "subtitles/intro.vtt", "subtitles": { - "zh": "/subtitles/intro.vtt", - "en": "/subtitles/intro_en.vtt" + "zh": "subtitles/intro.vtt", + "en": "subtitles/intro_en.vtt" }, - "bgmUrl": "/audio/calm_bgm.mp3", + "bgmUrl": "audio/calm_bgm.mp3", "bgmVolume": 0.6, "bgmCrossFade": 1.5, "videoMuted": true, @@ -105,8 +106,8 @@ "id": "investigation_site", "type": "image", "videoUrl": "", - "imageUrl": "/images/investigation_scene.jpg", - "subtitleUrl": "/subtitles/investigation.vtt", + "imageUrl": "images/investigation_scene.jpg", + "subtitleUrl": "subtitles/investigation.vtt", "hotspots": [ { "id": "hs_desk", @@ -142,7 +143,7 @@ }, "corridor": { "id": "corridor", - "videoUrl": "/videos/corridor.mp4", + "videoUrl": "videos/corridor.mp4", "hotspots": [ { "id": "hs_left", @@ -172,8 +173,8 @@ }, "left_door": { "id": "left_door", - "videoUrl": "/videos/left_door.mp4", - "subtitleUrl": "/subtitles/left_door.vtt", + "videoUrl": "videos/left_door.mp4", + "subtitleUrl": "subtitles/left_door.vtt", "choices": [ { "text": "与陌生人握手", @@ -192,9 +193,9 @@ }, "right_door": { "id": "right_door", - "videoUrl": "/videos/right_door.mp4", + "videoUrl": "videos/right_door.mp4", "skippable": false, - "bgmUrl": "/audio/tense_bgm.mp3", + "bgmUrl": "audio/tense_bgm.mp3", "bgmVolume": 0.7, "bgmCrossFade": 2.0, "videoMuted": true, @@ -216,7 +217,7 @@ }, "qte_success": { "id": "qte_success", - "videoUrl": "/videos/qte_success.mp4", + "videoUrl": "videos/qte_success.mp4", "choices": [ { "text": "继续前进", @@ -230,7 +231,7 @@ }, "qte_fail": { "id": "qte_fail", - "videoUrl": "/videos/qte_fail.mp4", + "videoUrl": "videos/qte_fail.mp4", "choices": [ { "text": "继续前进", @@ -244,7 +245,7 @@ }, "desk_detail": { "id": "desk_detail", - "videoUrl": "/videos/continue_ending.mp4", + "videoUrl": "videos/continue_ending.mp4", "choices": [ { "text": "返回调查现场", @@ -258,9 +259,9 @@ }, "stay": { "id": "stay", - "videoUrl": "/videos/stay_loop.mp4", - "subtitleUrl": "/subtitles/stay.vtt", - "bgmUrl": "/audio/calm_bgm.mp3", + "videoUrl": "videos/stay_loop.mp4", + "subtitleUrl": "subtitles/stay.vtt", + "bgmUrl": "audio/calm_bgm.mp3", "bgmVolume": 0.6, "videoMuted": true, "loopStart": 3.0, @@ -271,7 +272,7 @@ }, "trust_ending": { "id": "trust_ending", - "videoUrl": "/videos/trust_ending.mp4", + "videoUrl": "videos/trust_ending.mp4", "choices": [ { "text": "开启信任的旅程(需要 trust >= 80)", @@ -290,12 +291,12 @@ }, "secret_ending": { "id": "secret_ending", - "videoUrl": "/videos/continue_ending.mp4", + "videoUrl": "videos/continue_ending.mp4", "choices": [] }, "alone_ending": { "id": "alone_ending", - "videoUrl": "/videos/alone_ending.mp4", + "videoUrl": "videos/alone_ending.mp4", "choices": [], "onEnter": [ { "type": "set", "target": "completed_game", "value": 1 } @@ -303,7 +304,7 @@ }, "continue_ending": { "id": "continue_ending", - "videoUrl": "/videos/continue_ending.mp4", + "videoUrl": "videos/continue_ending.mp4", "choices": [] } } diff --git a/src/composables/useGameEngine.ts b/src/composables/useGameEngine.ts index b6db387..e086cf9 100644 --- a/src/composables/useGameEngine.ts +++ b/src/composables/useGameEngine.ts @@ -111,9 +111,43 @@ export function useGameEngine(videoEls: () => [HTMLVideoElement | null, HTMLVide store.setVideoTime(t) }) + function resolveAsset(base: string, path: string): string { + if (!path || path.startsWith('http://') || path.startsWith('https://') || path.startsWith('data:')) return path + const b = base.endsWith('/') ? base.slice(0, -1) : base + const p = path.startsWith('/') ? path : '/' + path + return b + p + } + + function applyAssetBase(data: GameData) { + const base = data.assetBase || '' + if (!base) return + for (const scene of Object.values(data.scenes)) { + if (scene.videoUrl) scene.videoUrl = resolveAsset(base, scene.videoUrl) + if (scene.subtitleUrl) scene.subtitleUrl = resolveAsset(base, scene.subtitleUrl) + if (scene.imageUrl) scene.imageUrl = resolveAsset(base, scene.imageUrl) + if (scene.bgmUrl) scene.bgmUrl = resolveAsset(base, scene.bgmUrl) + if (scene.subtitles) { + for (const k of Object.keys(scene.subtitles)) { + scene.subtitles[k] = resolveAsset(base, scene.subtitles[k]) + } + } + } + if (data.endings) { + for (const e of data.endings) { + if (e.thumbnail) e.thumbnail = resolveAsset(base, e.thumbnail) + } + } + if (data.chapters) { + for (const c of data.chapters) { + if (c.thumbnail) c.thumbnail = resolveAsset(base, c.thumbnail) + } + } + } + async function loadGame(dataUrl: string) { const resp = await fetch(dataUrl) const data: GameData = await resp.json() + applyAssetBase(data) engine.sceneManager.load(data) engine.stateManager.init(data.variables) store.setChapters(data.chapters || [])