bills, pewter gym, register, celadon mansion, tables, garbage cans

This commit is contained in:
DramaticShape
2026-08-01 23:48:23 -04:00
parent 47363b8d23
commit 91cc2d6f51
8 changed files with 3810 additions and 455 deletions
+899 -78
View File
File diff suppressed because it is too large Load Diff
+463 -81
View File
@@ -68,6 +68,47 @@ local RECESS_MAX = 24
local SHADE = { top = 0.95, south = 1.0, north = 0.68,
side = 0.78, bottom = 0.5 }
-- ------- how far a merged run may reach: the tile lattice
--
-- Merging is what keeps a 90k-voxel house down to ~2k quads, and under a
-- straight projection a run may be as long as it likes -- a straight line
-- is a straight line however finely it is cut. THE WORLD CURVE IS NOT
-- STRAIGHT. It drops every vertex by the square of its distance from the
-- focus (see WorldCurve), so a quad's interior is the CHORD of a parabola
-- its neighbours draw the arc of: a run of length L hangs k*L^2/4 below
-- the short quads butted against it, and the join tears open.
--
-- Nothing bounded a run's length before, and the runs that ran away were
-- the ones wearing a CONSTANT texel -- the roof's black eave outline, its
-- fascia, the shaded underside -- because a flat run has no art to break
-- it. Those reached 102px across a gym, which at V-CURVE 3 hangs some
-- three world pixels under the roof surface beside it: the eave tore off
-- the roof and the drop showed the building's dark interior through the
-- slot. (Strip runs, the drawing marching along the atlas, break at the
-- tileset's own boundaries and were never the problem.)
--
-- So a run stops at the next 8px lattice line. Buildings are stamped at
-- tx*8 (see stamp), so the model's lattice IS the map's: every quad in the
-- scene -- terrain, props, this -- now ends on the same lines, every join
-- is vertex-for-vertex, and the bend carries them together. What is left
-- is the sag WITHIN one cell, k*64/4, which is under a twentieth of a
-- world pixel at any rung.
--
-- It costs quads on a dense city map (Cerulean's object stream goes from
-- 35.7k to 41.6k, and its longest edge from 102px to 8px) and it costs them
-- whether the curve is on or not, which is the deliberate trade: the mesh
-- is cached per map and built asynchronously over seconds, so meshing for
-- the curve's sake only when the curve is on would mean rebuilding every
-- live map on a keypress.
local CELL = 8
-- How far a run starting at `a` may go before it crosses the next lattice
-- line. Floor-mod, so the awning's negative z lands on the same lines the
-- positive side does.
local function runCap(a)
return CELL - a % CELL
end
local function keyOf(tx, ty)
return (ty + 64) * 4096 + (tx + 64)
end
@@ -170,6 +211,39 @@ local function read(t, data, perRow)
local inside = {}
for i = 0, W * H - 1 do inside[i] = not outside[i] end
-- `scrub` names pixel rects where the drawing paints an object standing
-- ON the surface (Red's potted plant on the dining tabletop). The object
-- keeps its own standee -- the template's `keep` leaves its tiles
-- unclaimed -- so the band beneath it is the one surface the drawing
-- implies but never paints clear: every rect pixel takes the field
-- shade, sourced from the first field texel outside the rects, and the
-- model's top comes out as the plain surface the object sat on.
if t.scrub then
local function inRect(x, y)
for _, r in ipairs(t.scrub) do
if x >= r[1] and x <= r[3] and y >= r[2] and y <= r[4] then
return true
end
end
return false
end
local donor = nil
for i = 0, W * H - 1 do
if col[i] == GREY and inside[i]
and not inRect(i % W, math.floor(i / W)) then
donor = i
break
end
end
for i = 0, W * H - 1 do
if inRect(i % W, math.floor(i / W)) then
col[i] = GREY
ax[i], ay[i] = ax[donor], ay[donor]
inside[i] = true
end
end
end
return { W = W, H = H, col = col, ax = ax, ay = ay, inside = inside }
end
@@ -276,6 +350,14 @@ local function measure(sp, t)
end
end
-- The pane rule reads a LIGHT region the drawing seals behind a BLACK
-- frame. A drawing built the other way round -- the healing machine's
-- dark screens sealed behind their own white bezels -- inverts under
-- it: every lit edge sinks and the black panes stand proud, a black
-- lattice a voxel off the face. `panes = false` says the drawing does
-- not carry the rule's polarity, so the facade stays flush.
if t.panes == false then recess = {} end
-- One representative texel per shade, taken from the building's own art:
-- the roof's fascia and its undersides are geometry the drawing implies
-- but never paints, and they must still wear its palette (and pick up
@@ -300,7 +382,12 @@ local function measure(sp, t)
-- onto ground the drawing merely stands its legs on: the lab table's
-- third row is the walkable cell the player faces it from, and the
-- full-grid depth would stand the model in their path.
return { top = top, ytop = ytop, D = (t.depth or #t.tiles) * 8,
-- `depth` names the plot in TILE ROWS, which is the right grain for a
-- building. `depthPx` names it in voxels, for an object whose real
-- depth is not a whole tile row -- the Bike Shop toolbox is a box
-- standing in the middle of its own cell, not a thing that fills a plot.
return { top = top, ytop = ytop,
D = t.depthPx or ((t.depth or #t.tiles) * 8),
ground = ground,
recess = recess, interior = interior, shadeTexel = shadeTexel }
end
@@ -343,19 +430,308 @@ local function deskSetModel(sp, pr, t)
return sx
end
-- The parts list, shared by every base piece: a desk plane or an
-- open tray rim alike, `plane` is simply the height they ride.
local ytop = 0
local function buildParts(plane)
for _, p in ipairs(t.parts) do
Budget.tick()
local x0, x1 = p.x[1], p.x[2]
if p.kind == "flat" then
-- drawn row = depth row by default; `z` renames the origin when
-- the flat sits below the desk's own drawn top span (the Center
-- PC's keyboard)
local r0 = p.rows[1]
local z0 = p.z or r0
for sy = r0, p.rows[2] do
local z = z0 + (sy - r0)
if z >= 0 and z < D then
for sx = x0, x1 do
if inside[sy * W + sx] then put(sx, plane, z, sy * W + sx) end
end
end
end
elseif p.kind == "iso" then
-- An ISO part is drawn in 2:1 isometric -- a box TURNED 45
-- degrees to the map, so one rhombus carries its top, its front
-- and its side at once and no band or facade split can reach
-- them. Un-projecting it is that projection run backwards: the
-- box stands as a real diamond in plan and every voxel wears the
-- texel the drawing paints where that voxel projects TO. The
-- drawn top lands on the top, the screen on the screen-facing
-- side and the flank on the flank, and nothing is segmented by
-- hand -- which is the only way to get this right, because the
-- three faces meet on a diagonal no rectangle can name.
--
-- Everything but the depth centre falls out of the drawn rect,
-- because the projection fixes it: the half-width is the drawn
-- rhombus's x radius, HALF that again its z radius (2:1 is what
-- makes it isometric), the near corner's drawn row is the base
-- rhombus's front tip, and whatever drawn height is left once
-- that rhombus is accounted for is the box's own height. Bill's
-- computer: rx 6, rz 3, base centre row 10, and 6 voxels tall --
-- which puts its left corner's vertical edge at drawn rows
-- 4..10, exactly where the drawing paints one.
--
-- `plan` is the one thing the drawing CANNOT state: 2:1 is the
-- projection, not the object, so reading rz as the plan radius
-- too builds a box half as deep as it is wide -- a slab, not the
-- cube the drawing depicts. `plan` names the real z radius and
-- the drawn row is scaled into it, so a cube is `plan = rx` and
-- the drawing still lands on it pixel for pixel.
local pr0, pr1 = p.rows[1], p.rows[2]
local rx = math.floor((x1 - x0 + 1) / 2)
local rz = math.floor(rx / 2)
local plan = p.plan or rz
local oy = pr1 - rz
local h = oy - rz - pr0
local ytp = plane + h
if ytp > ytop then ytop = ytp end
for sx = x0, x1 do
-- doubled, so a rect of even width keeps its centre between
-- two columns instead of limping one to the left
local dx2 = 2 * sx - (x0 + x1)
for dz = -plan, plan do
local z = p.z + dz
local d2 = math.abs(dx2) * plan + 2 * math.abs(dz) * rx
if z >= 0 and z < D and d2 <= (2 * rx + 1) * plan then
-- the plan row scaled back into the drawn rhombus
local dzs = math.floor((2 * dz * rz + plan) / (2 * plan))
for y = 0, h do
local sy = oy + dzs - y
local i = sy * W + sx
if sy >= pr0 and sy <= pr1 and inside[i] then
put(sx, plane + y, z, i)
end
end
end
end
end
else
local tr0, tr1 = p.top[1], p.top[2]
local fr0, fr1 = p.facade[1], p.facade[2]
local pd = p.depth
-- `rise` lifts a part off the desk's top plane and `z` names its
-- back-most depth row (the field a flat part already carries). An
-- object STANDING on a desk needs neither: it starts on the plane
-- at the plot's back. The healing machine's console needs both --
-- it stands in the FRONT map row of a grid whose back row is the
-- wall band it leans against, and its screen head is MOUNTED on
-- the console's front two voxels above the body's top. Both come
-- off the drawing, not off taste.
local base = plane + (p.rise or 0)
local pz = p.z or 0
local ytp = base + (fr1 - fr0)
if ytp > ytop then ytop = ytp end
-- `inset` sinks an authored pane one voxel: the pane rule
-- applied by hand, for a part whose screen IS sealed behind its
-- own black frame while the template's `panes = false` (set for
-- the polarity-inverted panel elsewhere in the same drawing)
-- blocks the global pass. Same mechanism as a recess: the front
-- voxel is simply not placed.
local ins = p.inset
for sx = x0, x1 do
-- the lid: the part's drawn top laid across its depth from the
-- back, last row continuing forward; the front lid row is the
-- facade's own top row -- the drawn front-top edge. `stretch`
-- maps the drawn band over the whole depth instead, the tray's
-- rule: for a part authored DEEPER than its drawing (the house
-- stool grown past its drawn seat), clamping would print the
-- last row as a long smear off the back band's edge.
for z = pz, pz + pd - 1 do
local front = z == pz + pd - 1
local sy
if front then
sy = fr0
elseif p.stretch then
sy = math.min(tr0 + math.floor((z - pz) * (tr1 - tr0 + 1)
/ (pd - 1)), tr1)
else
sy = math.min(tr0 + z - pz, tr1)
end
while sy <= tr1 and not inside[sy * W + sx] do sy = sy + 1 end
local ok = sy <= tr1 or (front and inside[fr0 * W + sx])
if ok and z >= 0 and z < D then
put(sx, ytp, z, (front and fr0 or sy) * W + sx)
end
end
-- the body: facade rows anchored to the part's own base
for sy = fr0 + 1, fr1 do
local y = base + (fr1 - sy)
local i = sy * W + sx
if inside[i] then
local ix = interiorAt(sx, sy, x0, x1)
for z = pz, pz + pd - 1 do
if z >= 0 and z < D then
if z == pz + pd - 1 then
local sunk = ins and sx >= ins.x[1] and sx <= ins.x[2]
and sy >= ins.rows[1] and sy <= ins.rows[2]
if not sunk and not pr.recess[i] then put(sx, y, z, i) end
elseif z == pz then
put(sx, y, z, i)
else
put(sx, y, z, sy * W + ix)
end
end
end
end
end
end
end
end
end
-- A TRAY is an open container -- the drawing looks down INTO it, so its
-- top-view band is not a lid but the inside of the box, and the model
-- has to be hollow. Bands, all measured 1:1 like any other band table:
-- `top` is the opening (drawn row -> depth row), `front` the near wall
-- seen face-on (drawn row -> elevation), `x` the box's outer span and
-- `inner` the opening's, so the difference between them is the wall.
-- Four walls stand to the rim, the floor slab lies `floor` voxels thick
-- under the opening, and the cavity between them is left as AIR -- which
-- is the whole point, and what an extruded facade can never be. Parts (a
-- standing lid) then ride the rim like any object on a desk's plane.
if t.tray then
local tr = t.tray
local top0 = tr.top[1]
local fr0, fr1 = tr.front[1], tr.front[2]
local bx0, bx1 = tr.x[1], tr.x[2]
local ix0, ix1 = tr.inner[1], tr.inner[2]
local floor = tr.floor or 0
local plane = fr1 - fr0 + 1 -- the rim: the wall's height
-- Which drawn row lies at depth z. The far rim is the band's first
-- row and the near rim the front wall's own, and the drawn inside
-- STRETCHES over whatever depth is between them: a box deeper than
-- its drawing has rows to spare is the ordinary case once the plot
-- stops being the grid, and the alternative -- running out of rows
-- and repeating the last one -- would print the wrench twice.
local lo, hi = top0 + 1, tr.top[2] - 1 -- the drawn inside
local span = math.max(1, D - 3) -- interior depth rows - 1
local function trayRow(z)
if z == 0 then return top0 end
if z == D - 1 then return fr0 end
return lo + math.floor((z - 1) * (hi - lo) / span)
end
for sx = bx0, bx1 do
Budget.tick()
for z = 0, D - 1 do
local hollow = sx >= ix0 and sx <= ix1 and z > 0 and z < D - 1
for y = 0, (hollow and floor or plane - 1) do
if hollow or y == plane - 1 then
-- the opening seen from above: the tray's own floor and
-- whatever lies in it -- and the rim is the same band where
-- the wall meets it
local i = trayRow(z) * W + sx
if inside[i] then put(sx, y, z, i) end
else
-- the wall below the rim: the front band folded up it, the
-- drawn face on the front and back layers and the de-outlined
-- interior between, exactly as a facade extrudes.
--
-- NO recess pass here, and it must stay that way: a pane sinks
-- by DELETING its front voxel so the one behind becomes the
-- pane, and a container's wall is one voxel thick -- there is
-- nothing behind it, so the front panel simply opened a hole
-- straight into the box and you could see the wrench through it.
local sy = fr1 - y
local i = sy * W + sx
if inside[i] then
local px = (z == 0 or z == D - 1) and sx
or interiorAt(sx, sy, bx0, bx1)
put(sx, y, z, sy * W + px)
end
end
end
end
end
if plane > ytop then ytop = plane end
buildParts(plane)
return { at = function(x, y, z)
if x < 0 or x >= W or y < 0 or z < 0 or z >= D then
return nil
end
return vox[key(x, y, z)]
end,
W = W, ytop = ytop, zmin = 0, zmax = D - 1 }
end
-- No base piece at all: the drawing IS its parts (the house stool -- a
-- seat and its legs, nothing under them but floor). The plane the parts
-- anchor to is the ground itself.
if not t.desk then
buildParts(0)
return { at = function(x, y, z)
if x < 0 or x >= W or y < 0 or z < 0 or z >= D then
return nil
end
return vox[key(x, y, z)]
end,
W = W, ytop = ytop, zmin = 0, zmax = D - 1 }
end
-- The desk's top plane. Usually the drawing states it: the fascia and
-- base rows it paints below the objects ARE the front face, and their
-- row count is the height. Bill's desk paints neither inside its grid
-- -- its apron is drawn into the WALKABLE cell in front, and that cell
-- is left out on purpose so the chair standing there keeps its own
-- tiles -- so `plane` names the height directly and the body below the
-- lid is synthesized: the band table's own rim treatment, a shaded box
-- closed by the outline where it meets the floor, in the drawing's
-- shades via shadeTexel.
local f0, f1 = t.desk.fascia[1], t.desk.fascia[2]
local b0, b1 = t.desk.base[1], t.desk.base[2]
local plane = (b1 - b0 + 1) + (f1 - f0 + 1)
-- The desk's own PLOT, when the grid holds more than the desk. Bill's
-- grid runs on into the walkable cell, because the drawing puts the
-- desk's apron AND the chair pushed up to it in the same tiles -- so
-- the desk box has to stop at its own cell (`depth`) and stand on its
-- own ground line rather than the grid's, which the chair's feet set
-- eight rows lower. The base band's last row IS that ground line by
-- definition, and for every desk drawn inside its own grid it is the
-- measured one to the row (lab table, lab computers, Center PC, the
-- Bike Shop toolbox), so this changes nothing for them.
-- ...and in voxels (`depthPx`) plus a back origin (`z`) when the desk
-- is shallower than a tile row and leans against something: the
-- healing machine's cabinet is 10 deep -- its drawn top band's 9 rows
-- plus the front edge -- standing against the wall band, so its box
-- runs z 16..25 of a 32-deep plot.
local deskD = t.desk.depthPx or (t.desk.depth and t.desk.depth * 8) or D
local dz0 = t.desk.z or 0
local dz1 = dz0 + deskD - 1
local deskG = b1 + 1
-- The WALL element: the band the machine backs onto, whose tiles this
-- grid claims. The drawing shows it only as the stripe background
-- around the tower (the same standing as the potted plants' floor),
-- so the block cycles the drawing's own stripe unit -- real pixels of
-- column `x`, rows `cycle` -- at wall-band height over the back plot,
-- exactly what the neighbouring cells' `wall` pins render.
if t.wall then
local wl = t.wall
local c0, c1 = wl.cycle[1], wl.cycle[2]
local cn = c1 - c0 + 1
local wx = wl.x or 0
for y = 0, wl.h - 1 do
Budget.tick()
local sy = c0 + (wl.h - 1 - y) % cn
for sx = 0, W - 1 do
for z = 0, wl.depthPx - 1 do
put(sx, y, z, sy * W + wx)
end
end
end
end
-- the base band, extruded exactly like every lab table's
for sy = b0, b1 do
Budget.tick()
local y = ground - 1 - sy
local y = deskG - 1 - sy
for sx = 0, W - 1 do
if inside[sy * W + sx] then
local ix = interiorAt(sx, sy, 0, W - 1)
for z = 0, D - 1 do
local px = (z == 0 or z == D - 1) and sx or ix
for z = dz0, dz1 do
local px = (z == dz0 or z == dz1) and sx or ix
put(sx, y, z, sy * W + px)
end
end
@@ -364,91 +740,72 @@ local function deskSetModel(sp, pr, t)
for i in pairs(pr.recess) do
local sy = math.floor(i / W)
if sy >= b0 and sy <= b1 then
vox[key(i % W, ground - 1 - sy, D - 1)] = nil
vox[key(i % W, deskG - 1 - sy, dz1)] = nil
end
end
-- the slab: fascia rows wrap every side; the lid continues the
-- sibling tables' top -- black rim, white highlight courses along
-- the north and west, grey field
-- the slab: fascia rows wrap every side
for sy = f0, f1 do
Budget.tick()
local y = plane - 1 - (sy - f0)
for sx = 0, W - 1 do
for z = 0, D - 1 do put(sx, y, z, sy * W + sx) end
end
end
local field = t.desk.lid == "white" and WHITE or GREY
for sx = 0, W - 1 do
for z = 0, D - 1 do
local shade = field
if sx == 0 or sx == W - 1 or z == 0 or z == D - 1 then
shade = BLACK
elseif sx == 1 or z == 1 then
shade = WHITE
end
put(sx, plane - 1, z, pr.shadeTexel[shade])
for z = dz0, dz1 do put(sx, y, z, sy * W + sx) end
end
end
local ytop = plane
for _, p in ipairs(t.parts) do
Budget.tick()
local x0, x1 = p.x[1], p.x[2]
if p.kind == "flat" then
-- drawn row = depth row by default; `z` renames the origin when
-- the flat sits below the desk's own drawn top span (the Center
-- PC's keyboard)
local r0 = p.rows[1]
local z0 = p.z or r0
for sy = r0, p.rows[2] do
local z = z0 + (sy - r0)
if z >= 0 and z < D then
for sx = x0, x1 do
if inside[sy * W + sx] then put(sx, plane, z, sy * W + sx) end
if t.desk.top then
-- The lid wears the desk's own drawn top band -- the drawing DOES
-- paint this tabletop (the healing machine's white top face with
-- its lit west and shaded east strips), so nothing is synthesized
-- where it is visible: band rows map back-to-front, the first
-- fascia row is the drawn front-top edge, same rule as an upright
-- part's lid. Where a part's drawing occludes the band (the monitor
-- standing on it), the lid continues the nearest strip BESIDE the
-- part -- still the drawing's own pixels, the same sibling-pattern
-- rule every synthesized lid follows.
local tr0, tr1 = t.desk.top[1], t.desk.top[2]
for z = dz0, dz1 do
Budget.tick()
local sy = z == dz1 and f0 or math.min(tr0 + (z - dz0), tr1)
for sx = 0, W - 1 do
local px = sx
for _, p in ipairs(t.parts) do
local px0, px1 = p.x[1], p.x[2]
local r0, r1
if p.kind == "flat" or p.kind == "iso" then
r0, r1 = p.rows[1], p.rows[2]
else
r0, r1 = p.top[1], p.facade[2]
end
if sx >= px0 and sx <= px1 and sy >= r0 and sy <= r1 then
px = (sx - px0 < px1 - sx) and (px0 - 1) or (px1 + 1)
px = math.max(0, math.min(W - 1, px))
break
end
end
put(sx, plane - 1, z, sy * W + px)
end
else
local tr0, tr1 = p.top[1], p.top[2]
local fr0, fr1 = p.facade[1], p.facade[2]
local pd = p.depth
local ytp = plane + (fr1 - fr0)
if ytp > ytop then ytop = ytp end
for sx = x0, x1 do
-- the lid: the part's drawn top laid across its depth from the
-- back, last row continuing forward; the front lid row is the
-- facade's own top row -- the drawn front-top edge
for z = 0, pd - 1 do
local front = z == pd - 1
local sy = front and fr0 or math.min(tr0 + z, tr1)
while sy <= tr1 and not inside[sy * W + sx] do sy = sy + 1 end
local ok = sy <= tr1 or (front and inside[fr0 * W + sx])
if ok then
put(sx, ytp, z, (front and fr0 or sy) * W + sx)
end
end
-- the body: facade rows anchored to the desk's top plane
for sy = fr0 + 1, fr1 do
local y = plane + (fr1 - sy)
local i = sy * W + sx
if inside[i] then
local ix = interiorAt(sx, sy, x0, x1)
for z = 0, pd - 1 do
if z == pd - 1 then
if not pr.recess[i] then put(sx, y, z, i) end
elseif z == 0 then
put(sx, y, z, i)
else
put(sx, y, z, sy * W + ix)
end
end
end
end
else
-- the lid continues the sibling tables' top -- black rim, white
-- highlight courses along the north and west, grey field
local field = t.desk.lid == "white" and WHITE or GREY
for sx = 0, W - 1 do
for z = dz0, dz1 do
local shade = field
if sx == 0 or sx == W - 1 or z == dz0 or z == dz1 then
shade = BLACK
elseif sx == 1 or z == dz0 + 1 then
shade = WHITE
end
put(sx, plane - 1, z, pr.shadeTexel[shade])
end
end
end
if plane > ytop then ytop = plane end
buildParts(plane)
return { at = function(x, y, z)
if x < 0 or x >= W or y < 0 or z < 0 or z >= D then return nil end
return vox[key(x, y, z)]
@@ -638,7 +995,8 @@ local function emit(m, sp, atlasW, atlasH)
local function runX(y, z, dx, dy, dz, x)
local i0 = ci(x, y, z)
local strip, n = nil, 1
while true do
local cap = runCap(x)
while n < cap do
local nx = x + n
local i = ci(nx, y, z)
if not i or ci(nx + dx, y + dy, z + dz) then break end
@@ -730,8 +1088,8 @@ local function emit(m, sp, atlasW, atlasH)
while z <= zmax do
local i = ci(x, y, z)
if i and not ci(x + d, y, z) then
local n = 1
while z + n <= zmax do
local n, cap = 1, runCap(z)
while n < cap and z + n <= zmax do
local j = ci(x, y, z + n)
if j ~= i or ci(x + d, y, z + n) then break end
n = n + 1
@@ -840,7 +1198,7 @@ function Buildings.build(S, map, data, perRow)
end
built = models[key]
end
Buildings.stamp(S, map, built, tx, ty, bw, bh)
Buildings.stamp(S, map, built, tx, ty, bw, bh, t)
end
end
end
@@ -850,9 +1208,24 @@ end
-- One placement: claim its tiles (so the detector leaves them alone and
-- the mesher paints ground under them) and copy the model into place.
function Buildings.stamp(S, map, quads, tx, ty, bw, bh)
local shape = { class = "building", h = 0, art = "building",
flat = false, authored = true }
--
-- Two template fields alter what a claim means, for a drawing that
-- carries a STANDEE on its surface (Red's potted plant on the dining
-- table). `keep` names tile ids the stamp must NOT claim: their authored
-- pins stay live, so the standee scan still stands the object exactly as
-- it always did. `support` is the model's top plane in voxels: the claim
-- shape carries it as its height, which is what tells that scan the
-- standee's shelf -- a plain claim stays at h = 0, and Structures treats
-- a building claim with height as a full model (skip, never a second
-- box; see its support branches).
function Buildings.stamp(S, map, quads, tx, ty, bw, bh, t)
local shape = { class = "building", h = (t and t.support) or 0,
art = "building", flat = false, authored = true }
local keep = nil
if t and t.keep then
keep = {}
for _, id in ipairs(t.keep) do keep[id] = true end
end
-- the ground the building stands on: the commonest flat tile around its
-- feet, so a house on a path keeps its path
@@ -878,9 +1251,18 @@ function Buildings.stamp(S, map, quads, tx, ty, bw, bh)
for r = 0, bh - 1 do
for c = 0, bw - 1 do
local k = keyOf(tx + c, ty + r)
S.shapeAt[k] = shape
S.skip[k] = true
S.ground[k] = best or false
if keep and keep[S.tileAt[k]] then
-- unclaimed by request: the tile keeps its pin (the plant's
-- cutout pool) and the standee scan finds it there. Only the
-- ground is set now, so the scan's own claim of these tiles has
-- the building's floor to paint when no flat tile touches a
-- cluster ringed by its own furniture.
S.ground[k] = best or false
else
S.shapeAt[k] = shape
S.skip[k] = true
S.ground[k] = best or false
end
end
end
+1099 -154
View File
File diff suppressed because it is too large Load Diff
+177 -40
View File
@@ -71,6 +71,21 @@ local FALLBACK_HEIGHTS = {
-- body builds from the bark rows and the drawn ellipse projects onto
-- the hull's round top
stump = 16,
-- the same hull cut at both ends, hollowed and tapered: an OPEN bin
-- standing on a floor (the Vermilion Gym trash cans). The drawn mouth
-- ellipse projects onto the round top and down the well, the drawn base
-- ellipse is ground contact rather than body, and the plan narrows toward
-- the floor. Height is AUTHORED (the profile's can_height, which this
-- pin must be kept equal to so anything riding a can lands on its rim) --
-- the drawing's own straight run is only a couple of rows, because a GB
-- cell spends most of itself on the opening
can = 9,
-- round scenery drawn ONE cell wide and TWO cells TALL, standing on one
-- cell of plot: the Pokemon Centers' potted plants. Carved as one
-- 16x32x16 hull in the SOUTH (pot) cell -- the drawing's upper cell is
-- the object's height, not its depth. BOTH cells take the class; the
-- group build anchors on the north one (Structures.buildCylinders)
planter = 32,
billboard = 16,
signpost = 16,
post = 16,
@@ -91,6 +106,10 @@ local FALLBACK_HEIGHTS = {
desk = 24,
prop = 16,
cutout = 16,
-- a vehicle drawn SIDE-ON: the showroom bicycles. Standee height like
-- every other cutout pool -- what differs is the thickness (see
-- Structures' PINNED_DEPTH)
bike = 16,
console = 16,
relief = 3,
bookcase = 32,
@@ -127,6 +146,8 @@ local ART = {
cylinder = "cylinder",
canopy = "canopy",
stump = "cylinder",
can = "cylinder",
planter = "planter",
billboard = "billboard",
-- signposts share the billboard treatment but as their own pool at a
-- 2-voxel depth: a sign is a thin plate on a stick, and the standard
@@ -165,6 +186,13 @@ local ART = {
desk = "upright",
prop = "billboard",
cutout = "billboard",
-- a bicycle is a LINE drawing seen side-on, and its negative space --
-- the air inside the frame, between the wheel and the fork -- is what
-- makes it read as a bicycle at all. Its own pool at two voxels: any
-- thicker and the side faces of neighbouring strokes close those gaps
-- from every angle but dead-on, and six of them in a showroom come out
-- as one dark lump (which is what the 5px `prop` pool gave)
bike = "billboard",
-- a machine standing on furniture: the billboard treatment with
-- body, plus the one-object contract `cutout` has -- the drawing is
-- ringed by the furniture it sits on, and those edges must not be
@@ -183,6 +211,7 @@ local ART = {
local spec = nil -- the loaded data file, or false when absent
local cache = {} -- tileset id -> resolved shape list
local figCache = {} -- tileset id -> parsed figure masks, or false
local mntCache = {} -- tileset id -> parsed mounted masks, or false
local bgCache = {} -- tileset id -> prop background shades, or false
-- The shape profile ships with the mod (data/voxel_heights.lua) and is read
@@ -430,68 +459,158 @@ end
-- pixel by pixel (see data/voxel_heights.lua):
--
-- figures = { { w = <tiles across>,
-- depth = <voxels of body; ABSENT for a person>,
-- thin = { rows = <top rows>, depth = <voxels> },
-- flat = { x = { <lx0>, <lx1> }, rows = { <r0>, <r1> } },
-- tiles = { ...w*h tile ids, row-major... },
-- under = { ...w*h ids: what each tile wears once the
-- figure is lifted off it... },
-- pixels = { ...h*8 strings of w*8 chars, "." = not the
-- figure... } } }
--
-- No class: a figure is always a flat sprite card, drawn the way
-- SpriteBillboards draws a character (see Structures.buildFigures).
-- No class -- what the entry carries instead is a `depth`, or does not:
--
-- WITHOUT one it is a flat sprite card, drawn the way SpriteBillboards
-- draws a character. That is the right reading for a PERSON: a Gen 1
-- figure is a face-on 2D icon, and extruding one reconstructs a body
-- nobody drew (see Structures.buildFigures).
-- WITH one it is an OBJECT and gets the standee treatment every other
-- solid here gets -- a per-pixel slab in world space, standing on the
-- same furniture the card would have stood on. The Marts' cash
-- register is the case: a machine on a counter is a box, not an icon.
--
-- Two fields say which parts of such a drawing are NOT the extrusion,
-- because a solid drawn in one 16x16 GB cell still packs more than one
-- facing:
--
-- `thin` caps the thickness over the mask's top rows, for the part of
-- the drawing that is not the machine (the register's receipt curl).
-- `flat` names a rect of the mask that is a TOP-VIEW surface rather
-- than a face -- the register's keypad, whose keys lie ON its deck.
-- The rect lays horizontal one voxel proud of whatever the extrusion
-- leaves below it, at the elevation its BOTTOM row would have had,
-- with drawn row = depth row 1:1 (the mapping the lab tabletop is
-- drawn with). So a drawing whose front elevation is an L reads as
-- one: body up the side and along the base, keys lying in the notch.
--
-- Returned normalized: `mask` as a set keyed by ly * (w * 8) + lx, so
-- Structures can read it as a bitmap without re-parsing per position.
-- A malformed entry is dropped rather than half-applied -- a typo in a
-- mask should leave the couch alone, not carve a hole in it.
--
-- `mounted` (below) carries the same four fields, so the parse is shared,
-- and so are the optional ones that give an authored mask a BODY: `depth`,
-- `thin` and `flat` above. `depth` is left nil when unstated, because
-- absence is meaningful on a figure: no depth means the flat sprite card a
-- person is drawn as.
local function authoredMasks(list)
local out = {}
if type(list) ~= "table" then return out end
for _, f in ipairs(list) do
local ok = type(f) == "table" and type(f.w) == "number"
and type(f.tiles) == "table" and type(f.under) == "table"
and type(f.pixels) == "table"
local w = ok and math.floor(f.w) or 0
local h = (w >= 1) and (#f.tiles / w) or 0
ok = ok and w >= 1 and h >= 1 and h == math.floor(h)
and #f.under == #f.tiles and #f.pixels == h * 8
if ok then
for i = 1, h * 8 do
local row = f.pixels[i]
if type(row) ~= "string" or #row ~= w * 8 then
ok = false
break
end
end
end
if ok then
local mask, n = {}, 0
for ly = 0, h * 8 - 1 do
local row = f.pixels[ly + 1]
for lx = 0, w * 8 - 1 do
if row:sub(lx + 1, lx + 1) ~= "." then
mask[ly * (w * 8) + lx] = true
n = n + 1
end
end
end
local depth = tonumber(f.depth)
local thin = nil
if type(f.thin) == "table" and tonumber(f.thin.rows)
and tonumber(f.thin.depth) then
thin = { rows = math.floor(tonumber(f.thin.rows)),
depth = math.floor(tonumber(f.thin.depth)) }
end
local flat = nil
if type(f.flat) == "table" and type(f.flat.x) == "table"
and type(f.flat.rows) == "table" then
flat = { x0 = math.floor(f.flat.x[1]), x1 = math.floor(f.flat.x[2]),
r0 = math.floor(f.flat.rows[1]),
r1 = math.floor(f.flat.rows[2]) }
end
if n > 0 then
out[#out + 1] = { w = w, h = h, n = n, mask = mask,
tiles = f.tiles, under = f.under,
depth = depth and math.floor(depth) or nil,
thin = thin, flat = flat }
end
end
end
return out
end
function TileShape.figures(tilesetId)
local hit = figCache[tilesetId]
if hit ~= nil then return hit or nil end
local s = load()
local entry = s and s.tilesets and s.tilesets[tilesetId]
local list = entry and entry.figures
local out = {}
if type(list) == "table" then
for _, f in ipairs(list) do
local ok = type(f) == "table" and type(f.w) == "number"
and type(f.tiles) == "table" and type(f.under) == "table"
and type(f.pixels) == "table"
local w = ok and math.floor(f.w) or 0
local h = (w >= 1) and (#f.tiles / w) or 0
ok = ok and w >= 1 and h >= 1 and h == math.floor(h)
and #f.under == #f.tiles and #f.pixels == h * 8
if ok then
for i = 1, h * 8 do
local row = f.pixels[i]
if type(row) ~= "string" or #row ~= w * 8 then
ok = false
break
end
end
end
if ok then
local mask, n = {}, 0
for ly = 0, h * 8 - 1 do
local row = f.pixels[ly + 1]
for lx = 0, w * 8 - 1 do
if row:sub(lx + 1, lx + 1) ~= "." then
mask[ly * (w * 8) + lx] = true
n = n + 1
end
end
end
if n > 0 then
out[#out + 1] = { w = w, h = h, n = n, mask = mask,
tiles = f.tiles, under = f.under }
end
end
end
end
local out = authoredMasks(entry and entry.figures)
figCache[tilesetId] = (#out > 0) and out or false
return figCache[tilesetId] or nil
end
-- Hand-authored MOUNTED objects for one tileset: a thing drawn INTO the
-- wall band it hangs on, cut out by an explicit pixel mask and stood
-- proud of the wall's face.
--
-- Same authoring problem as `figures` and the same answer -- a class pin
-- resolves a whole 8x8 tile, and the detector cannot segment a drawing
-- that has no background margin to flood from. The Bike Shop's two wall
-- bicycles are the case: the shop's striped wall panel runs BEHIND them,
-- and its #555 stripes are a flood boundary, so a silhouette flood comes
-- back with the stripes attached to the bike.
--
-- Two things differ from a figure, and both follow from the object being
-- an object rather than a character:
--
-- it keeps its DRAWN ELEVATION. A figure stands on its own feet; a
-- mounted thing sits where the wall band draws it, so a bicycle hung
-- clear of the floor stays hung.
-- it has THICKNESS (`depth`, default 2), and it is built in world
-- space as a per-pixel slab jutting south of the band -- not as a
-- camera-facing sprite card. A bicycle drawn side-on is a plane
-- parallel to the wall, not a face-on icon.
--
-- mounted = { { w = <tiles across>,
-- depth = <voxels it juts into the room>,
-- tiles = { ...w*h tile ids, row-major... },
-- under = { ...w*h ids: what each tile wears once the
-- object is lifted off it (the plain panel)... },
-- pixels = { ...h*8 strings of w*8 chars, "." = wall... } } }
function TileShape.mounted(tilesetId)
local hit = mntCache[tilesetId]
if hit ~= nil then return hit or nil end
local s = load()
local entry = s and s.tilesets and s.tilesets[tilesetId]
local out = authoredMasks(entry and entry.mounted)
mntCache[tilesetId] = (#out > 0) and out or false
return mntCache[tilesetId] or nil
end
-- Which GB shades count as BACKGROUND for a pinned per-pixel prop, per tile
-- (a tileset entry's prop_bg). Returns tile id -> set of shade names, or nil.
--
@@ -564,12 +683,30 @@ function TileShape.bookcaseBackfill(tilesetId)
return mode == "above" and mode or nil
end
--- Does this tileset's `bookcase` run carry the measured pane RELIEF on
--- its front (a tileset entry's bookcase_relief)? Default yes: the class
--- almost always collapses a shelf, a rack or a display case, and every
--- one of those seals its contents behind a frame that should stand proud
--- of them.
---
--- A tileset says `bookcase_relief = false` when it borrows the collapse
--- for something that is NOT a shelf -- the League's gate walls and
--- pilasters, Bill's transporter drums -- where the drawing's light
--- regions are the masonry and the barrel, not panes, and sinking them
--- carves the surface instead of describing it.
function TileShape.bookcaseRelief(tilesetId)
local s = load()
local entry = s and s.tilesets and s.tilesets[tilesetId]
return not (entry and entry.bookcase_relief == false)
end
-- Drop the cache: a mod that shadows data/voxel_heights.lua or a tileset
-- record needs the next lookup to re-resolve (hot reload, mod toggle).
function TileShape.invalidate()
spec = nil
cache = {}
figCache = {}
mntCache = {}
bgCache = {}
end
+61 -9
View File
@@ -381,9 +381,32 @@ Water.EDGE_FADE = 0.14 -- reflection eased off over this much of the fra
--
-- The scene shader's own vertex path, plus the world position the geometry
-- was actually DRAWN at -- after the world curve, because that is the space
-- the depth buffer holds and therefore the space the march has to walk in.
-- (The curve only ever moves Y, so a fragment's world XZ is the same on both
-- sides of it and the ripple can be measured off this one too.)
-- the surface the eye MEETS lives in: which wave column a screen pixel is
-- looking at is a question about the geometry as drawn, and relief() answers
-- it there. (The curve only ever moves Y, so a fragment's world XZ is the
-- same on both sides of it and the ripple can be measured off this one too.)
--
-- WHAT IT REFLECTS is worked out on the other side of the bend, in the FLAT
-- world, and this is the same rule the rest of the mode keeps: the curve
-- tips the world away and the things standing on it do not lean with it (see
-- WorldCurve -- buildings stay upright, shadows are resolved before the bend
-- and ride along). A lake is one of those things. Reflect off the bowl the
-- bend has made instead and the far half of a pond is a mirror tilted twenty
-- degrees: it throws the ray past the vertical, where the sky ramp's own
-- measure -- a screen row, through the frame's matrix -- swings from one end
-- of the ramp to the other across a single column, and the pond comes out
-- with hard-edged patches of the wrong sky stamped into it -- the overhead
-- band and the horizon band abutting in the middle of a lake, which reads as
-- something other than water showing through. The same tilt sends the
-- screen-space march grazing along the bank instead of over it, which is the
-- other half: the dock and the roofs smeared across the harbour.
--
-- So the reflection is taken with the flat view ray about the flat normal,
-- exactly as it would be with the curve off -- and the MARCH still has to
-- walk the world as drawn, because that is what the depth buffer holds. Both
-- at once: the ray is straight in the flat world, and project() bends each
-- sample on its way to the screen, which is the same displacement the vertex
-- stage applies and therefore lands in the same place the geometry did.
local SHADER_SRC = [[
varying float vShade;
varying vec3 vSun;
@@ -435,6 +458,20 @@ uniform vec3 eye;
uniform vec2 screen; // the canvas, in pixels
uniform float cell; // one diorama pixel, in canvas pixels
uniform float pxAngle; // radians of view one screen pixel subtends
// The same bend the vertex stage applied. This stage has to undo it to get
// back to the flat world it reflects in, and re-apply it on every marched
// sample to get back to the screen. Declared in both stages, like `vp`, and
// both are highp here.
uniform vec3 curve; // xy = the focus in world XZ, z = k; 0 = off
// How far the bend has pushed the world down at world XZ `q` -- the vertex
// stage's own displacement, as a number this stage can add and subtract.
// Zero when the curve is off, which is the shader's "skip it" everywhere.
float bendDrop(vec2 q) {
if (curve.z <= 0.0) return 0.0;
vec2 d = q - curve.xy;
return dot(d, d) * curve.z;
}
// the sun's own pass, exactly as the scene shader reads it
uniform Image sunMap;
@@ -658,10 +695,14 @@ vec3 bodyAt(vec3 d, vec3 c, float parity) {
// clip-space Y flip is already baked into `vp`, and a canvas texture's v runs
// the same way its pixel rows do, so one 0.5x+0.5 answers for both.
//
// This is why the march walks in the world as DRAWN rather than as authored:
// the depth buffer holds the curved world, so a straight line in that space
// is the ray, and a straight line in the flat one would bend through it.
// The point arrives in the FLAT world -- the space the ray is straight in --
// and is bent here, by the same displacement the vertex stage applied, so it
// lands exactly where the geometry it is being compared against landed. That
// split is the whole trick: the reflection is worked out in a world that has
// not been tipped, and every sample of it is tipped on the way to the screen,
// so the march reads the depth buffer it actually has.
vec4 project(vec3 p) {
p.y -= bendDrop(p.xz);
vec4 c = vp * vec4(p, 1.0);
if (c.w <= 1e-6) return vec4(0.0, 0.0, 0.0, 0.0);
return vec4(c.xy / c.w * 0.5 + 0.5, c.z / c.w * 0.5 + 0.5, 1.0);
@@ -925,12 +966,23 @@ vec4 effect(mediump vec4 color, Image tex, mediump vec2 tc, mediump vec2 sc) {
// drawn at, with no smooth shading anywhere across it. (The depth test
// above is the one thing that stays per fragment: that is the hardware's
// own question and it is asked in screen space.)
vec3 view = normalize(vBent - eye);
//
// Answered on the FLAT sheet, which is where the bars are a slab of even
// thickness over a level plane -- the one thing relief() is built on. The
// bend translates every bar straight down by its own column's drop, so the
// field keeps its shape and only its height moves; undo that here and the
// walk is the walk it was written for. Try it in the world as DRAWN
// instead and the slab is a bowl: the backward step up the ray climbs the
// bowl's near side as fast as it climbs out of the water, the walk starts
// inside the sheet, and it hands back a column a pixel or three off -- per
// fragment, differently, which is a patch of noise rather than parallax.
vec3 sheet = vec3(vBent.x, vBent.y + bendDrop(vBent.xz), vBent.z);
vec3 view = normalize(sheet - eye);
vec3 hit;
vec2 col;
float face;
float axis;
relief(vBent, view, hit, col, face, axis);
relief(sheet, view, hit, col, face, axis);
// and the bar's centre, so a column is sampled and reflected from one
// place rather than from wherever inside it the fragment happened to land
vec3 surf = vec3(col.x + 0.5, hit.y, col.y + 0.5);
@@ -1001,7 +1053,7 @@ vec4 effect(mediump vec4 color, Image tex, mediump vec2 tc, mediump vec2 sc) {
vec3 rgb = mix(base, refl, clamp(f, 0.0, 1.0));
#ifdef VOXEL_GRID
rgb *= 1.0 - gridDark * columnSeam(hit, vBent, axis);
rgb *= 1.0 - gridDark * columnSeam(hit, sheet, axis);
#endif
return vec4(rgb, 1.0) * color;
}
+33 -2
View File
@@ -1800,9 +1800,21 @@ T.check(plain:find("waveNormal(vec2 q, float tilt)", 1, true) ~= nil
and plain:find("waveNormal(col,", 1, true) ~= nil,
"still one answer per column, so the surface stays pixel-quantised in "
.. "space while the value it reflects with is continuous")
T.check(plain:find("relief(vBent, view, hit, col, face, axis)", 1, true) ~= nil,
T.check(plain:find("relief(sheet, view, hit, col, face, axis)", 1, true) ~= nil,
"and the visible column is found by walking the view ray through the "
.. "slab, which is what makes a tall bar hide the short ones behind it")
-- ...over the FLAT sheet, which is the one thing that walk is built on: an
-- even slab over a level plane. The world curve drops each bar straight down
-- by its own column's drop, so undoing that drop hands relief() the field it
-- was written for. Walked in the world as DRAWN instead, the slab is a bowl:
-- the backward step up the ray climbs the bowl's near side as fast as it
-- climbs out of the water, the walk starts inside the sheet, and it returns a
-- column a pixel or three off -- differently per fragment, which is a
-- hard-edged patch of noise in the middle of a pond.
T.check(plain:find("vec3 sheet = vec3(vBent.x, vBent.y + bendDrop(vBent.xz), vBent.z)",
1, true) ~= nil,
"and it walks the sheet the mesh was AUTHORED as, the bend taken back off, "
.. "because a slab walk over a bowl starts inside the water")
-- the march's reach grows as one over the ray's descent, so a grazing camera
-- asks for hundreds of world pixels of it from a fixed number of samples --
-- which stepped over whole crests and smeared the surface into streaks
@@ -1830,7 +1842,7 @@ T.check(plain:find("waveUV(tc, col)", 1, true) ~= nil,
"and the column is what is handed to it")
-- the wireframe is ruled on the COLUMNS, not on the flat sheet they stand on
T.check(gridded:find("columnSeam(hit, vBent, axis)", 1, true) ~= nil,
T.check(gridded:find("columnSeam(hit, sheet, axis)", 1, true) ~= nil,
"with V-GRID on, the seams outline the column the ray landed on -- every "
.. "voxel of water its own block -- rather than ruling a grid across the "
.. "flat quad underneath and ignoring the bars entirely")
@@ -1839,6 +1851,25 @@ T.check(gridded:find("vec3 w = fwidth(base);", 1, true) ~= nil,
.. "between neighbouring fragments and its own derivative is a step")
T.check(plain:find("march(surf, r)", 1, true) ~= nil,
"the reflection marches from that column, not from the raw fragment")
-- The two halves of the world curve, and they pull opposite ways. WHAT the
-- lake reflects is worked out FLAT -- the same rule the rest of the mode
-- keeps, that the world tips away and the things standing on it do not lean
-- with it. Reflect off the bowl the bend has made instead and the far half
-- of a pond is a mirror tilted twenty degrees, throwing the ray past the
-- vertical, where the sky ramp's own measure (a screen row, through the
-- frame's matrix) swings from one end of the ramp to the other across a
-- single column and stamps hard-edged patches of the wrong sky into the
-- water. But WHERE it lands has to be found in the world as DRAWN, because
-- that is what the depth buffer holds -- so the ray stays straight in the
-- flat world and every sample of it is bent on the way to the screen, by the
-- vertex stage's own displacement.
T.check(plain:find("p.y -= bendDrop(p.xz);", 1, true) ~= nil,
"and every marched sample is bent into the world as DRAWN before it is "
.. "projected, because that is the world the depth buffer holds")
T.check(plain:find("vec3 r = reflect(view, n);", 1, true) ~= nil
and plain:find("reflect(view, vec3(0.0, 1.0, 0.0))", 1, true) ~= nil,
"while the reflection itself is taken about the FLAT normal, so a curved "
.. "world does not tip the lake the way it does not lean the buildings")
T.check(plain:find("mod(col.x + col.y, 2.0)", 1, true) ~= nil,
"and the dither's checkerboard is cut from the columns too, so a camera "
.. "pan slides the world through nothing")
+166
View File
@@ -214,6 +214,172 @@ Structures.buildFigures(twice, map, 0, 3, 8, 11)
T.eq(#twice.figures, 1,
"the repaint replaces the pattern, so a rescan cannot match it again")
-- ------- a figure with a DEPTH is an object, not a card
--
-- The Marts' cash register: the same authored-mask escape, but a machine
-- set down on a counter is a box seen from the front rather than a
-- face-on icon, so it builds as a per-pixel solid. Driven over a
-- synthetic copy of the counter's east arm, as all nine maps on the MART
-- id draw it at cell (1,5):
--
-- y=9 16 41 the work surface north of it
-- y=10 14 15 the register: keypad and receipt curl
-- y=11 30 31
-- y=12 16 41 the work surface it stands on
T.check(TileShape.figures("POKECENTER")[1].depth == nil,
"the seated man states no depth -- he stays a flat sprite card")
local regs = TileShape.figures("MART")
T.check(type(regs) == "table" and #regs == 1,
"MART carries exactly one figure")
local reg = regs[1]
T.eq(reg.w, 2, "the register is two tiles across")
T.eq(reg.h, 2, "and two tall")
T.eq(reg.n, 150, "the mask claims 150 pixels of the 256 it spans")
T.eq(reg.depth, 12, "its body is 12 voxels deep -- three quarters of the cell")
T.check(reg.thin and reg.thin.rows == 4 and reg.thin.depth == 2,
"the four rows above its drawn top edge are 2-voxel paper")
T.check(reg.flat and reg.flat.x0 == 2 and reg.flat.x1 == 8
and reg.flat.r0 == 4 and reg.flat.r1 == 11,
"and the keypad is a TOP-VIEW rect, not a face")
local MART_ROWS = { [9] = { 16, 41 }, [10] = { 14, 15 },
[11] = { 30, 31 }, [12] = { 16, 41 } }
local martS = { shapeAt = {}, tileAt = {}, figures = {}, skip = {},
ground = {}, runs = {}, objectQuads = {} }
for ty, row in pairs(MART_ROWS) do
for i, tile in ipairs(row) do
martS.tileAt[keyOf(1 + i, ty)] = tile
martS.shapeAt[keyOf(1 + i, ty)] = COUNTER
end
end
local martMap = {
tileset = { id = "MART", tilesPerRow = 16,
imageWidth = 128, imageHeight = 48 },
isWalkableCell = function() return false end,
}
Structures.buildFigures(martS, martMap, 2, 3, 9, 12)
T.eq(#martS.figures, 0, "no card was built -- it is a solid")
T.eq(#martS.objectQuads, 351,
"and it landed in the standee channel as 351 quads")
T.eq(martS.tileAt[keyOf(2, 10)], 16,
"its tiles wear the plain work surface now")
T.eq(martS.tileAt[keyOf(3, 11)], 41, "all four of them")
T.eq(martS.shapeAt[keyOf(2, 10)].class, "counter",
"and keep the counter box the machine stands on")
local rx0, rx1, ry0, ry1, rz0, rz1
for _, q in ipairs(martS.objectQuads) do
for c = 1, 4 do
local p = q[c]
rx0 = math.min(rx0 or p[1], p[1]); rx1 = math.max(rx1 or p[1], p[1])
ry0 = math.min(ry0 or p[2], p[2]); ry1 = math.max(ry1 or p[2], p[2])
rz0 = math.min(rz0 or p[3], p[3]); rz1 = math.max(rz1 or p[3], p[3])
end
end
T.eq(ry0, 8, "it stands ON the counter's 8px top plane, not the floor")
T.eq(ry1, 24, "and is its drawn 16px tall")
T.eq(rx0, 18, "west edge at the mask's column 2")
T.eq(rx1, 30, "east edge at column 13, inside its own cell (16..32)")
T.eq(rz1, 96, "its FRONT is the cell's own front edge, where it is drawn")
T.eq(rz0, 84, "and it grows north from there, 4 short of the cell's back")
-- the two thicknesses: the body at 8, the receipt curl at 2, the curl
-- centred in the body's own band rather than flush with its front
local bands = {}
for _, q in ipairs(martS.objectQuads) do
for c = 1, 4 do bands[q[c][3]] = true end
end
for _, z in ipairs({ 84, 89, 91, 96 }) do
T.check(bands[z], "the model has a face at z = " .. z)
end
local curl = {}
for _, q in ipairs(martS.objectQuads) do
local lo = math.min(q[1][2], q[2][2], q[3][2], q[4][2])
if lo >= 21 then for c = 1, 4 do curl[q[c][3]] = true end end
end
T.check(curl[89] and curl[91] and not curl[84] and not curl[96],
"clear of the arm's top only the 2-voxel paper band exists")
-- THE L. The base band (drawn rows 12-15) stands 4 above the counter and
-- the keypad lies on it as a horizontal plate, so the whole machine is
-- exactly three surfaces: a foot, an arm, and a deck in the notch.
local plate, deckTop = {}, 0
for _, q in ipairs(martS.objectQuads) do
local flatQuad = q[1][2] == q[2][2] and q[2][2] == q[3][2]
and q[3][2] == q[4][2]
if flatQuad and q[1][2] == 13 then
plate[#plate + 1] = q
elseif flatQuad and q[1][2] == 12 then
deckTop = deckTop + 1
end
end
T.eq(#plate, 83,
"the keypad lies FLAT: one top quad per masked voxel of the deck")
T.eq(deckTop, 7,
"on the base band's own top, which is 4 voxels up (drawn rows 12-15)")
local dz0, dz1
for _, q in ipairs(martS.objectQuads) do
if q[1][2] == 12 and q[3][2] == 12 then
for c = 1, 4 do
dz0 = math.min(dz0 or q[c][3], q[c][3])
dz1 = math.max(dz1 or q[c][3], q[c][3])
end
end
end
T.eq(dz0, 84, "and that deck runs the body's whole depth")
T.eq(dz1, 96, "-- plain behind the panel, covered by it in front")
local px0, px1, pz0, pz1
for _, q in ipairs(plate) do
for c = 1, 4 do
px0 = math.min(px0 or q[c][1], q[c][1]); px1 = math.max(px1 or q[c][1], q[c][1])
pz0 = math.min(pz0 or q[c][3], q[c][3]); pz1 = math.max(pz1 or q[c][3], q[c][3])
end
end
T.eq(px0, 18, "the deck spans the mask's columns 2..8")
T.eq(px1, 25, "-- the keypad panel and its own black rim")
T.eq(pz1, 96, "the deck reaches the body's front edge")
T.eq(pz0, 84, "and its back -- 8 drawn rows STRETCHED over 12 voxels")
-- the stretch is by whole voxels, centre-sampled: 8 drawn rows over 12
-- voxels of deck doubles every second one and blurs nothing
local perRow16, atlasH16 = 16, 48
local depthRow = {}
for _, q in ipairs(plate) do
local z = math.min(q[1][3], q[2][3], q[3][3], q[4][3])
depthRow[z] = math.floor(q.v * atlasH16)
end
local seen = {}
for z = 84, 95 do
T.check(depthRow[z] ~= nil, "deck voxel at z = " .. z .. " wears a texel")
seen[depthRow[z]] = (seen[depthRow[z]] or 0) + 1
end
T.eq(depthRow[95], 11, "the front voxel wears the keypad's own bottom rim")
T.eq(depthRow[84], 4, "the back one wears its top rim")
local doubled = 0
for _, n in pairs(seen) do
T.check(n == 1 or n == 2, "no drawn row spreads over more than two voxels")
if n == 2 then doubled = doubled + 1 end
end
T.eq(doubled, 4, "exactly four of the eight rows double -- 8 into 12")
-- and the arm still stands its drawn 8 rows above that deck, carrying
-- the paper: nothing in the notch reaches higher than the plate
local armTop, notchTop = 0, 0
for _, q in ipairs(martS.objectQuads) do
for c = 1, 4 do
if q[c][1] >= 25 then armTop = math.max(armTop, q[c][2])
elseif q[c][1] <= 24 then notchTop = math.max(notchTop, q[c][2]) end
end
end
T.eq(armTop, 24, "the arm and its receipt curl reach the drawn 16px")
T.eq(notchTop, 23,
"and west of it only the keys (13) and the paper overhanging them")
-- ------- prop_bg: the shades a pinned prop treats as background
--
-- The potted plants needed this: their pot's olive base is drawn flush on
+912 -91
View File
File diff suppressed because it is too large Load Diff