Sharpee
Tutorial / Fernhill Tutorial / The browser

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.