Stage 6: the overlay
Everything the Pack publishes as state can land on stream: the overlay is a small JS library the bridge serves to a browser source, exporting widgets that subscribe to your state keys. We add two — a toast per effect and a death counter — with one design rule throughout: the game is the content. Nothing fullscreen, single-line plates, translucent backgrounds, and the toast renders nothing while idle.
Declare what the overlay reads
Section titled “Declare what the overlay reads”[overlay.data]reads = ["deaths", "last_effect"]reads lists every state key the overlay subscribes to. The widget library
itself lives at overlay/dist/index.js (a committed build, so installing
the Pack doesn’t require Node).
A state key designed for toasts
Section titled “A state key designed for toasts”A toast is change-driven, and that exposes a subtlety: if the adapter
wrote just the effect name, two add_bot redeems in a row would produce no
second change — and no second toast. So the adapter writes an object with a
sequence number — plus the redeeming viewer’s name, which the invocation
frame carries as username whenever the platform could resolve one (manual
invokes have none, so the key is optional):
[state.last_effect]writer = "adapter"[state.last_effect.params_schema]type = "object"additionalProperties = falserequired = ["name", "seq"][state.last_effect.params_schema.properties.name]type = "string"maxLength = 32[state.last_effect.params_schema.properties.seq]type = "integer"minimum = 0[state.last_effect.params_schema.properties.by]type = "string"maxLength = 64last = {"name": event, "seq": value}if username: # msg.get("username") off the invocation last["by"] = str(username)[:64]send(sock, {"mpp": 2, "type": "state_write", "key": "last_effect", "value": last})Omit the key rather than writing null — the generated TypeScript map then
types it by?: string, and the widget’s <Show when={...}> handles both
shapes for free.
(say deliberately doesn’t write it — the message is already visible in the
game chat, and a toast would double it up.)
The widgets
Section titled “The widgets”The overlay is a Vite + Solid library exporting overlayWidgets; copy the
scaffold from the reference Pack (pack-hello-world/overlay/ — config,
src/lib/overlay-data/, dev harness) and regenerate the typed state map
from your manifest so subscribing to an undeclared key is a compile error:
cargo run -p bridge --bin emit-local-api-state -- \ pack.toml overlay/src/lib/overlay-data/local-api-state.tsThe toast subscribes to last_effect and turns seq bumps into 4-second
flashes:
interface Toast { label: string by?: string}
export function EffectToast(): JSX.Element { const conn = useBridge() const last = useSubscription('last_effect', conn)
const [visible, setVisible] = createSignal<Toast | null>(null) let armedSeq: number | undefined let timer: ReturnType<typeof setTimeout> | undefined
createEffect(() => { const value = last() if (value === undefined) return if (armedSeq === undefined) { armedSeq = value.seq // snapshot: arm, don't toast return } if (value.seq === armedSeq) return armedSeq = value.seq setVisible({ label: effectLabel(value.name), by: value.by }) clearTimeout(timer) timer = setTimeout(() => setVisible(null), 4000) }) onCleanup(() => clearTimeout(timer))
return ( <Show when={visible()}> {(toast) => ( <div style={plate}> ☢ {toast().label} <Show when={toast().by}>{(by) => <small>— {by()}</small>}</Show> </div> )} </Show> )}“Gravity shifted — doomguy_2026” puts the culprit on stream — half the
fun of the product, and one optional field of plumbing.
The “arm, don’t toast” branch matters: a fresh subscription’s snapshot carries the last value from before the overlay connected, and toasting it would replay a stale effect on every OBS refresh.
The death counter is the whole pattern in ten lines — subscribe, hide until the first value, render one compact plate:
export function DeathCounter(): JSX.Element { const conn = useBridge() const deaths = useSubscription('deaths', conn) return ( <Show when={deaths() !== undefined}> <div style={plate}>💀 {deaths()}</div> </Show> )}Preview without the bridge
Section titled “Preview without the bridge”The dev harness mounts the widgets over a fake game frame against a mock
data plane that streams scripted diff frames — so you can watch toasts
fire and the death count tick while judging exactly how much frame the
widgets cover:
cd overlaynpm installnpm run dev # open the printed URL; diffs stream for ~20 snpm run build # rebuild the committed dist/index.jsSuggested placement: toast top-center, death badge bottom-left. Both are single plates, so cropping and repositioning in OBS is trivial.
Next: the finishing touches.