From 8a3f5a9b421e2381dfb5e6c51bc6a6b009f1fcbb Mon Sep 17 00:00:00 2001 From: Natalie Date: Tue, 28 Apr 2026 19:56:11 -0400 Subject: [PATCH] =?UTF-8?q?feat(audio):=20=E2=9C=A8=20add=20url=20param=20?= =?UTF-8?q?state=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../designs/app/src/pages/AudioSystem.tsx | 112 ++++++++++++++++-- 1 file changed, 100 insertions(+), 12 deletions(-) diff --git a/.project/designs/app/src/pages/AudioSystem.tsx b/.project/designs/app/src/pages/AudioSystem.tsx index ef591eac..eb9d0720 100644 --- a/.project/designs/app/src/pages/AudioSystem.tsx +++ b/.project/designs/app/src/pages/AudioSystem.tsx @@ -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= — preselect resolution-playground entity (DEMO_ENTITIES.id) +// ?kind= — preselect event_kind (attack|hit|death|spawn|...) +// ?filter= — manifest browser substring filter +// ?real=1 — manifest browser "real .ogg only" toggle +// ?open= — pre-expand a manifest entry +// ?play= — 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("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 {