feat: global assetBase for scene JSON, convert demo to relative paths

This commit is contained in:
2026-06-10 11:01:21 +08:00
parent 76477050d3
commit 937e709dca
3 changed files with 64 additions and 28 deletions

View File

@@ -100,6 +100,7 @@ export interface GameData {
scenes: Record<string, SceneNode>
startScene: string
variables: Record<string, number>
assetBase?: string
chapters?: ChapterInfo[]
achievements?: AchievementDef[]
endings?: EndingDef[]

View File

@@ -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": []
}
}

View File

@@ -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 || [])