From d94b4dcf75a3c18becced9665e92f7a08c31aba4 Mon Sep 17 00:00:00 2001 From: DramaticShape Date: Mon, 27 Jul 2026 01:30:40 -0400 Subject: [PATCH 1/2] grow Viridian Forest real trees, stumps and signs The forest was raw detector output: ragged mixed-height tree boxes with 0px rim collars, stump rows fused into crate walls, and signs broken by the $32 water-fallback trap. Every tile of the tree drawing and the stumps now takes the per-cell voxel hull the border forest wears (a tree's four quarter-hulls tile into one lumpy canopy), signs take the signpost slab, and the white filler is flat ground. --- CHANGELOG.md | 18 ++++++++++++++++++ data/voxel_heights.lua | 25 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59541dc..074c15e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ ### Added +- **Viridian Forest grows real trees.** Nearly everything drawn in the + forest is ROUND, and the detector was boxing all of it: the big trees + came out as ragged mixed-height volumes (their sparse canopy-rim + tiles read 0px against 32px bodies, leaving gap-toothed hedge walls), + the stump rows merged into 16px crate walls wearing folded stump art, + and the trail signs were broken piles -- their $32 tile is the + water-fallback trap and recessed into a pond lip in the middle of the + woods. All of it is now profile-pinned to the treatments the rest of + the world already uses: every tile of the tree drawing (ball, rim + wisps, feet) and the stumps take the per-cell voxel HULL the overworld + border forest wears -- a tree spans 2x2 cells, so its four + quarter-hulls tile into one big lumpy canopy, and each stump becomes a + round bollard; the signs take the standing thin-slab `signpost` + treatment every town sign gets; and the white sparkle filler inside + the tree masses is flat ground instead of an invisible zero-height + box. The whole map now resolves to hulls, signs, ledges and ground -- + a detector sweep finds no stray boxed column anywhere. + - **Flowers stand up, and keep swaying.** The animated meadow tile ($03, the one tile the overworld animates by frame rewrite) now renders as a billboard one voxel deep: the drawing's darkest tones diff --git a/data/voxel_heights.lua b/data/voxel_heights.lua index a8e477e..16b3352 100644 --- a/data/voxel_heights.lua +++ b/data/voxel_heights.lua @@ -178,8 +178,33 @@ return { CAVERN = { ledge = { 5 }, }, + -- Viridian Forest. Nearly everything drawn here is ROUND, and the + -- detector was boxing all of it: the big trees came out as ragged + -- mixed-height volumes (their sparse canopy-edge tiles read 0px, + -- their bodies 32px), the stump rows merged into 16px crate walls + -- wearing folded stump art, and the trail sign was a broken pile -- + -- its $32 tile is the water-fallback trap and recessed into a pond + -- lip in the middle of the woods. FOREST = { ledge = { 46 }, + -- the big trees -- the ball ($05/$06/$15/$16/$25/$26), its rim + -- wisps ($04/$07/$17/$23/$24/$27) and its feet ($35/$36) -- and + -- the stumps ($02/$03/$12/$13): one voxel hull per 16x16 cell, + -- cut from the cell's own art, the treatment the overworld border + -- forest already wears. A tree spans 2x2 cells, so its four + -- quarter-hulls tile into one big lumpy canopy. Every tile of + -- the drawing is listed: the cell-rounding is per TILE, and a rim + -- tile left out keeps its detector reading -- which is exactly + -- the ragged 0/8/16/32px box collar the trees wore + cylinder = { 2, 3, 4, 5, 6, 7, 18, 19, 21, 22, 23, + 35, 36, 37, 38, 39, 53, 54 }, + -- the white sparkle filler inside the tree masses: flat, not an + -- invisible zero-height box + ground = { 0 }, + -- the trail signs: the standing thin-slab treatment every town + -- sign gets ($32 pinned is also what lifts it out of the water + -- trap) + signpost = { 33, 34, 49, 50 }, }, -- Oak's Lab (the tileset also serves the Fighting Dojo and Lance's From 5712f854b539dc91f6e9591955ab1d22e39cfba5 Mon Sep 17 00:00:00 2001 From: DramaticShape Date: Mon, 27 Jul 2026 01:43:08 -0400 Subject: [PATCH 2/2] one tall hull per forest tree; project stump ring faces onto round tops The trees' 2x2-cell drawings unfolded onto the ground as quarter-hull pancakes -- each tree read as cut in half. The carver is now parametric over its canvas: a `canopy` anchor tile carves the whole 2x2-cell drawing as ONE 32px hull, and stamps carry their footprint radius. The stumps' drawn tops are a cut face, not body: the `stump` class builds the hull from the bark rows and projects the ring ellipse across the round flat top, near arc south (stump_cap = the ellipse's drawn rows). --- CHANGELOG.md | 14 +++ data/voxel_heights.lua | 27 +++-- lib/ChunkMesher.lua | 14 +-- lib/Structures.lua | 232 +++++++++++++++++++++++++++++++---------- lib/TileShape.lua | 11 ++ 5 files changed, 229 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 074c15e..9fcdf78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,20 @@ box. The whole map now resolves to hulls, signs, ledges and ground -- a detector sweep finds no stray boxed column anywhere. + Two refinements over the first cut, both new hull-builder abilities. + A tree's drawing spans 2x2 CELLS, and per-cell hulls unfolded it onto + the ground -- the ball's top half sat one cell north of its bottom + half at the same elevation, reading as a tree cut in half. The new + `canopy` class pins the drawing's corner tile as a group anchor and + the whole 2x2-cell drawing carves as ONE 32px hull, so every tree is + a single tall round canopy (the carver is now parametric over its + canvas size, and hull stamps carry their footprint radius). And the + stumps' drawn tops are a CUT FACE -- an ellipse of growth rings seen + at an angle, not body: the new `stump` class builds the hull from the + bark rows alone and projects the ellipse across the round flat top, + near arc to the south (`stump_cap` names the ellipse's drawn height), + so the rings ride the round part in perspective. + - **Flowers stand up, and keep swaying.** The animated meadow tile ($03, the one tile the overworld animates by frame rewrite) now renders as a billboard one voxel deep: the drawing's darkest tones diff --git a/data/voxel_heights.lua b/data/voxel_heights.lua index 16b3352..622c56a 100644 --- a/data/voxel_heights.lua +++ b/data/voxel_heights.lua @@ -187,17 +187,24 @@ return { -- lip in the middle of the woods. FOREST = { ledge = { 46 }, - -- the big trees -- the ball ($05/$06/$15/$16/$25/$26), its rim - -- wisps ($04/$07/$17/$23/$24/$27) and its feet ($35/$36) -- and - -- the stumps ($02/$03/$12/$13): one voxel hull per 16x16 cell, - -- cut from the cell's own art, the treatment the overworld border - -- forest already wears. A tree spans 2x2 cells, so its four - -- quarter-hulls tile into one big lumpy canopy. Every tile of - -- the drawing is listed: the cell-rounding is per TILE, and a rim - -- tile left out keeps its detector reading -- which is exactly - -- the ragged 0/8/16/32px box collar the trees wore - cylinder = { 2, 3, 4, 5, 6, 7, 18, 19, 21, 22, 23, + -- the big trees: the whole 2x2-CELL drawing carves as ONE 32px + -- voxel hull, so a tree is a single tall canopy instead of four + -- ground-level quarter-pancakes ("cut in half", as the first + -- attempt read). `canopy` pins the drawing's top-left corner + -- tile ($04) as the group anchor; every other tile of the + -- drawing stays `cylinder` so the group build can verify and + -- claim its cells -- and so a stray partial drawing degrades to + -- per-cell hulls rather than boxes + canopy = { 4 }, + cylinder = { 5, 6, 7, 21, 22, 23, 35, 36, 37, 38, 39, 53, 54 }, + -- the stumps ($02/$03/$12/$13): a hull whose drawn top is a CUT + -- FACE. The body builds from the bark rows alone, and the drawn + -- ellipse of growth rings projects onto the hull's round flat + -- top -- its bottom arc to the south, the way the 2D art means + -- it. stump_cap is the ellipse's height in art rows + stump = { 2, 3, 18, 19 }, + stump_cap = 7, -- the white sparkle filler inside the tree masses: flat, not an -- invisible zero-height box ground = { 0 }, diff --git a/lib/ChunkMesher.lua b/lib/ChunkMesher.lua index 1f22c02..f17ea51 100644 --- a/lib/ChunkMesher.lua +++ b/lib/ChunkMesher.lua @@ -703,15 +703,17 @@ local function runGeometry(map, bodyOnly, masks, sink) -- round-tree stamps: the shared hull template translated per cell, -- through reusable scratch corners so expansion allocates nothing. - -- A hull spans at most its own 16px cell, so one rect test usually - -- answers for the whole stamp: strictly interior stamps keep every - -- quad, ring stamps buried under a neighbour body (or, body-only, ring - -- stamps full stop) skip without touching their quads. Only stamps - -- crossing a boundary walk quad by quad. + -- A hull spans at most its own footprint -- one 16px cell unless the + -- stamp carries a wider radius (the 2x2-cell canopy groups) -- so one + -- rect test usually answers for the whole stamp: strictly interior + -- stamps keep every quad, ring stamps buried under a neighbour body + -- (or, body-only, ring stamps full stop) skip without touching their + -- quads. Only stamps crossing a boundary walk quad by quad. local sc = { { 0, 0, 0 }, { 0, 0, 0 }, { 0, 0, 0 }, { 0, 0, 0 } } for _, st in ipairs(S.roundStamps or {}) do local mx, mz = st.mx, st.mz - local sx0, sz0, sx1, sz1 = mx - 8, mz - 8, mx + 8, mz + 8 + local sr = st.r or 8 + local sx0, sz0, sx1, sz1 = mx - sr, mz - sr, mx + sr, mz + sr local interior = sx0 > 0 and sx1 < bw and sz0 > 0 and sz1 < bh local overBody = sx1 > 0 and sx0 < bw and sz1 > 0 and sz0 < bh local keepAll, skipAll diff --git a/lib/Structures.lua b/lib/Structures.lua index 858f636..bd62a2d 100644 --- a/lib/Structures.lua +++ b/lib/Structures.lua @@ -508,15 +508,17 @@ end local ROUND_SHADE = { front = 1.0, back = 0.68, side = 0.78, top = 1.0, bottom = 0.55 } -local function roundTemplate(S, map, data, cx, cy, groundTiles) +local function roundTemplate(S, map, data, cx, cy, groundTiles, N, capRows) + N = N or 16 -- art canvas: 16 = one cell, 32 = 2x2 cells + local N2 = N / 2 local perRow = map.tileset.tilesPerRow or 16 local atlasW = map.tileset.imageWidth or 128 local atlasH = map.tileset.imageHeight or 48 - -- cell-space art access (16x16, row 0 = top) + -- cell-space art access (NxN, row 0 = top), anchored at cell (cx, cy) local function tileOf(px, py) - return S.tileAt[keyOf(cx * 2 + (px >= 8 and 1 or 0), - cy * 2 + (py >= 8 and 1 or 0))] + return S.tileAt[keyOf(cx * 2 + math.floor(px / 8), + cy * 2 + math.floor(py / 8))] end local function texel(px, py) local tile = tileOf(px, py) @@ -524,18 +526,18 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) math.floor(tile / perRow) * 8 + py % 8 end - -- shade class of every cell pixel, indexed py * 16 + px + -- shade class of every canvas pixel, indexed py * N + px local cls = {} - for py = 0, 15 do - for px = 0, 15 do + for py = 0, N - 1 do + for px = 0, N - 1 do local ax, ay = texel(px, py) local r, g, b, a = data:getPixel(ax, ay) - cls[py * 16 + px] = a == 0 and "off" - or Structures.shadeClass(math.min(r, g, b)) + cls[py * N + px] = a == 0 and "off" + or Structures.shadeClass(math.min(r, g, b)) end end - -- 4-connected flood from the cell border through `passable` classes + -- 4-connected flood from the canvas border through `passable` classes local function floodOutside(passable) local out, stack = {}, {} local function seed(i) @@ -544,16 +546,16 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) stack[#stack + 1] = i end end - for i = 0, 15 do - seed(i); seed(240 + i); seed(i * 16); seed(i * 16 + 15) + for i = 0, N - 1 do + seed(i); seed(N * (N - 1) + i); seed(i * N); seed(i * N + N - 1) end while #stack > 0 do local i = table.remove(stack) - local px = i % 16 + local px = i % N if px > 0 then seed(i - 1) end - if px < 15 then seed(i + 1) end - if i >= 16 then seed(i - 16) end - if i < 240 then seed(i + 16) end + if px < N - 1 then seed(i + 1) end + if i >= N then seed(i - N) end + if i < N * (N - 1) then seed(i + N) end end return out end @@ -562,23 +564,48 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) local out = floodOutside({ off = true, dark = true, light = true, white = true }) local mask, enclosed = {}, 0 - for i = 0, 255 do + for i = 0, N * N - 1 do if not out[i] then mask[i] = true if cls[i] ~= "black" then enclosed = enclosed + 1 end end end - if enclosed < 32 then + if enclosed < N * N / 8 then out = floodOutside({ off = true, light = true, white = true }) mask = {} - for i = 0, 255 do + for i = 0, N * N - 1 do if not out[i] and cls[i] ~= "off" then mask[i] = true end end end local any = nil - for i = 0, 255 do any = any or mask[i] end + for i = 0, N * N - 1 do any = any or mask[i] end if not any then return {} end + -- a CAPPED hull (the stump): the top capRows rows of the mask are the + -- drawn cut face -- a surface seen at an angle, not body. Strip them + -- from the mask and remember their art span; the top-face quads below + -- project that ellipse across the round cap. + local capY0, capY1 = nil, nil + if capRows and capRows > 0 then + local top = nil + for iy = 0, N - 1 do + for ix = 0, N - 1 do + if mask[iy * N + ix] then top = iy break end + end + if top then break end + end + if top then + capY0 = top + capY1 = math.min(top + capRows - 1, N - 2) + for iy = capY0, capY1 do + for ix = 0, N - 1 do mask[iy * N + ix] = nil end + end + any = nil + for i = 0, N * N - 1 do any = any or mask[i] end + if not any then return {} end + end + end + -- the ground the ball stands on: the drawing's own background names -- it. Score every flat ground tile the map places against the cell's -- unmasked light pixels and keep the closest -- mid-forest trees have @@ -593,9 +620,9 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) local ox = (t % perRow) * 8 local oy = math.floor(t / perRow) * 8 local score, n = 0, 0 - for py = 0, 15 do - for px = 0, 15 do - local i = py * 16 + px + for py = 0, N - 1 do + for px = 0, N - 1 do + local i = py * N + px local c = cls[i] if not mask[i] and (c == "light" or c == "white") then local ax, ay = texel(px, py) @@ -618,10 +645,10 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) local z0, z1, src = {}, {}, {} local loRow, hiRow = {}, {} local yBot = nil - for iy = 0, 15 do + for iy = 0, N - 1 do local lo, hi = nil, nil - for ix = 0, 15 do - if mask[iy * 16 + ix] then + for ix = 0, N - 1 do + if mask[iy * N + ix] then lo = lo or ix hi = ix end @@ -632,7 +659,7 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) local c = (lo + hi + 1) / 2 local hw = (hi - lo + 1) / 2 for ix = lo, hi do - local i = iy * 16 + ix + local i = iy * N + ix if mask[i] then local dx = ix + 0.5 - c local n = 1 @@ -640,7 +667,7 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) n = math.max(1, math.floor(2 * math.sqrt(hw * hw - dx * dx) + 0.5)) end - z0[i] = math.floor(8 - n / 2 + 0.5) + z0[i] = math.floor(N2 - n / 2 + 0.5) z1[i] = z0[i] + n src[i] = iy end @@ -650,28 +677,46 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) -- foot: rows under the mask repeat the bottom row's discs, wearing the -- bottom row's (outline-dark) pixels - for iy = yBot + 1, 15 do + for iy = yBot + 1, N - 1 do loRow[iy], hiRow[iy] = loRow[yBot], hiRow[yBot] for ix = loRow[yBot], hiRow[yBot] do - local b = yBot * 16 + ix + local b = yBot * N + ix if z0[b] then - local i = iy * 16 + ix + local i = iy * N + ix z0[i], z1[i], src[i] = z0[b], z1[b], yBot end end end + -- the round cap's top row and z extent, for the stump's ring + -- projection below + local capTopRow, capZ0, capZ1 = nil, nil, nil + if capY0 then + for iy = 0, N - 1 do + if loRow[iy] then capTopRow = iy break end + end + if capTopRow then + for ix = loRow[capTopRow], hiRow[capTopRow] do + local i = capTopRow * N + ix + if z0[i] then + capZ0 = math.min(capZ0 or z0[i], z0[i]) + capZ1 = math.max(capZ1 or z1[i], z1[i]) + end + end + end + end + local function solidAt(ix, iy, iz) - if ix < 0 or ix > 15 or iy < 0 or iy > 15 then return false end - local i = iy * 16 + ix + if ix < 0 or ix > N - 1 or iy < 0 or iy > N - 1 then return false end + local i = iy * N + ix return z0[i] ~= nil and iz >= z0[i] and iz < z1[i] end -- cap interiors sample the canopy a couple of rows below the rim, -- skipping outline-dark pixels local function deepTexel(ix, iy) - for iy2 = iy + 2, math.min(15, iy + 4) do - local i = iy2 * 16 + ix + for iy2 = iy + 2, math.min(N - 1, iy + 4) do + local i = iy2 * N + ix if mask[i] and cls[i] ~= "black" then return texel(ix, iy2) end end return texel(ix, iy) @@ -683,12 +728,12 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) -- ball paints solid black the moment the camera turns. The foot rows -- stay dark on purpose: their whole source row is outline-black. local function sideTexel(ix, iy) - local r = src[iy * 16 + ix] + local r = src[iy * N + ix] local dir = ix + ix < loRow[iy] + hiRow[iy] and 1 or -1 for step = 0, 3 do local x2 = ix + dir * step - local i2 = r * 16 + x2 - if x2 < 0 or x2 > 15 or not mask[i2] then break end + local i2 = r * N + x2 + if x2 < 0 or x2 > N - 1 or not mask[i2] then break end if cls[i2] ~= "black" then return texel(x2, r) end end return texel(ix, r) @@ -696,20 +741,20 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) local quads = {} - for iy = 0, 15 do + for iy = 0, N - 1 do if loRow[iy] then - local yB, yT = 15 - iy, 16 - iy + local yB, yT = N - 1 - iy, N - iy -- front and back: the drawing per-pixel, columns merged where they -- share a chord plane; a run never crosses the 8px atlas tile seam -- (its u range must interpolate inside one tile) local ix = loRow[iy] while ix <= hiRow[iy] do - local i = iy * 16 + ix + local i = iy * N + ix if z0[i] then local ix2 = ix while ix2 + 1 <= hiRow[iy] do - local j = iy * 16 + ix2 + 1 + local j = iy * N + ix2 + 1 if z0[j] == z0[i] and z1[j] == z1[i] and math.floor((ix2 + 1) / 8) == math.floor(ix / 8) then ix2 = ix2 + 1 @@ -721,8 +766,8 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) local ax1 = (texel(ix2, src[i])) local u0, u1 = (ax0 + 0.05) / atlasW, (ax1 + 0.95) / atlasW local v0, v1 = (ay + 0.05) / atlasH, (ay + 0.95) / atlasH - local x0, x1 = ix - 8, ix2 - 7 - local zF, zB = z1[i] - 8, z0[i] - 8 + local x0, x1 = ix - N2, ix2 - N2 + 1 + local zF, zB = z1[i] - N2, z0[i] - N2 quads[#quads + 1] = { { x0, yB, zF }, { x1, yB, zF }, { x1, yT, zF }, { x0, yT, zF }, uv = { { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } }, @@ -742,11 +787,11 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) -- sides, steps, undersides: constant-texel quads over the z runs a -- neighbour doesn't cover for ix = loRow[iy], hiRow[iy] do - local i = iy * 16 + ix + local i = iy * N + ix if z0[i] then local ax, ay = texel(ix, src[i]) local u, v = (ax + 0.5) / atlasW, (ay + 0.5) / atlasH - local x0, x1 = ix - 8, ix - 7 + local x0, x1 = ix - N2, ix - N2 + 1 -- exposed z pieces against one neighbouring column local function pieces(nx, ny, emit) @@ -757,7 +802,7 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) while iz2 + 1 < z1[i] and not solidAt(nx, ny, iz2 + 1) do iz2 = iz2 + 1 end - emit(iz - 8, iz2 - 7, iz, iz2) + emit(iz - N2, iz2 - N2 + 1, iz, iz2) iz = iz2 + 1 else iz = iz + 1 @@ -786,7 +831,20 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) u = tu, v = tv, shade = ROUND_SHADE.top, } end - if izA == z0[i] and izB == z1[i] - 1 and izB - izA >= 2 then + if capTopRow and iy == capTopRow and capZ1 then + -- the CUT FACE (a capped hull's top): project the drawn + -- ellipse across the round cap voxel row by voxel row -- + -- its top arc at the cap's north rim, its bottom arc at + -- the south, the perspective the 2D art already implies + for iz = izA, izB do + local t = capZ1 - 1 > capZ0 + and (iz - capZ0) / (capZ1 - 1 - capZ0) or 0 + local ry = capY0 + math.floor(t * (capY1 - capY0) + 0.5) + local cax, cay = texel(ix, ry) + top(iz - N2, iz - N2 + 1, + (cax + 0.5) / atlasW, (cay + 0.5) / atlasH) + end + elseif izA == z0[i] and izB == z1[i] - 1 and izB - izA >= 2 then -- the dome cap: outline on the rim cells, canopy inside local du, dv = deepTexel(ix, iy) top(zA, zA + 1, u, v) @@ -796,7 +854,7 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) top(zA, zB, u, v) end end) - if iy < 15 then + if iy < N - 1 then pieces(ix, iy + 1, function(zA, zB) quads[#quads + 1] = { { x0, yB, zB }, { x1, yB, zB }, { x1, yB, zA }, { x0, yB, zA }, @@ -844,23 +902,91 @@ function Structures.buildCylinders(S, map, x0, x1, y0, y1, groundTiles) end local tsid = tostring(map.tileset.id or map.tileset.image or "?") + -- the stump class's drawn-ellipse height, hand-authored per tileset + -- (the profile's stump_cap, in art rows) + local stumpCap = 6 + do + local okP, prof = pcall(V.data, "voxel_heights") + local entry = okP and type(prof) == "table" and prof.tilesets + and prof.tilesets[map.tileset.id] + if entry and type(entry.stump_cap) == "number" then + stumpCap = entry.stump_cap + end + end + + -- cells consumed by a 2x2 `canopy` group; the scan runs north to + -- south, west to east, so an anchor always claims its partners + -- before they are visited + local grouped = {} for cy = math.floor(y0 / 2), math.floor(y1 / 2) do for cx = math.floor(x0 / 2), math.floor(x1 / 2) do Budget.tick() + local ckey = cy * 8192 + cx local k = keyOf(cx * 2, cy * 2) - local s = S.shapeAt[k] + local s = (not grouped[ckey]) and S.shapeAt[k] or nil local near = cx * 2 >= -ROUND_RING and cx * 2 < tw + ROUND_RING and cy * 2 >= -ROUND_RING and cy * 2 < th + ROUND_RING - if s and s.art == "cylinder" and near then + if s and s.art == "canopy" and near then + -- ONE 32px hull over the 2x2-cell drawing. The partner cells + -- must be round-pinned too, or the drawing is partial (a map + -- edit, a mod's stray anchor tile) and the anchor is left + -- alone rather than carved into a half-empty giant. + local whole = true + for _, d in ipairs({ { 1, 0 }, { 0, 1 }, { 1, 1 } }) do + local ps = S.shapeAt[keyOf((cx + d[1]) * 2, (cy + d[2]) * 2)] + if not (ps and (ps.art == "cylinder" or ps.art == "canopy")) then + whole = false + end + end + if whole then + local ground = false + if data then + local ids = {} + for dy = 0, 3 do + for dx = 0, 3 do + ids[#ids + 1] = S.tileAt[keyOf(cx * 2 + dx, cy * 2 + dy)] + end + end + local sig = tsid .. "|g32|" .. gsig .. "|" + .. table.concat(ids, ":") + local tpl = roundCache[sig] + if not tpl then + local tq, tbg = roundTemplate(S, map, data, cx, cy, + groundTiles, 32) + tpl = { quads = tq, bg = tbg } + roundCache[sig] = tpl + end + ground = tpl.bg or false + S.roundStamps[#S.roundStamps + 1] = + { quads = tpl.quads, mx = cx * 16 + 16, mz = cy * 16 + 16, + r = 16 } + end + for dy = 0, 3 do + for dx = 0, 3 do + local tk = keyOf(cx * 2 + dx, cy * 2 + dy) + S.skip[tk] = true + S.ground[tk] = ground + end + end + grouped[ckey + 1] = true + grouped[ckey + 8192] = true + grouped[ckey + 8193] = true + end + elseif s and s.art == "cylinder" and near then + -- a `stump`-class cell is the same hull with a cut face: its + -- top capRows of drawing project onto the round top + local cap = s.class == "stump" and stumpCap or nil local ground = false if data then - local sig = tsid .. "|" .. gsig .. "|" .. table.concat({ + local sig = tsid .. (cap and ("|c" .. cap) or "") .. "|" + .. gsig .. "|" .. table.concat({ S.tileAt[k], S.tileAt[keyOf(cx * 2 + 1, cy * 2)], S.tileAt[keyOf(cx * 2, cy * 2 + 1)], S.tileAt[keyOf(cx * 2 + 1, cy * 2 + 1)] }, ":") local tpl = roundCache[sig] if not tpl then - local tq, tbg = roundTemplate(S, map, data, cx, cy, groundTiles) + local tq, tbg = roundTemplate(S, map, data, cx, cy, + groundTiles, 16, cap) tpl = { quads = tq, bg = tbg } roundCache[sig] = tpl end diff --git a/lib/TileShape.lua b/lib/TileShape.lua index 5384105..c9b15fa 100644 --- a/lib/TileShape.lua +++ b/lib/TileShape.lua @@ -56,6 +56,15 @@ local FALLBACK_HEIGHTS = { tree = 16, roof = 28, cylinder = 16, + -- big round scenery: a 2x2-CELL drawing carved as ONE 32px voxel hull + -- (Viridian Forest's trees). The class pins only the drawing's + -- top-left corner tile; the other cells stay `cylinder` and are + -- claimed by the group build (see Structures.buildCylinders) + canopy = 32, + -- a cylinder hull whose drawn top is a CUT FACE (tree stumps): the + -- body builds from the bark rows and the drawn ellipse projects onto + -- the hull's round top + stump = 16, billboard = 16, signpost = 16, post = 16, @@ -104,6 +113,8 @@ local ART = { fence = "upright", sign = "upright", cylinder = "cylinder", + canopy = "canopy", + stump = "cylinder", 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