mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 08:01:09 +02:00
initial commit
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
-- Voxel world mode: resolve every tile of a tileset to an extrusion shape.
|
||||
--
|
||||
-- Reads the hand-authored groups in data/voxel_heights.lua and fills the
|
||||
-- gaps from data the ROM extractor already emits. Resolution happens at two
|
||||
-- granularities, and the order matters:
|
||||
--
|
||||
-- per tile 1. a group named in data/voxel_heights.lua (hand-authored)
|
||||
-- per CELL 2. the cell is water -> "water"
|
||||
-- 3. the cell is walkable -> "ground"
|
||||
-- per tile 4. tile-level fallback: the map's water set -> "water",
|
||||
-- its walkable set -> "ground", else -> "wall"
|
||||
--
|
||||
-- The cell steps (TileShape.at) are the load-bearing part. Collision in
|
||||
-- this engine -- like the GB original -- is defined per 16x16 CELL, judged
|
||||
-- by the cell's bottom-left 8x8 tile alone. The other three tiles of a
|
||||
-- cell carry no collision meaning, and treating their walkable-list
|
||||
-- membership as one (which is what a pure per-tile lookup does) misfiles
|
||||
-- every decorative tile: flowers become 16px pillars, the gap tiles of a
|
||||
-- fence row become wall, grass tufts extrude. A tile in a walkable cell is
|
||||
-- ground the player is standing on, whatever the walkable list says about
|
||||
-- it; hand-authoring (rule 1) is the only thing that overrides that.
|
||||
--
|
||||
-- Rule 4 covers positions whose cell IS blocked: there, walkable-listed
|
||||
-- tiles (the gaps between fence posts) stay ground and the rest rise.
|
||||
--
|
||||
-- Every class also carries an ART mode, which is what the mesher renders:
|
||||
--
|
||||
-- flat ground/water/void: a single quad, no box.
|
||||
-- top ledge/roof: a box with its art on the TOP face -- things
|
||||
-- whose 2D art depicts a surface seen from above.
|
||||
-- upright wall/tree/fence/sign: a box whose SOUTH face reconstructs
|
||||
-- the 2D artwork standing up (the mesher's fold-up rule) --
|
||||
-- things whose 2D art depicts a surface seen face-on, which is
|
||||
-- most of Gen 1: interior walls, furniture, tree canopies,
|
||||
-- building facades.
|
||||
--
|
||||
-- Purely presentational: a shape decides how a tile DRAWS in voxel mode
|
||||
-- and nothing else. Collision still reads the same walkable list it
|
||||
-- always did.
|
||||
|
||||
-- the mod namespace (see main.lua): V.data loads a shipped data file
|
||||
local V = ...
|
||||
|
||||
local TileShape = {}
|
||||
|
||||
-- class -> height fallbacks, used when data/voxel_heights.lua is missing
|
||||
-- or omits a class. Same numbers the shipped file carries; a cell is 16x16.
|
||||
local FALLBACK_HEIGHTS = {
|
||||
ground = 0,
|
||||
water = -2,
|
||||
void = 0,
|
||||
ledge = 6,
|
||||
fence = 10,
|
||||
sign = 12,
|
||||
wall = 16,
|
||||
tree = 16,
|
||||
roof = 28,
|
||||
cylinder = 16,
|
||||
billboard = 16,
|
||||
signpost = 16,
|
||||
post = 16,
|
||||
grass = 0,
|
||||
-- interior furniture: face-on drawings the detector would otherwise
|
||||
-- raise to wall height (or merge into the wall). A bed is drawn from
|
||||
-- above and lies low; tables and desks are boxes at their real height;
|
||||
-- stairs become stepped geometry rising toward the named side.
|
||||
bed = 7,
|
||||
stool = 8,
|
||||
counter = 8,
|
||||
table = 12,
|
||||
desk = 24,
|
||||
prop = 16,
|
||||
cutout = 16,
|
||||
relief = 3,
|
||||
bookcase = 32,
|
||||
stair_e = 16,
|
||||
stair_w = 16,
|
||||
stair_down_e = 16,
|
||||
stair_down_w = 16,
|
||||
}
|
||||
|
||||
-- class -> how the mesher draws it (see the header). The last three are
|
||||
-- profile archetypes Structures.lua builds special geometry for:
|
||||
-- cylinder round-drawn cells (tree canopies) become voxel hulls cut
|
||||
-- from the art's darkest-pixel outline, round in depth
|
||||
-- billboard signs, props: the art stands as a thin per-pixel voxel
|
||||
-- slab, transparency respected
|
||||
-- post fence posts: the same thin per-pixel slab, but every CELL
|
||||
-- stands alone in its own depth band -- a north-south fence
|
||||
-- line is a march of separate posts, not one tall drawing
|
||||
-- (which is what a shared cluster would make of it)
|
||||
-- grass tall grass: flat ground PLUS two thin standing rows of
|
||||
-- tufts per tile (the art's top and bottom halves), each at
|
||||
-- its drawn depth -- the player walks between them
|
||||
local ART = {
|
||||
ground = "flat",
|
||||
water = "flat",
|
||||
void = "flat",
|
||||
ledge = "top",
|
||||
roof = "top",
|
||||
wall = "upright",
|
||||
tree = "upright",
|
||||
fence = "upright",
|
||||
sign = "upright",
|
||||
cylinder = "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
|
||||
-- 10px standee body reads as a chunk of furniture outdoors
|
||||
signpost = "billboard",
|
||||
post = "post",
|
||||
grass = "grass",
|
||||
-- furniture: a bed's art depicts its top surface; tables and desks are
|
||||
-- boxes whose fronts fold up (the mesher's authored-fold rule).
|
||||
-- Stools, `prop` and `cutout` are standee pools alongside `billboard`
|
||||
-- -- same per-pixel cutout, different thickness (see Structures'
|
||||
-- PINNED_DEPTH), and separate pools cluster separately so touching
|
||||
-- drawings never stack; a stool keeps its 8px height so a character
|
||||
-- standing on its (walkable) cell sits at seat height. Stairs are a
|
||||
-- profile archetype Structures builds real steps for -- rising flights
|
||||
-- for stairs leading up, sunken stairwells for stairs leading down
|
||||
bed = "top",
|
||||
stool = "billboard",
|
||||
-- half-cell furniture: a service counter, a low couch. One 8px band,
|
||||
-- so exactly the drawing's bottom row stands up as the front and
|
||||
-- every row above it rides the top face in drawn order -- which is
|
||||
-- also the only way to place a figure drawn INTO the furniture (the
|
||||
-- Center's seated man) without repeating him, since a taller box
|
||||
-- folds two rows upright and then repeats its north row across the
|
||||
-- top. Reads as something you lean on rather than a wall stub
|
||||
counter = "upright",
|
||||
table = "upright",
|
||||
desk = "upright",
|
||||
prop = "billboard",
|
||||
cutout = "billboard",
|
||||
relief = "relief",
|
||||
-- free-standing shelves: the drawing is TALL, not deep -- Structures
|
||||
-- collapses each drawn rank onto a one-cell-deep box at full height
|
||||
bookcase = "bookcase",
|
||||
stair_e = "stair",
|
||||
stair_w = "stair",
|
||||
stair_down_e = "stair",
|
||||
stair_down_w = "stair",
|
||||
}
|
||||
|
||||
local spec = nil -- the loaded data file, or false when absent
|
||||
local cache = {} -- tileset id -> resolved shape list
|
||||
|
||||
-- The shape profile ships with the mod (data/voxel_heights.lua) and is read
|
||||
-- through the mod's own file loader rather than package.path: a mod's
|
||||
-- directory is not on it, and may live inside a mounted .love archive that
|
||||
-- plain require cannot reach either. Absent or broken degrades to the
|
||||
-- derived defaults, which is a rougher-looking world rather than no world.
|
||||
local function load()
|
||||
if spec == nil then
|
||||
local ok, s = pcall(V.data, "voxel_heights")
|
||||
spec = (ok and type(s) == "table") and s or false
|
||||
end
|
||||
return spec or nil
|
||||
end
|
||||
|
||||
function TileShape.heights()
|
||||
local s = load()
|
||||
local out = {}
|
||||
for class, h in pairs(FALLBACK_HEIGHTS) do out[class] = h end
|
||||
for class, h in pairs(s and s.heights or {}) do
|
||||
if type(h) == "number" and FALLBACK_HEIGHTS[class] then out[class] = h end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
-- tile id -> class, from the hand-authored groups for one tileset. Unknown
|
||||
-- class names are dropped rather than trusted: a typo in the data file
|
||||
-- should degrade to the derived default, not invent a zero-height class.
|
||||
local function authoredGroups(tilesetId, heights)
|
||||
local s = load()
|
||||
local entry = s and s.tilesets and s.tilesets[tilesetId]
|
||||
local out = {}
|
||||
if not entry then return out end
|
||||
for class, tiles in pairs(entry) do
|
||||
if heights[class] and type(tiles) == "table" then
|
||||
for _, t in ipairs(tiles) do out[t] = class end
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
local function shapeFor(class, heights, authored)
|
||||
return { class = class, h = heights[class] or 0,
|
||||
art = ART[class] or "upright",
|
||||
-- grass draws its flat ground base like any walkable tile; the
|
||||
-- standing tuft rows are additive geometry from Structures
|
||||
flat = ART[class] == "flat" or class == "grass",
|
||||
authored = authored or false }
|
||||
end
|
||||
|
||||
-- Resolved TILE-LEVEL shapes for the tileset `map` uses: a list indexed by
|
||||
-- tile id holding { class, h, art, flat, authored }, plus `classes`, one
|
||||
-- canonical shape per class for the cell-level overrides in TileShape.at.
|
||||
-- Cached per tileset id -- this table depends only on the tileset record
|
||||
-- and the data file, both constant for a given id. The per-map part of
|
||||
-- resolution (cell walkability) lives in TileShape.at, NOT here.
|
||||
function TileShape.forMap(map)
|
||||
local tileset = map.tileset
|
||||
local id = tileset.id
|
||||
if cache[id] then return cache[id] end
|
||||
|
||||
local heights = TileShape.heights()
|
||||
local authored = authoredGroups(id, heights)
|
||||
local count = math.floor((tileset.imageWidth or 128) / 8)
|
||||
* math.floor((tileset.imageHeight or 48) / 8)
|
||||
|
||||
local shapes = { classes = {} }
|
||||
for class in pairs(FALLBACK_HEIGHTS) do
|
||||
shapes.classes[class] = shapeFor(class, heights)
|
||||
end
|
||||
for t = 0, count - 1 do
|
||||
local class = authored[t]
|
||||
if class then
|
||||
shapes[t] = shapeFor(class, heights, true)
|
||||
elseif t == tileset.grassTile then
|
||||
-- derived pin: every tileset already names its tall-grass tile, so
|
||||
-- the standing-tuft treatment needs no profile entry anywhere
|
||||
shapes[t] = shapeFor("grass", heights, true)
|
||||
elseif map.waterTiles and map.waterTiles[t] then
|
||||
shapes[t] = shapes.classes.water
|
||||
elseif map.walkable and map.walkable[t] then
|
||||
shapes[t] = shapes.classes.ground
|
||||
else
|
||||
shapes[t] = shapes.classes.wall
|
||||
end
|
||||
end
|
||||
shapes.count = count
|
||||
cache[id] = shapes
|
||||
return shapes
|
||||
end
|
||||
|
||||
-- The shape of the tile at TILE coordinates (tx, ty) -- the full
|
||||
-- resolution including the cell-granularity steps (see the header).
|
||||
-- `shapes` is the table forMap returned for this map; `tile` is
|
||||
-- map:tileAt(tx, ty), passed in because every caller already has it.
|
||||
function TileShape.at(map, shapes, tile, tx, ty)
|
||||
local s = shapes[tile]
|
||||
if not s or s.authored then return s end
|
||||
local cx = math.floor(tx / 2)
|
||||
local cy = math.floor(ty / 2)
|
||||
if map:isWaterCell(cx, cy) then return shapes.classes.water end
|
||||
if map:isWalkableCell(cx, cy) then return shapes.classes.ground end
|
||||
return s
|
||||
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 = {}
|
||||
end
|
||||
|
||||
return TileShape
|
||||
Reference in New Issue
Block a user