mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 07:20:55 +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
|
||||
|
||||
## 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
|
||||
|
||||
### Added
|
||||
|
||||
@@ -65,6 +65,11 @@
|
||||
-- monitor on its desk). prop is a second pool
|
||||
-- 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
|
||||
-- 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
|
||||
@@ -124,6 +129,40 @@ return {
|
||||
-- fence-textured tower. `post` extracts each cell alone, so these
|
||||
-- render as the same thin posts, marching north
|
||||
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
|
||||
|
||||
+108
-21
@@ -704,15 +704,10 @@ function ChunkMesher.build(map, bodyOnly, masks)
|
||||
return sink.finish()
|
||||
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)
|
||||
local S = Structures.forMap(map)
|
||||
if #S.grassQuads == 0 then return nil end
|
||||
local function quadsMesh(quads)
|
||||
if #quads == 0 then return nil end
|
||||
local verts, indices, n = {}, {}, 0
|
||||
for _, q in ipairs(S.grassQuads) do
|
||||
for _, q in ipairs(quads) do
|
||||
for i = 1, 4 do
|
||||
local c = q[i]
|
||||
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)
|
||||
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
|
||||
|
||||
local function entry(id)
|
||||
@@ -736,11 +758,12 @@ local function entry(id)
|
||||
end
|
||||
|
||||
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]
|
||||
if mesh and mesh.release then pcall(mesh.release, mesh) end
|
||||
c[slot] = nil
|
||||
end
|
||||
c.stale = nil
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------- async builds
|
||||
@@ -778,26 +801,48 @@ end
|
||||
local function runJob(job)
|
||||
local map = job.map
|
||||
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)
|
||||
if (gen[job.id] or 0) ~= job.gen then return end
|
||||
c.grass = (okG and grass) or false
|
||||
local okF, flowers = pcall(buildFlowerMesh, map)
|
||||
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
|
||||
local sink = newSink()
|
||||
runGeometry(map, job.slot == "body", job.masks, sink)
|
||||
local mesh = sink.finish()
|
||||
if (gen[job.id] or 0) ~= job.gen then return end
|
||||
c[job.slot] = mesh or false
|
||||
if (gen[job.id] or 0) ~= job.gen then
|
||||
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
|
||||
|
||||
-- Queue a build unless the slot is already cached or queued. Returns the
|
||||
-- cached mesh when there is one (false-cached misses return nil).
|
||||
-- `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)
|
||||
local slot = bodyOnly and "body" or "full"
|
||||
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 job = jobIndex[key]
|
||||
if not job then
|
||||
@@ -808,7 +853,7 @@ function ChunkMesher.request(map, bodyOnly, masks, urgent)
|
||||
elseif urgent then
|
||||
job.urgent = true
|
||||
end
|
||||
return nil
|
||||
return (c and c[slot]) or nil
|
||||
end
|
||||
|
||||
function ChunkMesher.pending()
|
||||
@@ -871,17 +916,26 @@ end
|
||||
function ChunkMesher.get(map, bodyOnly, masks)
|
||||
local slot = bodyOnly and "body" or "full"
|
||||
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)
|
||||
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
|
||||
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)
|
||||
if not ok then
|
||||
print("[warn] voxel mesh build failed for " .. tostring(map.id)
|
||||
.. ": " .. tostring(mesh))
|
||||
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 job = jobIndex[key]
|
||||
if job then finishJob(job, true) end
|
||||
@@ -901,6 +955,39 @@ function ChunkMesher.grass(map)
|
||||
return c and c.grass or nil
|
||||
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
|
||||
-- are released -- GPU buffer and LOVE's CPU copy both -- and their
|
||||
-- Structures analysis dropped. The live set is the current map plus its
|
||||
|
||||
+38
-3
@@ -193,7 +193,7 @@ function Structures.forMap(map)
|
||||
-- terrain.)
|
||||
S = { shapeAt = shapeAt, tileAt = tileAt, outdoor = Map.isOutdoor(def),
|
||||
runs = {}, skip = {}, ground = {}, objectQuads = {},
|
||||
grassQuads = {}, roundStamps = {} }
|
||||
grassQuads = {}, flowerQuads = {}, roundStamps = {} }
|
||||
Buildings.build(S, map, pixels(tileset), perRow)
|
||||
|
||||
-- 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)
|
||||
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
|
||||
-- cylinders): no flat neighbour to vote with, so fall back to the
|
||||
-- map's commonest ground tile
|
||||
@@ -1847,7 +1868,17 @@ function Structures.buildObject(S, map, region, cluster,
|
||||
end
|
||||
for _, c in ipairs(cluster.tiles) do
|
||||
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,
|
||||
-- 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
|
||||
@@ -2130,7 +2161,11 @@ end
|
||||
|
||||
function Structures.buildFlowers(S, map, tw, th, x0, x1, y0, y1, data)
|
||||
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 tx = x0, x1 do
|
||||
Budget.tick()
|
||||
|
||||
@@ -460,6 +460,14 @@ local function castShadows(state, terrain, nbMesh, posed, cx, cy, vw, vh,
|
||||
ShadowMap.draw(nbMesh[i], atlasFor(nb.map),
|
||||
Mat4.translate(nb.ox, 0, nb.oy))
|
||||
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
|
||||
local def = p.sprite.def
|
||||
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),
|
||||
Mat4.translate(nb.ox, 0, nb.oy), pull)
|
||||
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()
|
||||
end
|
||||
|
||||
@@ -344,9 +344,13 @@ end)
|
||||
-- tree that is no longer there. The 2D tile renderer invalidates its own
|
||||
-- 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)
|
||||
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)
|
||||
|
||||
-- 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
|
||||
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
|
||||
-- camera without reaching into this mod's file layout
|
||||
mod.exports.lib = V
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "DRAMATIC_SHAPE",
|
||||
"name": "Dramatic Shape Voxel Mod",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"api": 2,
|
||||
"entry": "main.lua",
|
||||
"profile": "content",
|
||||
|
||||
@@ -535,8 +535,10 @@ local ChunkMesher = run.loader.exports.DRAMATIC_SHAPE.lib.require("ChunkMesher")
|
||||
local Runtime = require("src.mods.Runtime")
|
||||
|
||||
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.refresh = function(id) refreshed[#refreshed + 1] = id or "<every map>" end
|
||||
|
||||
Runtime.emit("map.reloaded", { mapId = "PALLET_TOWN", reason = "colors" })
|
||||
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" })
|
||||
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" })
|
||||
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.refresh = realRefresh
|
||||
|
||||
-- ------- the non-colour palette modes must not come through as SGB
|
||||
--
|
||||
|
||||
@@ -94,6 +94,8 @@ return function(game)
|
||||
if S then
|
||||
local def = ow.map.def
|
||||
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
|
||||
for ty = 0, def.height * 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,
|
||||
first and (" -- first at cell " .. math.floor(first[1] / 2)
|
||||
.. "," .. math.floor(first[2] / 2)) or ""))
|
||||
::continue::
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user