Add per-node scene music volume control
Author a music track and volume (0-100%) per story node; the runtime scales it to 0-1 and applies it without restarting the track when only the level changes. Adds migration 024, threads music_volume through the repository/runtime, and gives the node inspector a volume slider. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@ export type RuntimeUtterance = {
|
||||
}
|
||||
export type RuntimeNode = {
|
||||
id: string; kind: 'cutscene' | 'dialogue' | 'level'; label: string
|
||||
componentKey?: string | null; levelSlug?: string | null; musicUrl?: string | null
|
||||
componentKey?: string | null; levelSlug?: string | null; musicUrl?: string | null; musicVolume?: number
|
||||
utterances?: RuntimeUtterance[]; rootId?: string | null
|
||||
}
|
||||
export type PlaythroughState = { playthrough: PlaythroughSummary; node: RuntimeNode | null }
|
||||
@@ -60,7 +60,7 @@ export interface NarrativeRepository {
|
||||
deletePose(npcId: string, poseKey: string): Promise<NpcDto | null>
|
||||
}
|
||||
|
||||
type GraphNodeRow = { id: string; node_type: string; label: string; component_key: string | null; level_template_version_id: string | null; music_asset_id: string | null }
|
||||
type GraphNodeRow = { id: string; node_type: string; label: string; component_key: string | null; level_template_version_id: string | null; music_asset_id: string | null; music_volume: number }
|
||||
|
||||
export function createNarrativeRepository(pool: Pool, objectStorage: ObjectStorage): NarrativeRepository {
|
||||
// ---- Runtime: walking the story graph -------------------------------------
|
||||
@@ -97,12 +97,13 @@ export function createNarrativeRepository(pool: Pool, objectStorage: ObjectStora
|
||||
}
|
||||
|
||||
async function resolveNodeForPlay(nodeId: string, levelSlug: string | null): Promise<RuntimeNode | null> {
|
||||
const node = (await pool.query<GraphNodeRow>('SELECT id,node_type,label,component_key,level_template_version_id,music_asset_id FROM osint.story_nodes WHERE id=$1', [nodeId])).rows[0]
|
||||
const node = (await pool.query<GraphNodeRow>('SELECT id,node_type,label,component_key,level_template_version_id,music_asset_id,music_volume FROM osint.story_nodes WHERE id=$1', [nodeId])).rows[0]
|
||||
if (!node) return null
|
||||
const musicUrl = node.music_asset_id ? `/api/assets/${node.music_asset_id}` : null
|
||||
if (node.node_type === 'cutscene') return { id: node.id, kind: 'cutscene', label: node.label, componentKey: node.component_key, musicUrl }
|
||||
if (node.node_type === 'level') return { id: node.id, kind: 'level', label: node.label, levelSlug, musicUrl }
|
||||
if (node.node_type === 'dialogue') return { id: node.id, kind: 'dialogue', label: node.label, musicUrl, ...(await resolveDialogueGraph(node.id)) }
|
||||
const musicVolume = node.music_volume / 100
|
||||
if (node.node_type === 'cutscene') return { id: node.id, kind: 'cutscene', label: node.label, componentKey: node.component_key, musicUrl, musicVolume }
|
||||
if (node.node_type === 'level') return { id: node.id, kind: 'level', label: node.label, levelSlug, musicUrl, musicVolume }
|
||||
if (node.node_type === 'dialogue') return { id: node.id, kind: 'dialogue', label: node.label, musicUrl, musicVolume, ...(await resolveDialogueGraph(node.id)) }
|
||||
return null // gates are auto-resolved during advance and never surfaced
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user