Removes operator-private material from the shipped tree, corrects documentation
that had drifted from the code, deletes dead code, and applies a consistent
prose style.
Private material moved out of the public tree (docs/private/, git-ignored):
- the wallet keyfile recovery runbook and its helper scripts, which together
published the mainnet wallet file path, the service account, backup snapshot
filenames, and the wallet RPC listener state. The helper duplicated the same
constants, so relocating the document alone would not have removed them.
- the stagenet parallel-candidate runbook and its fixtures.
- the dated production-state record, the internal remediation plan, and the
TODO list. None are reference documentation for an outside reader.
Redacted or genericised:
- operator home-directory paths in shipped scripts and units. The asset build
script now takes KENNEY_ASSETS_DIR instead of a hard-coded path.
- private LAN addresses in tests, env examples and deploy docs, replaced with
RFC 5737 documentation addresses.
- the default alert sender and the notification hub example.
- the Monero wallet unit now matches the Wownero one: a dedicated service
account and a config file, instead of the operator's login account with the
wallet password in argv where any local user can read it from /proc.
Ignore rules now cover every .env variant rather than the bare name, plus local
agent state. The deployment docs instruct operators to create .env.mainnet and
.env.stagenet, which the exact-name rule would have allowed to be committed.
Documentation corrected against the code: per-chain dungeon level counts, the
premium unlock rule, Tavern chat scope, backup retention, the chain-profile
fallback, the environment template location, and several sections describing
shipped work as pending.
Dead code removed: configPersistence, rpccalls, a duplicate vendored ROT.js
bundle, the unused tile-picker overlay, the unreachable legacy game-size path
and its vestigial parameter, spectator stubs that shadowed the real
SpectatorManager, and commented-out debugging. nip98 now imports the shared
nostr loader rather than duplicating it.
Prose style: em-dashes removed throughout in favour of colons, semicolons and
commas; comments restated as present-tense descriptions of the code rather than
narration of past changes.
127 suites / 1015 tests pass.
9.7 KiB
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 aprojectionoftopdown,iso, or3d. - 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:
/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.fireandRK.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.
lightGridmust 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 to0.38 + 0.62 * lband 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.