feat(audio): volume в проекте, DnD, параллельные плейлисты
Громкость треков сцены/игры сохраняется в проекте; порядок через DnD; несколько плейлистов сразу; автоимя из файла; стоп превью при запуске сессии. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,11 +17,11 @@ void test('collectReferencedAssetIds: превью, видео и аудио', (
|
||||
previewThumbAssetId: 'th' as AssetId,
|
||||
media: {
|
||||
videos: ['v1' as AssetId],
|
||||
audios: [{ assetId: 'a1' as AssetId, autoplay: true, loop: true }],
|
||||
audios: [{ assetId: 'a1' as AssetId, autoplay: true, loop: true, volume: 1 }],
|
||||
},
|
||||
},
|
||||
},
|
||||
campaignAudios: [{ assetId: 'ca1' as AssetId, autoplay: true, loop: true }],
|
||||
campaignAudios: [{ assetId: 'ca1' as AssetId, autoplay: true, loop: true, volume: 1 }],
|
||||
materials: [{ id: 'm1', name: 'Map', assetId: 'mat1' as AssetId }],
|
||||
npcs: [{ id: 'n1', name: 'Guard', avatarAssetId: 'npc1' as AssetId }],
|
||||
} as unknown as Project;
|
||||
|
||||
@@ -69,7 +69,7 @@ void test('readProjectJsonFromZip: campaignAudios round-trips inside project.jso
|
||||
},
|
||||
scenes: {},
|
||||
assets: {},
|
||||
campaignAudios: [{ assetId, autoplay: true, loop: false }],
|
||||
campaignAudios: [{ assetId, autoplay: true, loop: false, volume: 1 }],
|
||||
currentSceneId: null,
|
||||
currentGraphNodeId: null,
|
||||
sceneGraphNodes: [],
|
||||
|
||||
@@ -45,12 +45,13 @@ import type {
|
||||
ProjectNpcGroup,
|
||||
ProjectNpcRelation,
|
||||
Scene,
|
||||
SceneAudioRef,
|
||||
SceneGraphEdge,
|
||||
SceneGraphNode,
|
||||
SceneId,
|
||||
SceneTrap,
|
||||
} from '../../shared/types';
|
||||
import { PROJECT_SCHEMA_VERSION } from '../../shared/types';
|
||||
import { normalizeSceneAudioRef, PROJECT_SCHEMA_VERSION } from '../../shared/types';
|
||||
import { normalizeMaterialLegend } from '../../shared/types/materialLegend';
|
||||
import { normalizeSceneToken, type SceneToken } from '../../shared/types/appTokens';
|
||||
import {
|
||||
@@ -817,7 +818,14 @@ export class ZipProjectStore {
|
||||
: null),
|
||||
...(patch.grid !== undefined ? { grid: normalizeSceneGrid(patch.grid) } : null),
|
||||
...(patch.settings ? { settings: { ...base.settings, ...patch.settings } } : null),
|
||||
...(patch.media ? { media: { ...base.media, ...patch.media } } : null),
|
||||
...(patch.media
|
||||
? {
|
||||
media: {
|
||||
videos: patch.media.videos ?? base.media.videos,
|
||||
audios: (patch.media.audios ?? base.media.audios).map((a) => normalizeSceneAudioRef(a)),
|
||||
},
|
||||
}
|
||||
: null),
|
||||
...(patch.layout ? { layout: { ...base.layout, ...patch.layout } } : null),
|
||||
};
|
||||
|
||||
@@ -1180,8 +1188,9 @@ export class ZipProjectStore {
|
||||
else
|
||||
media.audios.push({
|
||||
assetId: asset.id,
|
||||
autoplay: sc.settings.autoplayAudio,
|
||||
loop: sc.settings.loopAudio,
|
||||
autoplay: false,
|
||||
loop: false,
|
||||
volume: 1,
|
||||
});
|
||||
}
|
||||
return {
|
||||
@@ -1232,7 +1241,7 @@ export class ZipProjectStore {
|
||||
for (const asset of staged) {
|
||||
assets[asset.id] = asset;
|
||||
if (asset.type !== 'audio') continue;
|
||||
campaignAudios.push({ assetId: asset.id, autoplay: false, loop: false });
|
||||
campaignAudios.push({ assetId: asset.id, autoplay: false, loop: false, volume: 1 });
|
||||
}
|
||||
return { ...p, assets, campaignAudios };
|
||||
});
|
||||
@@ -1245,7 +1254,10 @@ export class ZipProjectStore {
|
||||
async setCampaignAudios(audios: Project['campaignAudios']): Promise<Project> {
|
||||
const open = this.openProject;
|
||||
if (!open) throw new Error('No open project');
|
||||
await this.updateProject((p) => ({ ...p, campaignAudios: Array.isArray(audios) ? audios : [] }));
|
||||
const normalized = (Array.isArray(audios) ? audios : [])
|
||||
.filter((a) => a && typeof a === 'object' && a.assetId)
|
||||
.map((a) => normalizeSceneAudioRef(a));
|
||||
await this.updateProject((p) => ({ ...p, campaignAudios: normalized }));
|
||||
const latest = this.getOpenProject();
|
||||
if (!latest) throw new Error('No open project');
|
||||
return latest;
|
||||
@@ -2624,20 +2636,26 @@ function normalizeScene(s: Scene): Scene {
|
||||
const audios = rawAudios
|
||||
.map((a) => {
|
||||
if (typeof a === 'string') {
|
||||
return {
|
||||
return normalizeSceneAudioRef({
|
||||
assetId: a as AssetId,
|
||||
autoplay: Boolean((s.settings as { autoplayAudio?: boolean } | undefined)?.autoplayAudio),
|
||||
loop: Boolean((s.settings as { loopAudio?: boolean } | undefined)?.loopAudio),
|
||||
};
|
||||
volume: 1,
|
||||
});
|
||||
}
|
||||
if (a && typeof a === 'object') {
|
||||
const obj = a as { assetId?: AssetId; autoplay?: boolean; loop?: boolean };
|
||||
const obj = a as { assetId?: AssetId; autoplay?: boolean; loop?: boolean; volume?: unknown };
|
||||
if (!obj.assetId) return null;
|
||||
return { assetId: obj.assetId, autoplay: Boolean(obj.autoplay), loop: Boolean(obj.loop) };
|
||||
return normalizeSceneAudioRef({
|
||||
assetId: obj.assetId,
|
||||
autoplay: Boolean(obj.autoplay),
|
||||
loop: Boolean(obj.loop),
|
||||
volume: obj.volume,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((x): x is { assetId: AssetId; autoplay: boolean; loop: boolean } => Boolean(x));
|
||||
.filter((x): x is SceneAudioRef => Boolean(x));
|
||||
|
||||
return {
|
||||
...s,
|
||||
@@ -2682,15 +2700,20 @@ function normalizeProject(p: Project): Project {
|
||||
const rawCampaignAudios = (p as unknown as { campaignAudios?: unknown[] }).campaignAudios;
|
||||
const campaignAudios = (Array.isArray(rawCampaignAudios) ? rawCampaignAudios : [])
|
||||
.map((a) => {
|
||||
if (typeof a === 'string') return { assetId: a as AssetId, autoplay: false, loop: false };
|
||||
if (typeof a === 'string') return normalizeSceneAudioRef({ assetId: a as AssetId, autoplay: false, loop: false, volume: 1 });
|
||||
if (a && typeof a === 'object') {
|
||||
const obj = a as { assetId?: AssetId; autoplay?: boolean; loop?: boolean };
|
||||
const obj = a as { assetId?: AssetId; autoplay?: boolean; loop?: boolean; volume?: unknown };
|
||||
if (!obj.assetId) return null;
|
||||
return { assetId: obj.assetId, autoplay: Boolean(obj.autoplay), loop: Boolean(obj.loop) };
|
||||
return normalizeSceneAudioRef({
|
||||
assetId: obj.assetId,
|
||||
autoplay: Boolean(obj.autoplay),
|
||||
loop: Boolean(obj.loop),
|
||||
volume: obj.volume,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((x): x is { assetId: AssetId; autoplay: boolean; loop: boolean } => Boolean(x));
|
||||
.filter((x): x is SceneAudioRef => Boolean(x));
|
||||
const rawMaterials = (p as unknown as { materials?: unknown[] }).materials;
|
||||
const materials = (Array.isArray(rawMaterials) ? rawMaterials : [])
|
||||
.map((m) => {
|
||||
|
||||
Reference in New Issue
Block a user