Sound: per-node scene music + synthesized SFX (zero-dep)
- migration 023: story_nodes.music_asset_id (references the asset store). - src/audio.ts: a tiny audio manager — one looping scene-music track (fade, dedup, gesture-primed autoplay) and synthesized one-shot SFX; global mute. - Runtime: RuntimeNode.musicUrl; music follows the current node (null inherits, a finished playthrough stops). SFX blips on dialogue advance/choice (not in the editor preview). Floating mute toggle during play. - Graph inspector: a "Scene music" picker sourced from uploaded audio assets. No external library (we did not adopt react-winamp — it is a React 16 CRA app, not an installable package). Bundle grew ~2 KB. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+4
-1
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useMemo, useRef, useState, type FC } from 'react'
|
||||
import { audio } from './audio'
|
||||
|
||||
export type RuntimeUtterance = { id: string; utterer: 'npc' | 'player'; speaker: { name: string; role: string }; poseUrl: string | null; text: string; childIds: string[]; terminalKey: string | null }
|
||||
export type RuntimeNode = { id: string; kind: 'cutscene' | 'dialogue' | 'level'; label: string; componentKey?: string | null; levelSlug?: string | null; utterances?: RuntimeUtterance[]; rootId?: string | null }
|
||||
export type RuntimeNode = { id: string; kind: 'cutscene' | 'dialogue' | 'level'; label: string; componentKey?: string | null; levelSlug?: string | null; musicUrl?: string | null; utterances?: RuntimeUtterance[]; rootId?: string | null }
|
||||
export type PlaythroughSummary = { id: string; mysterySlug: string; levelSlug: string | null; status: string }
|
||||
export type PlaythroughState = { playthrough: PlaythroughSummary; node: RuntimeNode | null }
|
||||
|
||||
@@ -84,6 +85,7 @@ export function DialoguePlayer({ node, onExit, inline, startId }: { node: { utte
|
||||
}, [currentId, fullText, reduced]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const pick = (choice: RuntimeUtterance) => {
|
||||
if (!inline) audio.sfx('choice')
|
||||
if (choice.childIds.length > 0) setCurrentId(choice.childIds[0])
|
||||
else onExit(choice.terminalKey ?? undefined)
|
||||
}
|
||||
@@ -93,6 +95,7 @@ export function DialoguePlayer({ node, onExit, inline, startId }: { node: { utte
|
||||
if (!done) { setCharCount(fullText.length); return }
|
||||
if (children.length === 0) { onExit(current.terminalKey ?? undefined); return }
|
||||
if (options.length > 0) return // a branch — wait for a choice
|
||||
if (!inline) audio.sfx('advance')
|
||||
setCurrentId(children[0].id) // linear next line
|
||||
}
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user