The browser: sound, images, and channels
Everything so far runs identically in the terminal and the browser. This chapter is the layer that only the browser can hear and see, and the discipline that keeps text players whole.
The degradation rule
Every media beat in Fernhill follows one shape: gate the cue on
client has <capability>, and let the prose you already wrote be the
text-mode experience. A text client reads every gate as false, so
nothing breaks, nothing leaks, and nobody gets a lesser story: they get
the story.
Declared assets
Media files are declared data, typo-checked at compile like everything
else. The files themselves live in your project's assets/ directory
(assets/audio/…, assets/images/…) and ship with the browser build:
define sound night-wind from "audio/night-wind.wav"
define sound boiler-thump from "audio/boiler-thump.wav"
define music dawn-theme from "audio/dawn-theme.wav"
define image folly-photograph from "images/folly-photograph.png"
Sound, music, image cues
Cues are one-line statements wherever a rule already narrates:
state running, terminal
on enter
…
phrase pipes-warm
play sound boiler-thump when client has sound
end on
create the framed photograph
after examining it
show image folly-photograph when client has images
end after
after entering it while the player has the deed
play music dawn-theme when client has music
win fernhill-saved
end after
The thump rides the boiler's ignition line; the photograph appears when examined (its prose is the text degradation); the dawn theme plays under the winning ending.
Ambient beds
An ambient is a looping bed of sound. The Grounds' crossing reactions (The world) start and stop the night wind:
after entering it
phrase cold-returns
play ambient night-wind when client has sound
end after
after leaving it
stop ambient when client has sound
end after
A bare play ambient plays into the default bed, main; a bare
stop ambient stops only that bed. Stories that want layers name them
(play ambient surf in shore, stop ambient in shore) after declaring
each named bed with a one-liner beside the assets:
define ambient shore
Named beds and custom image layers (define layer floorplan, then
show image map in floorplan) must be declared before use; a typo is a
load error with a suggestion, and it never leaves you with a silently
empty second bed.
Dynamic channels: throw anything at the client
A dynamic channel is a story-declared data feed to the client, not media-specific; it can carry any JSON your story projects. Fernhill's case clock feeds one. The chime daemon emits a payloaded event, and a channel declaration projects it:
on every turn while it is ticking and one chance in 8
phrase clock-chime
emit estate-clock with hour "evening" when evening
emit estate-clock with hour "past midnight" when midnight
end on
define channel clock
mode replace
gated by sidebar
return "The clock: (hour)" from estate-clock
end channel
emit <event-id> with <field> <value> carries data; the channel returns
finished text from the turn's last matching event, its (hour) slot
projecting the named field. Text clients never see any of it (the sidebar
gate reads false); the chime prose is their whole experience, which is
exactly the degradation rule again.
In the browser, every dynamic channel is visible by default. A channel
you don't place renders in the generic sidebar panel, so a pure-Chord story
gets working client output with zero TypeScript. When you want its text to
land somewhere specific, name an element for it in a custom
browser/index.html — the channel's returned text renders into the element
whose id matches the channel id. Fernhill's status bar carries one:
<span id="clock"></span>
The clock channel returns "The clock: (hour)", and that finished text
renders straight into #clock by the channel-id ↔ element-name convention —
no story TypeScript, and Fernhill ships no browser-entry.ts. If no named
element exists, the value falls back to the generic sidebar panel.
Building and shipping
sharpee build
produces dist/web/<id>/, a self-contained page carrying your .story
source (compiled in the browser at boot), the engine, your assets, and
the client. Serve it from any static host:
npx serve dist/web/<id>
The build also writes dist/<story>.ir.json, the compiled story IR, for
IDE and tooling use.
Testing both worlds
Fernhill's transcript suite runs the whole game text-only, including a
media-degrade transcript asserting every media beat's prose shows and
no asset name or event key ever leaks into text. The browser side is
proven by driving the built page in a real browser and observing the
cues: the photograph mounting, the wind bed and the thump actually
fetching, the clock panel updating. Test the degradation in transcripts;
trust the browser only after you've seen it.
That's the estate, top to bottom: a world model, a tool chain, three people, a machine, a night that runs out, and a deed in a steel box. All of it is fact. Back to the overview.