SOURCE / PINNED RELEASE
Made of little things.
Browser Side Quest
- Release
- b9d42f5768e4…
- Author-recorded commit
- c267690145fa…
- License
- LICENSE
- Author’s source reference
- nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.shakespeare.diy%2F/browser-side-quest
Archive hash verified: 6d4b90b8a5f328f7…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
# Browser Side Quest — project brief
Browser Side Quest is a port of Mozilla's BrowserQuest (Little Workshop, 2012) to a NIP-5D napplet:
the same world, art, sounds, entities, combat, drops and achievements, played
solo or in shared worlds over NAP-WEBRTC, with Nostr identity and profiles.
Source of truth for porting: `refs/BrowserQuest` (submodule, commit af32d24).
## Licences and content changes
- **Licences.** The game code is MPL-2.0 and the art, maps and sounds are CC-BY-SA 3.0.
- Credits are shown on the About parchment.
- `package.json` and `napplet.json` declare `MPL-2.0 AND CC-BY-SA-3.0 AND CC-BY-3.0` (the last for the music).
- **Music.** The original music was never published with the source. The seven area tracks (village, beach, forest, cave, desert, lavaland, boss) come from the community fork `browserquest/BrowserQuest` and are CC BY 3.0.
- They are re-encoded to 64 kbps to keep the single-file build small. Credits are in `src/assets/music/README.md` and on the About parchment.
- **Third-party characters and quotes.** These were replaced with original content:
- The Octocat and Nyan NPCs reuse village sprites and have new lines.
- The Firefox costume is now an original "flame" recolor of the ruby armor; the achievement is "Fired Up".
- NPC lines that quoted songs or films (rick, king, guard, agent, desertnpc) were rewritten. The "Rickroll'd" achievement is now "Encore".
- Third-party logos (Mozilla tab, HTML5 and social icons) were blanked from the shipped UI sheets.
- **Bundled assets.** Only the 1x art is bundled. The renderer upscales it at an integer scale.
## Name and listing
- **Name.** The napplet is called "Browser Side Quest" (`browser-side-quest`). The source comments, the credits and the wire IDs (`browserquest/1`, `bq-world-N`) keep the BrowserQuest name as attribution.
- **Logo.** The original logo art and its sparkles are unchanged. A CSS overlay writes "Side" above the gap between "Browser" and "Quest" with a small caret, like a proofreader's insertion, in the logo's palette and the GraphicPixel font.
- **Listing.** `napplet.json` holds the title, description, topics, archetypes and optional domains (identity, storage, common, resource, webrtc, config; nothing required).
- The icon, `assets/icon.png`, is built from the game's sprites: the starting warrior on grass with a gold "!" quest marker.
- The preview, `preview.png`, is a 960×600 @2x gameplay capture at the starting camp.
## Fixes beyond the original
- **Title-screen backdrop.** The port shows the world map behind the intro parchment, where the original showed wood. The loop now runs before the game starts, so animated tiles (lava, water) and high tiles (tree tops, roofs) are drawn there too, instead of leaving black holes.
- **HUD bar placement.** The bar sits inside the map's border, directly under the map, like the original `#canvasborder`. The chat box and the player list are anchored to it.
- **Speech bubbles.** The original fixed each bubble's top two tiles above the speaker, which only lined up with its own bubble height. The port anchors the tail's tip a few pixels above the top of the speaker's sprite instead. That matches the original placement at any font size.
- **Nostr name.** A signed-in player plays under their Nostr profile name, cut to the original 15 characters. The identity is watched for changes:
- **On the menus:** the name fills the new-character field and replaces the saved name on the returning-player screen. A login or account switch replaces a typed name; an identity that only arrives late at boot keeps it.
- **In game:** a login, account switch or newly loaded profile renames the character by re-entering the world in place, since the protocol has no rename message. A dead player takes the new name on respawn. A name the player typed is never replaced by a late boot identity, and signing out keeps the current name.
- **Profile not loaded yet:** if only the npub fallback is known after a login, the profile is retried at 2, 5 and 10 s.
- The intro doesn't wait for the identity; it is applied when it arrives.
- **Zone groups follow the player.** The original server updated a player's 28×12 zone group only on the client's `ZONE` message. The client sent it when its fixed 30×14 camera flipped screens, which lined up with the groups. The port's camera is fitted to the frame, so the world updates membership from the player's position on every move. `ZONE` still works.
- **Chat reaches visible neighbours.** The original sent chat to the speaker's own group only, which matched its fixed screen. The port sends chat to the adjacent groups, the same ones that receive the speaker's moves, so anyone who can see you hears you.
- **Chat Enter.** Sending a message with Enter no longer reopens the chat box.
- **From the community fork** `browserquest/BrowserQuest`:
- Items are picked up when walked over (44da4d4). This matters for keyboard and gamepad play.
- The player keeps chasing a target that moves away (8e953e0).
- The world drops a player's stale attacker link to a mob that lost its target.
## Sound
- **Sound effects use the Web Audio API**, not the original 4-channel `<audio>` pools.
- **Why:** in today's browsers, starting an `<audio>` element took 1–146 ms (usually about 90 ms, measured in Chrome with real output). That is as long as the 0.1–0.16 s hit sounds. Hits came late and unevenly, and busy pools dropped 4 of 8 rapid hits.
- **How:** Web Audio decodes each sound once and starts it immediately from one persistent output. Each sound keeps the original cap of 4 copies, but the oldest copy is stopped instead of dropping the new one. Leading MP3 padding (about 34 ms in WebKit) is trimmed at decode.
- **Music stays on `<audio>`**, with the original area fades. Decoding 7 minutes of music would take about 150 MB.
- **Unlock.** The host frame (`sandbox="allow-scripts"`, `allow="fullscreen"`) grants no autoplay, so the `AudioContext` is created and resumed by the first pointer or key gesture inside the napplet (the Play click).
- **No fetch.** Sounds are inlined as base64 (`?inline`) and decoded in-process; the napplet CSP's `connect-src 'none'` blocks `fetch(data:)`.
- **Fallback.** Without Web Audio, or for a sound that fails to decode, the original `<audio>` pools are used.
## Visual direction
- **Look.** Keep the original art direction: parchment dialogs, a dark wood HUD bar, GraphicPixel type and gold accents.
- **Colors.** App-owned colors (`FOLLOW_HOST_THEME` is not used; the game ignores host themes). Gameplay colors never follow host branding.
- **Scaling.** Dialog and HUD art scale with `--u` and `--h` (art pixels per CSS pixel).
- Frames too small for the parchment art switch to a plain "compact" panel.
- Below 160×120 a notice asks for a larger frame.
- **World view.** The game canvas shows as close to the original 30×14-tile desktop view as the frame allows.
- The scale is an integer from 1 to 8 device pixels per art pixel.
- The minimum view is 9×7 tiles; at 200×160 it shows what fits.
## Architecture
```
src/shared gametypes (original numbering + PLAYERINFO=27 extension), formulas,
properties, format checker — identical values to the original
src/sim worldserver port: World, Player session, mobs, areas, chests,
checkpoints, map, Scheduler (all timers), snapshot (migration)
src/client client port: Game (game.js), Renderer, Updater, entities, map,
pathfinder (A*), camera, audio, storage (SDK), app (UI/HUD)
src/net Transport seam; LocalTransport (same tab), SoloWorlds,
ShardNetwork (NAP-WEBRTC host-run shards)
src/input keyboard / gamepad / touch pad → Game.stepPlayer / interact
src/social identity + common.getProfile + resource avatars
```
### Trust model
The trust model is the original game's:
- The client reports `HIT`, `HURT` and `AGGRO`, and the world (host) computes damage.
- Guests trust the host.
- Shared worlds are cooperative, not adversarially secure.
- Nostr pubkeys announced by other players are self-claimed. They are used only for display (name and avatar in the player list).
### Multiplayer (`src/net/shard.ts`)
- **Rooms and capacity.**
- Room `bq-world-N` with protocol `browserquest/1` is an open rendezvous.
- Each world holds 8 players: the host plus 7 guests, matching NAP-WEBRTC's 8 remote peers.
- A full world sends newcomers to N+1. The player list can switch worlds.
- **Host election.**
- Peers identify each other by a random per-tab node id carried in every envelope.
- A host beacons every 2 s.
- If there is no beacon within 1.5 s, the lowest node id hosts.
- With two hosts, the lower id wins.
- **Messages.**
- The host runs `World`. Its own player uses `LocalTransport`.
- Guests send `{t:'c', m:[original msgs]}` envelopes.
- The host flushes batched per-guest queues `{t:'s', b:{node:[msgs]}}` at 20 Hz, each at most 14 KiB.
- **Migration.**
- The host broadcasts a mob snapshot every 5 s.
- When the host leaves or goes silent for more than 7 s, the survivors re-elect.
- The new host restores the snapshot, and players re-enter at their last position.
- **Fallback.** Without `webrtc`, with consent blocked, or with the "Play online" setting off, the player gets a private solo world.
### NAP domains (all optional; none in `requires`)
| Domain | Use | Without it |
| --- | --- | --- |
| identity | character name from the Nostr profile (identity profile, then `common.getProfile`, then a short npub); own pubkey | type a name |
| storage | save (`bq-data`, original shape) | in-memory only |
| common + resource | player-list names, npubs, avatars | names only |
| webrtc | shared worlds | solo world |
| config | sound, touch pad, online play | defaults |
## Controls
- **Pointer and touch:** the original click or tap to move, attack, loot, talk and open.
- **Keyboard:**
- Arrows or WASD walk continuously.
- Space attacks or interacts with what is adjacent, or engages the nearest mob within 3 tiles.
- Enter opens chat; Esc closes windows and stops chasers.
- **Gamepad:**
- The left stick or D-pad moves; A attacks, interacts or confirms.
- B cancels; X opens chat; Y opens achievements; Start toggles sound.
- **Touch:** an on-screen pad and action button, which can be switched off in settings.
## Verification
- `pnpm verify`: guidance, sim parity tests, type-check, build.
- `pnpm test:conformance`: the build is conformant in a real `allow-scripts` iframe.
- `pnpm test:multiplayer`: two real browsers over NAP-WEBRTC at 50±15 ms. Covers:
- host and guest roles, mutual visibility, population and chat;
- guest input to movement within 50 ms, and the guest's movement seen by the host;
- host-leave migration.
- **Frame sizes:** checked at 1100×700, 900×600, 320×560 and 200×160 with headless screenshots.
