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.
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.