diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bb313c..e6f0ad3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,207 @@ # Changelog +## 1.4.3 + +### Added + +- **The furniture of the whole game goes through the building + pipeline.** 1.4.1 put four drawings through it; this is the rest of + the rooms. Every one of them is the same read -- the drawing's own + bands say what is a top seen from above, what is a face seen head-on, + and where the thing ends on the floor -- and every one of them + replaces a pinned box that wore its drawing as a decal. The pins all + stay as the degradation path, neutralized wherever a template stamps. + + - **The bookcase, the commonest piece of furniture in the game** -- + 58 placements across two drawings on the town-house atlas (books + and a bowl on each shelf at the west end of eighteen homes, books + on both at the east), plus Red's and the Copycat's pair. Pinned + `desk` it was a 24px box with the books painted on its flat front. + Modelled it is 23 voxels of cabinet with its top seen from above, + and every book, bowl and door panel sunk a voxel behind the frame + the drawing seals it in. + - **Celadon's display cabinets** -- the tall one with the trophy + behind its glass and the short one beside it, band for band the + same object as the town house's on another atlas, which is what + makes the pair read as one line of furniture: 23 voxels and 15, + exactly the 8 rows of drawing between them. + - **The dining table, everywhere it is drawn** -- the generic town + house's at 18 placements, Red's and the Copycat's, and the chief's + long table at four cells wide. All of them the lab table's read at + a different width, all of them 6 voxels, all of them standing on + the ground line their legs are drawn stopping at rather than on + the grid's floor. + - **The stool at every one of those tables** -- 94 placements on the + house atlas alone, ten more in Red's and the Copycat's, and the Fan + Club's four members' chairs, a different drawing that is + pixel-identical from the seat down. The first template with no base + piece at all: a stool is drawn mid-cell over its own floor, so it + is a desk-set of exactly one part, seat lid over legs with the + floor showing between them. + - **The Pokemon Center's healing machine** -- two variants, 24 + placements, plus the Indigo Plateau lobby's pair. A wall-height + cabinet with its monitor perched on the front of its top face, + drawn across two map rows because it towers over the 16px band + behind it, which the volume path could only read as more wall. The + hoses leaving its side are modelled as hoses, at the elevation and + the depth the two stacked motifs put them; the west machine's + keyboard is a shelf at counter height wearing its own top-view art. + - **Bill's desk, and the Silph president's** -- the same drawing in + both rooms. Its terminal is drawn in 2:1 isometric, turned 45 + degrees to the map, and builds as a cube rather than the slab a 2:1 + reading gives; the kinked dark run between keyboard and computer is + raised to the keyboard's height and reads as the cable it is. The + desk stops at its own two cells because the artist drew its apron + into the walkable cell in front, sharing tiles with the chair + pushed up to it -- so the chair is modelled as a part of the desk. + - **The Bike Shop's open toolbox.** The drawing looks down INTO the + tray, which is why every solid treatment failed it -- as a + `billboard` the whole cell went up as one 10-voxel slab wearing the + drawing as a decal. `tray` builds four walls, a floor and air + between them, with the lid standing open on its hinge. + + What the template language grew to carry them: `tray`; a `desk` band + that lays its top face flat as a lid; the `box`, `flat` and `iso` + part kinds; `stretch` for a band mapped over a deeper plot than it + was drawn on; `inset` for a pane sunk by hand; `panes = false` where + the global recess pass has the polarity backwards; a `wall` element + so a template can keep the band behind it solid; `plane` for a height + the drawing states elsewhere; and `scrub`/`keep`/`support`, which let + a template model a surface while leaving an object standing on it to + its own standee -- Red's potted plant on the dining table. + +- **Round bins: the `can` class.** Vermilion Gym's switch puzzle stands + fifteen galvanised trash cans in a row, and the S.S. Anne redraws the + same object pixel for pixel as its galley barrels. Left to the thin + standee pool they were flat discs on edge -- fifteen coins standing + in a row; pinned a plain `cylinder` the drawing's base arc revolves + too and they came out as barrels balanced on a three-voxel stem. + `can` is the round hull cut at both ends, hollowed and tapered: the + drawn mouth ellipse projects across the top and down the well so you + look into the bin, the drawn base ellipse is ground contact rather + than body, and the plan narrows toward the floor. The two ellipses + are measured off the pixels; the height, the well and the taper are + authored, and the entry says why. + +- **The rock gyms' boulders are round.** 87 placements over Pewter's + walls and maze and Bruno's clusters, and every one of them was a + square bar wearing a boulder texture in relief -- the repeat-aware + scenery path extruding the whole drawing as one course. Each cell is + now a hull whose plan is its own drawn width profile turned in depth: + a dome full-width from the drawn shoulder down, tapering over the top + five rows exactly where the art tapers, with the floor's corner + diamonds opening between them the way the drawing has them. Still + 16px, so nothing standing on or beside a rock moves. + +- **The potted plant stands as a plant.** The most repeated interior + prop in the game -- 78 placements over 13 maps, six per Pokemon + Center -- and its urn was rendering as a hollow black frame, because + the drawing's foot lies flush on the block's bottom edge and the + background vote took the plant's own darks away with the floor. Named + outright as light and white instead, it stands as one organic + silhouette 32px tall over its two stacked cells, crown overhanging + the stem. `planter` carries the same reading for a round drawing + stacked two cells high on one cell of plot. + +- **Bicycles, in both places the Bike Shop draws them.** The six on the + showroom floor get their own pool at two voxels rather than the thin + pool's five: a bike is a line drawing, and at five voxels every + stroke closes the gap to its neighbour with its own side faces, so + from any angle but dead-on the air inside the frames filled in and + the six came out as one dark lump. And the two against the north wall + get `mounted`, a new authored-mask escape for a thing drawn INTO a + wall band: it holds the wall's plane as a thin per-pixel slab instead + of standing up as a sprite card, and it keeps its drawn elevation, so + a bicycle hung clear of the floor stays hung. Its mask is measured + rather than hand-drawn -- the plain panel tile composited across the + same grid and the background flooded in through the pixels that still + match it, which separates bicycle from stripe exactly. + +- **The Marts' cash register is a machine, not a decal.** An authored + figure may now state a `depth`, which makes it an object rather than + a person: a per-pixel solid standing on the counter instead of the + flat card that turned edge-on with the camera. And the drawing is not + a box -- its black linework packs two facings, an L of base and arm + around a keypad that is the machine's deck seen from above. `flat` + lays that rect horizontal in the notch of the L, and `thin` gives the + receipt curl a paper's thickness where the body's would have made it + a wedge. + +- **Shelf fronts have relief.** Everything the `bookcase` collapse is + used for is a shelf, a rack or a display case, and all of them seal + their contents behind the drawing's own black frame -- so those + regions now sink a voxel, the same rule a facade's window panes are + recessed by, and the books stand in the shelf instead of being + painted on it. A tileset that borrows the collapse for something that + is not a shelf says `bookcase_relief = false`: the League's masonry + and pilasters, whose courses are the wall itself, and Bill's + transporter drums, whose light regions are a lit barrel. + +### Changed + +- **Class heights now follow the models under them.** A tileset's + `heights` gets stools at 5 and tables at 6 in the houses, Bill's desk + at 8, and cans at 9 -- each of them the drawn elevation the new + template or hull stands at, so whoever sits on a stool sits on the + seat, and whatever object sprite stands on a table lands on the + modelled top rather than three voxels over it or under it. +- The healing machines' two flanks leave the `wall` pin for the thin + standee pool. They are equipment standing beside the console -- a + pair of pipes and a keyboard -- and as wall each was boxed into a + solid 16px half-cell wearing its drawing in relief. + +### Fixed + +- **Water no longer hides behind water.** The reflective pass writes no + depth -- the depth canvas is detached for the length of it so the + shader can read it -- so nothing put a lake in the buffer and no lake + could occlude another; the sheets were simply painted in mesh order. + Flat water never showed it, one plane, a farther sheet always landing + farther down the screen. The world curve ends that: it drops the far + side of the map into the near field of view, and a sea a hundred and + fifty tiles away came out rasterised on top of the pond at the + player's feet, tall grass and all -- water and terrain "from the + other side of the map", not reflected but there. The water meshes now + go down flat first, through the ordinary scene shader with depth + writes on, and the reflective pass draws over what survived. The + buffer holds the surface, so the pass's own test throws the far sheet + away; the reflection copy holds it too, so a ray grazing another part + of the lake reads water rather than the void behind it; and a frame + that cannot run the pass at all is unchanged, because the flat draw + is the fallback that was already there. +- **Reflections under the world curve.** The bend tips the world away + and the things standing on it do not lean with it -- and a lake is + one of those things. Reflected off the bowl the bend makes, the far + half of a pond was a mirror tilted twenty degrees: it threw the ray + past the vertical, where the sky ramp's own measure swings from one + end to the other across a single column, and hard-edged patches of + the wrong sky stamped into the water; the same tilt sent the + screen-space march grazing along the bank rather than over it, which + is what smeared the dock and the roofs across the harbour. What the + water reflects is now worked out in the flat world, exactly as it + would be with the curve off, and every marched sample is bent on its + way to the screen by the vertex stage's own displacement -- so the + ray is straight where it should be and lands where the geometry did. + The wave columns are read on the flat sheet too: the relief walk is + built on an even slab over a level plane, and in the curved world + that slab is a bowl, which handed back a column a pixel or three off + per fragment -- a patch of noise in the middle of a pond. +- **Merged runs tore open under the curve.** A quad's interior is the + chord of a parabola its neighbours draw the arc of, so a long run + hangs below the short quads butted against it. Nothing bounded a + run's length, and the ones that ran away were those wearing a + constant texel -- a roof's black eave outline, its fascia, its shaded + underside -- because a flat run has no art to break it. At 102px + across a gym the eave tore off the roof and the slot showed the + building's dark interior through it. Runs now stop at the next 8px + lattice line, which is the lattice buildings are stamped on and the + one every other quad in the scene already ends on, so every join is + vertex-for-vertex and the bend carries them together. It costs quads + whether the curve is on or not -- Cerulean's object stream goes from + 35.7k to 41.6k -- and that is deliberate: the mesh is cached per map + and built over seconds, so meshing for the curve's sake only when the + curve is on would mean rebuilding every live map on a keypress. + ## 1.4.1 ### Added diff --git a/data/voxel_heights.lua b/data/voxel_heights.lua index a98c059..55c075c 100644 --- a/data/voxel_heights.lua +++ b/data/voxel_heights.lua @@ -4198,6 +4198,60 @@ return { roofRows = 28, roofBack = 24, roofFront = 0, roofCycle = { 2, 23 }, slab = 3, frontEave = 0, ledge = nil, }, + -- F10: the town house's BOOKCASE -- the commonest piece of + -- furniture in the game at 58 placements across three drawings, + -- and the piece this family was really for: pinned `desk` it was a + -- 24px box with a flat front and the books PAINTED on it. Band for + -- band it is the Celadon display cabinet (F05/F06 under MANSION) + -- on a different atlas, and it takes those numbers unchanged: rows + -- 0-8 the top seen from above (black rim, white highlight courses + -- along the north and west, grey field, the front corner shaded at + -- row 8); row 9 the top's own black front edge -- which is what the + -- rim treatment paints, so slab = 1 and that row folds into the + -- roof band instead of extruding under it; 10-31 the front: two + -- shelves in their dark openings over the cupboard's two panelled + -- doors. Every book, bowl and door panel is a non-black region the + -- drawing seals behind its own black frame, so the measured pane + -- pass sinks each one a voxel and the frames stand proud of them -- + -- the relief the `bookcase` class now carries too, arrived at the + -- same way. + -- + -- Nine drawn top rows over a 32px plot: the rims map 1:1 (the drawn + -- front-corner shading lands one voxel behind the front edge) and + -- the uniform field cycles between. BOTH cell rows of the plot are + -- blocked and both are cabinet drawing, so D is the whole grid -- + -- the case is built into the room's north wall. 23 voxels tall, + -- one under the `desk` pin it replaces and exactly the Celadon + -- trophy case's stand; the `desk` pins stay as the degradation + -- path, neutralized wherever this stamps. + -- + -- This drawing carries books and a bowl on each shelf: 36 + -- placements, the west end of eighteen homes. + { + id = "house_bookcase_bowls", + tiles = { + { 38, 41 }, + { 14, 15 }, + { 14, 15 }, + { 30, 31 }, + }, + roofRows = 10, roofBack = 8, roofFront = 2, roofCycle = { 2, 7 }, + slab = 1, frontEave = 0, ledge = nil, + }, + -- F10 again: its twin at the east end, books on both shelves -- 22 + -- placements, and the only one of the two that Cerulean's trashed + -- house also puts at the west end. + { + id = "house_bookcase_books", + tiles = { + { 38, 41 }, + { 48, 49 }, + { 48, 49 }, + { 30, 31 }, + }, + roofRows = 10, roofBack = 8, roofFront = 2, roofCycle = { 2, 7 }, + slab = 1, frontEave = 0, ledge = nil, + }, -- F09: the stool at every one of those tables (94 placements on -- this tileset) -- the first template with NO base piece. The -- drawing is one object, a round seat on legs, drawn MID-CELL over @@ -4268,6 +4322,22 @@ return { keep = { 40, 54, 55, 56, 57 }, support = 6, }, + -- F10 again: Red's and the Copycat's bookcase pair, cells (0,0) + -- and (1,0) of both maps (4 placements). The same object as the + -- town house's, one shelf of each of its two drawings -- 48/49 + -- books above, 34/35 below -- and the same band numbers; see HOUSE + -- above for the read. + { + id = "reds_bookcase", + tiles = { + { 38, 41 }, + { 48, 49 }, + { 34, 35 }, + { 50, 51 }, + }, + roofRows = 10, roofBack = 8, roofFront = 2, roofCycle = { 2, 7 }, + slab = 1, frontEave = 0, ledge = nil, + }, -- F09 again: the stools around it (10 placements across Red's and -- the Copycat's ground floors), pixel for pixel the house -- tileset's drawing. Same part table; see HOUSE above. diff --git a/main.lua b/main.lua index 01aaf0f..f0b4883 100644 --- a/main.lua +++ b/main.lua @@ -890,7 +890,7 @@ mod.hooks:wrap("world.tod", function(next, tod, ctx) return DayNight.tod() end) -mod.exports.version = "1.4.1" +mod.exports.version = "1.4.3" -- 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 diff --git a/manifest.json b/manifest.json index b2382d4..1f6184c 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "DRAMATIC_SHAPE", "name": "Dramatic Shape Voxel Mod", - "version": "1.4.1", + "version": "1.4.3", "api": 2, "entry": "main.lua", "profile": "content", diff --git a/tests/bike_shop_shots.lua b/tests/bike_shop_shots.lua new file mode 100644 index 0000000..7a896ed --- /dev/null +++ b/tests/bike_shop_shots.lua @@ -0,0 +1,79 @@ +-- Scratch driver: shots of the Bike Shop showroom, for the bicycle +-- voxelization. Two viewpoints -- the north wall (the two bikes drawn +-- INTO the wall band) and the showroom floor (the six standing bikes). +-- +-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/bike_shop_shots.lua \ +-- SHOT_DIR=.scratchpad/bikes AB_TAG=before lovec.exe . +return function(game) + local U = dofile("tests/drivers/util.lua") + local Pipelines = require("src.render.Pipelines") + + local ROOT = (os.getenv("SHOT_DIR") or "shots/bikes") + .. "/" .. (os.getenv("AB_TAG") or "before") + + local handle = game.mods.exports["DRAMATIC_SHAPE"] + if not (handle and handle.lib) then + print("[bike] DRAMATIC_SHAPE mod not loaded") + return + end + local V = handle.lib + local DayNight = V.require("DayNight") + local ChunkMesher = V.require("ChunkMesher") + local Voxel = V.require("VoxelState") + + require("src.world.OverworldController").rollEncounter = function() return nil end + local TileRenderer = require("src.render.TileRenderer") + TileRenderer.tick = function() end + TileRenderer.animFrame = function() return 0 end + DayNight.setting:sync("day") + + pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null') + pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul') + + local Zoom = require("src.render.Zoom") + pcall(function() + game.save.options.zoom = 1 + Zoom.applyOptions(game.save.options) + end) + + local function settle() + for _ = 1, 900 do + if ChunkMesher.pending() == 0 then break end + U.wait(1) + end + for _ = 1, 300 do + if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end + U.wait(1) + end + U.wait(40) + end + + -- cells: wall bikes ride cell row 0 (tile cols 1-3 and 6-8); the six + -- floor bikes stand in cell columns 0 and 2, rows 1-2 and 4-5 + local SCENES = { + { x = 2, y = 2, face = "up", label = "wall" }, + { x = 3, y = 3, face = "up", label = "room" }, + { x = 2, y = 4, face = "left", label = "floor" }, + { x = 3, y = 6, face = "up", label = "wide" }, + -- the two toolboxes, cells (6,6) and (7,7) + { x = 5, y = 6, face = "right", label = "tools" }, + { x = 6, y = 4, face = "down", label = "tools2" }, + } + + local shots = 0 + for _, s in ipairs(SCENES) do + for _, rung in ipairs({ 3, 5 }) do + U.teleport(game, "BIKE_SHOP", s.x, s.y, s.face) + Pipelines.setLevel("voxel", rung) + Pipelines.setLevel("tiltshift", 0) + settle() + local path = ("%s/%s_v%d.png"):format(ROOT, s.label, rung) + game.capturePath = path + U.wait(6) + local f = io.open(path, "rb") + if f then f:close() shots = shots + 1 + else print("[bike] capture missed: " .. path) end + end + end + print(("[bike] %d shots into %s"):format(shots, ROOT)) +end diff --git a/tests/bills_desk_shots.lua b/tests/bills_desk_shots.lua new file mode 100644 index 0000000..f6c7898 --- /dev/null +++ b/tests/bills_desk_shots.lua @@ -0,0 +1,77 @@ +-- Scratch driver: shots of Bill's desk, for the `bills_desk` +-- voxelization. The desk fills cells (1,4) and (2,4) of Bill's house +-- with its chair in the walkable cell (1,5) below it, so these are the +-- angles you can actually stand at: head-on from the floor two cells +-- south, and from either flank. +-- +-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/bills_desk_shots.lua \ +-- SHOT_DIR=.scratchpad/billsdesk AB_TAG=after lovec.exe . +return function(game) + local U = dofile("tests/drivers/util.lua") + local Pipelines = require("src.render.Pipelines") + + local ROOT = (os.getenv("SHOT_DIR") or "shots/billsdesk") + .. "/" .. (os.getenv("AB_TAG") or "after") + + local handle = game.mods.exports["DRAMATIC_SHAPE"] + if not (handle and handle.lib) then + print("[bills] DRAMATIC_SHAPE mod not loaded") + return + end + local V = handle.lib + local DayNight = V.require("DayNight") + local ChunkMesher = V.require("ChunkMesher") + local Voxel = V.require("VoxelState") + + require("src.world.OverworldController").rollEncounter = function() return nil end + local TileRenderer = require("src.render.TileRenderer") + TileRenderer.tick = function() end + TileRenderer.animFrame = function() return 0 end + DayNight.setting:sync("day") + + pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null') + pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul') + + local Zoom = require("src.render.Zoom") + pcall(function() + game.save.options.zoom = 1 + Zoom.applyOptions(game.save.options) + end) + + local function settle() + for _ = 1, 900 do + if ChunkMesher.pending() == 0 then break end + U.wait(1) + end + for _ = 1, 300 do + if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end + U.wait(1) + end + U.wait(40) + end + + local SCENES = { + { x = 1, y = 6, face = "up", label = "headon" }, + { x = 2, y = 6, face = "up", label = "headon_e" }, + { x = 4, y = 5, face = "left", label = "east" }, + { x = 0, y = 5, face = "right", label = "west" }, + } + + local shots = 0 + for _, s in ipairs(SCENES) do + for _, rung in ipairs({ 3, 5 }) do + U.teleport(game, "BILLS_HOUSE", s.x, s.y, s.face) + Pipelines.setLevel("voxel", rung) + Pipelines.setLevel("tiltshift", 0) + settle() + local path = ("%s/%s_v%d.png"):format(ROOT, s.label, rung) + game.capturePath = path + U.wait(6) + local f = io.open(path, "rb") + if f then f:close() shots = shots + 1 + else print("[bills] capture missed: " .. path) end + end + end + print(("[bills] %d shots into %s"):format(shots, ROOT)) + love.event.quit() +end diff --git a/tests/chief_house_shots.lua b/tests/chief_house_shots.lua new file mode 100644 index 0000000..2fec3a8 --- /dev/null +++ b/tests/chief_house_shots.lua @@ -0,0 +1,81 @@ +-- Scratch driver: shots of the Celadon chief's house, for the display +-- cabinet and long table voxelizations. Three viewpoints -- the +-- cabinet rank along the north wall, the long table in the middle of +-- the room, and a wide shot with both in frame. +-- +-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/chief_house_shots.lua \ +-- SHOT_DIR=.scratchpad/chief AB_TAG=after lovec.exe . +return function(game) + local U = dofile("tests/drivers/util.lua") + local Pipelines = require("src.render.Pipelines") + + local ROOT = (os.getenv("SHOT_DIR") or "shots/chief") + .. "/" .. (os.getenv("AB_TAG") or "after") + + local handle = game.mods.exports["DRAMATIC_SHAPE"] + if not (handle and handle.lib) then + print("[chief] DRAMATIC_SHAPE mod not loaded") + return + end + local V = handle.lib + local DayNight = V.require("DayNight") + local ChunkMesher = V.require("ChunkMesher") + local Voxel = V.require("VoxelState") + + require("src.world.OverworldController").rollEncounter = function() return nil end + local TileRenderer = require("src.render.TileRenderer") + TileRenderer.tick = function() end + TileRenderer.animFrame = function() return 0 end + DayNight.setting:sync("day") + + pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null') + pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul') + + local Zoom = require("src.render.Zoom") + pcall(function() + game.save.options.zoom = 1 + Zoom.applyOptions(game.save.options) + end) + + local function settle() + for _ = 1, 900 do + if ChunkMesher.pending() == 0 then break end + U.wait(1) + end + for _ = 1, 300 do + if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end + U.wait(1) + end + U.wait(40) + end + + -- the cabinets occupy cells 2..5 of rows 0-1; the long table cells + -- 2..5 of rows 3-4; the player walks rows 2 and 5 + local SCENES = { + { x = 3, y = 2, face = "up", label = "cabinets" }, + { x = 5, y = 2, face = "up", label = "bookcase" }, + { x = 3, y = 5, face = "up", label = "table" }, + { x = 1, y = 5, face = "right", label = "wide" }, + -- the same rank on CELADON_MANSION_1F, where it stands against the + -- interior partition and the grids start on an ODD tile row + { map = "CELADON_MANSION_1F", x = 2, y = 4, face = "up", + label = "mansion1f" }, + } + + local shots = 0 + for _, s in ipairs(SCENES) do + for _, rung in ipairs({ 3, 5 }) do + U.teleport(game, s.map or "CELADON_CHIEF_HOUSE", s.x, s.y, s.face) + Pipelines.setLevel("voxel", rung) + Pipelines.setLevel("tiltshift", 0) + settle() + local path = ("%s/%s_v%d.png"):format(ROOT, s.label, rung) + game.capturePath = path + U.wait(6) + local f = io.open(path, "rb") + if f then f:close() shots = shots + 1 + else print("[chief] capture missed: " .. path) end + end + end + print(("[chief] %d shots into %s"):format(shots, ROOT)) +end diff --git a/tests/heal_machine_shots.lua b/tests/heal_machine_shots.lua new file mode 100644 index 0000000..7f148cd --- /dev/null +++ b/tests/heal_machine_shots.lua @@ -0,0 +1,78 @@ +-- Scratch driver: shots of the Pokemon Center healing machines behind +-- the counter, for the center_heal_machine voxelization. The pair +-- stands at cells (1,0):(2,1) and (6,0):(7,1) of every Center; the +-- nurse aisle (row 2) is the row you can actually face them from, and +-- the public floor south of the counter gives the wide view. +-- +-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/heal_machine_shots.lua \ +-- SHOT_DIR=.scratchpad/healshots AB_TAG=after lovec.exe . +return function(game) + local U = dofile("tests/drivers/util.lua") + local Pipelines = require("src.render.Pipelines") + + local ROOT = (os.getenv("SHOT_DIR") or "shots/healmachine") + .. "/" .. (os.getenv("AB_TAG") or "after") + + local handle = game.mods.exports["DRAMATIC_SHAPE"] + if not (handle and handle.lib) then + print("[heal] DRAMATIC_SHAPE mod not loaded") + return + end + local V = handle.lib + local DayNight = V.require("DayNight") + local ChunkMesher = V.require("ChunkMesher") + local Voxel = V.require("VoxelState") + + require("src.world.OverworldController").rollEncounter = function() return nil end + local TileRenderer = require("src.render.TileRenderer") + TileRenderer.tick = function() end + TileRenderer.animFrame = function() return 0 end + DayNight.setting:sync("day") + + pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null') + pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul') + + local Zoom = require("src.render.Zoom") + pcall(function() + game.save.options.zoom = 1 + Zoom.applyOptions(game.save.options) + end) + + local function settle() + for _ = 1, 900 do + if ChunkMesher.pending() == 0 then break end + U.wait(1) + end + for _ = 1, 300 do + if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end + U.wait(1) + end + U.wait(40) + end + + local SCENES = { + { x = 1, y = 2, face = "up", label = "west_head" }, + { x = 2, y = 2, face = "up", label = "west_keyboard" }, + { x = 3, y = 2, face = "left", label = "west_side" }, + { x = 6, y = 2, face = "up", label = "east_head" }, + { x = 3, y = 4, face = "up", label = "wide" }, + } + + local shots = 0 + for _, s in ipairs(SCENES) do + for _, rung in ipairs({ 3, 5 }) do + U.teleport(game, "VIRIDIAN_POKECENTER", s.x, s.y, s.face) + Pipelines.setLevel("voxel", rung) + Pipelines.setLevel("tiltshift", 0) + settle() + local path = ("%s/%s_v%d.png"):format(ROOT, s.label, rung) + game.capturePath = path + U.wait(6) + local f = io.open(path, "rb") + if f then f:close() shots = shots + 1 + else print("[heal] capture missed: " .. path) end + end + end + print(("[heal] %d shots into %s"):format(shots, ROOT)) + love.event.quit() +end diff --git a/tests/house_furniture_shots.lua b/tests/house_furniture_shots.lua new file mode 100644 index 0000000..4a3a136 --- /dev/null +++ b/tests/house_furniture_shots.lua @@ -0,0 +1,98 @@ +-- Scratch driver: shots of the house dining tables and stools, for the +-- band-table + no-desk-part voxelization. Every generic home places the +-- table at cells (3,3):(4,4) with four stools around it (Blue's house has +-- Daisy seated at hers); Red's and the Copycat's ground floors place the +-- same furniture one cell lower, with the potted plant CUTOUT standing on +-- the tabletop -- the standee the table template must support, not +-- swallow. +-- +-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/house_furniture_shots.lua \ +-- SHOT_DIR=.scratchpad/housefurn AB_TAG=after lovec.exe . +return function(game) + local U = dofile("tests/drivers/util.lua") + local Pipelines = require("src.render.Pipelines") + + local ROOT = (os.getenv("SHOT_DIR") or "shots/housefurn") + .. "/" .. (os.getenv("AB_TAG") or "after") + + local handle = game.mods.exports["DRAMATIC_SHAPE"] + if not (handle and handle.lib) then + print("[housefurn] DRAMATIC_SHAPE mod not loaded") + return + end + local V = handle.lib + local DayNight = V.require("DayNight") + local ChunkMesher = V.require("ChunkMesher") + local Voxel = V.require("VoxelState") + + require("src.world.OverworldController").rollEncounter = function() return nil end + local TileRenderer = require("src.render.TileRenderer") + TileRenderer.tick = function() end + TileRenderer.animFrame = function() return 0 end + DayNight.setting:sync("day") + + pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null') + pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul') + + local Zoom = require("src.render.Zoom") + pcall(function() + game.save.options.zoom = 1 + Zoom.applyOptions(game.save.options) + end) + + local function settle() + for _ = 1, 900 do + if ChunkMesher.pending() == 0 then break end + U.wait(1) + end + for _ = 1, 300 do + if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end + U.wait(1) + end + U.wait(40) + end + + local SCENES = { + -- head on: below Blue's table looking north over a stool at it, + -- Daisy seated at the left one + { map = "BLUES_HOUSE", x = 3, y = 5, face = "up", label = "blues_front" }, + -- from the east, table and both east stools in profile + { map = "BLUES_HOUSE", x = 6, y = 3, face = "left", label = "blues_side" }, + -- from the north wall looking south down over the tabletop + { map = "BLUES_HOUSE", x = 4, y = 2, face = "down", label = "blues_over" }, + -- close beside a stool: seat top, legs and the gap between them + { map = "BLUES_HOUSE", x = 2, y = 5, face = "up", label = "stool_close" }, + -- Red's table head on from the south: the plant cutout standing on + -- the modelled tabletop + { map = "REDS_HOUSE_1F", x = 4, y = 6, face = "up", label = "reds_front" }, + -- and from the east along the stool row, plant in profile + { map = "REDS_HOUSE_1F", x = 6, y = 4, face = "left", label = "reds_side" }, + -- the Fan Club's four members' chairs round the boardroom table: + -- from the south of the west pair, both stools stacked in profile + { map = "POKEMON_FAN_CLUB", x = 1, y = 5, face = "up", + label = "club_west_pair" }, + -- across the table from the west, both pairs and the octagon between + { map = "POKEMON_FAN_CLUB", x = 0, y = 3, face = "right", + label = "club_across" }, + -- close on the east pair from the north, looking down over the seats + { map = "POKEMON_FAN_CLUB", x = 6, y = 2, face = "down", + label = "club_over" }, + } + + local shots = 0 + for _, s in ipairs(SCENES) do + for _, rung in ipairs({ 3, 5 }) do + U.teleport(game, s.map, s.x, s.y, s.face) + Pipelines.setLevel("voxel", rung) + Pipelines.setLevel("tiltshift", 0) + settle() + local path = ("%s/%s_v%d.png"):format(ROOT, s.label, rung) + game.capturePath = path + U.wait(6) + local f = io.open(path, "rb") + if f then f:close() shots = shots + 1 + else print("[housefurn] capture missed: " .. path) end + end + end + print(("[housefurn] %d shots into %s"):format(shots, ROOT)) +end diff --git a/tests/mart_shots.lua b/tests/mart_shots.lua new file mode 100644 index 0000000..428c4dc --- /dev/null +++ b/tests/mart_shots.lua @@ -0,0 +1,76 @@ +-- Scratch driver: shots of a Poke Mart's clerk counter, for the cash +-- register voxelization. The register is drawn at cell (1,5) of the 4x4 +-- shop layout every Mart shares, in the middle of the counter's east arm, +-- so these are the three angles you can actually stand at: head-on from +-- the aisle, side-on from the east, and over the counter's south arm. +-- +-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/mart_shots.lua \ +-- SHOT_DIR=.scratchpad/register AB_TAG=after lovec.exe . +return function(game) + local U = dofile("tests/drivers/util.lua") + local Pipelines = require("src.render.Pipelines") + + local ROOT = (os.getenv("SHOT_DIR") or "shots/register") + .. "/" .. (os.getenv("AB_TAG") or "after") + + local handle = game.mods.exports["DRAMATIC_SHAPE"] + if not (handle and handle.lib) then + print("[mart] DRAMATIC_SHAPE mod not loaded") + return + end + local V = handle.lib + local DayNight = V.require("DayNight") + local ChunkMesher = V.require("ChunkMesher") + local Voxel = V.require("VoxelState") + + require("src.world.OverworldController").rollEncounter = function() return nil end + local TileRenderer = require("src.render.TileRenderer") + TileRenderer.tick = function() end + TileRenderer.animFrame = function() return 0 end + DayNight.setting:sync("day") + + pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null') + pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul') + + local Zoom = require("src.render.Zoom") + pcall(function() + game.save.options.zoom = 1 + Zoom.applyOptions(game.save.options) + end) + + local function settle() + for _ = 1, 900 do + if ChunkMesher.pending() == 0 then break end + U.wait(1) + end + for _ = 1, 300 do + if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end + U.wait(1) + end + U.wait(40) + end + + local SCENES = { + { x = 1, y = 7, face = "up", label = "aisle" }, + { x = 2, y = 5, face = "left", label = "side" }, + { x = 2, y = 6, face = "left", label = "over" }, + } + + local shots = 0 + for _, s in ipairs(SCENES) do + for _, rung in ipairs({ 3, 5 }) do + U.teleport(game, "VIRIDIAN_MART", s.x, s.y, s.face) + Pipelines.setLevel("voxel", rung) + Pipelines.setLevel("tiltshift", 0) + settle() + local path = ("%s/%s_v%d.png"):format(ROOT, s.label, rung) + game.capturePath = path + U.wait(6) + local f = io.open(path, "rb") + if f then f:close() shots = shots + 1 + else print("[mart] capture missed: " .. path) end + end + end + print(("[mart] %d shots into %s"):format(shots, ROOT)) + love.event.quit() +end diff --git a/tests/potted_plant_shots.lua b/tests/potted_plant_shots.lua new file mode 100644 index 0000000..533bdba --- /dev/null +++ b/tests/potted_plant_shots.lua @@ -0,0 +1,91 @@ +-- Scratch driver: shots of the potted plants, for the plant standee +-- voxelization. Every Center places three side-by-side pairs on its +-- bottom row -- crowns at cells (0,6)/(1,6), (6,6)/(7,6), (12,6)/(13,6), +-- pots below at y=7 -- and INDIGO_PLATEAU_LOBBY (the MART tileset id, +-- same atlas) lines four of them along its hall at cells (12,10)..(15,10). +-- +-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/potted_plant_shots.lua \ +-- SHOT_DIR=.scratchpad/plants AB_TAG=after lovec.exe . +return function(game) + local U = dofile("tests/drivers/util.lua") + local Pipelines = require("src.render.Pipelines") + + local ROOT = (os.getenv("SHOT_DIR") or "shots/plants") + .. "/" .. (os.getenv("AB_TAG") or "after") + + local handle = game.mods.exports["DRAMATIC_SHAPE"] + if not (handle and handle.lib) then + print("[plant] DRAMATIC_SHAPE mod not loaded") + return + end + local V = handle.lib + local DayNight = V.require("DayNight") + local ChunkMesher = V.require("ChunkMesher") + local Voxel = V.require("VoxelState") + + require("src.world.OverworldController").rollEncounter = function() return nil end + local TileRenderer = require("src.render.TileRenderer") + TileRenderer.tick = function() end + TileRenderer.animFrame = function() return 0 end + DayNight.setting:sync("day") + + pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null') + pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul') + + local Zoom = require("src.render.Zoom") + pcall(function() + game.save.options.zoom = 1 + Zoom.applyOptions(game.save.options) + end) + + local function settle() + for _ = 1, 900 do + if ChunkMesher.pending() == 0 then break end + U.wait(1) + end + for _ = 1, 300 do + if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end + U.wait(1) + end + U.wait(40) + end + + local SCENES = { + -- east of the left pair, looking west along the bottom row: both + -- plants in profile, crown overhang and pot silhouette side-on + { map = "VIRIDIAN_POKECENTER", x = 3, y = 6, face = "left", + label = "pair_side" }, + -- north of the left pair, looking south down over the crowns + { map = "VIRIDIAN_POKECENTER", x = 1, y = 5, face = "down", + label = "pair_over" }, + -- head on: standing below the middle pair looking north at it + { map = "VIRIDIAN_POKECENTER", x = 6, y = 7, face = "up", + label = "pair_front" }, + -- close up beside the east pair's pot + { map = "VIRIDIAN_POKECENTER", x = 11, y = 7, face = "right", + label = "close" }, + -- the Plateau lobby's row of four (MART tileset id), along the row + { map = "INDIGO_PLATEAU_LOBBY", x = 11, y = 10, face = "right", + label = "lobby_row" }, + -- and head on from the hall below + { map = "INDIGO_PLATEAU_LOBBY", x = 13, y = 12, face = "up", + label = "lobby_front" }, + } + + local shots = 0 + for _, s in ipairs(SCENES) do + for _, rung in ipairs({ 3, 5 }) do + U.teleport(game, s.map, s.x, s.y, s.face) + Pipelines.setLevel("voxel", rung) + Pipelines.setLevel("tiltshift", 0) + settle() + local path = ("%s/%s_v%d.png"):format(ROOT, s.label, rung) + game.capturePath = path + U.wait(6) + local f = io.open(path, "rb") + if f then f:close() shots = shots + 1 + else print("[plant] capture missed: " .. path) end + end + end + print(("[plant] %d shots into %s"):format(shots, ROOT)) +end diff --git a/tests/roof_curve_shots.lua b/tests/roof_curve_shots.lua new file mode 100644 index 0000000..71c06b0 --- /dev/null +++ b/tests/roof_curve_shots.lua @@ -0,0 +1,80 @@ +-- Scratch driver: the Cerulean gym and the houses beside it, shot at +-- several camera rungs with V-CURVE walked OFF..3, to see what the world +-- bend does to a building's roof. +-- +-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/roof_curve_shots.lua \ +-- SHOT_DIR=.scratchpad/roofcurve AB_TAG=before lovec.exe . +return function(game) + local U = dofile("tests/drivers/util.lua") + local Pipelines = require("src.render.Pipelines") + + local ROOT = (os.getenv("SHOT_DIR") or "shots/roofcurve") + .. "/" .. (os.getenv("AB_TAG") or "after") + + local handle = game.mods.exports["DRAMATIC_SHAPE"] + if not (handle and handle.lib) then + print("[roof] DRAMATIC_SHAPE mod not loaded") + return + end + local V = handle.lib + local DayNight = V.require("DayNight") + local ChunkMesher = V.require("ChunkMesher") + local Voxel = V.require("VoxelState") + local WorldCurve = V.require("WorldCurve") + + require("src.world.OverworldController").rollEncounter = function() return nil end + local TileRenderer = require("src.render.TileRenderer") + TileRenderer.tick = function() end + TileRenderer.animFrame = function() return 0 end + DayNight.setting:sync("day") + + pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null') + pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul') + + local Zoom = require("src.render.Zoom") + pcall(function() + game.save.options.zoom = 1 + Zoom.applyOptions(game.save.options) + end) + + local function settle() + for _ = 1, 900 do + if ChunkMesher.pending() == 0 then break end + U.wait(1) + end + for _ = 1, 300 do + if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end + U.wait(1) + end + U.wait(30) + end + + -- the gym door is CERULEAN_CITY (30,19); the bike shop and the row of + -- houses along the west side give a second, smaller roof in frame + local SCENES = { + { map = "CERULEAN_CITY", x = 30, y = 20, face = "up", label = "gym" }, + { map = "CERULEAN_CITY", x = 27, y = 21, face = "up", label = "gymwide" }, + { map = "PALLET_TOWN", x = 5, y = 6, face = "up", label = "house" }, + } + + local shots = 0 + for _, s in ipairs(SCENES) do + for _, rung in ipairs({ 3, 5 }) do + for _, curve in ipairs({ 0, 3 }) do + U.teleport(game, s.map, s.x, s.y, s.face) + Pipelines.setLevel("voxel", rung) + Pipelines.setLevel("tiltshift", 0) + WorldCurve.setting:setIndex(curve + 1, game) + settle() + local path = ("%s/%s_v%d_c%d.png"):format(ROOT, s.label, rung, curve) + game.capturePath = path + U.wait(8) + local f = io.open(path, "rb") + if f then f:close() shots = shots + 1 + else print("[roof] capture missed: " .. path) end + end + end + end + print(("[roof] %d shots into %s"):format(shots, ROOT)) + love.event.quit() +end diff --git a/tests/roof_span_probe.lua b/tests/roof_span_probe.lua new file mode 100644 index 0000000..99e9563 --- /dev/null +++ b/tests/roof_span_probe.lua @@ -0,0 +1,47 @@ +-- Scratch probe: how long do a building model's merged quads get? +-- +-- A quad's longest world-space edge is what decides how far its CHORD +-- falls below the world curve's parabola, so this is the number that says +-- whether the bend can crack the mesh open. +-- +-- BUILD_MAP=CERULEAN_CITY POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/roof_span_probe.lua lovec . +return function(game) + local U = dofile("tests/drivers/util.lua") + local mapId = os.getenv("BUILD_MAP") or "CERULEAN_CITY" + U.teleport(game, mapId, tonumber(os.getenv("BUILD_X") or "30"), + tonumber(os.getenv("BUILD_Y") or "20"), "up") + U.wait(30) + + local V = game.mods.exports["DRAMATIC_SHAPE"] + V = V and V.lib + local Structures = V and V.require("Structures") + local ow = game.overworld + if not (Structures and ow and ow.map) then + print("[span] mod or map unavailable") + love.event.quit() + return + end + local S = Structures.forMap(ow.map) + local hist, worst = {}, 0 + for _, q in ipairs(S.objectQuads) do + local dx = math.max(q[1][1], q[2][1], q[3][1], q[4][1]) + - math.min(q[1][1], q[2][1], q[3][1], q[4][1]) + local dz = math.max(q[1][3], q[2][3], q[3][3], q[4][3]) + - math.min(q[1][3], q[2][3], q[3][3], q[4][3]) + local dy = math.max(q[1][2], q[2][2], q[3][2], q[4][2]) + - math.min(q[1][2], q[2][2], q[3][2], q[4][2]) + local span = math.max(dx, dz, dy) + local bucket = span <= 8 and "<=8" or (span <= 16 and "<=16" + or (span <= 32 and "<=32" or (span <= 64 and "<=64" or ">64"))) + bucket = bucket .. (q.own and " bld" or " prop") + hist[bucket] = (hist[bucket] or 0) + 1 + if span > worst then worst = span end + end + print(("[span] %d object quads, longest edge %d px"):format(#S.objectQuads, worst)) + for _, b in ipairs({ "<=8", "<=16", "<=32", "<=64", ">64" }) do + for _, kind in ipairs({ " bld", " prop" }) do + print(("[span] %-10s %d"):format(b .. kind, hist[b .. kind] or 0)) + end + end + love.event.quit() +end diff --git a/tests/round_regress_shots.lua b/tests/round_regress_shots.lua new file mode 100644 index 0000000..246b302 --- /dev/null +++ b/tests/round_regress_shots.lua @@ -0,0 +1,75 @@ +-- Scratch driver: one shot of every OTHER user of the round-hull builder +-- (tree canopies, boulders, hedges, stumps, the Center planter), to check +-- that the `can` class's base cut is the identity it is supposed to be for +-- everything that does not ask for it. +-- +-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/round_regress_shots.lua \ +-- SHOT_DIR=.scratchpad/round AB_TAG=after lovec.exe . +return function(game) + local U = dofile("tests/drivers/util.lua") + local Pipelines = require("src.render.Pipelines") + + local ROOT = (os.getenv("SHOT_DIR") or "shots/round") + .. "/" .. (os.getenv("AB_TAG") or "after") + + local handle = game.mods.exports["DRAMATIC_SHAPE"] + if not (handle and handle.lib) then return end + local V = handle.lib + local DayNight = V.require("DayNight") + local ChunkMesher = V.require("ChunkMesher") + local Voxel = V.require("VoxelState") + + require("src.world.OverworldController").rollEncounter = function() return nil end + local TileRenderer = require("src.render.TileRenderer") + TileRenderer.tick = function() end + TileRenderer.animFrame = function() return 0 end + DayNight.setting:sync("day") + + pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null') + pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul') + + local Zoom = require("src.render.Zoom") + pcall(function() + game.save.options.zoom = 1 + Zoom.applyOptions(game.save.options) + end) + + local function settle() + for _ = 1, 900 do + if ChunkMesher.pending() == 0 then break end + U.wait(1) + end + for _ = 1, 300 do + if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end + U.wait(1) + end + U.wait(40) + end + + local SCENES = { + { map = "VIRIDIAN_FOREST", x = 16, y = 20, face = "up", label = "forest" }, + { map = "PEWTER_GYM", x = 4, y = 10, face = "up", label = "boulders" }, + { map = "CELADON_GYM", x = 4, y = 8, face = "up", label = "hedges" }, + { map = "VIRIDIAN_POKECENTER", x = 6, y = 5, face = "up", label = "planter" }, + { map = "PALLET_TOWN", x = 5, y = 8, face = "up", label = "trees" }, + } + + local shots = 0 + for _, s in ipairs(SCENES) do + local ok = pcall(U.teleport, game, s.map, s.x, s.y, s.face) + if ok then + Pipelines.setLevel("voxel", 5) + Pipelines.setLevel("tiltshift", 0) + settle() + local path = ("%s/%s.png"):format(ROOT, s.label) + game.capturePath = path + U.wait(6) + local f = io.open(path, "rb") + if f then f:close() shots = shots + 1 + else print("[round] capture missed: " .. path) end + else + print("[round] teleport failed: " .. s.map) + end + end + print(("[round] %d shots into %s"):format(shots, ROOT)) +end diff --git a/tests/shelf_relief_shots.lua b/tests/shelf_relief_shots.lua new file mode 100644 index 0000000..633fe37 --- /dev/null +++ b/tests/shelf_relief_shots.lua @@ -0,0 +1,92 @@ +-- Scratch driver: shots of the `bookcase` class across the tilesets that +-- pin it, for the shelf-front relief. Two of them are NOT shelves -- +-- the League's gate walls and the terraces on PLATEAU -- and are here as +-- the control: their courses run edge to edge, so nothing should sink. +-- +-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/shelf_relief_shots.lua \ +-- SHOT_DIR=.scratchpad/shelves AB_TAG=before lovec.exe . +return function(game) + local U = dofile("tests/drivers/util.lua") + local Pipelines = require("src.render.Pipelines") + + local ROOT = (os.getenv("SHOT_DIR") or "shots/shelves") + .. "/" .. (os.getenv("AB_TAG") or "before") + + local handle = game.mods.exports["DRAMATIC_SHAPE"] + if not (handle and handle.lib) then + print("[shelf] DRAMATIC_SHAPE mod not loaded") + return + end + local V = handle.lib + local DayNight = V.require("DayNight") + local ChunkMesher = V.require("ChunkMesher") + local Voxel = V.require("VoxelState") + + require("src.world.OverworldController").rollEncounter = function() return nil end + local TileRenderer = require("src.render.TileRenderer") + TileRenderer.tick = function() end + TileRenderer.animFrame = function() return 0 end + DayNight.setting:sync("day") + + pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null') + pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul') + + local Zoom = require("src.render.Zoom") + pcall(function() + game.save.options.zoom = 1 + Zoom.applyOptions(game.save.options) + end) + + local function settle() + for _ = 1, 900 do + if ChunkMesher.pending() == 0 then break end + U.wait(1) + end + for _ = 1, 300 do + if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end + U.wait(1) + end + U.wait(40) + end + + local SCENES = { + { map = "OAKS_LAB", x = 7, y = 2, face = "up", label = "dojo_lab" }, + { map = "CELADON_MANSION_2F", x = 2, y = 4, face = "up", label = "mansion_2f" }, + { map = "CELADON_MART_2F", x = 5, y = 5, face = "up", label = "lobby_mart" }, + { map = "MUSEUM_1F", x = 2, y = 4, face = "up", label = "museum" }, + { map = "VIRIDIAN_MART", x = 3, y = 5, face = "up", label = "mart" }, + { map = "SS_ANNE_CAPTAINS_ROOM", x = 5, y = 2, face = "up", label = "ship" }, + -- the controls, both of them tilesets that borrow the collapse for + -- something that is NOT a shelf and say so with `bookcase_relief = + -- false`: the League's masonry gate walls, and Bill's transporter + -- drums. Nothing in either may move. + { map = "INDIGO_PLATEAU", x = 2, y = 5, face = "up", label = "plateau" }, + { map = "BILLS_HOUSE", x = 2, y = 3, face = "up", label = "bills" }, + -- the house shelves: pinned `desk`, NOT `bookcase`, so they go + -- through the world mesher's box fold and this relief never reaches + -- them. Here to show the gap. + { map = "REDS_HOUSE_1F", x = 1, y = 2, face = "up", label = "reds" }, + { map = "BLUES_HOUSE", x = 1, y = 2, face = "up", label = "blues" }, + } + + local shots = 0 + for _, s in ipairs(SCENES) do + local ok = pcall(U.teleport, game, s.map, s.x, s.y, s.face) + -- twice: the first load of the session has no mesh to settle against + pcall(U.teleport, game, s.map, s.x, s.y, s.face) + if ok then + Pipelines.setLevel("voxel", 5) + Pipelines.setLevel("tiltshift", 0) + settle() + local path = ("%s/%s.png"):format(ROOT, s.label) + game.capturePath = path + U.wait(6) + local f = io.open(path, "rb") + if f then f:close() shots = shots + 1 + else print("[shelf] capture missed: " .. path) end + else + print("[shelf] teleport failed: " .. s.map) + end + end + print(("[shelf] %d shots into %s"):format(shots, ROOT)) +end diff --git a/tests/ship_can_shots.lua b/tests/ship_can_shots.lua new file mode 100644 index 0000000..6eb535f --- /dev/null +++ b/tests/ship_can_shots.lua @@ -0,0 +1,78 @@ +-- Scratch driver: shots of the SS Anne's galley barrels, which are Lt. +-- Surge's trash can redrawn on the ship atlas. Three down the kitchen's +-- east wall at cells (13,5)/(13,7)/(13,9), one in the captain's room at +-- (4,1), and one each in the two ship-interior houses at (7,7). +-- +-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/ship_can_shots.lua \ +-- SHOT_DIR=.scratchpad/ssanne AB_TAG=after lovec.exe . +return function(game) + local U = dofile("tests/drivers/util.lua") + local Pipelines = require("src.render.Pipelines") + + local ROOT = (os.getenv("SHOT_DIR") or "shots/ssanne") + .. "/" .. (os.getenv("AB_TAG") or "after") + + local handle = game.mods.exports["DRAMATIC_SHAPE"] + if not (handle and handle.lib) then + print("[ship] DRAMATIC_SHAPE mod not loaded") + return + end + local V = handle.lib + local DayNight = V.require("DayNight") + local ChunkMesher = V.require("ChunkMesher") + local Voxel = V.require("VoxelState") + + require("src.world.OverworldController").rollEncounter = function() return nil end + local TileRenderer = require("src.render.TileRenderer") + TileRenderer.tick = function() end + TileRenderer.animFrame = function() return 0 end + DayNight.setting:sync("day") + + pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null') + pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul') + + local Zoom = require("src.render.Zoom") + pcall(function() + game.save.options.zoom = 1 + Zoom.applyOptions(game.save.options) + end) + + local function settle() + for _ = 1, 900 do + if ChunkMesher.pending() == 0 then break end + U.wait(1) + end + for _ = 1, 300 do + if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end + U.wait(1) + end + U.wait(40) + end + + local SCENES = { + { map = "SS_ANNE_KITCHEN", x = 12, y = 11, face = "up", label = "galley_up" }, + { map = "SS_ANNE_KITCHEN", x = 12, y = 3, face = "down", label = "galley_down" }, + { map = "SS_ANNE_KITCHEN", x = 11, y = 7, face = "right", label = "galley_side" }, + { map = "SS_ANNE_CAPTAINS_ROOM", x = 4, y = 4, face = "up", label = "captain" }, + { map = "CERULEAN_BADGE_HOUSE", x = 6, y = 7, face = "right", label = "house" }, + } + + local shots = 0 + for _, s in ipairs(SCENES) do + local ok = pcall(U.teleport, game, s.map, s.x, s.y, s.face) + if ok then + Pipelines.setLevel("voxel", 5) + Pipelines.setLevel("tiltshift", 0) + settle() + local path = ("%s/%s.png"):format(ROOT, s.label) + game.capturePath = path + U.wait(6) + local f = io.open(path, "rb") + if f then f:close() shots = shots + 1 + else print("[ship] capture missed: " .. path) end + else + print("[ship] teleport failed: " .. s.map) + end + end + print(("[ship] %d shots into %s"):format(shots, ROOT)) +end diff --git a/tests/trash_can_shots.lua b/tests/trash_can_shots.lua new file mode 100644 index 0000000..408165e --- /dev/null +++ b/tests/trash_can_shots.lua @@ -0,0 +1,80 @@ +-- Scratch driver: shots of Vermilion Gym's trash cans, for the trash can +-- voxelization. The fifteen cans stand on odd cell columns 1..9 in cell +-- rows 7, 9 and 11; the sixteenth is up at cell (6,1) beside the leader's +-- platform. Even columns are open floor, so the player can be parked +-- between two cans and look along a row. +-- +-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/trash_can_shots.lua \ +-- SHOT_DIR=.scratchpad/cans AB_TAG=before lovec.exe . +return function(game) + local U = dofile("tests/drivers/util.lua") + local Pipelines = require("src.render.Pipelines") + + local ROOT = (os.getenv("SHOT_DIR") or "shots/cans") + .. "/" .. (os.getenv("AB_TAG") or "before") + + local handle = game.mods.exports["DRAMATIC_SHAPE"] + if not (handle and handle.lib) then + print("[can] DRAMATIC_SHAPE mod not loaded") + return + end + local V = handle.lib + local DayNight = V.require("DayNight") + local ChunkMesher = V.require("ChunkMesher") + local Voxel = V.require("VoxelState") + + require("src.world.OverworldController").rollEncounter = function() return nil end + local TileRenderer = require("src.render.TileRenderer") + TileRenderer.tick = function() end + TileRenderer.animFrame = function() return 0 end + DayNight.setting:sync("day") + + pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null') + pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul') + + local Zoom = require("src.render.Zoom") + pcall(function() + game.save.options.zoom = 1 + Zoom.applyOptions(game.save.options) + end) + + local function settle() + for _ = 1, 900 do + if ChunkMesher.pending() == 0 then break end + U.wait(1) + end + for _ = 1, 300 do + if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end + U.wait(1) + end + U.wait(40) + end + + local SCENES = { + -- head on into the middle of the field, cans left, right and ahead + { x = 4, y = 12, face = "up", label = "field" }, + -- close up: standing between two cans of the bottom row + { x = 2, y = 11, face = "left", label = "close" }, + -- along the row, so the cans line up in depth + { x = 4, y = 13, face = "up", label = "row" }, + -- from the north, looking back down over all three rows + { x = 4, y = 6, face = "down", label = "over" }, + } + + local shots = 0 + for _, s in ipairs(SCENES) do + for _, rung in ipairs({ 3, 5 }) do + U.teleport(game, "VERMILION_GYM", s.x, s.y, s.face) + Pipelines.setLevel("voxel", rung) + Pipelines.setLevel("tiltshift", 0) + settle() + local path = ("%s/%s_v%d.png"):format(ROOT, s.label, rung) + game.capturePath = path + U.wait(6) + local f = io.open(path, "rb") + if f then f:close() shots = shots + 1 + else print("[can] capture missed: " .. path) end + end + end + print(("[can] %d shots into %s"):format(shots, ROOT)) +end diff --git a/tests/water_curve_shots.lua b/tests/water_curve_shots.lua new file mode 100644 index 0000000..26d8f1f --- /dev/null +++ b/tests/water_curve_shots.lua @@ -0,0 +1,121 @@ +-- Scratch driver: water shot with V-CURVE walked OFF..3, to see what the +-- world bend does to the reflective pass. +-- +-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/water_curve_shots.lua \ +-- SHOT_DIR=.scratchpad/watercurve AB_TAG=before lovec.exe . +return function(game) + local U = dofile("tests/drivers/util.lua") + local Pipelines = require("src.render.Pipelines") + + local ROOT = (os.getenv("SHOT_DIR") or "shots/watercurve") + .. "/" .. (os.getenv("AB_TAG") or "after") + + local handle = game.mods.exports["DRAMATIC_SHAPE"] + if not (handle and handle.lib) then + print("[water] DRAMATIC_SHAPE mod not loaded") + return + end + local V = handle.lib + local DayNight = V.require("DayNight") + local ChunkMesher = V.require("ChunkMesher") + local Voxel = V.require("VoxelState") + local WorldCurve = V.require("WorldCurve") + local Water = V.require("Water") + -- WATER_RUNG=sky drops the screen-space march and leaves the sky path, to + -- tell an artefact of the one from an artefact of the other + if os.getenv("WATER_RUNG") then Water.setting:sync(os.getenv("WATER_RUNG")) end + + require("src.world.OverworldController").rollEncounter = function() return nil end + local TileRenderer = require("src.render.TileRenderer") + TileRenderer.tick = function() end + TileRenderer.animFrame = function() return 0 end + DayNight.setting:sync(os.getenv("WATER_TIME") or "night") + + pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null') + pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul') + + local Zoom = require("src.render.Zoom") + pcall(function() + game.save.options.zoom = 1 + Zoom.applyOptions(game.save.options) + end) + + local function settle() + for _ = 1, 900 do + if ChunkMesher.pending() == 0 then break end + U.wait(1) + end + for _ = 1, 300 do + if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end + U.wait(1) + end + U.wait(40) + end + + -- Stand on the walkable cell just north of the widest run of water on the + -- map, so a scene is picked by where the water actually is rather than by + -- a coordinate guessed off the block list. + local TileShape = V.require("TileShape") + local function shore(map) + local def = map.def + local shapes = TileShape.forMap(map) + local function classAt(cx, cy) + local tx, ty = cx * 2, cy * 2 + local s = TileShape.at(map, shapes, map:tileAt(tx, ty), tx, ty) + return s and s.class + end + local best, bestRun = nil, 0 + for cy = 1, def.height * 2 - 1 do + local run, start = 0, nil + for cx = 0, def.width * 2 - 1 do + if classAt(cx, cy) == "water" then + start = start or cx + run = run + 1 + if run > bestRun and classAt(cx, cy - 1) ~= "water" then + bestRun, best = run, { x = start + math.floor(run / 2), y = cy - 1 } + end + else + run, start = 0, nil + end + end + end + return best + end + + local SCENES = { + { map = "PALLET_TOWN", label = "pallet" }, + { map = "CERULEAN_CITY", label = "cerulean" }, + { map = "VERMILION_CITY", x = 18, y = 27, label = "vermilion" }, + { map = "ROUTE_24", label = "route24" }, + { map = "VIRIDIAN_CITY", label = "viridian" }, + } + + local shots = 0 + for _, s in ipairs(SCENES) do + if not s.x then + -- teleport once so the map is loaded, then let the scan place us + U.teleport(game, s.map, 1, 1, "down") + U.wait(4) + local spot = game.overworld and game.overworld.map and shore(game.overworld.map) + if spot then s.x, s.y = spot.x, spot.y else s.x, s.y = 1, 1 end + print(("[water] %s shore at (%d,%d)"):format(s.label, s.x, s.y)) + end + for _, rung in ipairs({ 3, 4 }) do + for _, curve in ipairs({ 0, 3 }) do + U.teleport(game, s.map, s.x, s.y, s.face or "down") + Pipelines.setLevel("voxel", rung) + Pipelines.setLevel("tiltshift", 0) + WorldCurve.setting:setIndex(curve + 1, game) + settle() + local path = ("%s/%s_v%d_c%d.png"):format(ROOT, s.label, rung, curve) + game.capturePath = path + U.wait(8) + local f = io.open(path, "rb") + if f then f:close() shots = shots + 1 + else print("[water] capture missed: " .. path) end + end + end + end + print(("[water] %d shots into %s"):format(shots, ROOT)) + love.event.quit() +end diff --git a/tools/building_voxels.py b/tools/building_voxels.py index bcc9078..c8718c0 100644 --- a/tools/building_voxels.py +++ b/tools/building_voxels.py @@ -723,6 +723,65 @@ TEMPLATES = { slab=3, front_eave=0, ledge=None, tileset="reds_house", scrub=[(10, 5, 21, 15)], ), + # F10: the town house's bookcase -- the commonest piece of furniture + # in the game at 58 placements across three drawings, and the reason + # this family exists: the `desk` box it used to be had a flat front + # with the books PAINTED on it. Band for band it is the Celadon + # cabinet (F05/F06) on a different atlas, and it takes those numbers + # unchanged: 0-8 the top seen from above (black rim, white highlight + # courses along the north and west, grey field, the front corner + # shaded at row 8), row 9 the top's own black front edge -- so + # slab = 1 and that row folds into the roof band -- and 10-31 the + # front: two shelves in their dark openings over the cupboard's two + # panelled doors. Every one of those is a non-black region the + # drawing seals behind its own black frame, so the measured pane + # pass sinks each a voxel and the frames stand proud of them. + # Nine drawn top rows over a 32px plot: the rims map 1:1 and the + # uniform field cycles between. BOTH cell rows of the plot are + # blocked and both are cabinet drawing, so D is the whole grid -- + # the case is built into the room's north wall. 23 voxels tall, + # one under the `desk` pin it replaces, and the same 23 the Celadon + # trophy case stands. + # + # This one carries books and a bowl on each shelf; 36 placements, + # the west end of eighteen homes. + "house_bookcase_bowls": dict( + tiles=[ + [38, 41], + [14, 15], + [14, 15], + [30, 31], + ], + roof_rows=10, roof_back=8, roof_front=2, roof_cycle=(2, 7), + slab=1, front_eave=0, ledge=None, tileset="house", + ), + # F10 again: its twin at the east end, books on both shelves -- + # 22 placements, and the only drawing of the two that the trashed + # house in Cerulean puts at the west end as well. + "house_bookcase_books": dict( + tiles=[ + [38, 41], + [48, 49], + [48, 49], + [30, 31], + ], + roof_rows=10, roof_back=8, roof_front=2, roof_cycle=(2, 7), + slab=1, front_eave=0, ledge=None, tileset="house", + ), + # F10 once more, on the reds_house atlas: Red's and the Copycat's + # ground-floor pair (4 placements). The same object, one shelf of + # each drawing -- 48/49 books above, 34/35 below -- and the same + # band numbers. + "reds_bookcase": dict( + tiles=[ + [38, 41], + [48, 49], + [34, 35], + [50, 51], + ], + roof_rows=10, roof_back=8, roof_front=2, roof_cycle=(2, 7), + slab=1, front_eave=0, ledge=None, tileset="reds_house", + ), # F09: the stool at every one of those tables -- the first template # with NO base piece. The drawing is one object, a round seat on # legs, drawn MID-CELL over its own floor (rows 0-4 are the room