Files
wownerogue/docs/RENDER_PACKS.md
T
jwinterm fad2a695bd Wownerogue
A browser-based roguelike synchronized with crypto block timing. A run is a race
against the next block: reach the exit before the block lands, or the house wins.
The same codebase runs the solo dungeon, the social Tavern, and operator-selected
PvP and race rulesets, with separate free and paid leaderboards.

Self-hostable under the MIT License. See README.md to run it, docs/ARCHITECTURE.md
for the design, and docs/DEPLOY.md to operate an instance.
2026-07-27 08:23:01 -04:00

152 lines
9.7 KiB
Markdown

# Render kit: modes, packs and FX
The client render kit (`html/js/render/`, namespace `RK`) draws the same `Scene` in several
techniques and art styles, so the tavern and the dungeon are interchangeable environments and new
art can be shipped as a product without touching gameplay code.
## Modes vs. packs
- **Render modes are techniques** (how a scene is drawn): `Tiled`, `ASCII`, `Iso`, `3D`
(`renderModes.js`). Each declares a `projection` of `topdown`, `iso`, or `3d`.
- **Packs are the sets and styles within a projection**, chosen from the pack picker and registered
in `packRegistry.js` / `assetPacks.js`.
- A premium mode is usable once the account has any unlocked pack for that mode's projection
(`RK.canUseMode`).
- The scene model is kind-based (`floor`, `wall`, `table`, `torch`, `monster`, and so on), so one
pack renders both the tavern (`sceneFromTavern`) and the dungeon (`sceneFromGameState`). That is
what makes packs interchangeable across town and dungeon.
Registered packs (`assetPacks.js`):
| Pack id | Projection | Label |
|---------|-----------|-------|
| `original` | topdown | Original Tiles |
| `roguelike-interior` | topdown | Roguelike Interior |
| `roguelike-dungeon` | topdown | Roguelike Dungeon |
| `iso-dungeon` | iso | Isometric Dungeon |
| `iso-medieval` | iso | Medieval Town |
| `kenney-3d-characters` | 3d | Animated 3D |
The active pack is stored per projection (`rk_pack_<projection>` in `localStorage`, an in-memory map
under Node). A stored pack that is unregistered or locked is ignored in favour of the first unlocked
pack for that projection.
Entitlement is per pack, not per purchase: each pack maps to a row in the operator-owned
`cosmetic_catalog` table and resolves through `src/multiplayer/entitlements.js` as free, grant-only,
lifetime-credits threshold, or subscription tier. `entitlements.js` also carries a built-in default
catalog used when the table is unavailable. See `MONETIZATION.md`.
For browser QA, `?unlock=1` sets a sticky local bypass for render-mode and pack display gates and
`?unlock=0` clears it. It affects only that browser's `localStorage`, grants no server entitlement,
and is not an operator access-control mechanism.
## Gameplay focus contract
Single-player keyboard movement is accepted only while `#game-display` owns browser focus, so typing
in chat or another form cannot move the player. Choosing Tiled, ASCII, Iso, 3D, a pack, or a camera
control immediately returns focus to the gameplay area without scrolling (`RK.SPGame.focusGameplay`).
## Local 3D runtime
Three.js `0.160.0` is an exact production dependency. The server advertises the local version in
`/runtime-config.js` and publishes only the browser build and the two required addons:
```text
/vendor/three/0.160.0/three.module.min.js
/vendor/three/0.160.0/addons/loaders/GLTFLoader.js
/vendor/three/0.160.0/addons/utils/SkeletonUtils.js
```
The modules are imported lazily the first time 3D is selected (`RK.ensureThree`), so ASCII and Tiled
users never download them. Local delivery is always enabled in the application runtime and takes
precedence over external sources. `RENDERER_CDN_ENABLED=true` makes the jsDelivr renderer source
path eligible only in a runtime where local delivery is disabled, and widens the CSP to trust
third-party script. All checked-in environment profiles keep it off, so Three.js stays same-origin.
Library delivery is separate from model delivery. Runtime GLBs under `html/assets/generated/3d/` are
gitignored and must be provisioned independently; the renderer keeps a low-poly placeholder while a
model cannot load and removes it once the animated GLB is attached.
## The FX layer (`fxLayer.js`, `RK.fx`)
Pure-canvas animated FX, CSP-safe and with no WebGL, shared by the tiled and iso renderers so a
scene lights up the same way in either projection:
- `RK.fx.fire` and `RK.fx.flame`: dancing two-layer flame plus warm glow, for torches and hearths.
- `RK.fx.hazard`: pulsing overlay for hazard tiles (`lava`, `poison`, `spikes`), taking a
footprint-clip callback so it fits square cells and iso diamonds alike.
- `RK.fx.flicker`, `RK.fx.pulse`, `RK.fx.glow`: the primitives.
A tile carries `fx: 'fire'` or `hazard: '<kind>'` in the scene legend (`sceneModel.js`). The
renderer draws only the tile's base in the static pass and lets `RK.fx` paint the animated layer in
the live loop. The tavern places wall braziers and a hearth. Dungeon hazards are keyed off the map
characters `L`, `P`, and `^` in `dungeonTileKind`, so a generator that emits them renders hazards
with no renderer change.
Torches are wall-mounted: the generator places them only on wall cells adjacent to floor, so
`DUNGEON_LEGEND.torch` uses `over: 'wall'` and the fire branch draws the wall base plus the
procedural flame.
## Lighting model (iso and 3D)
Both premium projections light the dungeon rather than tinting it, and both derive lighting from the
same `scene.lightGrid` the 2D modes use, so switching projection changes the technique and never
what a player is allowed to see.
**Iso** composites in one pass per frame:
- tiles are shaded opaque (`_shadedImage`, quantized to six cached levels) rather than alpha-faded.
Multiplying a sprite's own pixels toward a cold near-black keeps distant stone solid and unlit;
alpha-fading would make remembered walls translucent and let the tiles behind them show through.
- light pools (`_lightPool`) are additive, squashed to the ground plane, and emitted by torches
(flickering), the player's lantern, and objective beacons on the stairs and treasure.
- crevice AO (`_wallAO`) darkens floor cells by how many walls they touch.
- embers rise off each flame, seeded per cell so they do not reshuffle every frame.
- a cold blue wash (`_memoryWash`) separates remembered rooms from torchlit ones by colour
temperature. The wash colour must stay darker than the tiles it covers: a mid-tone blue lifts
memory brighter than the lit centre and inverts the depth hierarchy.
**3D** (`threeRenderer.js`) draws the level as three InstancedMeshes (walls, ground, props), so a
70x35 dungeon costs three draw calls and a fog-of-war update is a few thousand matrix writes instead
of thousands of mesh allocations. Wall and floor materials are generated on a canvas at startup
(coursed stone, irregular flagstones, both doubling as bump maps), so there is nothing extra to
download and nothing for the CSP to block. On top of that: a low warm key light with a
player-tracking shadow frustum, a pool of six point lights reassigned each frame to the nearest
braziers, a hand-held lantern, drifting dust motes, and a wall cutaway that squashes the walls
between the isometric camera and the player.
Two constraints that are easy to get wrong:
- Fog is measured from the camera, which sits about 17 world units back along the isometric offset,
not from the player. A fog band tuned as if it started at the player puts the player a quarter of
the way into fog and crushes the scene to near-black.
- `lightGrid` must not be applied to albedo at full strength in 3D. The point lights already provide
falloff, so multiplying by the grid as well double-darkens every surface. 3D compresses it to
`0.38 + 0.62 * lb` and lets the lamps shape the scene.
Features (`entrance`, `exit`, `treasure`, items) are set dressing, not avatars, in both projections:
stairs with a beacon shaft, a gold chest, a spinning pickup. Rendering them through the entity path
would draw each one as a humanoid, which makes the entrance look like a second player standing at
the map origin.
## Module reference
| File | Role |
|------|------|
| `sceneModel.js` | Renderer-agnostic `Scene` plus the tavern and game-state adapters, the dungeon legend, and tile-character mapping. Handles both array grids and the client's sparse `{y:{x:v}}` maps. |
| `renderModes.js` | Mode registry, entitlement checks, renderer factory, and lazy Three.js loading. |
| `tileRenderer.js` | Top-down atlas renderer. `over` compositing (object tiles drawn on a base tile), torch-lit vignette, ember and flicker loop, animated fire and hazard emitters. |
| `asciiRenderer.js` | Glyph renderer for the free ASCII mode. |
| `isoRenderer.js` | Canvas isometric projection. Orientation-aware walls (`_S` for x-runs, `_W` for y-runs, corner tile), floor variety, contact shadows, vignette. Kenney iso dungeon set (`html/assets/kenney/iso-dungeon/`). |
| `threeRenderer.js` | Same-origin Three.js scene. Instanced level geometry, procedural stone and flagstone materials, torch light pool and shadows, dedicated feature and monster meshes, dust motes, wall cutaway, fog-aware entity retention, animated GLB cloning, camera follow, and full renderer and resource cleanup. |
| `packRegistry.js` | Multi-pack-per-projection registry: active-pack selection persisted per projection, entitlement-gated, graceful fallback. Node-testable. |
| `assetPacks.js` | Pack definitions and asset payloads; registers the shipped packs. |
| `zoomControl.js` | `RK.attachZoom(host)` and `RK.attachCamera(host)`: wheel and pinch zoom (0.4 to 4.0), double-click reset, pixelated scaling. |
| `fxLayer.js` | Shared canvas FX primitives, fire, and hazard overlays. |
| `catSprites.js` | Animated tavern cat (Pet Cats Pack idle strips, 10 frames of 50x50). |
| `chainAmbience.js` | `RK.mountChainAmbience(host)`: drifting top-block-hash fragments, a block-height readout, and a ripple when a block lands. Consumes the public `blockheight` broadcast, is decoration only, and pauses and prunes itself while the tab is hidden. |
| `spGameRenderer.js` | Single-player render bridge (`RK.SPGame`): player-centred camera, local 3D lazy-load, gameplay-focus restoration, and fallback to the legacy render engine when the kit is unavailable. |
Tests: `test/renderScene.test.js`, `test/renderPackResolver.test.js`, `test/packRegistry_ultra.test.js`,
`test/rendererCdnPolicy.test.js`, `test/pvpUiPolish.test.js`.