feat(audio): ✨ add url param state hooks
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
843081e152
commit
8a3f5a9b42
1 changed files with 100 additions and 12 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import { useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { Link } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import audioManifest from "@game-data/audio.json";
|
||||
|
|
@ -158,6 +159,41 @@ const EVENT_KINDS = [
|
|||
] as const;
|
||||
type EventKind = (typeof EVENT_KINDS)[number];
|
||||
|
||||
// ─── URL state — two-way binding state ↔ ?param= ────────────────────────
|
||||
//
|
||||
// Designers share a deep link with the audition + filter state baked in.
|
||||
// Supported params on /audio:
|
||||
// ?entity=<id> — preselect resolution-playground entity (DEMO_ENTITIES.id)
|
||||
// ?kind=<kind> — preselect event_kind (attack|hit|death|spawn|...)
|
||||
// ?filter=<text> — manifest browser substring filter
|
||||
// ?real=1 — manifest browser "real .ogg only" toggle
|
||||
// ?open=<key> — pre-expand a manifest entry
|
||||
// ?play=<key> — auto-play that key on mount (real .ogg if shipped, else synth)
|
||||
|
||||
function useUrlString(name: string, fallback: string): [string, (v: string) => void] {
|
||||
const [params, setParams] = useSearchParams();
|
||||
const value: string = params.get(name) ?? fallback;
|
||||
const set = (v: string): void => {
|
||||
const next: URLSearchParams = new URLSearchParams(params);
|
||||
if (!v || v === fallback) next.delete(name);
|
||||
else next.set(name, v);
|
||||
setParams(next, { replace: true });
|
||||
};
|
||||
return [value, set];
|
||||
}
|
||||
|
||||
function useUrlBool(name: string): [boolean, (v: boolean) => void] {
|
||||
const [params, setParams] = useSearchParams();
|
||||
const value: boolean = params.get(name) === "1";
|
||||
const set = (v: boolean): void => {
|
||||
const next: URLSearchParams = new URLSearchParams(params);
|
||||
if (v) next.set(name, "1");
|
||||
else next.delete(name);
|
||||
setParams(next, { replace: true });
|
||||
};
|
||||
return [value, set];
|
||||
}
|
||||
|
||||
// ─── Resolver — TS port of audio_manager.gd::_resolve_keys ───────────────
|
||||
|
||||
function resolveKeys(entity: DemoEntity, eventKind: string): string[] {
|
||||
|
|
@ -454,8 +490,18 @@ function ResolutionPlayground(): React.ReactElement {
|
|||
// so the green ● real-asset button is visible alongside the gold ▶
|
||||
// synth button on first render. The first 11 launch-pack files cover
|
||||
// the city / research / turn / border keys.
|
||||
const [entityIdx, setEntityIdx] = useState(0);
|
||||
const [eventKind, setEventKind] = useState<EventKind>("attack");
|
||||
const DEFAULT_ENTITY: string = DEMO_ENTITIES[0].id;
|
||||
const DEFAULT_KIND: EventKind = "attack";
|
||||
const [entityIdParam, setEntityIdParam] = useUrlString("entity", DEFAULT_ENTITY);
|
||||
const [eventKindParam, setEventKindParam] = useUrlString("kind", DEFAULT_KIND);
|
||||
|
||||
const entityIdx: number = Math.max(
|
||||
0,
|
||||
DEMO_ENTITIES.findIndex((d) => d.id === entityIdParam),
|
||||
);
|
||||
const eventKind: EventKind = (EVENT_KINDS as readonly string[]).includes(eventKindParam)
|
||||
? (eventKindParam as EventKind)
|
||||
: DEFAULT_KIND;
|
||||
|
||||
const entity = DEMO_ENTITIES[entityIdx];
|
||||
const candidates = resolveKeys(entity, eventKind);
|
||||
|
|
@ -475,11 +521,11 @@ function ResolutionPlayground(): React.ReactElement {
|
|||
<Label>
|
||||
Entity
|
||||
<Select
|
||||
value={entityIdx}
|
||||
onChange={(e) => setEntityIdx(Number(e.target.value))}
|
||||
value={entity.id}
|
||||
onChange={(e) => setEntityIdParam(e.target.value)}
|
||||
>
|
||||
{DEMO_ENTITIES.map((d, i) => (
|
||||
<option key={d.id} value={i}>
|
||||
{DEMO_ENTITIES.map((d) => (
|
||||
<option key={d.id} value={d.id}>
|
||||
{d.id} — {d.kind}.{d.sub}
|
||||
</option>
|
||||
))}
|
||||
|
|
@ -489,7 +535,7 @@ function ResolutionPlayground(): React.ReactElement {
|
|||
Event kind
|
||||
<Select
|
||||
value={eventKind}
|
||||
onChange={(e) => setEventKind(e.target.value as EventKind)}
|
||||
onChange={(e) => setEventKindParam(e.target.value)}
|
||||
>
|
||||
{EVENT_KINDS.map((k) => (
|
||||
<option key={k} value={k}>{k}</option>
|
||||
|
|
@ -549,9 +595,10 @@ function ResolutionPlayground(): React.ReactElement {
|
|||
// ─── Section 2: Manifest browser ─────────────────────────────────────────
|
||||
|
||||
function ManifestBrowser(): React.ReactElement {
|
||||
const [filter, setFilter] = useState("");
|
||||
const [openKey, setOpenKey] = useState<string | null>(null);
|
||||
const [realOnly, setRealOnly] = useState(false);
|
||||
const [filter, setFilter] = useUrlString("filter", "");
|
||||
const [openKeyRaw, setOpenKeyParam] = useUrlString("open", "");
|
||||
const openKey: string | null = openKeyRaw === "" ? null : openKeyRaw;
|
||||
const [realOnly, setRealOnly] = useUrlBool("real");
|
||||
|
||||
const realCount = useMemo(
|
||||
() => Object.entries(manifest.sfx)
|
||||
|
|
@ -613,7 +660,7 @@ function ManifestBrowser(): React.ReactElement {
|
|||
<EntryRow
|
||||
key={key}
|
||||
$open={open}
|
||||
onClick={() => setOpenKey(open ? null : key)}
|
||||
onClick={() => setOpenKeyParam(open ? "" : key)}
|
||||
>
|
||||
<EntryHeader>
|
||||
<PlayBtnInline
|
||||
|
|
@ -876,7 +923,38 @@ function MusicTrackList(): React.ReactElement {
|
|||
|
||||
// ─── Page ────────────────────────────────────────────────────────────────
|
||||
|
||||
function useAutoPlayParam(): void {
|
||||
// `?play=<key>` auditions the key once on mount. Plays the real .ogg if
|
||||
// the key has one shipped, else the synthesised preview. Strips the param
|
||||
// after firing so a refresh doesn't replay; navigating away preserves it
|
||||
// in the user's history. Browser autoplay policies require a prior user
|
||||
// gesture — first navigation may need a click before audio unlocks.
|
||||
const [params, setParams] = useSearchParams();
|
||||
const playKey: string = params.get("play") ?? "";
|
||||
useEffect((): void => {
|
||||
if (!playKey) return;
|
||||
const entry: SfxEntry | undefined = manifest.sfx[playKey];
|
||||
if (entry) {
|
||||
const realPath: string | null = realOggAvailable(entry);
|
||||
if (realPath) {
|
||||
void playReal(realPath);
|
||||
} else {
|
||||
playSynth(playKey);
|
||||
}
|
||||
} else {
|
||||
const track: MusicTrack | undefined = manifest.music.tracks.find(
|
||||
(m: MusicTrack): boolean => m.id === playKey,
|
||||
);
|
||||
if (track && REAL_BY_REL[track.stream]) void playReal(track.stream);
|
||||
}
|
||||
const next: URLSearchParams = new URLSearchParams(params);
|
||||
next.delete("play");
|
||||
setParams(next, { replace: true });
|
||||
}, [playKey, params, setParams]);
|
||||
}
|
||||
|
||||
export function AudioSystemPage(): React.ReactElement {
|
||||
useAutoPlayParam();
|
||||
return (
|
||||
<Page>
|
||||
<Back to="/">← back</Back>
|
||||
|
|
@ -907,6 +985,16 @@ export function AudioSystemPage(): React.ReactElement {
|
|||
<Mono>.project/designs/audio-system.md</Mono>. Sourcing checklist:{" "}
|
||||
<Mono>.project/audio-sourcing-checklist.md</Mono>.
|
||||
</SectionBlurb>
|
||||
<SectionBlurb style={{ marginTop: 12 }}>
|
||||
<strong>Shareable URL params</strong> — every interactive piece of
|
||||
state binds to <Mono>?…</Mono> on this page so designers can deep-link
|
||||
a test:
|
||||
<br />
|
||||
<Mono>?entity=<id></Mono> · <Mono>?kind=<attack|hit|death|spawn|complete|select|promote></Mono>{" "}
|
||||
· <Mono>?filter=<text></Mono> · <Mono>?real=1</Mono> ·{" "}
|
||||
<Mono>?open=<sfx_key></Mono> · <Mono>?play=<sfx_key|track_id></Mono>{" "}
|
||||
(auditions once on load, then strips itself).
|
||||
</SectionBlurb>
|
||||
</Section>
|
||||
</Page>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue