Log inGet started
▣
module · drop-in viewer
asset⌬ modulemoduleprimary: init.luau·originates fromworld 05cb76a4-7…

city

The district: the tile grid every other slice stands on, and the painted city built from it. One tile is one world unit; tile `(tx, ty)` covers world x in `[tx, tx+1)` and y in `[ty, ty+1)`, so the surface on top of tile row `ty` is at world **y = ty + 1**.

byclaude-code @ aura·posted 2mo ago
What it does

game.city — Ashcross

The district: the tile grid every other slice stands on, and the painted city built from it. One tile is one world unit; tile (tx, ty) covers world x in [tx, tx+1) and y in [ty, ty+1), so the surface on top of tile row ty is at world y = ty + 1.

The district is 26 columns wide and runs from y = -8 to y = 103. Columns 0-1 and 24-25 are its own walls, and everything outside that box reads solid, so nothing can walk off the edge of Ashcross and the camera can never look past it.

The climb, bottom to top

y
-8 … 9The flooded well. Black water, standing miasma on it, six ledges alternating up the shaft, a chain, a fire on a shelf. You start on the bottom ledge at (9.5, -3).
9 … 25The street. Two shuttered houses facing each other, the mouth of the well as a hole in the road, a brazier, a stone stair up the west side and boards running back east between the roofs.
25 … 42The shuttered houses. Higher storeys, tighter boards, and the first rotten plank — placed off the path, so the lesson costs a cache and not the run.
42 … 58The scaffold. Lashed poles, planks between them, miasma standing in the base, and a rotten plank that is on the way up.
58 … 74The gantries. Wide spans between houses, a brazier, and a pool of miasma sitting on one of the gantries.
74 … 92The cathedral buttress. A stepped mass of pale masonry climbing east, then a pier you work up the face of on mason's planks.
92 … 104The lazaretto gate. A terrace broken open at x 9-11 — that hole is the only way up onto it — and the arch itself at x 12-17.

Every step of the critical path is within a rise of 2 and a gap of 3 world units, and every one has two tiles of headroom, so the whole climb is walkable with a plain jump. The instruments open shortcuts and side branches; they are never the only way up. That invariant is checked, not assumed — see "Checking it" below.

What it provides

Installed the moment the module is required, before start() runs:

NB.provide.solidAt(x, y)      -- boolean: that world point is inside solid tile
NB.provide.oneWayTopAt(x, y)  -- number?: surface y of a drop-through plank
NB.provide.inMiasmaAt(x, y)   -- boolean: standing miasma

oneWayTopAt answers for the tile the point is in, and also for a point sitting up to 0.05 above a plank's surface, so a doctor standing exactly on a plank still reads as being on it.

Everything else is under NB.ext.city:

spawn{ x = 9.5, y = -3 } — the ledge in the well the round starts on
gate{ x, y, w, h } — the lazaretto arch; entering it emits "gate.reached"
boundswhat Camera2D:setBounds was given
braziers{ { x, y }, … } — censer refills; standing at one emits "brazier" every frame
caches{ { x, y, id }, … } — id is a NB.TINCTURES key; taking one emits "pickup"
patientSpots{ { x, y, face, note }, … } — ledges with a wall behind them, for the rounds slice to put a patient on. face is -1 or 1: which side the shuttered wall is
routeevery standable surface of the climb, bottom to top, as { x0, x1, x, y, note, side }. x0..x1 is the span you can stand on, measured off the finished grid. side = true marks a branch off the critical path
ladderAt(x, y)a ladder or a chain you may climb
isRotten(x, y)that plank is rotten
probeBoard(x, y)"sound" / "rotten" / nil — what the cane's probe finds under a point
breakBoard(x, y)Theriac's weight goes through a rotten plank: the whole plank goes, collision and picture together
surfaceUnder(x, y)the y of the first surface below a point
pulse(dt)the district's per-frame work; CityRuntime calls it, nothing else should

Emits "brazier", "pickup", "gate.reached". Calls Camera2D:setBounds(6.5, 19.5, 0.5, 95.5) and NB.claimVisual("city").

How it is drawn

The grid is data; the picture is a separate pass over it. Runs of solid tile are merged greedily into rectangles and each rectangle is one quad wearing art.tiles.tile — a shader written for this slice that repeats a painted sheet across the quad and picks a different cell per tile from a hash of where that tile sits. So a twenty-tile wall is one draw and is never the same cell twenty times. The whole district is a little over two hundred pieces.

The painted sheets live in /zero/source/art/tiles/: masonry, timber, facade (2x2 grids of cells) plus props and gate (cut out of flat magenta by the shader's chroma key). Each surface names a flat palette colour as well, which is what it wears if a sheet is missing — so a gap in the art reads as dim stonework rather than a wall of white cards.

There are no lights in Ashcross. Braziers, tincture caches, lit windows and the lantern over the gate are soft discs of amber laid over the scene by the same shader's radial mode, and the lit lip along the top of every ledge is top_light. That is the entire lighting model of the city.

Everything this slice spawns is temporary and parented under CityFabric, itself under CityRoot. The district is rebuilt from this file on every load and is never written into the scene.

Checking it

local City = require("game.city")
City.build()             -- rebuild the picture; safe to call repeatedly
print(City.dump(8, 26))  -- an ASCII elevation of a slice of the grid

dump renders # solid stone, H house facade, = beam, - plank, x rotten plank, ~ water, , miasma, . air.

Notes for the other slices

  • Camera2D:driveTo does not clamp to the bounds this slice sets — the follow path clamps, the override path does not, so a scripted move can take the shot outside the district.
  • The route spans are measured off the finished grid, including the headroom test, so a patrol that stays between x0 and x1 never walks into masonry.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

game.city — Ashcross itself. ONE SLICE OWNS THIS FILE. It owns the district: the tile grid and the collision every other slice stands on, the geometry painted from it, the braziers, the tincture caches, the standing miasma and the lazaretto gate.

ModelCityRuntime

key(tx: number, ty: number) → number

argtypedescription
txnumber
tynumber

put(tx: number, ty: number, kind: string) → void

argtypedescription
txnumber
tynumber
kindstring

rect(x0: number, y0: number, x1: number, y1: number, kind: string) → void

argtypedescription
x0number
y0number
x1number
y1number
kindstring

ledge(row: number, x0: number, x1: number, kind: string) → void

A standable ledge: one row of tile with its surface at `row + 1`.

argtypedescription
rownumber
x0number
x1number
kindstring

markMiasma(x0: number, y0: number, x1: number, y1: number) → void

argtypedescription
x0number
y0number
x1number
y1number

markLadder(tx: number, y0: number, y1: number) → void

argtypedescription
txnumber
y0number
y1number

step(x: number, surfaceY: number, note: string) → void

argtypedescription
xnumber
surfaceYnumber
notestring

sideStep(x: number, surfaceY: number, note: string) → void

A surface that is NOT on the critical path: a detour worth a cache, a place to be routed around. It is in the route so patrols and hints can use it, and flagged so the climb reads as one chain when it is walked.

argtypedescription
xnumber
surfaceYnumber
notestring

measureRoute( ) → void

The span of each recorded surface is READ BACK OFF THE GRID once the whole district is laid down, rather than restated at every call site where it could drift out of step with the tiles actually placed.

standable(tx: number) → boolean

A tile counts as part of the surface only if a doctor FITS on it. Without the headroom test a ledge merges with the wall it is built into and the span runs off into solid masonry.

argtypedescription
txnumber

prop(kind: string, x: number, y: number, w: number, h: number) → void

argtypedescription
kindstring
xnumber
ynumber
wnumber
hnumber

buildGrid( ) → void

kindAt(tx: number, ty: number) → string

The registries — installed FIRST, because the traverse slice needs them

argtypedescription
txnumber
tynumber

solidAt(x: number, y: number) → boolean

argtypedescription
xnumber
ynumber

oneWayAtCell(tx: number, ty: number) → boolean

argtypedescription
txnumber
tynumber

oneWayTopAt(x: number, y: number) → number

argtypedescription
xnumber
ynumber

inMiasmaAt(x: number, y: number) → boolean

argtypedescription
xnumber
ynumber

ladderAt(x: number, y: number) → boolean

argtypedescription
xnumber
ynumber

isRotten(x: number, y: number) → boolean

argtypedescription
xnumber
ynumber

probeBoard(x: number, y: number) → string

The cane's probe: tap the plank under a point and report what it is.

argtypedescription
xnumber
ynumber

surfaceUnder(x: number, y: number) → number

The first surface below a point, or nil if there is nothing under it.

argtypedescription
xnumber
ynumber

loadTextures( ) → void

glowMaterial(name: string) → any

argtypedescription
namestring

surfaceMaterial(name: string, w: number, h: number, seed: number) → any

argtypedescription
namestring
wnumber
hnumber
seednumber

propMaterial(name: string) → any

argtypedescription
namestring

quad(x: number, y: number, w: number, h: number, z: number, mat: any) → any

argtypedescription
xnumber
ynumber
wnumber
hnumber
znumber
matany

mergeRects(match: (string?) → void

Greedy rectangle merge over one kind of tile: the whole point of the tile shader is that a rectangle of any size is one quad at the right texel density, so the district is a hundred draws rather than two thousand.

argtypedescription
match(string?

mergeMask(mask: { [number]: boolean }) → void

argtypedescription
mask{ [number]: boolean }

isKind(name: string) → void

argtypedescription
namestring

paint( ) → void

rearm( ) → void

Put the district's one-shot latches back: the gate is un-reached and every tincture cache is full again. Without this a second run has no charcoal to pick up and a gate that can never be reached, because both are remembered for the life of the session rather than for the life of a round.

pulse(dt: number) → void

argtypedescription
dtnumber

despawnFabric( ) → void

Build

build( ) → void

install( ) → void

The registries go in the moment this module is required, not when start() runs, because a slice that boots first must not find the placeholder floor.

breakBoard(x: number, y: number) → void

argtypedescription
xnumber
ynumber

start( ) → void

dump(y0: number, y1: number) → string

An ASCII elevation of the grid, for reading the climb without rendering it.

argtypedescription
y0number
y1number
⌬ Metadata
descriptionThe district of Ashcross: its tile grid, collision queries, one-way platforms, rotten boards, braziers, tincture caches, miasma pools and the lazaretto gate. Installs NB.provide.solidAt / oneWayTopAt / inMiasmaAt.

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

30items
▦
texture · born here
❒asset
▲ 0↑ born
▦
texture · born here
❒asset
▲ 0↑ born
▦
texture · born here
❒asset
▲ 0↑ born
▣
module · born here
❒asset
# nb <!-- zero:scaffolded-readme --> <!-- Replace the line below with what this module provides and who consumes it, then delete both comment lines. --> TODO: describe this module.
▲ 0↑ born
◇
component · born here
❒asset
# CityRuntime The district's heartbeat, and the only per-frame work the city slice does. Ashcross is a tile grid plus a picture, and neither of those needs a frame. What does need one is the handful of things the city notices about the doctor: standing at a brazier (which refills the censer), walking into a tincture cache, and reaching the lazaretto gate. This component calls `NB.ext.city.pulse(dt)` once a frame in play, and that is all it does. It is a component rather than a task loop on purpose: the work then belongs to an entity, starts and stops with the scene, and leaves nothing running behind it. The city slice spawns exactly one, temporary, under `CityRoot`. Nothing else should add it, and nothing else should call `pulse`. Runs at `executionOrder = 40`, after the doctor has moved this frame.
▲ 0↑ born
✦
shader · born here
❒asset
# art.tiles.tile The surface Ashcross itself is painted with. The **city** slice owns it; the doctor and the infected are drawn with `art.sprite` instead. A city made of one-unit tiles drawn as one quad per tile is thousands of entities and a wall that repeats the same cell twenty times. This shader takes the other route: **one quad covers a whole run of tiles**, and the variation happens per tile inside the fragment shader. | | | |---|---| | **Repeat** | `tiles_x` x `tiles_y` is how many tiles the quad covers, so a six-by-three block of masonry is a single draw at correct texel density instead of eighteen quads or one stretched picture. | | **Variation** | the sheet holds `variants` painted cells starting at `frame_base`; each tile hashes its own position, picks one, and mirrors itself at `flip_chance`. | | **Painted light** | `top_light` lays a warm band along the edge facing the sky and `shade_bottom` drops the underside into soot — which is what makes a flat quad read as a ledge. `radial` turns the whole quad into a soft pool of light instead, which is how every brazier, lantern and lit window in the district is lit. There are no scene lights in this game; this is the lighting. | | **Keying** | same contract as `art.sprite` — the image generators paint a flat background rather than writing alpha, so props cut themselves out here. | ## Two things that cost time, written down **A keyed piece samples mip 0.** A lower mip has already averaged the flat background into the subject, and that average is neither — it survives the key and comes out as a magenta rind, or, on a piece drawn small, as a magenta bar where a chain should be. **The key reads the RAW sample; the tint is applied after it.** Tinting first moves the background away from the colour the key is looking for and the key quietly stops working. Even sampling mip 0, bilinear filtering under heavy minification still blends the key colour into the subject, so `key_tolerance` has to be generous — around 0.78 for a piece drawn a few tiles wide — and `key_despill` pulls the surviving edge toward its own grey so the cut-out does not wear a rind of the colour it was painted over. ## Properties `base_color` · `emissive` · `base_color_texture` · `frames_x` `frames_y` `frame_base` `variants` `seed` `flip_chance` · `tiles_x` `tiles_y` `inset` · `top_light` `top_color` `top_band` `shade_bottom` `v_up` · `fog_mix` `fog_color` · `radial` `radial_power` · `key_mode` `key_color` `key_tolerance` `key_softness` `key_despill` `alpha_cutoff` `v_up = 1` when the quad's `v = 0` edge is the one pointing at the sky, which is the case for a plane stood up with `{0.7071068, 0, 0, 0.7071068}`.
▲ 0↑ born
▦
texture · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
# Camera2D — the camera of Ashcross The only camera in Nine Bells. Orthographic, side-on, never rotating, never leaving the `NB.CAMERA.z` plane; it looks straight down -Z at a flat city on a 1080x1440 portrait screen, which covers sixteen world units vertically and only twelve across. ## In play It follows `NB.state.doctor` — not an entity, because the traverse slice runs its own 2D physics and writes the doctor's position into the blackboard. There is a deadzone the doctor moves inside without the camera answering (narrow horizontally, because there is almost no horizontal room to give away), a lead in the direction of travel scaled by run speed, and a softer vertical time constant so a landing reads as weight. It publishes where it ended up into `NB.state.cameraX` / `cameraY`, which is what the backdrop reads for its scripted parallax and what every slice reads to decide whether a thing is on screen. ## When a moment takes it | Call | What it does | |---|---| | `:panTo(x, y, seconds, ease)` | a TIMED move along an eased curve; `:isPanning()` says when it has arrived | | `:zoomTo(height, seconds, ease)` | a TIMED change of lens. 16 is the playing frame, 10 a push onto a face, 22 the district opening out | | `:setDrift(vx, vy)` | a slow creep added to a driven shot — a held frame that drifts is alive, one that is nailed down is a still | | `:kick(amount)` | the shake: a bell, a hard landing, a floor giving way | | `:snapTo(x, y)` | a cut. Take the override too (`:driveTo`) or the follow walks it straight back | | `:driveTo(x, y)` | the untimed, damped drive | | `:release()` | hand the shot back to the doctor, cancelling any pan and drift | | `:setBounds(minX, maxX, minY, maxY)` | the city slice's limits on the FOLLOW. A driven shot ignores them: a staged moment may look above the roofline | | `:pose()` | where the shot is, with the shake taken back out | Ease names: `linear` `inQuad` `outQuad` `inOutQuad` `outCubic` `inOutCubic` `inOutSine` `outExpo`. ## It also carries the staging clock `game.stage` installs `NB.ext.stageTick` and `NB.ext.stagePostTick`; this component calls them once a frame, in edit mode as well as in play, so a moment can be rehearsed without entering play and locking the VFS for the other slices. The pre-tick runs before the pose, so a cue that starts a pan this frame is answered this frame; the post-tick runs after it, which is where anything held in front of the lens is placed so it never lags the shake. Both calls are guarded: a staging module mid-edit can never take the camera down with it. ## Usage ```lua rig.component.add("Camera", { projection = "orthographic", behavior = asset.resolve("game.Camera2D", "component"), }) ``` Owned by the staging slice. See `/zero/source/design/spec.md`.
▲ 0↑ born
▦
texture · born here
❒asset
▲ 0↑ born
backing path · game/city.module

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported. This asset, its contents, and its direct deps are clean as of the latest commit.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.