mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 08:21:06 +02:00
1.0.4: flower/bush/statue/trash-can billboards, in-place mesh refresh
Flowers stand as 1px animated cutouts on their own pulled mesh (own-cell flowers hide behind the walker, the cell south still overdraws feet). Cuttable bushes, gym statues (single-block plinth) and Lt. Surge's trash cans stand as 5-voxel prop cutouts; prop_ground names the tile painted beneath. Block edits refresh meshes in place instead of blinking to 2D.
This commit is contained in:
+104
@@ -1,5 +1,109 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 1.0.4
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **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
|
||||||
|
plus everything they enclose are cut out per pixel, and the ground
|
||||||
|
beneath is synthesized from the commonest flat neighbour, exactly
|
||||||
|
like the ground under a detected prop.
|
||||||
|
|
||||||
|
The interesting part is that the cutout still animates. A mesh is
|
||||||
|
static, so the geometry spans the UNION of the mask over the base art
|
||||||
|
and all three animation frames, and the animation lives entirely in
|
||||||
|
the texture: TerrainAtlas already rewrites the flower's slot in the
|
||||||
|
private animated atlas each step, and for this tile it now writes
|
||||||
|
only the current frame's mask opaque with everything else keyed to
|
||||||
|
alpha 0 -- which the voxel shader discards, and the shadow pass with
|
||||||
|
it. The standing silhouette trims itself frame by frame in texture
|
||||||
|
space, off the same engine clock as the flat path, without a vertex
|
||||||
|
moving. The class is derived, not authored: any frames-animated tile
|
||||||
|
resolves to the new `flower` class with no profile entry, the same
|
||||||
|
way tall grass derives from `grassTile` (hand-authoring still wins).
|
||||||
|
|
||||||
|
- **The cuttable bush is a standing cutout.** The four tiles Cut
|
||||||
|
deletes ($2D/$2E/$3D/$3E -- across the whole tileset they appear only
|
||||||
|
in the five cut-tree blocks) are pinned to the thin `prop` pool: a
|
||||||
|
per-pixel standee 5 voxels deep, black-outline segmented with its
|
||||||
|
enclosed pixels kept, the drawn grass dither flooding away. It
|
||||||
|
stands on plain grass ($2C) -- the very tile Cut leaves behind per
|
||||||
|
field.cutTreeSwaps -- via the profile's new `prop_ground` key, which
|
||||||
|
names the tile painted under a pinned prop instead of whatever flat
|
||||||
|
tile its neighbours vote in.
|
||||||
|
|
||||||
|
- **Gym statues: a solid plinth, a standing bird.** The statue pair
|
||||||
|
flanking every badge gym's aisle (and Bruno's room) is one cell of
|
||||||
|
figure over one cell of plinth. The plinth ($22/$23/$32/$33) is
|
||||||
|
pinned `wall`: a solid 16px block. The figure ($02/$38/$12/$13) is
|
||||||
|
pinned `prop`: a 5-voxel cutout that stands ON the plinth through the
|
||||||
|
authored-box support rule, its checkered background flooded away and
|
||||||
|
the pixels its outline encloses kept.
|
||||||
|
|
||||||
|
The whole statue keeps ONE cell of footprint. The support rule used
|
||||||
|
to extend the box under the claimed cell (the monitor-on-desk path),
|
||||||
|
which marched the plinth a second block backwards; a figure whose
|
||||||
|
support is a FULL-HEIGHT block now collapses instead -- the drawn
|
||||||
|
figure cell becomes synthesized floor, since the block below already
|
||||||
|
carries the whole base. Furniture supports keep the extension: their
|
||||||
|
drawn cell is the furniture's own upper rows, and floor there would
|
||||||
|
amputate the desk. The round boulder drawn beside some statues is
|
||||||
|
deliberately NOT pinned -- it also tiles wall-to-wall as Pewter's
|
||||||
|
rock rows, which are scenery for the detector.
|
||||||
|
|
||||||
|
- **Lt. Surge's trash cans stand up.** The can ($0B/$0C/$1B/$1C, the
|
||||||
|
lone graphic of blocks 38/39) takes the same treatment as the
|
||||||
|
cuttable bush: a 5-voxel `prop` cutout, black-outline segmented with
|
||||||
|
its enclosed pixels kept, standing on the gyms' main floor tile
|
||||||
|
($11) via `prop_ground`.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Cut trees now vanish in voxel mode -- and grow back.** The
|
||||||
|
engine's Cut path swapped the block with a raw `setBlock` + renderer
|
||||||
|
rebuild, never emitting `world.block_replaced` -- so this mod's
|
||||||
|
listener (which rebuilds the map's mesh exactly for this) never
|
||||||
|
heard about it, and the diorama kept showing the tree. The engine
|
||||||
|
now routes Cut through `replaceBlock`
|
||||||
|
(src/world/OverworldController.lua), whose whole purpose -- per its
|
||||||
|
own comment, "Victory Road barriers, Cut trees" -- is that same swap
|
||||||
|
plus the event. The regrowth path had the same hole one door away:
|
||||||
|
cut trees are restored block by block when the map is re-entered,
|
||||||
|
and the card-key doors are stamped closed on floor load, both
|
||||||
|
through the same silent `setBlock` -- with the mesh cache staying
|
||||||
|
warm across a round trip (that is what prevLive is for), the world
|
||||||
|
kept showing the stump you left. Both paths now announce each block.
|
||||||
|
|
||||||
|
- **A block edit no longer blinks the world down to 2D.** The
|
||||||
|
listener used to drop the edited map's mesh outright, and mesh
|
||||||
|
builds are asynchronous -- so cutting a tree (or stepping out of a
|
||||||
|
door onto a map whose trees just regrew) flashed the flat 2D world
|
||||||
|
for the frames the rebuild took. `ChunkMesher.refresh` rebuilds in
|
||||||
|
place instead: the stale mesh keeps drawing, the replacement cooks
|
||||||
|
in the background, and each slot swaps as its build lands -- the
|
||||||
|
tree pops out (or back in) with the scene never leaving 3D.
|
||||||
|
|
||||||
|
- **Flowers cull the player correctly from every angle.** The flower
|
||||||
|
billboards were baked into the terrain mesh, which draws without
|
||||||
|
the characters' camera-ward pull -- so a walker standing among
|
||||||
|
flowers won the depth test against ALL of them, including the
|
||||||
|
flower south of their feet that should overdraw them. The flower
|
||||||
|
quads now ride their own mesh, drawn after the characters with
|
||||||
|
exactly the characters' pull (the tall-grass trick): the flower in
|
||||||
|
front of a walker occludes their feet, the one behind them hides,
|
||||||
|
at every camera angle. Unlike grass the flower mesh still casts
|
||||||
|
shadows -- it is a handful of cutouts per meadow, not thousands of
|
||||||
|
tufts.
|
||||||
|
|
||||||
|
- The `voxel_anim_probe` driver crashed on engine builds without the
|
||||||
|
optional `TileRenderer.animFrame` seam, and again on tilesets whose
|
||||||
|
animation list carries a "toggle" entry (spinner rooms), which claims
|
||||||
|
a tile LIST rather than one slot. It now reads the clock through the
|
||||||
|
mod's own fallback chain and skips toggle entries in the placement
|
||||||
|
census.
|
||||||
|
|
||||||
## 1.0.3
|
## 1.0.3
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -65,6 +65,11 @@
|
|||||||
-- monitor on its desk). prop is a second pool
|
-- monitor on its desk). prop is a second pool
|
||||||
-- so two touching cutouts stay separate
|
-- so two touching cutouts stay separate
|
||||||
--
|
--
|
||||||
|
-- A tileset entry may also carry `prop_ground` (not a class): a map of
|
||||||
|
-- prop tile id -> ground tile id naming the tile painted under that
|
||||||
|
-- pinned prop, overriding the flat-neighbour vote -- the cuttable bush
|
||||||
|
-- stands on the grass Cut leaves behind, whatever borders it.
|
||||||
|
--
|
||||||
-- Whole BUILDINGS are not tile pins -- one drawing packs a roof seen from
|
-- Whole BUILDINGS are not tile pins -- one drawing packs a roof seen from
|
||||||
-- above, a facade seen face-on and sloped ends as diagonal silhouettes,
|
-- above, a facade seen face-on and sloped ends as diagonal silhouettes,
|
||||||
-- and no single class covers that. They live in the `buildings` list at
|
-- and no single class covers that. They live in the `buildings` list at
|
||||||
@@ -124,6 +129,40 @@ return {
|
|||||||
-- fence-textured tower. `post` extracts each cell alone, so these
|
-- fence-textured tower. `post` extracts each cell alone, so these
|
||||||
-- render as the same thin posts, marching north
|
-- render as the same thin posts, marching north
|
||||||
post = { 14, 85 },
|
post = { 14, 85 },
|
||||||
|
-- the cuttable bush ($2D/$2E/$3D/$3E, the four tiles Cut deletes
|
||||||
|
-- -- across the whole tileset they appear only in the five
|
||||||
|
-- cut-tree blocks): a standing per-pixel cutout 5 voxels deep,
|
||||||
|
-- black-outline segmented with the pixels the outline encloses
|
||||||
|
-- kept, its drawn grass dither flooding away as background
|
||||||
|
prop = { 45, 46, 61, 62 },
|
||||||
|
-- the ground painted under those pinned props, by the prop tile's
|
||||||
|
-- own id: the bush stands on plain grass ($2C) -- the very tile
|
||||||
|
-- Cut leaves behind (field.cutTreeSwaps' after-blocks) -- rather
|
||||||
|
-- than whatever flat tile its neighbours vote
|
||||||
|
prop_ground = { [45] = 44, [46] = 44, [61] = 44, [62] = 44 },
|
||||||
|
},
|
||||||
|
|
||||||
|
-- The badge gyms and Bruno's room (one tileset): the bird statues
|
||||||
|
-- that flank every gym's aisles, and Lt. Surge's trash cans. A
|
||||||
|
-- statue is one cell of figure ($02/$38/$12/$13) drawn over one
|
||||||
|
-- cell of plinth ($22/$23/$32/$33); the trash can is one cell
|
||||||
|
-- ($0B/$0C/$1B/$1C, blocks 38/39 only). Across the whole blockset
|
||||||
|
-- none of these tiles appears anywhere else. The plinth stays a
|
||||||
|
-- SOLID 16px block; the figure stands ON it as a per-pixel cutout
|
||||||
|
-- 5 voxels deep (the thin `prop` pool), collapsing to the plinth's
|
||||||
|
-- single cell of footprint (Structures' wall-support rule); the
|
||||||
|
-- can is the same 5-voxel cutout at floor level. Both are
|
||||||
|
-- black-outline segmented -- backgrounds flood away, the pixels
|
||||||
|
-- the outline encloses stay. Everything stands on the gyms' main
|
||||||
|
-- floor tile ($11), which is also Bruno's floor.
|
||||||
|
-- (The round boulder drawn beside some statues, $07/$08/$17/$18,
|
||||||
|
-- is NOT pinned: it also tiles wall-to-wall as Pewter's rock rows,
|
||||||
|
-- which are scenery for the detector, not a lone prop.)
|
||||||
|
GYM = {
|
||||||
|
wall = { 34, 35, 50, 51 },
|
||||||
|
prop = { 2, 56, 18, 19, 11, 12, 27, 28 },
|
||||||
|
prop_ground = { [2] = 17, [56] = 17, [18] = 17, [19] = 17,
|
||||||
|
[11] = 17, [12] = 17, [27] = 17, [28] = 17 },
|
||||||
},
|
},
|
||||||
|
|
||||||
-- ledge pairs from data.field.tilePairs
|
-- ledge pairs from data.field.tilePairs
|
||||||
|
|||||||
+108
-21
@@ -704,15 +704,10 @@ function ChunkMesher.build(map, bodyOnly, masks)
|
|||||||
return sink.finish()
|
return sink.finish()
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The tall-grass rows as their own mesh: VoxelScene draws it AFTER the
|
local function quadsMesh(quads)
|
||||||
-- characters so the southern row of a grass cell still overdraws a
|
if #quads == 0 then return nil end
|
||||||
-- walker's feet (characters stamp over terrain, Gen 1 style, so ordinary
|
|
||||||
-- terrain could never do this).
|
|
||||||
local function buildGrassMesh(map)
|
|
||||||
local S = Structures.forMap(map)
|
|
||||||
if #S.grassQuads == 0 then return nil end
|
|
||||||
local verts, indices, n = {}, {}, 0
|
local verts, indices, n = {}, {}, 0
|
||||||
for _, q in ipairs(S.grassQuads) do
|
for _, q in ipairs(quads) do
|
||||||
for i = 1, 4 do
|
for i = 1, 4 do
|
||||||
local c = q[i]
|
local c = q[i]
|
||||||
local uv = q.uv and q.uv[i] or { q.u, q.v }
|
local uv = q.uv and q.uv[i] or { q.u, q.v }
|
||||||
@@ -724,6 +719,33 @@ local function buildGrassMesh(map)
|
|||||||
return Voxel3D.newMesh(verts, indices)
|
return Voxel3D.newMesh(verts, indices)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- The tall-grass rows as their own mesh: VoxelScene draws it AFTER the
|
||||||
|
-- characters so the southern row of a grass cell still overdraws a
|
||||||
|
-- walker's feet (characters stamp over terrain, Gen 1 style, so ordinary
|
||||||
|
-- terrain could never do this).
|
||||||
|
local function buildGrassMesh(map)
|
||||||
|
return quadsMesh(Structures.forMap(map).grassQuads)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The flower billboards as their own mesh, for the same reason as the
|
||||||
|
-- grass one: it draws AFTER the characters WITH the same camera-ward
|
||||||
|
-- pull, so a flower south of a walker occludes their feet and one north
|
||||||
|
-- of them hides behind them. Baked into the terrain mesh they lost that
|
||||||
|
-- depth fight against the pulled character card whenever the player
|
||||||
|
-- stood among flowers. Unlike grass this mesh still CASTS shadows (the
|
||||||
|
-- sun pass draws it): a handful of flowers per meadow, not thousands of
|
||||||
|
-- tufts.
|
||||||
|
local function buildFlowerMesh(map)
|
||||||
|
return quadsMesh(Structures.forMap(map).flowerQuads)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Replace a cached slot, releasing whatever mesh it held.
|
||||||
|
local function swapSlot(c, slot, mesh)
|
||||||
|
local old = c[slot]
|
||||||
|
if old and old ~= mesh and old.release then pcall(old.release, old) end
|
||||||
|
c[slot] = mesh
|
||||||
|
end
|
||||||
|
|
||||||
-- ------------------------------------------------------------- the cache
|
-- ------------------------------------------------------------- the cache
|
||||||
|
|
||||||
local function entry(id)
|
local function entry(id)
|
||||||
@@ -736,11 +758,12 @@ local function entry(id)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function releaseEntry(c)
|
local function releaseEntry(c)
|
||||||
for _, slot in ipairs({ "full", "body", "grass" }) do
|
for _, slot in ipairs({ "full", "body", "grass", "flowers" }) do
|
||||||
local mesh = c[slot]
|
local mesh = c[slot]
|
||||||
if mesh and mesh.release then pcall(mesh.release, mesh) end
|
if mesh and mesh.release then pcall(mesh.release, mesh) end
|
||||||
c[slot] = nil
|
c[slot] = nil
|
||||||
end
|
end
|
||||||
|
c.stale = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ---------------------------------------------------------- async builds
|
-- ---------------------------------------------------------- async builds
|
||||||
@@ -778,26 +801,48 @@ end
|
|||||||
local function runJob(job)
|
local function runJob(job)
|
||||||
local map = job.map
|
local map = job.map
|
||||||
local c = entry(job.id)
|
local c = entry(job.id)
|
||||||
if c.grass == nil then
|
if c.grass == nil or c.flowers == nil or (c.stale and c.stale.aux) then
|
||||||
local okG, grass = pcall(buildGrassMesh, map)
|
local okG, grass = pcall(buildGrassMesh, map)
|
||||||
if (gen[job.id] or 0) ~= job.gen then return end
|
local okF, flowers = pcall(buildFlowerMesh, map)
|
||||||
c.grass = (okG and grass) or false
|
if (gen[job.id] or 0) ~= job.gen then
|
||||||
|
if okG and grass and grass.release then pcall(grass.release, grass) end
|
||||||
|
if okF and flowers and flowers.release then
|
||||||
|
pcall(flowers.release, flowers)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
swapSlot(c, "grass", (okG and grass) or false)
|
||||||
|
swapSlot(c, "flowers", (okF and flowers) or false)
|
||||||
|
if c.stale then c.stale.aux = nil end
|
||||||
end
|
end
|
||||||
local sink = newSink()
|
local sink = newSink()
|
||||||
runGeometry(map, job.slot == "body", job.masks, sink)
|
runGeometry(map, job.slot == "body", job.masks, sink)
|
||||||
local mesh = sink.finish()
|
local mesh = sink.finish()
|
||||||
if (gen[job.id] or 0) ~= job.gen then return end
|
if (gen[job.id] or 0) ~= job.gen then
|
||||||
c[job.slot] = mesh or false
|
if mesh and mesh.release then pcall(mesh.release, mesh) end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
swapSlot(c, job.slot, mesh or false)
|
||||||
|
if c.stale then
|
||||||
|
c.stale[job.slot] = nil
|
||||||
|
if not (c.stale.full or c.stale.body or c.stale.aux) then
|
||||||
|
c.stale = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Queue a build unless the slot is already cached or queued. Returns the
|
-- Queue a build unless the slot is already cached or queued. Returns the
|
||||||
-- cached mesh when there is one (false-cached misses return nil).
|
-- cached mesh when there is one (false-cached misses return nil).
|
||||||
-- `urgent` marks the current map's meshes: pump() gives those a bigger
|
-- `urgent` marks the current map's meshes: pump() gives those a bigger
|
||||||
-- slice and runs them before neighbour jobs.
|
-- slice and runs them before neighbour jobs. A slot refresh() marked
|
||||||
|
-- stale queues its rebuild AND keeps handing back the old mesh, so a
|
||||||
|
-- one-block edit never drops the scene to the flat 2D path while the
|
||||||
|
-- replacement cooks.
|
||||||
function ChunkMesher.request(map, bodyOnly, masks, urgent)
|
function ChunkMesher.request(map, bodyOnly, masks, urgent)
|
||||||
local slot = bodyOnly and "body" or "full"
|
local slot = bodyOnly and "body" or "full"
|
||||||
local c = cache[map.id]
|
local c = cache[map.id]
|
||||||
if c and c[slot] ~= nil then return c[slot] or nil end
|
local stale = c and c.stale and (c.stale[slot] or c.stale.aux)
|
||||||
|
if c and c[slot] ~= nil and not stale then return c[slot] or nil end
|
||||||
local key = jobKey(map.id, slot)
|
local key = jobKey(map.id, slot)
|
||||||
local job = jobIndex[key]
|
local job = jobIndex[key]
|
||||||
if not job then
|
if not job then
|
||||||
@@ -808,7 +853,7 @@ function ChunkMesher.request(map, bodyOnly, masks, urgent)
|
|||||||
elseif urgent then
|
elseif urgent then
|
||||||
job.urgent = true
|
job.urgent = true
|
||||||
end
|
end
|
||||||
return nil
|
return (c and c[slot]) or nil
|
||||||
end
|
end
|
||||||
|
|
||||||
function ChunkMesher.pending()
|
function ChunkMesher.pending()
|
||||||
@@ -871,17 +916,26 @@ end
|
|||||||
function ChunkMesher.get(map, bodyOnly, masks)
|
function ChunkMesher.get(map, bodyOnly, masks)
|
||||||
local slot = bodyOnly and "body" or "full"
|
local slot = bodyOnly and "body" or "full"
|
||||||
local c = entry(map.id)
|
local c = entry(map.id)
|
||||||
if c.grass == nil then
|
if c.grass == nil or c.flowers == nil or (c.stale and c.stale.aux) then
|
||||||
local okG, grass = pcall(buildGrassMesh, map)
|
local okG, grass = pcall(buildGrassMesh, map)
|
||||||
c.grass = (okG and grass) or false
|
local okF, flowers = pcall(buildFlowerMesh, map)
|
||||||
|
swapSlot(c, "grass", (okG and grass) or false)
|
||||||
|
swapSlot(c, "flowers", (okF and flowers) or false)
|
||||||
|
if c.stale then c.stale.aux = nil end
|
||||||
end
|
end
|
||||||
if c[slot] == nil then
|
if c[slot] == nil or (c.stale and c.stale[slot]) then
|
||||||
local ok, mesh = pcall(ChunkMesher.build, map, bodyOnly, masks)
|
local ok, mesh = pcall(ChunkMesher.build, map, bodyOnly, masks)
|
||||||
if not ok then
|
if not ok then
|
||||||
print("[warn] voxel mesh build failed for " .. tostring(map.id)
|
print("[warn] voxel mesh build failed for " .. tostring(map.id)
|
||||||
.. ": " .. tostring(mesh))
|
.. ": " .. tostring(mesh))
|
||||||
end
|
end
|
||||||
c[slot] = (ok and mesh) or false
|
swapSlot(c, slot, (ok and mesh) or false)
|
||||||
|
if c.stale then
|
||||||
|
c.stale[slot] = nil
|
||||||
|
if not (c.stale.full or c.stale.body or c.stale.aux) then
|
||||||
|
c.stale = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
local key = jobKey(map.id, slot)
|
local key = jobKey(map.id, slot)
|
||||||
local job = jobIndex[key]
|
local job = jobIndex[key]
|
||||||
if job then finishJob(job, true) end
|
if job then finishJob(job, true) end
|
||||||
@@ -901,6 +955,39 @@ function ChunkMesher.grass(map)
|
|||||||
return c and c.grass or nil
|
return c and c.grass or nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function ChunkMesher.flowers(map)
|
||||||
|
local c = cache[map.id]
|
||||||
|
return c and c.flowers or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Rebuild a map's meshes IN PLACE: the stale meshes keep drawing while
|
||||||
|
-- replacements cook, and each slot swaps as its build lands. This is
|
||||||
|
-- the block-edit path (a cut tree, a door stamp) -- invalidate() drops
|
||||||
|
-- the mesh outright, and until the async rebuild landed the scene fell
|
||||||
|
-- to the flat 2D path, a whole-world blink for a one-block edit.
|
||||||
|
function ChunkMesher.refresh(mapId)
|
||||||
|
if not mapId then return ChunkMesher.invalidate() end
|
||||||
|
local c = cache[mapId]
|
||||||
|
-- nothing drawable cached: the plain drop costs nothing visible
|
||||||
|
if not (c and (c.full or c.body)) then
|
||||||
|
return ChunkMesher.invalidate(mapId)
|
||||||
|
end
|
||||||
|
Structures.invalidate(mapId)
|
||||||
|
gen[mapId] = (gen[mapId] or 0) + 1
|
||||||
|
for i = #jobs, 1, -1 do
|
||||||
|
local job = jobs[i]
|
||||||
|
if job.id == mapId then
|
||||||
|
jobIndex[jobKey(job.id, job.slot)] = nil
|
||||||
|
table.remove(jobs, i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- false-cached slots count as stale too: a retry after a failed build
|
||||||
|
-- is exactly a rebuild
|
||||||
|
c.stale = { aux = true,
|
||||||
|
full = (c.full ~= nil) or nil,
|
||||||
|
body = (c.body ~= nil) or nil }
|
||||||
|
end
|
||||||
|
|
||||||
-- Evict everything outside `live` (a set of map ids): far maps' meshes
|
-- Evict everything outside `live` (a set of map ids): far maps' meshes
|
||||||
-- are released -- GPU buffer and LOVE's CPU copy both -- and their
|
-- are released -- GPU buffer and LOVE's CPU copy both -- and their
|
||||||
-- Structures analysis dropped. The live set is the current map plus its
|
-- Structures analysis dropped. The live set is the current map plus its
|
||||||
|
|||||||
+38
-3
@@ -193,7 +193,7 @@ function Structures.forMap(map)
|
|||||||
-- terrain.)
|
-- terrain.)
|
||||||
S = { shapeAt = shapeAt, tileAt = tileAt, outdoor = Map.isOutdoor(def),
|
S = { shapeAt = shapeAt, tileAt = tileAt, outdoor = Map.isOutdoor(def),
|
||||||
runs = {}, skip = {}, ground = {}, objectQuads = {},
|
runs = {}, skip = {}, ground = {}, objectQuads = {},
|
||||||
grassQuads = {}, roundStamps = {} }
|
grassQuads = {}, flowerQuads = {}, roundStamps = {} }
|
||||||
Buildings.build(S, map, pixels(tileset), perRow)
|
Buildings.build(S, map, pixels(tileset), perRow)
|
||||||
|
|
||||||
-- Fold doors into their buildings. A door cell is WALKABLE (the player
|
-- Fold doors into their buildings. A door cell is WALKABLE (the player
|
||||||
@@ -418,6 +418,27 @@ function Structures.forMap(map)
|
|||||||
Structures.buildFlowers(S, map, tw, th, x0, x1, y0, y1, data)
|
Structures.buildFlowers(S, map, tw, th, x0, x1, y0, y1, data)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- ---- authored ground under pinned props ----
|
||||||
|
-- The profile can name the tile a pinned prop stands on (a tileset
|
||||||
|
-- entry's prop_ground: prop tile id -> ground tile id), overriding
|
||||||
|
-- the neighbour vote. The cuttable bush stands on the plain grass
|
||||||
|
-- Cut itself leaves behind, not on whatever path its neighbours
|
||||||
|
-- happen to vote in.
|
||||||
|
do
|
||||||
|
local okP, prof = pcall(V.data, "voxel_heights")
|
||||||
|
local entry = okP and type(prof) == "table" and prof.tilesets
|
||||||
|
and prof.tilesets[tileset.id]
|
||||||
|
local pg = entry and entry.prop_ground
|
||||||
|
if type(pg) == "table" then
|
||||||
|
for k, skipped in pairs(S.skip) do
|
||||||
|
if skipped then
|
||||||
|
local g = pg[S.tileAt[k]]
|
||||||
|
if g then S.ground[k] = g end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- unresolved claimed ground (a hull with no art match, headless
|
-- unresolved claimed ground (a hull with no art match, headless
|
||||||
-- cylinders): no flat neighbour to vote with, so fall back to the
|
-- cylinders): no flat neighbour to vote with, so fall back to the
|
||||||
-- map's commonest ground tile
|
-- map's commonest ground tile
|
||||||
@@ -1847,7 +1868,17 @@ function Structures.buildObject(S, map, region, cluster,
|
|||||||
end
|
end
|
||||||
for _, c in ipairs(cluster.tiles) do
|
for _, c in ipairs(cluster.tiles) do
|
||||||
local k = keyOf(c[1], c[2])
|
local k = keyOf(c[1], c[2])
|
||||||
if support then
|
if support and support.class == "wall" then
|
||||||
|
-- a figure drawn above a FULL-HEIGHT block (the gym statue on its
|
||||||
|
-- plinth) is a statue on a pillar with ONE cell of footprint: the
|
||||||
|
-- block below already carries the whole base, so the drawn cell
|
||||||
|
-- becomes synthesized floor rather than a second block marching
|
||||||
|
-- the base backwards. Furniture supports (a monitor on its desk)
|
||||||
|
-- keep the box-extension below -- their drawn cell is the
|
||||||
|
-- furniture's own upper rows, and floor there would amputate it.
|
||||||
|
S.skip[k] = true
|
||||||
|
S.ground[k] = best
|
||||||
|
elseif support then
|
||||||
-- the claimed tile keeps rendering as the box the prop stands on,
|
-- the claimed tile keeps rendering as the box the prop stands on,
|
||||||
-- wearing the art its own ROW would have without the drawing (the
|
-- wearing the art its own ROW would have without the drawing (the
|
||||||
-- trim row stays trim); only when the whole row is the prop does
|
-- trim row stays trim); only when the whole row is the prop does
|
||||||
@@ -2130,7 +2161,11 @@ end
|
|||||||
|
|
||||||
function Structures.buildFlowers(S, map, tw, th, x0, x1, y0, y1, data)
|
function Structures.buildFlowers(S, map, tw, th, x0, x1, y0, y1, data)
|
||||||
local templates = {}
|
local templates = {}
|
||||||
local quads = S.objectQuads
|
-- flowerQuads, not objectQuads: flowers sit on WALKABLE cells, so
|
||||||
|
-- their mesh draws after the characters with the character pull
|
||||||
|
-- (ChunkMesher's flower mesh) -- terrain-baked they lose the depth
|
||||||
|
-- fight against the pulled card whenever the player stands among them
|
||||||
|
local quads = S.flowerQuads
|
||||||
for ty = y0, y1 do
|
for ty = y0, y1 do
|
||||||
for tx = x0, x1 do
|
for tx = x0, x1 do
|
||||||
Budget.tick()
|
Budget.tick()
|
||||||
|
|||||||
@@ -460,6 +460,14 @@ local function castShadows(state, terrain, nbMesh, posed, cx, cy, vw, vh,
|
|||||||
ShadowMap.draw(nbMesh[i], atlasFor(nb.map),
|
ShadowMap.draw(nbMesh[i], atlasFor(nb.map),
|
||||||
Mat4.translate(nb.ox, 0, nb.oy))
|
Mat4.translate(nb.ox, 0, nb.oy))
|
||||||
end
|
end
|
||||||
|
-- flower billboards live outside the terrain mesh (they draw after the
|
||||||
|
-- characters, pulled -- see render), but the sun still sees them: a
|
||||||
|
-- handful of cutouts per meadow, unlike the grass left out below
|
||||||
|
ShadowMap.draw(ChunkMesher.flowers(state.map), atlasFor(state.map), nil)
|
||||||
|
for _, nb in ipairs(state.neighbors or {}) do
|
||||||
|
ShadowMap.draw(ChunkMesher.flowers(nb.map), atlasFor(nb.map),
|
||||||
|
Mat4.translate(nb.ox, 0, nb.oy))
|
||||||
|
end
|
||||||
for _, p in ipairs(posed) do
|
for _, p in ipairs(posed) do
|
||||||
local def = p.sprite.def
|
local def = p.sprite.def
|
||||||
local frame, mirror = frameFor(def, p.facing, p.phase, p.flip)
|
local frame, mirror = frameFor(def, p.facing, p.phase, p.flip)
|
||||||
@@ -559,6 +567,23 @@ function VoxelScene.render(state, w, h, vw, vh, paletteFor)
|
|||||||
Voxel3D.draw(ChunkMesher.grass(nb.map), atlasFor(nb.map),
|
Voxel3D.draw(ChunkMesher.grass(nb.map), atlasFor(nb.map),
|
||||||
Mat4.translate(nb.ox, 0, nb.oy), pull)
|
Mat4.translate(nb.ox, 0, nb.oy), pull)
|
||||||
end
|
end
|
||||||
|
-- flower billboards: pulled like the characters and the grass, MINUS
|
||||||
|
-- the depth of 8 world pixels along the view (8 sin a -- the camera
|
||||||
|
-- looks along (0, -cos a, -sin a), so that is exactly one tile row of
|
||||||
|
-- northness). A pure depth handicap with zero screen drift: every
|
||||||
|
-- flower is judged as if it stood one tile row further north. The
|
||||||
|
-- character card's feet plane sits at its cell's MIDDLE (py + 8), so
|
||||||
|
-- a flower on the walker's own cell (z +4 or +12 across the cell)
|
||||||
|
-- lands behind the card and the player obscures the patch they stand
|
||||||
|
-- ON, while the nearest flower of the cell south (+20) stays in front
|
||||||
|
-- and keeps overdrawing their feet.
|
||||||
|
local fpull = math.max(0, pull - 8 * math.sin(math.max(Voxel.angle, 0.05)))
|
||||||
|
Voxel3D.draw(ChunkMesher.flowers(state.map), atlasFor(state.map), nil,
|
||||||
|
fpull)
|
||||||
|
for _, nb in ipairs(state.neighbors or {}) do
|
||||||
|
Voxel3D.draw(ChunkMesher.flowers(nb.map), atlasFor(nb.map),
|
||||||
|
Mat4.translate(nb.ox, 0, nb.oy), fpull)
|
||||||
|
end
|
||||||
|
|
||||||
return Voxel3D.endScene()
|
return Voxel3D.endScene()
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -344,9 +344,13 @@ end)
|
|||||||
-- tree that is no longer there. The 2D tile renderer invalidates its own
|
-- tree that is no longer there. The 2D tile renderer invalidates its own
|
||||||
-- caches off the same edit.
|
-- caches off the same edit.
|
||||||
|
|
||||||
|
-- refresh, not invalidate: the stale mesh keeps drawing while the
|
||||||
|
-- replacement builds in the background, so a one-block edit (Cut, a
|
||||||
|
-- door stamp, the tree regrowing on re-entry) repopulates in place
|
||||||
|
-- instead of blinking the whole scene down to the flat 2D path
|
||||||
mod.events:on("world.block_replaced", function(payload)
|
mod.events:on("world.block_replaced", function(payload)
|
||||||
local mapId = payload and (payload.mapId or (payload.map and payload.map.id))
|
local mapId = payload and (payload.mapId or (payload.map and payload.map.id))
|
||||||
if mapId then ChunkMesher.invalidate(mapId) end
|
if mapId then ChunkMesher.refresh(mapId) end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- A reloaded map is rebuilt from scratch (warps that re-enter the same map,
|
-- A reloaded map is rebuilt from scratch (warps that re-enter the same map,
|
||||||
@@ -372,7 +376,7 @@ mod.events:on("map.reloaded", function(payload)
|
|||||||
if mapId then ChunkMesher.invalidate(mapId) end
|
if mapId then ChunkMesher.invalidate(mapId) end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
mod.exports.version = "1.0.3"
|
mod.exports.version = "1.0.4"
|
||||||
-- exposed so a companion mod can pin its own tiles' shapes or read the
|
-- exposed so a companion mod can pin its own tiles' shapes or read the
|
||||||
-- camera without reaching into this mod's file layout
|
-- camera without reaching into this mod's file layout
|
||||||
mod.exports.lib = V
|
mod.exports.lib = V
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "DRAMATIC_SHAPE",
|
"id": "DRAMATIC_SHAPE",
|
||||||
"name": "Dramatic Shape Voxel Mod",
|
"name": "Dramatic Shape Voxel Mod",
|
||||||
"version": "1.0.3",
|
"version": "1.0.4",
|
||||||
"api": 2,
|
"api": 2,
|
||||||
"entry": "main.lua",
|
"entry": "main.lua",
|
||||||
"profile": "content",
|
"profile": "content",
|
||||||
|
|||||||
@@ -535,8 +535,10 @@ local ChunkMesher = run.loader.exports.DRAMATIC_SHAPE.lib.require("ChunkMesher")
|
|||||||
local Runtime = require("src.mods.Runtime")
|
local Runtime = require("src.mods.Runtime")
|
||||||
|
|
||||||
local realInvalidate = ChunkMesher.invalidate
|
local realInvalidate = ChunkMesher.invalidate
|
||||||
local dropped = {}
|
local realRefresh = ChunkMesher.refresh
|
||||||
|
local dropped, refreshed = {}, {}
|
||||||
ChunkMesher.invalidate = function(id) dropped[#dropped + 1] = id or "<every map>" end
|
ChunkMesher.invalidate = function(id) dropped[#dropped + 1] = id or "<every map>" end
|
||||||
|
ChunkMesher.refresh = function(id) refreshed[#refreshed + 1] = id or "<every map>" end
|
||||||
|
|
||||||
Runtime.emit("map.reloaded", { mapId = "PALLET_TOWN", reason = "colors" })
|
Runtime.emit("map.reloaded", { mapId = "PALLET_TOWN", reason = "colors" })
|
||||||
T.eq(#dropped, 0,
|
T.eq(#dropped, 0,
|
||||||
@@ -551,11 +553,16 @@ T.eq(dropped[1], "PALLET_TOWN", "and drops exactly the map that reloaded")
|
|||||||
Runtime.emit("map.reloaded", { mapId = "VIRIDIAN_CITY" })
|
Runtime.emit("map.reloaded", { mapId = "VIRIDIAN_CITY" })
|
||||||
T.eq(#dropped, 2, "a reload with no stated reason is treated as a real one")
|
T.eq(#dropped, 2, "a reload with no stated reason is treated as a real one")
|
||||||
|
|
||||||
-- and the edits that genuinely change geometry are untouched by any of this
|
-- a block edit (Cut, a door stamp, the tree regrowing on re-entry) is a
|
||||||
|
-- REFRESH, not a drop: the stale mesh keeps drawing while the rebuild
|
||||||
|
-- cooks, so the scene never blinks down to the flat 2D path
|
||||||
Runtime.emit("world.block_replaced", { mapId = "PALLET_TOWN" })
|
Runtime.emit("world.block_replaced", { mapId = "PALLET_TOWN" })
|
||||||
T.eq(#dropped, 3, "a replaced block still drops the mesh it changed")
|
T.eq(#dropped, 2, "a replaced block does not drop the mesh outright")
|
||||||
|
T.eq(#refreshed, 1, "it refreshes the mesh in place instead")
|
||||||
|
T.eq(refreshed[1], "PALLET_TOWN", "and refreshes exactly the edited map")
|
||||||
|
|
||||||
ChunkMesher.invalidate = realInvalidate
|
ChunkMesher.invalidate = realInvalidate
|
||||||
|
ChunkMesher.refresh = realRefresh
|
||||||
|
|
||||||
-- ------- the non-colour palette modes must not come through as SGB
|
-- ------- the non-colour palette modes must not come through as SGB
|
||||||
--
|
--
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ return function(game)
|
|||||||
if S then
|
if S then
|
||||||
local def = ow.map.def
|
local def = ow.map.def
|
||||||
for _, spec in ipairs(specs) do
|
for _, spec in ipairs(specs) do
|
||||||
|
if not spec.tile then goto continue end -- "toggle" specs claim
|
||||||
|
-- tile LISTS, not one slot
|
||||||
local flat, prop, run, first = 0, 0, 0, nil
|
local flat, prop, run, first = 0, 0, 0, nil
|
||||||
for ty = 0, def.height * 4 - 1 do
|
for ty = 0, def.height * 4 - 1 do
|
||||||
for tx = 0, def.width * 4 - 1 do
|
for tx = 0, def.width * 4 - 1 do
|
||||||
@@ -112,6 +114,7 @@ return function(game)
|
|||||||
.. " %d inside a prop%s"):format(spec.tile, flat, run, prop,
|
.. " %d inside a prop%s"):format(spec.tile, flat, run, prop,
|
||||||
first and (" -- first at cell " .. math.floor(first[1] / 2)
|
first and (" -- first at cell " .. math.floor(first[1] / 2)
|
||||||
.. "," .. math.floor(first[2] / 2)) or ""))
|
.. "," .. math.floor(first[2] / 2)) or ""))
|
||||||
|
::continue::
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user