Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c82598b24c | |||
| 47363b8d23 | |||
| 752653e243 | |||
| 6887f5d951 | |||
| a140980b1d | |||
| eb231d221e |
+157
@@ -1,5 +1,66 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 1.4.1
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Furniture through the building pipeline.** The band-table voxelizer
|
||||||
|
that models whole buildings from their own drawings (lib/Buildings.lua)
|
||||||
|
now reads interior furniture too, and the first four drawings are in:
|
||||||
|
|
||||||
|
- **F01, the starter-ball table in Oak's lab** -- the tabletop's 16
|
||||||
|
drawn rows lay flat over a 16px plot (1:1, the first template that
|
||||||
|
never cycles), the black/#555/black edge band folds into the slab's
|
||||||
|
own rim, and the base extrudes with its corner feet. Six voxels
|
||||||
|
tall, exactly the drawn elevation.
|
||||||
|
- **F03, the empty north table beside it** -- the same band table on a
|
||||||
|
grid two tiles narrower.
|
||||||
|
- **F02, the lab's computer desk** -- the first DESK-SET template: the
|
||||||
|
drawing segments into PARTS, each classified by the surface it
|
||||||
|
depicts. The monitor and the computer tower stand upright on the
|
||||||
|
desk wearing their own drawn tops as lids; the keyboards and the
|
||||||
|
mouse lie flat in front of them; the sheet of paper on the right
|
||||||
|
lies flat across the desk. Flat parts keep the drawing's own rule --
|
||||||
|
drawn row IS depth row, the same 1:1 the tabletop is drawn with --
|
||||||
|
so an object's height on the drawing is its position on the desk.
|
||||||
|
The Hall of Fame's recording machine is this drawing tile for tile
|
||||||
|
on the GYM atlas, and models identically for free.
|
||||||
|
- **F04, the Center PC** -- the desk-set read again: a Mac-style unit
|
||||||
|
with its screen and drive slot in relief, standing at the back of a
|
||||||
|
low white-topped desk with its keyboard lying at the front edge.
|
||||||
|
Eleven Pokemon Centers, plus the Indigo Plateau lobby, whose MART
|
||||||
|
tileset shares the atlas.
|
||||||
|
|
||||||
|
Two measurements had to stop being assumptions for furniture to fit
|
||||||
|
the pipeline: the GROUND LINE is now read off the drawing (a building
|
||||||
|
ends on the black threshold row it stands on; a table's legs stop two
|
||||||
|
rows short of theirs, and extruding against the grid floated them in
|
||||||
|
the air), and a template may name its PLOT (`depth`) when the matched
|
||||||
|
grid runs past it onto the walkable floor the legs merely stand on.
|
||||||
|
Both are identities for every existing building.
|
||||||
|
|
||||||
|
- **The Center couch has a backrest.** The couch is drawn from above --
|
||||||
|
back-and-arm strip down the west side, cushions and seams on the east
|
||||||
|
-- and rendered as one seat-high box. The new `backrest` class raises
|
||||||
|
the drawn back strip to 12px over the 8px seat, in every Center and
|
||||||
|
the Celadon Hotel. The man sitting on it keeps his seat: the figure
|
||||||
|
anchor now scans under his card for the tallest authored upright (his
|
||||||
|
cushion) instead of reading the corner tile, which is the backrest
|
||||||
|
now.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Sprites ride at the height the art actually stands.** Class heights
|
||||||
|
can now be overridden per tileset (a tileset entry's `heights`), and
|
||||||
|
DOJO's lab tables use it: they are drawn 6px tall, not the default
|
||||||
|
table's 12, so the starter balls sit exactly on the modelled tabletop
|
||||||
|
-- and the volume-built north tables drop to the same height, keeping
|
||||||
|
every table in the room level.
|
||||||
|
- The Center PC's old rendering -- a 12px table box with the unit as a
|
||||||
|
flat standee on it -- retires wherever the F04 template stamps; the
|
||||||
|
pins stay only as the degradation path when the shape profile is
|
||||||
|
absent.
|
||||||
|
|
||||||
## 1.4.0
|
## 1.4.0
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -116,6 +177,72 @@
|
|||||||
ever lands half-way between two pixels and changing a wavelength moves the
|
ever lands half-way between two pixels and changing a wavelength moves the
|
||||||
speed with it.
|
speed with it.
|
||||||
|
|
||||||
|
- **AA, a new options row: OFF / 2X / 4X.** Everything else in this game is
|
||||||
|
flat art blitted at whole pixels. This mode's world is real geometry seen
|
||||||
|
through a perspective camera, and a polygon edge that lands at an angle
|
||||||
|
across the pixel grid is the one place where a hard stair-step is not a
|
||||||
|
stylistic choice -- a roof ridge, a ledge lip, a tree's silhouette against
|
||||||
|
the sky, the leaning card of a character. At the shallow rungs, where the
|
||||||
|
diorama reads most like a photograph of a model, they crawl as the camera
|
||||||
|
drifts.
|
||||||
|
|
||||||
|
The row is SUPERSAMPLING: the whole pass renders into a canvas larger than
|
||||||
|
the window and is folded back down at the end. The ladder is samples per
|
||||||
|
display pixel, so 2X is a canvas root-two wider and taller and 4X one
|
||||||
|
exactly twice the size -- an honest 2x2 box.
|
||||||
|
|
||||||
|
Two alternatives were tried against what this pass already is, and both
|
||||||
|
lost:
|
||||||
|
|
||||||
|
- **MSAA** would have taken the water with it. The reflections read the
|
||||||
|
frame's own depth buffer as a texture, and a multisampled depth
|
||||||
|
attachment is not something a fragment shader in this dialect can sample.
|
||||||
|
The row would have quietly switched the WATER row off.
|
||||||
|
|
||||||
|
- **An edge filter** (FXAA and its relatives) works from the finished
|
||||||
|
colour alone, so it would be guessing where the edges are out of one
|
||||||
|
sample per pixel -- inventing detail it never rendered, and unable to
|
||||||
|
tell a geometry edge from the boundary between two texels of a tileset.
|
||||||
|
|
||||||
|
Rendering larger has neither problem, and nothing in the frame had to be
|
||||||
|
taught about it: every pass already measures itself in the canvas it was
|
||||||
|
handed, so the sky's dither, the water's ray march, the shadow lookups and
|
||||||
|
the camera itself come out the same picture at a higher sample rate. It
|
||||||
|
antialiases the geometry, the alpha-cut outline of a sprite card, the
|
||||||
|
wireframe and the reflections at once, because none of them know it is
|
||||||
|
happening.
|
||||||
|
|
||||||
|
And it softens the ARTWORK with them, which is worth saying plainly. A
|
||||||
|
tileset texel out here is not a screen pixel, it is a quad in a perspective
|
||||||
|
view, and its boundary crosses the pixel grid at the same arbitrary angle a
|
||||||
|
roof ridge does -- so the fold averages across it exactly as it averages
|
||||||
|
across the ridge. That is what an honest extra sample says about that
|
||||||
|
pixel, and it is also the trade the row is: the diorama comes out smoother,
|
||||||
|
not sharper. Which is why it is a row and not something that is simply on.
|
||||||
|
|
||||||
|
Two things are quoted in DISPLAY pixels rather than canvas ones and are
|
||||||
|
multiplied up to match: the voxel wireframe's line width -- left alone it
|
||||||
|
would fold down to half a line, so turning the smoothing up would appear to
|
||||||
|
fade the grid out -- and the scale the overworld's FX closures draw at.
|
||||||
|
|
||||||
|
The fold is a shader rather than a scaled draw, because the void this pass
|
||||||
|
renders into is a transparent BLACK: averaging a straight-alpha edge against
|
||||||
|
it drags the colour toward black as well as toward transparent, and the
|
||||||
|
engine's composite then multiplies by that alpha a second time. Every
|
||||||
|
silhouette against the sky would have come out ringed with a dark fringe --
|
||||||
|
the exact artefact the row exists to remove. So the taps are premultiplied
|
||||||
|
before they are averaged and divided back out after.
|
||||||
|
|
||||||
|
The staged battle gets it too, on its own canvas: the arena is folded back
|
||||||
|
to the window's pixel size before the depth-of-field pass and the HUDs go
|
||||||
|
on, so the world is smoothed and the pics, panels and text box stay the
|
||||||
|
chunky GB art they are.
|
||||||
|
|
||||||
|
OFF by default, and **FULL neither sets it nor takes the row away** -- it
|
||||||
|
is the one row that is not a knob on the look but on what the look COSTS,
|
||||||
|
and only the player knows what their machine can carry. No hotkey, for the
|
||||||
|
same reason: it is set once, not flicked while walking.
|
||||||
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@@ -216,6 +343,36 @@
|
|||||||
depth16 as the floor every GLES3 device can read. Refused all four, the
|
depth16 as the floor every GLES3 device can read. Refused all four, the
|
||||||
reflections are lost and nothing else, exactly as before.
|
reflections are lost and nothing else, exactly as before.
|
||||||
|
|
||||||
|
- **Under BACK SPRITES some of your own Pokemon were see-through -- Pikachu,
|
||||||
|
Seel, Dewgong, Chansey, Jigglypuff -- with the arena showing through the
|
||||||
|
middle of them.** Those back pics are drawn as OUTLINES: everything inside
|
||||||
|
the ink is the lightest shade, the decoder keys that shade to nothing, and
|
||||||
|
on hardware it did not matter because the field behind them was white too.
|
||||||
|
|
||||||
|
BattlePics already put that paper back by flooding the background inward and
|
||||||
|
filling whatever it could not reach, and along the bottom of a figure it told
|
||||||
|
a narrow opening (a belly the drawing ran out of, sealed) from a wide one (a
|
||||||
|
stride, left open for the world to show through). Right for a mon standing
|
||||||
|
on the map -- but the pinned back pic is not on the map, it is on the text
|
||||||
|
box with its feet on row 96, and there is white box under its lowest row
|
||||||
|
rather than arena. Every one of those mons leaks out through an opening far
|
||||||
|
too wide to read as a drain, so the flood walked straight up inside them.
|
||||||
|
|
||||||
|
A pic on the box is now told so, and its bottom edge seals: nothing reaches
|
||||||
|
it from below at any width, and the rule stops being a heuristic -- paper is
|
||||||
|
whatever the background cannot walk to from the left, the right or the top.
|
||||||
|
Twelve of the game's 151 back pics turn on this; the other 139 come back
|
||||||
|
byte-identical, and no front pic is touched at all.
|
||||||
|
|
||||||
|
**And a hole is filled with the pic's own paper rather than with white.**
|
||||||
|
Shade 0 is only white while the pic is still grays, and pics arrive here
|
||||||
|
after the bake -- a species SGB colour, a BGP fade mid-animation, PAL_BLACK
|
||||||
|
across the whole screen while the blackout text is up. A hardcoded white
|
||||||
|
belly would have been the one lit thing on a blacked-out mon. The lightest
|
||||||
|
shade still standing in the pic is that colour, and every one of the game's
|
||||||
|
battler pics keeps at least one such pixel -- an eye, a highlight down a
|
||||||
|
cheek -- so what goes back is the baked shade itself.
|
||||||
|
|
||||||
### Known
|
### Known
|
||||||
|
|
||||||
- Screen-space reflections can only reflect what is in the frame. A tree just
|
- Screen-space reflections can only reflect what is in the frame. A tree just
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ menu.
|
|||||||
| `8`, or the **3D-BTL** options row | ON / OFF — fight on the map instead of on a white field |
|
| `8`, or the **3D-BTL** options row | ON / OFF — fight on the map instead of on a white field |
|
||||||
| `9`, or the **WATER** options row | FULL / SKY / OFF — waves and reflections on water. **SKY** gives the surface its pixel-tall wave columns and puts the sky, the sun, the moon and the cast in them; **FULL** adds a screen-space ray march that also reflects the shoreline, the trees and the buildings standing behind it |
|
| `9`, or the **WATER** options row | FULL / SKY / OFF — waves and reflections on water. **SKY** gives the surface its pixel-tall wave columns and puts the sky, the sun, the moon and the cast in them; **FULL** adds a screen-space ray march that also reflects the shoreline, the trees and the buildings standing behind it |
|
||||||
| the **BACK SPRITES** options row | OFF / ON — keep your own Pokémon on the battle menu, seen from behind in its classic slot, instead of standing it on the map; the foe is still out there. Only on the menu while **3D-BTL** is on, because it decides nothing without it |
|
| the **BACK SPRITES** options row | OFF / ON — keep your own Pokémon on the battle menu, seen from behind in its classic slot, instead of standing it on the map; the foe is still out there. Only on the menu while **3D-BTL** is on, because it decides nothing without it |
|
||||||
|
| the **AA** options row | OFF / 2X / 4X — smooth the stair-stepped edges of the 3D world by rendering the diorama larger than the window and folding it back down. The ladder is samples per display pixel: 2X is a canvas root-two wider and taller, 4X one exactly twice the size. Every edge in the projected picture softens with the silhouettes — the tileset's own texels are quads in a perspective view and cross the pixel grid at the same arbitrary angles — so the diorama reads smoother rather than sharper. The most expensive row in the mod, so it is OFF by default and **FULL** leaves it alone |
|
||||||
| the **DAYTIME** options row | SYNC / DAY / NIGHT / DUSK / DAWN / CYCLE — what time it is outdoors, on the diorama *and* on the flat 2D world; held at SYNC (and off the menu) while VOXEL is FULL |
|
| the **DAYTIME** options row | SYNC / DAY / NIGHT / DUSK / DAWN / CYCLE — what time it is outdoors, on the diorama *and* on the flat 2D world; held at SYNC (and off the menu) while VOXEL is FULL |
|
||||||
|
|
||||||
**3D-BTL** is on by default and is independent of **VOXEL**: battles draw
|
**3D-BTL** is on by default and is independent of **VOXEL**: battles draw
|
||||||
|
|||||||
+191
-6
@@ -110,6 +110,9 @@ return {
|
|||||||
bed = 7,
|
bed = 7,
|
||||||
stool = 8,
|
stool = 8,
|
||||||
counter = 8,
|
counter = 8,
|
||||||
|
-- the raised back band of low seating (the Center couch's back and
|
||||||
|
-- arm strip): half again the 8px seat it rises over
|
||||||
|
backrest = 12,
|
||||||
table = 12,
|
table = 12,
|
||||||
desk = 24,
|
desk = 24,
|
||||||
prop = 16,
|
prop = 16,
|
||||||
@@ -579,6 +582,12 @@ return {
|
|||||||
-- auto-extracting into standing prisms, and the ball/Pokedex
|
-- auto-extracting into standing prisms, and the ball/Pokedex
|
||||||
-- sprites ride the table's authored height.
|
-- sprites ride the table's authored height.
|
||||||
table = { 41, 42, 57, 59, 78, 79 },
|
table = { 41, 42, 57, 59, 78, 79 },
|
||||||
|
-- These tables are drawn 6px tall (3px slab edge over 3px base --
|
||||||
|
-- see the lab_table entry under `buildings`), not the default
|
||||||
|
-- table's 12: the override keeps the volume-built north tables
|
||||||
|
-- and the band-built starter table one height, and stands the
|
||||||
|
-- ball/Pokedex sprites exactly on the top face of both.
|
||||||
|
heights = { table = 6 },
|
||||||
},
|
},
|
||||||
|
|
||||||
-- Red's room and the Copycat's room (one tileset). The detector reads
|
-- Red's room and the Copycat's room (one tileset). The detector reads
|
||||||
@@ -699,14 +708,25 @@ return {
|
|||||||
-- one clean band, so the drawn front panel stands up and the
|
-- one clean band, so the drawn front panel stands up and the
|
||||||
-- counter top stays on top; at 12 they read as wall stubs
|
-- counter top stays on top; at 12 they read as wall stubs
|
||||||
counter = { 8, 10, 24, 25, 56, 90, 91,
|
counter = { 8, 10, 24, 25, 56, 90, 91,
|
||||||
-- and the lounge couch with the man sitting on it.
|
-- and the lounge couch's SEAT column with the man
|
||||||
-- Same half-cell box: its bottom row (42/43, the front
|
-- sitting on it. Same half-cell box: its bottom row
|
||||||
-- base) stands up as the couch's front, and the three
|
-- (43, the front base) stands up as the couch's
|
||||||
-- rows above it -- cushion (38/39) and the man
|
-- front, and the rows above it -- cushion (39) and
|
||||||
-- (36/37 head, 52/53 face) -- ride the top face in
|
-- the man (37 head, 53 face) -- ride the top face in
|
||||||
-- drawn order, each exactly once. See the note below
|
-- drawn order, each exactly once. See the note below
|
||||||
-- on why he cannot be stood upright.
|
-- on why he cannot be stood upright.
|
||||||
36, 37, 38, 39, 42, 43, 52, 53 },
|
37, 39, 43, 53 },
|
||||||
|
-- The couch's WEST tile column: the drawing's left strip is the
|
||||||
|
-- couch's back and arm running north-south (the seat's cushions
|
||||||
|
-- and seams fill the east column), so it rises over the 8px seat
|
||||||
|
-- the way a couch back does instead of lying flush in the same
|
||||||
|
-- box. Per-tile granularity puts the drawn ~6px strip plus a
|
||||||
|
-- 2px sliver of cushion on the raised band -- invisible at tile
|
||||||
|
-- scale, and the alternative is no backrest at all. The figure
|
||||||
|
-- anchor scans for the tallest authored UPRIGHT under the man
|
||||||
|
-- (see Structures.buildFigure), so he keeps sitting at seat
|
||||||
|
-- height beside it.
|
||||||
|
backrest = { 36, 38, 42, 52 },
|
||||||
-- standing per-pixel props, black-outline segmented: the healing
|
-- standing per-pixel props, black-outline segmented: the healing
|
||||||
-- machines' screen tops (58/59/74/75, drawn above the pinned
|
-- machines' screen tops (58/59/74/75, drawn above the pinned
|
||||||
-- bodies so they stand ON them) and the PC (66/70/82/86), which
|
-- bodies so they stand ON them) and the PC (66/70/82/86), which
|
||||||
@@ -3231,5 +3251,170 @@ return {
|
|||||||
slab = 4, frontEave = 4, ledge = nil,
|
slab = 4, frontEave = 4, ledge = nil,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
DOJO = {
|
||||||
|
-- F01: the starter-ball table in Oak's lab (one placement in the
|
||||||
|
-- game: OAKS_LAB cell 6,3) -- the first FURNITURE through the
|
||||||
|
-- band pipeline, and the first drawing whose plot is smaller than
|
||||||
|
-- its grid. Its 24 rows read: 0-15 the tabletop seen from above
|
||||||
|
-- (black rim, white highlight course, grey field); 16-18 the top
|
||||||
|
-- slab's own front edge, black/#555/black -- which is exactly
|
||||||
|
-- what the rim treatment paints, so slab = 3 and those rows fold
|
||||||
|
-- into the roof band instead of extruding under it; 19-21 the
|
||||||
|
-- base band, corner feet and the inset dark panel between them.
|
||||||
|
--
|
||||||
|
-- The legs stand on open FLOOR: the measured ground line lands
|
||||||
|
-- two rows short of the grid (see Buildings measure), and `depth`
|
||||||
|
-- keeps the plot to the blocked cell row -- the grid's third row
|
||||||
|
-- is the walkable cell the player faces the table from, matched
|
||||||
|
-- so the flat leg art is claimed off the floor, not so the model
|
||||||
|
-- stands on it. 16 top rows onto a 16px plot map 1:1: roofBack
|
||||||
|
-- covers the whole depth, nothing cycles, and roofCycle is
|
||||||
|
-- unreachable behind it. The Poke Ball sprites ride the `table`
|
||||||
|
-- pin's height (VoxelScene.groundAt reads the collision tile, not
|
||||||
|
-- this model), so the tileset entry above overrides that height
|
||||||
|
-- to the 6px this drawing actually stands.
|
||||||
|
{
|
||||||
|
id = "lab_table",
|
||||||
|
tiles = {
|
||||||
|
{ 41, 59, 59, 59, 59, 42 },
|
||||||
|
{ 78, 57, 57, 57, 57, 79 },
|
||||||
|
{ 88, 89, 89, 89, 89, 90 },
|
||||||
|
},
|
||||||
|
roofRows = 19, roofBack = 16, roofFront = 0, roofCycle = { 2, 13 },
|
||||||
|
slab = 3, frontEave = 0, ledge = nil, depth = 2,
|
||||||
|
},
|
||||||
|
-- F02: the computer desk on the lab's west side (OAKS_LAB cell
|
||||||
|
-- 0,1) -- the one DESK-SET template: the pipeline's region
|
||||||
|
-- classification at PART granularity (see Buildings
|
||||||
|
-- deskSetModel). The desk is the sibling lab table (fascia rows
|
||||||
|
-- 16-18, base 19-21); on it stand a monitor over its keyboard
|
||||||
|
-- (left), a computer tower over a keyboard and mouse (middle),
|
||||||
|
-- and a sheet of paper LYING FLAT (right). Upright parts anchor
|
||||||
|
-- their drawn bottom row to the desk's top plane and wear their
|
||||||
|
-- own drawn tops as lids; flat parts lie one voxel proud at
|
||||||
|
-- drawn row = depth row -- the same 1:1 the tabletop itself is
|
||||||
|
-- drawn with. The roof fields are inert (roofRows = 0 keeps the
|
||||||
|
-- recess scan over the whole drawing, which is what sinks the
|
||||||
|
-- monitor's screen and the tower's slots). Same drawing, same
|
||||||
|
-- grid, stands in the Hall of Fame on the GYM atlas --
|
||||||
|
-- registered there below.
|
||||||
|
{
|
||||||
|
id = "lab_computers",
|
||||||
|
tiles = {
|
||||||
|
{ 91, 92, 93, 94 },
|
||||||
|
{ 54, 55, 85, 95 },
|
||||||
|
{ 88, 89, 89, 90 },
|
||||||
|
},
|
||||||
|
roofRows = 0, roofBack = 0, roofFront = 0, roofCycle = { 0, 0 },
|
||||||
|
slab = 0, frontEave = 0, ledge = nil, depth = 2,
|
||||||
|
desk = { fascia = { 16, 18 }, base = { 19, 21 } },
|
||||||
|
parts = {
|
||||||
|
{ kind = "upright", x = { 2, 13 }, top = { 0, 2 },
|
||||||
|
facade = { 3, 10 }, depth = 4 }, -- the monitor
|
||||||
|
{ kind = "flat", x = { 1, 13 }, rows = { 11, 14 } }, -- keyboard
|
||||||
|
{ kind = "upright", x = { 14, 21 }, top = { 0, 3 },
|
||||||
|
facade = { 4, 10 }, depth = 6 }, -- the tower
|
||||||
|
{ kind = "flat", x = { 14, 21 }, rows = { 11, 14 } }, -- keys+mouse
|
||||||
|
{ kind = "flat", x = { 22, 30 }, rows = { 1, 14 } }, -- the paper
|
||||||
|
},
|
||||||
|
},
|
||||||
|
-- F03: the empty north table beside it (OAKS_LAB cell 2,1): the
|
||||||
|
-- starter table's band table verbatim on a grid two tiles
|
||||||
|
-- narrower.
|
||||||
|
{
|
||||||
|
id = "lab_table_small",
|
||||||
|
tiles = {
|
||||||
|
{ 41, 59, 59, 42 },
|
||||||
|
{ 78, 57, 57, 79 },
|
||||||
|
{ 88, 89, 89, 90 },
|
||||||
|
},
|
||||||
|
roofRows = 19, roofBack = 16, roofFront = 0, roofCycle = { 2, 13 },
|
||||||
|
slab = 3, frontEave = 0, ledge = nil, depth = 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
POKECENTER = {
|
||||||
|
-- F04: the PC in every Center's northeast corner (11
|
||||||
|
-- placements; the Indigo Plateau lobby's twin is registered
|
||||||
|
-- under MART below). The lab desk-set read again: a Mac-style
|
||||||
|
-- unit drawn face-on -- white top band (rows 0-3), bezel, screen
|
||||||
|
-- and drive slot (4-14) -- standing at the back of a low desk
|
||||||
|
-- whose front face is rows 20-23; the drawn top around the unit
|
||||||
|
-- is WHITE, which is what `lid` carries. The keyboard rows 17-19
|
||||||
|
-- are drawn below the desk's 16px top span, so the flat part's
|
||||||
|
-- `z` puts it at the desk's front edge. The old billboard pins
|
||||||
|
-- for these tiles (66/70/82/86, desk 9/88) stay as the
|
||||||
|
-- degradation path -- the claim neutralizes them wherever this
|
||||||
|
-- template stamps.
|
||||||
|
{
|
||||||
|
id = "center_pc",
|
||||||
|
tiles = {
|
||||||
|
{ 66, 70 },
|
||||||
|
{ 82, 86 },
|
||||||
|
{ 9, 88 },
|
||||||
|
},
|
||||||
|
roofRows = 0, roofBack = 0, roofFront = 0, roofCycle = { 0, 0 },
|
||||||
|
slab = 0, frontEave = 0, ledge = nil, depth = 2,
|
||||||
|
desk = { fascia = { 20, 21 }, base = { 22, 23 }, lid = "white" },
|
||||||
|
parts = {
|
||||||
|
{ kind = "upright", x = { 2, 13 }, top = { 0, 3 },
|
||||||
|
facade = { 4, 14 }, depth = 6 }, -- the unit
|
||||||
|
{ kind = "flat", x = { 2, 13 }, rows = { 17, 19 },
|
||||||
|
z = 13 }, -- keyboard
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
MART = {
|
||||||
|
-- F04 again: the Indigo Plateau lobby's PC (cell 15,7) -- the
|
||||||
|
-- MART tileset shares the POKECENTER atlas, so this is the same
|
||||||
|
-- drawing tile for tile. Same part table as the POKECENTER entry
|
||||||
|
-- above.
|
||||||
|
{
|
||||||
|
id = "center_pc",
|
||||||
|
tiles = {
|
||||||
|
{ 66, 70 },
|
||||||
|
{ 82, 86 },
|
||||||
|
{ 9, 88 },
|
||||||
|
},
|
||||||
|
roofRows = 0, roofBack = 0, roofFront = 0, roofCycle = { 0, 0 },
|
||||||
|
slab = 0, frontEave = 0, ledge = nil, depth = 2,
|
||||||
|
desk = { fascia = { 20, 21 }, base = { 22, 23 }, lid = "white" },
|
||||||
|
parts = {
|
||||||
|
{ kind = "upright", x = { 2, 13 }, top = { 0, 3 },
|
||||||
|
facade = { 4, 14 }, depth = 6 }, -- the unit
|
||||||
|
{ kind = "flat", x = { 2, 13 }, rows = { 17, 19 },
|
||||||
|
z = 13 }, -- keyboard
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
GYM = {
|
||||||
|
-- F02 again: the Hall of Fame's recording machine is the lab's
|
||||||
|
-- computer desk drawing, tile for tile, on the GYM atlas (one
|
||||||
|
-- placement: HALL_OF_FAME cell 4,1). Same part table as the DOJO
|
||||||
|
-- entry above.
|
||||||
|
{
|
||||||
|
id = "lab_computers",
|
||||||
|
tiles = {
|
||||||
|
{ 91, 92, 93, 94 },
|
||||||
|
{ 54, 55, 85, 95 },
|
||||||
|
{ 88, 89, 89, 90 },
|
||||||
|
},
|
||||||
|
roofRows = 0, roofBack = 0, roofFront = 0, roofCycle = { 0, 0 },
|
||||||
|
slab = 0, frontEave = 0, ledge = nil, depth = 2,
|
||||||
|
desk = { fascia = { 16, 18 }, base = { 19, 21 } },
|
||||||
|
parts = {
|
||||||
|
{ kind = "upright", x = { 2, 13 }, top = { 0, 2 },
|
||||||
|
facade = { 3, 10 }, depth = 4 }, -- the monitor
|
||||||
|
{ kind = "flat", x = { 1, 13 }, rows = { 11, 14 } }, -- keyboard
|
||||||
|
{ kind = "upright", x = { 14, 21 }, top = { 0, 3 },
|
||||||
|
facade = { 4, 10 }, depth = 6 }, -- the tower
|
||||||
|
{ kind = "flat", x = { 14, 21 }, rows = { 11, 14 } }, -- keys+mouse
|
||||||
|
{ kind = "flat", x = { 22, 30 }, rows = { 1, 14 } }, -- the paper
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
-- Voxel world mode: anti-aliasing, by supersampling.
|
||||||
|
--
|
||||||
|
-- Everything else in this mod is flat art blitted at whole pixels; this one
|
||||||
|
-- pass is real geometry seen through a perspective camera, and a polygon
|
||||||
|
-- edge that lands at an angle across the pixel grid is the one place in the
|
||||||
|
-- game where a hard stair-step is not a stylistic choice. A roof ridge, a
|
||||||
|
-- ledge lip, a tree's silhouette against the sky and the leaning card of a
|
||||||
|
-- character are all cut by an edge that has no reason to line up with
|
||||||
|
-- anything, and at the shallow rungs -- where the diorama reads most like a
|
||||||
|
-- photograph of a model -- they crawl as the camera drifts.
|
||||||
|
--
|
||||||
|
-- SUPERSAMPLING, not MSAA and not a filter over the finished frame, for two
|
||||||
|
-- reasons that both come out of what the pass already is:
|
||||||
|
--
|
||||||
|
-- MSAA would take the water with it. The reflections read the frame's own
|
||||||
|
-- DEPTH buffer as a texture (Voxel3D.beginWater), and a multisampled depth
|
||||||
|
-- attachment is not a thing a fragment shader in this dialect can sample.
|
||||||
|
-- The row would have quietly switched the other row off.
|
||||||
|
--
|
||||||
|
-- An edge filter (FXAA and its relatives) works from the finished colour
|
||||||
|
-- alone, and would be GUESSING where the edges are out of one sample per
|
||||||
|
-- pixel -- inventing detail it never rendered, and unable to tell a
|
||||||
|
-- geometry edge from the boundary between two texels of a tileset.
|
||||||
|
--
|
||||||
|
-- Rendering the pass larger and folding it back down has neither problem:
|
||||||
|
-- the depth buffer stays an ordinary texture, every pass in the frame keeps
|
||||||
|
-- working in the canvas it was handed, and the fold is an average of samples
|
||||||
|
-- that were each rendered honestly. It antialiases everything at once --
|
||||||
|
-- geometry, the alpha-cut outline of a sprite card, the wireframe, the
|
||||||
|
-- water's ray march -- because none of them know it is happening.
|
||||||
|
--
|
||||||
|
-- Be clear about what "everything" means: the artwork softens too. A tileset
|
||||||
|
-- texel out here is not a screen pixel, it is a quad in a perspective view,
|
||||||
|
-- and its boundary crosses the pixel grid at the same arbitrary angle a roof
|
||||||
|
-- ridge does -- so the fold averages across it exactly as it averages across
|
||||||
|
-- the ridge. That is what an honest extra sample says about that pixel, and
|
||||||
|
-- it is also the trade the row IS: the diorama comes out smoother, not
|
||||||
|
-- sharper. Which is why this is a row and not something that is simply on.
|
||||||
|
--
|
||||||
|
-- What it costs is pixels, which is the whole of why this is a row and not
|
||||||
|
-- something that is simply on: 2X is half again as many in each direction,
|
||||||
|
-- 4X is twice, and the scene pass is the most expensive thing in the frame.
|
||||||
|
|
||||||
|
-- the mod namespace (see main.lua): V.require loads a sibling module
|
||||||
|
local V = ...
|
||||||
|
|
||||||
|
local ModSetting = V.require("ModSetting")
|
||||||
|
|
||||||
|
local AntiAlias = {}
|
||||||
|
|
||||||
|
-- the key under options.modOptions.DRAMATIC_SHAPE, shared by the row in
|
||||||
|
-- OPTIONS and the mod manager's own settings page for this mod
|
||||||
|
AntiAlias.KEY = "aa"
|
||||||
|
AntiAlias.LABEL = "AA"
|
||||||
|
|
||||||
|
-- The ladder is SAMPLES PER DISPLAY PIXEL, which is how an AA setting reads
|
||||||
|
-- everywhere else, and the canvas scale each rung costs is its square root:
|
||||||
|
-- 2 samples is a canvas 1.41x wider and taller, 4 is one exactly twice the
|
||||||
|
-- size. OFF is the default -- this is a cost knob, and a mod should not
|
||||||
|
-- quietly spend four times the fill rate of the machine it lands on.
|
||||||
|
AntiAlias.setting = ModSetting.new(AntiAlias.KEY, AntiAlias.LABEL,
|
||||||
|
{ 0, 2, 4 }, { "OFF", "2X", "4X" })
|
||||||
|
|
||||||
|
-- The scale the pass currently open was actually expanded by (see expand).
|
||||||
|
-- 1 while there is no supersampling in force, which is also what every
|
||||||
|
-- reader gets on a frame that never opened a pass at all.
|
||||||
|
local live = 1
|
||||||
|
|
||||||
|
function AntiAlias.samples()
|
||||||
|
return tonumber(AntiAlias.setting:get()) or 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- What the row ASKS for. The scale in force is `factor()`, which is this
|
||||||
|
-- clamped to what the driver will actually allocate.
|
||||||
|
local function wanted()
|
||||||
|
local n = AntiAlias.samples()
|
||||||
|
if n <= 1 then return 1 end
|
||||||
|
return math.sqrt(n)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The biggest canvas this driver admits to, or nil where it will not say.
|
||||||
|
-- A 4K window at 4X asks for 7680 across, which is past the limit on plenty
|
||||||
|
-- of hardware and every phone -- and a refused canvas is not a softer
|
||||||
|
-- diorama, it is beginScene returning false and the whole mode falling back
|
||||||
|
-- to the flat 2D path.
|
||||||
|
local function textureLimit()
|
||||||
|
if not (love.graphics and love.graphics.getSystemLimits) then return nil end
|
||||||
|
local ok, limits = pcall(love.graphics.getSystemLimits)
|
||||||
|
return (ok and limits and limits.texturesize) or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The size to render `w` x `h` display pixels at, and the size everything
|
||||||
|
-- inside the pass then measures itself in.
|
||||||
|
--
|
||||||
|
-- Also where `live` is set, which is why this must be called once per pass
|
||||||
|
-- immediately before beginScene: the wireframe's line width and the FX
|
||||||
|
-- overlay's sprite scale are both quoted in DISPLAY pixels and have to be
|
||||||
|
-- multiplied up into canvas ones, and the honest multiplier is the one this
|
||||||
|
-- returned rather than the one the row asked for.
|
||||||
|
function AntiAlias.expand(w, h)
|
||||||
|
local s = wanted()
|
||||||
|
local max = textureLimit()
|
||||||
|
if max and max > 0 then
|
||||||
|
-- clamped rather than abandoned: a window too big for 4X can usually
|
||||||
|
-- still carry some of it, and half a rung of smoothing is worth more
|
||||||
|
-- than a row that silently does nothing at that size
|
||||||
|
s = math.min(s, max / math.max(1, w), max / math.max(1, h))
|
||||||
|
end
|
||||||
|
if not (s > 1.01) then
|
||||||
|
live = 1
|
||||||
|
return w, h
|
||||||
|
end
|
||||||
|
local ew, eh = math.floor(w * s + 0.5), math.floor(h * s + 0.5)
|
||||||
|
live = ew / math.max(1, w)
|
||||||
|
return ew, eh
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The scale the open pass was expanded by; 1 when it was not.
|
||||||
|
function AntiAlias.factor()
|
||||||
|
return live
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ------- the fold
|
||||||
|
--
|
||||||
|
-- One target per pass (the free-roam world and the battle's arena are alive
|
||||||
|
-- at different moments but reallocating on every battle entry and exit is
|
||||||
|
-- what the scene canvas's own slots exist to avoid), reallocated only when
|
||||||
|
-- that pass's DISPLAY size changes -- a window resize, or the row itself
|
||||||
|
-- moving, which changes the source and not this.
|
||||||
|
|
||||||
|
local targets = {}
|
||||||
|
|
||||||
|
local function targetFor(slot, w, h)
|
||||||
|
local t = targets[slot]
|
||||||
|
if not (t and t.w == w and t.h == h) then
|
||||||
|
local ok, c = pcall(love.graphics.newCanvas, w, h)
|
||||||
|
if not (ok and c) then return nil end
|
||||||
|
-- nearest, like the canvas it stands in for: this one is composited a
|
||||||
|
-- canvas pixel to a display pixel, and the smoothing has already happened
|
||||||
|
pcall(c.setFilter, c, "nearest", "nearest")
|
||||||
|
if t and t.canvas and t.canvas.release then pcall(t.canvas.release, t.canvas) end
|
||||||
|
t = { canvas = c, w = w, h = h }
|
||||||
|
targets[slot] = t
|
||||||
|
end
|
||||||
|
return t.canvas
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The box filter, and the whole of why it is a shader rather than a scaled
|
||||||
|
-- draw with linear filtering on.
|
||||||
|
--
|
||||||
|
-- The void this pass renders into is cleared to a TRANSPARENT BLACK, and at
|
||||||
|
-- the rungs below FULL a good deal of the frame is still that. Averaging a
|
||||||
|
-- straight-alpha edge against it drags the result toward black as well as
|
||||||
|
-- toward transparent, and then the engine's own composite multiplies by that
|
||||||
|
-- alpha a second time -- so every silhouette against the void would come out
|
||||||
|
-- ringed with a dark fringe, which is exactly the artefact the row is here to
|
||||||
|
-- remove.
|
||||||
|
--
|
||||||
|
-- So the taps are premultiplied before they are averaged and divided back out
|
||||||
|
-- after, which is the arithmetic that makes an edge pixel mean "half covered
|
||||||
|
-- by this colour" instead of "covered by half of this colour".
|
||||||
|
--
|
||||||
|
-- Four taps, half a source texel from the destination centre. At 4X those
|
||||||
|
-- land dead on the four texel centres the destination pixel covers, so it is
|
||||||
|
-- an exact 2x2 box; at 2X the source grid does not divide, and the bilinear
|
||||||
|
-- fetch under each tap widens the box a little rather than missing samples.
|
||||||
|
local SHADER = [[
|
||||||
|
uniform vec2 tap; // half a SOURCE texel, in uv
|
||||||
|
vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) {
|
||||||
|
vec4 a = Texel(tex, tc + vec2(-tap.x, -tap.y));
|
||||||
|
vec4 b = Texel(tex, tc + vec2( tap.x, -tap.y));
|
||||||
|
vec4 c = Texel(tex, tc + vec2(-tap.x, tap.y));
|
||||||
|
vec4 d = Texel(tex, tc + vec2( tap.x, tap.y));
|
||||||
|
float al = (a.a + b.a + c.a + d.a) * 0.25;
|
||||||
|
if (al <= 0.0) return vec4(0.0);
|
||||||
|
vec3 sum = a.rgb * a.a + b.rgb * b.a + c.rgb * c.a + d.rgb * d.a;
|
||||||
|
return vec4(sum * 0.25 / al, al) * color;
|
||||||
|
}
|
||||||
|
]]
|
||||||
|
|
||||||
|
local shader = nil -- nil = untried, false = unavailable
|
||||||
|
|
||||||
|
local function getShader()
|
||||||
|
if shader == nil then
|
||||||
|
local ok, sh = pcall(love.graphics.newShader, SHADER)
|
||||||
|
shader = (ok and sh) or false
|
||||||
|
end
|
||||||
|
return shader or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Fold `canvas` down to `w` x `h` and hand back the result.
|
||||||
|
--
|
||||||
|
-- Returns the input untouched when there is nothing to fold -- the row is
|
||||||
|
-- off, or the canvas already IS that size -- so a caller can run it
|
||||||
|
-- unconditionally, and so can a headless test run. A target that would not
|
||||||
|
-- allocate is the same answer: the pass is lost either way if this hands back
|
||||||
|
-- something the wrong size, so it hands back the input and the frame draws at
|
||||||
|
-- the size it was rendered.
|
||||||
|
function AntiAlias.resolve(canvas, w, h, slot)
|
||||||
|
if not canvas then return canvas end
|
||||||
|
local ok, cw, ch = pcall(canvas.getDimensions, canvas)
|
||||||
|
if not ok or (cw == w and ch == h) then return canvas end
|
||||||
|
local target = targetFor(slot or "world", w, h)
|
||||||
|
if not target then return canvas end
|
||||||
|
|
||||||
|
local sh = getShader()
|
||||||
|
local prevBlend, prevAlpha = love.graphics.getBlendMode()
|
||||||
|
-- the scene canvas filters nearest for its usual 1:1 blit; the taps want
|
||||||
|
-- linear, put back below so every other pass finds what it expects
|
||||||
|
pcall(canvas.setFilter, canvas, "linear", "linear")
|
||||||
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
|
-- replace, not alpha-blend: this is an image-processing copy, and the alpha
|
||||||
|
-- the shader worked out has to land as itself rather than be composited
|
||||||
|
-- against whatever the target held
|
||||||
|
love.graphics.setBlendMode("replace", "premultiplied")
|
||||||
|
if sh then
|
||||||
|
love.graphics.setShader(sh)
|
||||||
|
pcall(sh.send, sh, "tap", { 0.5 / cw, 0.5 / ch })
|
||||||
|
end
|
||||||
|
local drew = pcall(function()
|
||||||
|
love.graphics.setCanvas(target)
|
||||||
|
love.graphics.clear(0, 0, 0, 0)
|
||||||
|
love.graphics.draw(canvas, 0, 0, 0, w / cw, h / ch)
|
||||||
|
end)
|
||||||
|
love.graphics.setCanvas()
|
||||||
|
love.graphics.setShader()
|
||||||
|
love.graphics.setBlendMode(prevBlend or "alpha", prevAlpha)
|
||||||
|
pcall(canvas.setFilter, canvas, "nearest", "nearest")
|
||||||
|
return drew and target or canvas
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Drop the GPU objects (window resize, hot reload).
|
||||||
|
function AntiAlias.invalidate()
|
||||||
|
for slot, t in pairs(targets) do
|
||||||
|
if t.canvas and t.canvas.release then pcall(t.canvas.release, t.canvas) end
|
||||||
|
targets[slot] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function AntiAlias.row()
|
||||||
|
return AntiAlias.setting:row()
|
||||||
|
end
|
||||||
|
|
||||||
|
return AntiAlias
|
||||||
+97
-16
@@ -57,6 +57,26 @@
|
|||||||
-- and they come back untouched because that is what their own shape says, not
|
-- and they come back untouched because that is what their own shape says, not
|
||||||
-- because they were special-cased.
|
-- because they were special-cased.
|
||||||
--
|
--
|
||||||
|
-- The drain/mouth cut is for a pic STANDING ON THE MAP, where a mouth is a
|
||||||
|
-- real hole with real ground behind it. A pic PINNED TO THE MENU has no such
|
||||||
|
-- hole to be: under BACK SPRITES the player's mon is drawn in the GB's own
|
||||||
|
-- slot with its feet flush on the text box (BattleState.backPlacement pins
|
||||||
|
-- row 96), so the only thing under its lowest row is white box. Nothing can
|
||||||
|
-- reach it from below, whatever the opening's width, and the caller says so
|
||||||
|
-- by asking for a SEALED BOTTOM -- for which the rule stops being a heuristic
|
||||||
|
-- and becomes exact: paper is whatever the background cannot walk to from the
|
||||||
|
-- left, the right or the top.
|
||||||
|
--
|
||||||
|
-- That is the difference between a Pikachu that reads as a mon and one that
|
||||||
|
-- reads as wireframe. The pale-bodied back pics -- Pikachu, Seel, Dewgong,
|
||||||
|
-- Chansey, Jigglypuff -- are drawn as OUTLINES: everything inside the ink is
|
||||||
|
-- shade 0 and every one of them is keyed away, so the figure is a rim with the
|
||||||
|
-- arena showing through it. Each one also has a wide opening along its bottom,
|
||||||
|
-- which the drain cut correctly reads as a mouth and the sealed bottom
|
||||||
|
-- correctly does not. Twelve of this game's 151 back pics turn on it; the
|
||||||
|
-- other 139 come back byte-identical either way, because they had nothing
|
||||||
|
-- under them the flood was getting in through.
|
||||||
|
--
|
||||||
-- The silhouette is untouched, so the mon still cuts cleanly against the
|
-- The silhouette is untouched, so the mon still cuts cleanly against the
|
||||||
-- world; only its insides stop being see-through.
|
-- world; only its insides stop being see-through.
|
||||||
--
|
--
|
||||||
@@ -71,14 +91,23 @@ local V = ...
|
|||||||
|
|
||||||
local BattlePics = {}
|
local BattlePics = {}
|
||||||
|
|
||||||
-- Cached by the image the engine handed over. Weak keys, so a pic that goes
|
-- Cached by the image the engine handed over, one table per bottom rule --
|
||||||
-- out of scope takes its filled twin with it rather than pinning a texture
|
-- the same pic answers differently sealed and unsealed, and a single table
|
||||||
-- for the session.
|
-- would hand the wrong twin back to whichever caller asked second. Weak keys,
|
||||||
local cache = setmetatable({}, { __mode = "k" })
|
-- so a pic that goes out of scope takes its filled twin with it rather than
|
||||||
|
-- pinning a texture for the session.
|
||||||
|
local function newCache()
|
||||||
|
return {
|
||||||
|
[false] = setmetatable({}, { __mode = "k" }),
|
||||||
|
[true] = setmetatable({}, { __mode = "k" }),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
local cache = newCache()
|
||||||
|
|
||||||
-- What an enclosed hole is filled with. White, because white is what the
|
-- What an enclosed hole is filled with when the pic itself offers nothing
|
||||||
-- battle field was: this restores the pixel the artist drew and the engine
|
-- better. White, because white is what the battle field was: this restores the
|
||||||
-- then keyed away, it does not invent a new one.
|
-- pixel the artist drew and the engine then keyed away, it does not invent a
|
||||||
|
-- new one.
|
||||||
BattlePics.FILL = { 1, 1, 1, 1 }
|
BattlePics.FILL = { 1, 1, 1, 1 }
|
||||||
|
|
||||||
-- Anything at or under this alpha counts as keyed-out rather than drawn.
|
-- Anything at or under this alpha counts as keyed-out rather than drawn.
|
||||||
@@ -154,6 +183,42 @@ local function inkBounds(data, w, h)
|
|||||||
return x0, y0, x1, y1
|
return x0, y0, x1, y1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- The colour the keyed-away shade would have had: the LIGHTEST colour still
|
||||||
|
-- standing in the pic.
|
||||||
|
--
|
||||||
|
-- Pure white is only the right answer while the pic is still grays, and by the
|
||||||
|
-- time it reaches here it usually is not. picImage hands a pic over AFTER the
|
||||||
|
-- bake -- a species SGB colour, a BGP fade mid-animation, PAL_BLACK for the
|
||||||
|
-- whole screen while the blackout text is up -- and shade 0 travels with the
|
||||||
|
-- rest. A white belly inside a blacked-out mon would be the one lit thing on a
|
||||||
|
-- dark screen; inside a warm-palette mon it would be a cold patch the artist
|
||||||
|
-- never drew.
|
||||||
|
--
|
||||||
|
-- So the paper is read off the pic rather than assumed, which needs shade 0 to
|
||||||
|
-- have survived somewhere in it. It always has: every one of this game's 151
|
||||||
|
-- back pics keeps at least one opaque shade-0 pixel -- a highlight down a
|
||||||
|
-- cheek, the white of an eye -- because only the shade-0 pixels the decoder
|
||||||
|
-- could reach were keyed. So what comes back is the baked shade 0 itself, not
|
||||||
|
-- an approximation of it, and it tracks every palette the engine picks without
|
||||||
|
-- being told which one that was.
|
||||||
|
--
|
||||||
|
-- Ranked by channel sum, which orders four DMG shades exactly: a palette maps
|
||||||
|
-- all three channels monotonically, so lightest by sum is lightest full stop.
|
||||||
|
local function paperColor(data, x0, y0, x1, y1)
|
||||||
|
local best, pr, pg, pb = -1, nil, nil, nil
|
||||||
|
for y = y0, y1 do
|
||||||
|
for x = x0, x1 do
|
||||||
|
local r, g, b, a = data:getPixel(x, y)
|
||||||
|
if a > CUT then
|
||||||
|
local lum = r + g + b
|
||||||
|
if lum > best then best, pr, pg, pb = lum, r, g, b end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if best < 0 then return nil end
|
||||||
|
return pr, pg, pb
|
||||||
|
end
|
||||||
|
|
||||||
-- The widest opening along the bottom of a figure that still counts as a drain
|
-- The widest opening along the bottom of a figure that still counts as a drain
|
||||||
-- rather than a mouth. See the header for the measurements either side of it.
|
-- rather than a mouth. See the header for the measurements either side of it.
|
||||||
BattlePics.DRAIN = 6
|
BattlePics.DRAIN = 6
|
||||||
@@ -161,7 +226,9 @@ BattlePics.DRAIN = 6
|
|||||||
-- Mark every transparent pixel the BACKGROUND can reach, flooding inward from
|
-- Mark every transparent pixel the BACKGROUND can reach, flooding inward from
|
||||||
-- the edges of the artwork's box: the left, the right and the top whole, and
|
-- the edges of the artwork's box: the left, the right and the top whole, and
|
||||||
-- along the bottom only those openings wide enough to be background rather
|
-- along the bottom only those openings wide enough to be background rather
|
||||||
-- than the underside of a figure the drawing ran out of.
|
-- than the underside of a figure the drawing ran out of -- or none of them at
|
||||||
|
-- all, for a pic whose feet are on the text box and which therefore has
|
||||||
|
-- nothing behind its lowest row to let in.
|
||||||
--
|
--
|
||||||
-- Confined to the box as well as seeded from it, so the empty frame under a
|
-- Confined to the box as well as seeded from it, so the empty frame under a
|
||||||
-- short pic cannot walk around a sealed drain and come back up through it.
|
-- short pic cannot walk around a sealed drain and come back up through it.
|
||||||
@@ -169,7 +236,7 @@ BattlePics.DRAIN = 6
|
|||||||
-- An explicit stack rather than recursion: a 56x56 pic is three thousand
|
-- An explicit stack rather than recursion: a 56x56 pic is three thousand
|
||||||
-- pixels and a keyed-out background is most of them, which is a deeper call
|
-- pixels and a keyed-out background is most of them, which is a deeper call
|
||||||
-- chain than is worth risking for no gain.
|
-- chain than is worth risking for no gain.
|
||||||
local function markOutside(data, w, h, x0, y0, x1, y1)
|
local function markOutside(data, w, h, x0, y0, x1, y1, sealBottom)
|
||||||
local outside = {}
|
local outside = {}
|
||||||
local stack, top = {}, 0
|
local stack, top = {}, 0
|
||||||
local function clear(x, y)
|
local function clear(x, y)
|
||||||
@@ -191,7 +258,10 @@ local function markOutside(data, w, h, x0, y0, x1, y1)
|
|||||||
push(x1, y)
|
push(x1, y)
|
||||||
end
|
end
|
||||||
-- the bottom, run by run: a wide one is the gap between two legs and lets
|
-- the bottom, run by run: a wide one is the gap between two legs and lets
|
||||||
-- the world through, a narrow one is where a belly ran out and is sealed
|
-- the world through, a narrow one is where a belly ran out and is sealed.
|
||||||
|
-- Skipped whole for a pic on the box, where even the widest of them has
|
||||||
|
-- white paper behind it rather than arena.
|
||||||
|
if not sealBottom then
|
||||||
local x = x0
|
local x = x0
|
||||||
while x <= x1 do
|
while x <= x1 do
|
||||||
if clear(x, y1) then
|
if clear(x, y1) then
|
||||||
@@ -204,6 +274,7 @@ local function markOutside(data, w, h, x0, y0, x1, y1)
|
|||||||
x = x + 1
|
x = x + 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
while top > 0 do
|
while top > 0 do
|
||||||
local key = stack[top]
|
local key = stack[top]
|
||||||
top = top - 1
|
top = top - 1
|
||||||
@@ -219,9 +290,15 @@ end
|
|||||||
-- The pic with its enclosed holes filled, or the pic itself when that could
|
-- The pic with its enclosed holes filled, or the pic itself when that could
|
||||||
-- not be done (no pixel access, a driver that refused the readback). Never
|
-- not be done (no pixel access, a driver that refused the readback). Never
|
||||||
-- nil for a non-nil argument: a caller must always have something to draw.
|
-- nil for a non-nil argument: a caller must always have something to draw.
|
||||||
function BattlePics.filled(img)
|
--
|
||||||
|
-- sealBottom for a pic pinned to the text box rather than standing on the map:
|
||||||
|
-- see the header. A caller that does not say defaults to the map, which is
|
||||||
|
-- where all but one of this mod's pics are.
|
||||||
|
function BattlePics.filled(img, sealBottom)
|
||||||
if not img then return img end
|
if not img then return img end
|
||||||
local hit = cache[img]
|
sealBottom = sealBottom and true or false
|
||||||
|
local slot = cache[sealBottom]
|
||||||
|
local hit = slot[img]
|
||||||
if hit ~= nil then return hit or img end
|
if hit ~= nil then return hit or img end
|
||||||
|
|
||||||
local made = nil
|
local made = nil
|
||||||
@@ -231,8 +308,12 @@ function BattlePics.filled(img)
|
|||||||
local w, h = data:getDimensions()
|
local w, h = data:getDimensions()
|
||||||
local x0, y0, x1, y1 = inkBounds(data, w, h)
|
local x0, y0, x1, y1 = inkBounds(data, w, h)
|
||||||
if not x0 then return end -- a pic with nothing drawn in it
|
if not x0 then return end -- a pic with nothing drawn in it
|
||||||
local outside = markOutside(data, w, h, x0, y0, x1, y1)
|
local outside = markOutside(data, w, h, x0, y0, x1, y1, sealBottom)
|
||||||
local fill = BattlePics.FILL
|
local fill = BattlePics.FILL
|
||||||
|
local pr, pg, pb = paperColor(data, x0, y0, x1, y1)
|
||||||
|
local fr = pr or fill[1]
|
||||||
|
local fg = pg or fill[2]
|
||||||
|
local fb = pb or fill[3]
|
||||||
local changed = false
|
local changed = false
|
||||||
-- only inside the box: everything beyond it is frame the artist never
|
-- only inside the box: everything beyond it is frame the artist never
|
||||||
-- reached, and filling that would put the mon in a white rectangle
|
-- reached, and filling that would put the mon in a white rectangle
|
||||||
@@ -242,7 +323,7 @@ function BattlePics.filled(img)
|
|||||||
if not outside[row + x] then
|
if not outside[row + x] then
|
||||||
local _, _, _, a = data:getPixel(x, y)
|
local _, _, _, a = data:getPixel(x, y)
|
||||||
if a <= CUT then
|
if a <= CUT then
|
||||||
data:setPixel(x, y, fill[1], fill[2], fill[3], fill[4])
|
data:setPixel(x, y, fr, fg, fb, fill[4])
|
||||||
changed = true
|
changed = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -255,12 +336,12 @@ function BattlePics.filled(img)
|
|||||||
made = out
|
made = out
|
||||||
end)
|
end)
|
||||||
|
|
||||||
cache[img] = (ok and made) or false
|
slot[img] = (ok and made) or false
|
||||||
return made or img
|
return made or img
|
||||||
end
|
end
|
||||||
|
|
||||||
function BattlePics.invalidate()
|
function BattlePics.invalidate()
|
||||||
cache = setmetatable({}, { __mode = "k" })
|
cache = newCache()
|
||||||
end
|
end
|
||||||
|
|
||||||
return BattlePics
|
return BattlePics
|
||||||
|
|||||||
+12
-2
@@ -42,6 +42,7 @@ local BattleCam = V.require("BattleCam")
|
|||||||
local BattleBillboard = V.require("BattleBillboard")
|
local BattleBillboard = V.require("BattleBillboard")
|
||||||
local VoxelGrid = V.require("VoxelGrid")
|
local VoxelGrid = V.require("VoxelGrid")
|
||||||
local DayNight = V.require("DayNight")
|
local DayNight = V.require("DayNight")
|
||||||
|
local AntiAlias = V.require("AntiAlias")
|
||||||
local PaletteFX = require("src.render.PaletteFX")
|
local PaletteFX = require("src.render.PaletteFX")
|
||||||
local Map = require("src.world.Map")
|
local Map = require("src.world.Map")
|
||||||
|
|
||||||
@@ -427,7 +428,16 @@ function BattleScene.render(state, arena, textures, token)
|
|||||||
-- its own canvas slot: this renders at the window's pixel size and the
|
-- its own canvas slot: this renders at the window's pixel size and the
|
||||||
-- free-roam pass does too, but the two are alive at different moments
|
-- free-roam pass does too, but the two are alive at different moments
|
||||||
-- and a shared slot would reallocate on every battle entry and exit
|
-- and a shared slot would reallocate on every battle entry and exit
|
||||||
if not Voxel3D.beginScene(pw, ph, cx, cy, vw, vh, sky, "battle") then
|
--
|
||||||
|
-- AA, if the row asks for it, renders it larger still and folds it back
|
||||||
|
-- to pw x ph below (see AntiAlias). The framing is untouched by that:
|
||||||
|
-- the lens was widened by the window's RATIO to the letterbox and the
|
||||||
|
-- rig solved in the GB's own frame, so a bigger canvas is more samples
|
||||||
|
-- of the identical shot -- which is why the pins below still measure in
|
||||||
|
-- pw and ph, and why the HUDs and the depth of field, drawn onto the
|
||||||
|
-- folded canvas afterwards, stay the chunky GB art they are.
|
||||||
|
local rw, rh = AntiAlias.expand(pw, ph)
|
||||||
|
if not Voxel3D.beginScene(rw, rh, cx, cy, vw, vh, sky, "battle") then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
Voxel3D.draw(terrain, atlasFor(host), nil)
|
Voxel3D.draw(terrain, atlasFor(host), nil)
|
||||||
@@ -500,7 +510,7 @@ function BattleScene.render(state, arena, textures, token)
|
|||||||
Mat4.translate(nb.ox, 0, nb.oy), fpull,
|
Mat4.translate(nb.ox, 0, nb.oy), fpull,
|
||||||
ShadowMap.snug(Mat4.translate(nb.ox, 0, nb.oy)))
|
ShadowMap.snug(Mat4.translate(nb.ox, 0, nb.oy)))
|
||||||
end
|
end
|
||||||
local canvas = Voxel3D.endScene()
|
local canvas = AntiAlias.resolve(Voxel3D.endScene(), pw, ph, "battle")
|
||||||
if not canvas then return end
|
if not canvas then return end
|
||||||
|
|
||||||
local vp = Voxel3D.vp
|
local vp = Voxel3D.vp
|
||||||
|
|||||||
+180
-6
@@ -190,7 +190,24 @@ local function measure(sp, t)
|
|||||||
top[x] = r
|
top[x] = r
|
||||||
end
|
end
|
||||||
|
|
||||||
local wallH = H - roofRows
|
-- The drawing's own ground line: the row after the last drawn one. A
|
||||||
|
-- building ends on the black threshold row it stands on (ground == H),
|
||||||
|
-- but furniture is drawn standing on open floor -- the lab table's
|
||||||
|
-- legs stop two rows short of its grid -- and extruding against H
|
||||||
|
-- would float it that far above its own plot.
|
||||||
|
local ground = roofRows
|
||||||
|
for sy = H - 1, roofRows, -1 do
|
||||||
|
local drawn = false
|
||||||
|
for sx = 0, W - 1 do
|
||||||
|
if sp.inside[sy * W + sx] then drawn = true break end
|
||||||
|
end
|
||||||
|
if drawn then
|
||||||
|
ground = sy + 1
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local wallH = ground - roofRows
|
||||||
local ytop = wallH - 1 + t.slab
|
local ytop = wallH - 1 + t.slab
|
||||||
|
|
||||||
-- Side faces must not come out as slabs of outline black: where the
|
-- Side faces must not come out as slabs of outline black: where the
|
||||||
@@ -279,20 +296,175 @@ local function measure(sp, t)
|
|||||||
-- sprite taller than its footprint -- the tower's 16-row drawing
|
-- sprite taller than its footprint -- the tower's 16-row drawing
|
||||||
-- stands on the 8 rows of it that are actually on the map, and D = H
|
-- stands on the 8 rows of it that are actually on the map, and D = H
|
||||||
-- would have pushed its body 64px south into the town plaza.
|
-- would have pushed its body 64px south into the town plaza.
|
||||||
return { top = top, ytop = ytop, D = #t.tiles * 8,
|
-- `depth` (in tile rows) names the plot when the grid runs PAST it
|
||||||
|
-- onto ground the drawing merely stands its legs on: the lab table's
|
||||||
|
-- third row is the walkable cell the player faces it from, and the
|
||||||
|
-- full-grid depth would stand the model in their path.
|
||||||
|
return { top = top, ytop = ytop, D = (t.depth or #t.tiles) * 8,
|
||||||
|
ground = ground,
|
||||||
recess = recess, interior = interior, shadeTexel = shadeTexel }
|
recess = recess, interior = interior, shadeTexel = shadeTexel }
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ----------------------------------------------------------------- build --
|
-- ----------------------------------------------------------------- build --
|
||||||
|
|
||||||
|
-- A desk with separately-classified objects on it (a template's `parts`
|
||||||
|
-- list): the methodology's region classification at part granularity.
|
||||||
|
-- Upright parts anchor their drawn bottom row to the desk's top plane
|
||||||
|
-- and wear their own drawn tops as lids; flat parts (a keyboard, a
|
||||||
|
-- sheet of paper) lie one voxel proud at drawn row = depth row -- the
|
||||||
|
-- same 1:1 the tabletop itself is drawn with, so an object's height ON
|
||||||
|
-- the drawing is its position ON the desk. The desk is the lab-table
|
||||||
|
-- slab + base; its lid is the one synthesized surface in the model
|
||||||
|
-- (the objects cover every drawn pixel of the tabletop), continued
|
||||||
|
-- from the sibling tables' pattern in the drawing's own shades.
|
||||||
|
-- tools/building_voxels.py `build_desk_set` is the reference twin.
|
||||||
|
local function deskSetModel(sp, pr, t)
|
||||||
|
local W, H, D = sp.W, sp.H, pr.D
|
||||||
|
local ground = pr.ground
|
||||||
|
local col, inside = sp.col, sp.inside
|
||||||
|
local vox = {}
|
||||||
|
local function key(x, y, z) return (y * D + z) * W + x end
|
||||||
|
local function put(x, y, z, i) vox[key(x, y, z)] = i end
|
||||||
|
|
||||||
|
-- de-outline walk bounded to the part, so a part's side faces show
|
||||||
|
-- its own material and never the neighbour's (the sprite-wide walk
|
||||||
|
-- the facade path uses would cross the black seam between units)
|
||||||
|
local function interiorAt(sx, sy, lo, hi)
|
||||||
|
local i = sy * W + sx
|
||||||
|
if col[i] ~= BLACK then return sx end
|
||||||
|
local step = sx < math.floor((lo + hi) / 2) and 1 or -1
|
||||||
|
for d = 1, 3 do
|
||||||
|
local nx = sx + step * d
|
||||||
|
if nx >= lo and nx <= hi then
|
||||||
|
local ni = sy * W + nx
|
||||||
|
if inside[ni] and col[ni] ~= BLACK then return nx end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return sx
|
||||||
|
end
|
||||||
|
|
||||||
|
local f0, f1 = t.desk.fascia[1], t.desk.fascia[2]
|
||||||
|
local b0, b1 = t.desk.base[1], t.desk.base[2]
|
||||||
|
local plane = (b1 - b0 + 1) + (f1 - f0 + 1)
|
||||||
|
|
||||||
|
-- the base band, extruded exactly like every lab table's
|
||||||
|
for sy = b0, b1 do
|
||||||
|
Budget.tick()
|
||||||
|
local y = ground - 1 - sy
|
||||||
|
for sx = 0, W - 1 do
|
||||||
|
if inside[sy * W + sx] then
|
||||||
|
local ix = interiorAt(sx, sy, 0, W - 1)
|
||||||
|
for z = 0, D - 1 do
|
||||||
|
local px = (z == 0 or z == D - 1) and sx or ix
|
||||||
|
put(sx, y, z, sy * W + px)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for i in pairs(pr.recess) do
|
||||||
|
local sy = math.floor(i / W)
|
||||||
|
if sy >= b0 and sy <= b1 then
|
||||||
|
vox[key(i % W, ground - 1 - sy, D - 1)] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- the slab: fascia rows wrap every side; the lid continues the
|
||||||
|
-- sibling tables' top -- black rim, white highlight courses along
|
||||||
|
-- the north and west, grey field
|
||||||
|
for sy = f0, f1 do
|
||||||
|
Budget.tick()
|
||||||
|
local y = plane - 1 - (sy - f0)
|
||||||
|
for sx = 0, W - 1 do
|
||||||
|
for z = 0, D - 1 do put(sx, y, z, sy * W + sx) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local field = t.desk.lid == "white" and WHITE or GREY
|
||||||
|
for sx = 0, W - 1 do
|
||||||
|
for z = 0, D - 1 do
|
||||||
|
local shade = field
|
||||||
|
if sx == 0 or sx == W - 1 or z == 0 or z == D - 1 then
|
||||||
|
shade = BLACK
|
||||||
|
elseif sx == 1 or z == 1 then
|
||||||
|
shade = WHITE
|
||||||
|
end
|
||||||
|
put(sx, plane - 1, z, pr.shadeTexel[shade])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local ytop = plane
|
||||||
|
for _, p in ipairs(t.parts) do
|
||||||
|
Budget.tick()
|
||||||
|
local x0, x1 = p.x[1], p.x[2]
|
||||||
|
if p.kind == "flat" then
|
||||||
|
-- drawn row = depth row by default; `z` renames the origin when
|
||||||
|
-- the flat sits below the desk's own drawn top span (the Center
|
||||||
|
-- PC's keyboard)
|
||||||
|
local r0 = p.rows[1]
|
||||||
|
local z0 = p.z or r0
|
||||||
|
for sy = r0, p.rows[2] do
|
||||||
|
local z = z0 + (sy - r0)
|
||||||
|
if z >= 0 and z < D then
|
||||||
|
for sx = x0, x1 do
|
||||||
|
if inside[sy * W + sx] then put(sx, plane, z, sy * W + sx) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local tr0, tr1 = p.top[1], p.top[2]
|
||||||
|
local fr0, fr1 = p.facade[1], p.facade[2]
|
||||||
|
local pd = p.depth
|
||||||
|
local ytp = plane + (fr1 - fr0)
|
||||||
|
if ytp > ytop then ytop = ytp end
|
||||||
|
for sx = x0, x1 do
|
||||||
|
-- the lid: the part's drawn top laid across its depth from the
|
||||||
|
-- back, last row continuing forward; the front lid row is the
|
||||||
|
-- facade's own top row -- the drawn front-top edge
|
||||||
|
for z = 0, pd - 1 do
|
||||||
|
local front = z == pd - 1
|
||||||
|
local sy = front and fr0 or math.min(tr0 + z, tr1)
|
||||||
|
while sy <= tr1 and not inside[sy * W + sx] do sy = sy + 1 end
|
||||||
|
local ok = sy <= tr1 or (front and inside[fr0 * W + sx])
|
||||||
|
if ok then
|
||||||
|
put(sx, ytp, z, (front and fr0 or sy) * W + sx)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- the body: facade rows anchored to the desk's top plane
|
||||||
|
for sy = fr0 + 1, fr1 do
|
||||||
|
local y = plane + (fr1 - sy)
|
||||||
|
local i = sy * W + sx
|
||||||
|
if inside[i] then
|
||||||
|
local ix = interiorAt(sx, sy, x0, x1)
|
||||||
|
for z = 0, pd - 1 do
|
||||||
|
if z == pd - 1 then
|
||||||
|
if not pr.recess[i] then put(sx, y, z, i) end
|
||||||
|
elseif z == 0 then
|
||||||
|
put(sx, y, z, i)
|
||||||
|
else
|
||||||
|
put(sx, y, z, sy * W + ix)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return { at = function(x, y, z)
|
||||||
|
if x < 0 or x >= W or y < 0 or z < 0 or z >= D then return nil end
|
||||||
|
return vox[key(x, y, z)]
|
||||||
|
end,
|
||||||
|
W = W, ytop = ytop, zmin = 0, zmax = D - 1 }
|
||||||
|
end
|
||||||
|
|
||||||
-- The voxel model as a lookup: `at(x, y, z)` is the index of the sprite
|
-- The voxel model as a lookup: `at(x, y, z)` is the index of the sprite
|
||||||
-- pixel that voxel wears, or nil. Build ORDER is expressed as lookup
|
-- pixel that voxel wears, or nil. Build ORDER is expressed as lookup
|
||||||
-- order -- roof first, so it overwrites the walls it intersects, and walls
|
-- order -- roof first, so it overwrites the walls it intersects, and walls
|
||||||
-- are trimmed to its underside so nothing pokes through the surface.
|
-- are trimmed to its underside so nothing pokes through the surface.
|
||||||
local function model(sp, pr, t)
|
local function model(sp, pr, t)
|
||||||
|
if t.parts then return deskSetModel(sp, pr, t) end
|
||||||
local W, H, D = sp.W, sp.H, pr.D
|
local W, H, D = sp.W, sp.H, pr.D
|
||||||
local slab, roofRows = t.slab, t.roofRows
|
local slab, roofRows = t.slab, t.roofRows
|
||||||
local top, ytop = pr.top, pr.ytop
|
local top, ytop, ground = pr.top, pr.ytop, pr.ground
|
||||||
|
|
||||||
-- The roof's drawn span. A sprite inset from its box (B03) leaves outer
|
-- The roof's drawn span. A sprite inset from its box (B03) leaves outer
|
||||||
-- columns undrawn in the roof band; they carry no roof at all, and the
|
-- columns undrawn in the roof band; they carry no roof at all, and the
|
||||||
@@ -367,16 +539,18 @@ local function model(sp, pr, t)
|
|||||||
|
|
||||||
-- the awning: the band juts two voxels past the walls, front and back
|
-- the awning: the band juts two voxels past the walls, front and back
|
||||||
if ledge0 and (z == -2 or z == -1 or z == D or z == D + 1) then
|
if ledge0 and (z == -2 or z == -1 or z == D or z == D + 1) then
|
||||||
local sy = H - 1 - y
|
local sy = ground - 1 - y
|
||||||
if sy >= ledge0 and sy <= ledge1 and sp.inside[sy * W + x] then
|
if sy >= ledge0 and sy <= ledge1 and sp.inside[sy * W + x] then
|
||||||
return sy * W + x
|
return sy * W + x
|
||||||
end
|
end
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- the facade, extruded straight back over the footprint
|
-- the facade, extruded straight back over the footprint. Rows map
|
||||||
|
-- against the measured ground line, not the grid's last row: the two
|
||||||
|
-- differ only for furniture standing on open floor (see measure).
|
||||||
if z < 0 or z >= D then return nil end
|
if z < 0 or z >= D then return nil end
|
||||||
local sy = H - 1 - y
|
local sy = ground - 1 - y
|
||||||
local i = sy * W + x
|
local i = sy * W + x
|
||||||
if y == 0 and not sp.inside[i] and sy > 0 and sp.inside[i - W] then
|
if y == 0 and not sp.inside[i] and sy > 0 and sp.inside[i - W] then
|
||||||
-- the drawing's last row is the ground the building stands on, so
|
-- the drawing's last row is the ground the building stands on, so
|
||||||
|
|||||||
+32
-1
@@ -104,6 +104,31 @@ function OverworldBattle.backPinned()
|
|||||||
return OverworldBattle.backSetting:get() and true or false
|
return OverworldBattle.backSetting:get() and true or false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Whether a pic is the one drawn in the GB's own slot with its feet on the
|
||||||
|
-- text box, rather than geometry standing out on the map.
|
||||||
|
--
|
||||||
|
-- Exactly the player's side under BACK SPRITES -- its mon, or the trainer back
|
||||||
|
-- that holds the slot until "Go!" -- because that is the only pic this mod
|
||||||
|
-- ever leaves flat (see drawPicsLayer below). The foe is a billboard on its
|
||||||
|
-- tile whichever mode is on, and with the mode off the player's side is one
|
||||||
|
-- too, so both of those keep the open bottom that lets the arena through a
|
||||||
|
-- stride. What the answer buys is in BattlePics: a pic on the box has nothing
|
||||||
|
-- behind its lowest row, so its bottom edge seals.
|
||||||
|
-- Read by TRUTHINESS rather than against nil, because sideTexture blanks the
|
||||||
|
-- side it is not rendering by setting the field to FALSE (see OFF) and holds
|
||||||
|
-- it that way for the whole render -- during which the pic layer runs, and
|
||||||
|
-- picImage asks this. A nil test passes a `false` straight through to the
|
||||||
|
-- index below, and the error comes out of sideTexture into the pcall that
|
||||||
|
-- calls it: the foe's billboard is dropped for the frame and the Pokemon
|
||||||
|
-- simply is not there.
|
||||||
|
function OverworldBattle.pinnedPic(battle, img)
|
||||||
|
if not (battle and img) then return false end
|
||||||
|
if not OverworldBattle.backPinned() then return false end
|
||||||
|
if img == battle.playerBackPic then return true end
|
||||||
|
local player = battle.player
|
||||||
|
return (player and img == player.sprite) and true or false
|
||||||
|
end
|
||||||
|
|
||||||
-- ------- both mons face you
|
-- ------- both mons face you
|
||||||
--
|
--
|
||||||
-- Standing on a map, seen from in front, a Pokemon showing you its BACK is
|
-- Standing on a map, seen from in front, a Pokemon showing you its BACK is
|
||||||
@@ -831,11 +856,17 @@ function OverworldBattle.install()
|
|||||||
-- behind it. There is a world back there now, so they are filled here
|
-- behind it. There is a world back there now, so they are filled here
|
||||||
-- instead -- see BattlePics, which puts the paper back without touching
|
-- instead -- see BattlePics, which puts the paper back without touching
|
||||||
-- the silhouette.
|
-- the silhouette.
|
||||||
|
--
|
||||||
|
-- The pinned pic is told that its feet are on the box, which is what lets
|
||||||
|
-- the pale-bodied back sprites be filled at all: their bellies leak out
|
||||||
|
-- through an opening too wide to read as a drain, and only the box under
|
||||||
|
-- them settles that it is not a hole. Passed the pre-bake image, because
|
||||||
|
-- that is the one the battle holds a reference to.
|
||||||
local innerPic = BattleState.picImage
|
local innerPic = BattleState.picImage
|
||||||
function BattleState:picImage(img)
|
function BattleState:picImage(img)
|
||||||
local out = innerPic(self, img)
|
local out = innerPic(self, img)
|
||||||
if not OverworldBattle.shot() then return out end
|
if not OverworldBattle.shot() then return out end
|
||||||
return BattlePics.filled(out)
|
return BattlePics.filled(out, OverworldBattle.pinnedPic(self, img))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- While a billboard texture is being rendered both pics are put in the same
|
-- While a billboard texture is being rendered both pics are put in the same
|
||||||
|
|||||||
+12
-4
@@ -2253,15 +2253,23 @@ local function buildFigure(S, map, fig, tx, ty, perRow)
|
|||||||
-- He stands ON the furniture he was drawn into -- the same lift a pinned
|
-- He stands ON the furniture he was drawn into -- the same lift a pinned
|
||||||
-- prop above a pinned box takes (see buildObject), and gated the same
|
-- prop above a pinned box takes (see buildObject), and gated the same
|
||||||
-- way: a thing set down on furniture occupies a BLOCKED cell, while a
|
-- way: a thing set down on furniture occupies a BLOCKED cell, while a
|
||||||
-- seat you merely walk up to is in a walkable one.
|
-- seat you merely walk up to is in a walkable one. The row under his
|
||||||
|
-- card is SCANNED for the tallest authored upright rather than read at
|
||||||
|
-- its west corner: the corner tile can be furniture that is not his
|
||||||
|
-- seat (the couch's raised backrest column stands there, `top` art and
|
||||||
|
-- taller than the cushion he actually sits on).
|
||||||
local baseY = 0
|
local baseY = 0
|
||||||
local bs = S.shapeAt[keyOf(tx, ty + fig.h)]
|
|
||||||
local blocked = not map:isWalkableCell(math.floor(tx / 2),
|
local blocked = not map:isWalkableCell(math.floor(tx / 2),
|
||||||
math.floor((ty + fig.h - 1) / 2))
|
math.floor((ty + fig.h - 1) / 2))
|
||||||
if blocked and bs and bs.authored and bs.art == "upright"
|
if blocked then
|
||||||
and (bs.h or 0) > 0 then
|
for dx = 0, fig.w - 1 do
|
||||||
|
local bs = S.shapeAt[keyOf(tx + dx, ty + fig.h)]
|
||||||
|
if bs and bs.authored and bs.art == "upright"
|
||||||
|
and (bs.h or 0) > baseY then
|
||||||
baseY = bs.h
|
baseY = bs.h
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local atlasW = map.tileset.imageWidth or 128
|
local atlasW = map.tileset.imageWidth or 128
|
||||||
local atlasH = map.tileset.imageHeight or 48
|
local atlasH = map.tileset.imageHeight or 48
|
||||||
|
|||||||
@@ -83,6 +83,10 @@ local FALLBACK_HEIGHTS = {
|
|||||||
bed = 7,
|
bed = 7,
|
||||||
stool = 8,
|
stool = 8,
|
||||||
counter = 8,
|
counter = 8,
|
||||||
|
-- the raised back band of low seating: the Center couch's west strip
|
||||||
|
-- is drawn from above like the rest of the couch, but depicts the
|
||||||
|
-- back and arm rising over the 8px seat
|
||||||
|
backrest = 12,
|
||||||
table = 12,
|
table = 12,
|
||||||
desk = 24,
|
desk = 24,
|
||||||
prop = 16,
|
prop = 16,
|
||||||
@@ -145,6 +149,9 @@ local ART = {
|
|||||||
-- profile archetype Structures builds real steps for -- rising flights
|
-- profile archetype Structures builds real steps for -- rising flights
|
||||||
-- for stairs leading up, sunken stairwells for stairs leading down
|
-- for stairs leading up, sunken stairwells for stairs leading down
|
||||||
bed = "top",
|
bed = "top",
|
||||||
|
-- a backrest's art is the couch seen from above, so like the bed it
|
||||||
|
-- rides the top face of its taller box
|
||||||
|
backrest = "top",
|
||||||
stool = "billboard",
|
stool = "billboard",
|
||||||
-- half-cell furniture: a service counter, a low couch. One 8px band,
|
-- 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
|
-- so exactly the drawing's bottom row stands up as the front and
|
||||||
@@ -295,6 +302,24 @@ function TileShape.forMap(map)
|
|||||||
if cache[id] then return cache[id] end
|
if cache[id] then return cache[id] end
|
||||||
|
|
||||||
local heights = TileShape.heights()
|
local heights = TileShape.heights()
|
||||||
|
-- Per-tileset height overrides (a tileset entry's `heights`): the class
|
||||||
|
-- vocabulary is global but the drawings are not -- the DOJO lab tables
|
||||||
|
-- are drawn 6px tall where the default `table` is 12 -- and the height
|
||||||
|
-- a sprite RIDES at (VoxelScene.groundAt) must be the height the art
|
||||||
|
-- actually stands, or the starter balls float over their own table.
|
||||||
|
-- Same gate as the global list: known classes, numbers only.
|
||||||
|
do
|
||||||
|
local s = load()
|
||||||
|
local entry = s and s.tilesets and s.tilesets[id]
|
||||||
|
local over = entry and entry.heights
|
||||||
|
if type(over) == "table" then
|
||||||
|
for class, h in pairs(over) do
|
||||||
|
if type(h) == "number" and FALLBACK_HEIGHTS[class] then
|
||||||
|
heights[class] = h
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
local authored = authoredGroups(id, heights)
|
local authored = authoredGroups(id, heights)
|
||||||
local count = math.floor((tileset.imageWidth or 128) / 8)
|
local count = math.floor((tileset.imageWidth or 128) / 8)
|
||||||
* math.floor((tileset.imageHeight or 48) / 8)
|
* math.floor((tileset.imageHeight or 48) / 8)
|
||||||
|
|||||||
+1
-1
@@ -715,7 +715,7 @@ function Voxel3D.beginScene(w, h, cx, cy, vw, vh, sky, slot)
|
|||||||
pcall(sh.send, sh, "sunTexel", { texel, texel })
|
pcall(sh.send, sh, "sunTexel", { texel, texel })
|
||||||
if grid then
|
if grid then
|
||||||
pcall(sh.send, sh, "gridDark", VoxelGrid.DARK)
|
pcall(sh.send, sh, "gridDark", VoxelGrid.DARK)
|
||||||
pcall(sh.send, sh, "gridWidth", VoxelGrid.WIDTH)
|
pcall(sh.send, sh, "gridWidth", VoxelGrid.width())
|
||||||
end
|
end
|
||||||
-- ordinary shading until the silhouette pass asks for otherwise. Sent
|
-- ordinary shading until the silhouette pass asks for otherwise. Sent
|
||||||
-- every frame rather than once, because a scene that opened mid-ghost --
|
-- every frame rather than once, because a scene that opened mid-ghost --
|
||||||
|
|||||||
@@ -44,6 +44,19 @@ VoxelGrid.DARK = 0.45
|
|||||||
-- 1.0 here is the one-pixel wireframe.
|
-- 1.0 here is the one-pixel wireframe.
|
||||||
VoxelGrid.WIDTH = 1.0
|
VoxelGrid.WIDTH = 1.0
|
||||||
|
|
||||||
|
-- The same width in the CANVAS pixels the shader measures in, which is what
|
||||||
|
-- every sender of it actually wants.
|
||||||
|
--
|
||||||
|
-- The two are the same number until AA renders the pass larger than the
|
||||||
|
-- window (see AntiAlias): there a canvas pixel is a fraction of a display
|
||||||
|
-- one, and a width left at 1.0 would come out a half or a quarter of a line
|
||||||
|
-- after the fold -- the wireframe fading as the smoothing goes up, which
|
||||||
|
-- reads as one row breaking the other. Scaled, it stays a one-pixel seam and
|
||||||
|
-- simply gains the antialiasing everything else in the frame just gained.
|
||||||
|
function VoxelGrid.width()
|
||||||
|
return VoxelGrid.WIDTH * V.require("AntiAlias").factor()
|
||||||
|
end
|
||||||
|
|
||||||
-- where it persists and the rows that cycle it (see ModSetting)
|
-- where it persists and the rows that cycle it (see ModSetting)
|
||||||
VoxelGrid.setting = ModSetting.new(VoxelGrid.KEY, VoxelGrid.LABEL,
|
VoxelGrid.setting = ModSetting.new(VoxelGrid.KEY, VoxelGrid.LABEL,
|
||||||
{ false, true }, { "OFF", "ON" })
|
{ false, true }, { "OFF", "ON" })
|
||||||
|
|||||||
+1
-1
@@ -1192,7 +1192,7 @@ function Water.begin(ctx)
|
|||||||
if ctx.grid then
|
if ctx.grid then
|
||||||
local VoxelGrid = V.require("VoxelGrid")
|
local VoxelGrid = V.require("VoxelGrid")
|
||||||
send("gridDark", VoxelGrid.DARK)
|
send("gridDark", VoxelGrid.DARK)
|
||||||
send("gridWidth", VoxelGrid.WIDTH)
|
send("gridWidth", VoxelGrid.width())
|
||||||
end
|
end
|
||||||
|
|
||||||
Water.sendSky(sh, ctx)
|
Water.sendSky(sh, ctx)
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ local BattleExit = V.require("BattleExit")
|
|||||||
local DayNight = V.require("DayNight")
|
local DayNight = V.require("DayNight")
|
||||||
local DayTint = V.require("DayTint")
|
local DayTint = V.require("DayTint")
|
||||||
local Water = V.require("Water")
|
local Water = V.require("Water")
|
||||||
|
local AntiAlias = V.require("AntiAlias")
|
||||||
|
|
||||||
-- Forward declaration: the voxel pipeline's update hook (registered below)
|
-- Forward declaration: the voxel pipeline's update hook (registered below)
|
||||||
-- calls this, and it is defined further down with the settings it drives.
|
-- calls this, and it is defined further down with the settings it drives.
|
||||||
@@ -203,20 +204,34 @@ mod.content.render_pipelines:register("voxel", {
|
|||||||
-- a magnified low-res image, while the FX closures keep drawing in
|
-- a magnified low-res image, while the FX closures keep drawing in
|
||||||
-- world-pixel units.
|
-- world-pixel units.
|
||||||
local sw, sh = sceneSize(ctx)
|
local sw, sh = sceneSize(ctx)
|
||||||
local canvas = VoxelScene.render(ctx.state, sw, sh,
|
-- With AA on, the whole pass runs into a canvas BIGGER than the window
|
||||||
|
-- and is folded back down at the end (see AntiAlias). Nothing between
|
||||||
|
-- these two lines knows: every pass in the frame measures itself in the
|
||||||
|
-- canvas it was handed, so the sky's dither, the water's march and the
|
||||||
|
-- camera itself all come out the same picture at a higher sample rate.
|
||||||
|
local rw, rh = AntiAlias.expand(sw, sh)
|
||||||
|
local canvas = VoxelScene.render(ctx.state, rw, rh,
|
||||||
ctx.vw, ctx.vh, ctx.paletteFor)
|
ctx.vw, ctx.vh, ctx.paletteFor)
|
||||||
if not canvas then return nil end -- fall back to the 2D path
|
if not canvas then return nil end -- fall back to the 2D path
|
||||||
if Voxel3D.beginOverlay() then
|
if Voxel3D.beginOverlay() then
|
||||||
|
-- the FX closures are ordinary 2D draws sized in DISPLAY pixels, and
|
||||||
|
-- they are drawing into the supersampled canvas alongside everything
|
||||||
|
-- else -- so the scale goes up with it, or the "!" bubble lands the
|
||||||
|
-- right place at half the size. project() already answers in canvas
|
||||||
|
-- pixels, so only the scale needs saying.
|
||||||
ctx.drawFx(function(wx, wy) return Voxel3D.project(wx, 0, wy) end,
|
ctx.drawFx(function(wx, wy) return Voxel3D.project(wx, 0, wy) end,
|
||||||
ctx.scale)
|
ctx.scale * AntiAlias.factor())
|
||||||
Voxel3D.endOverlay()
|
Voxel3D.endOverlay()
|
||||||
end
|
end
|
||||||
return canvas
|
-- and back to the window's own size, which is what the engine composites
|
||||||
|
-- one canvas pixel to one display pixel. A pass-through when AA is off.
|
||||||
|
return AntiAlias.resolve(canvas, sw, sh, "world")
|
||||||
end,
|
end,
|
||||||
|
|
||||||
invalidate = function()
|
invalidate = function()
|
||||||
Voxel3D.invalidate()
|
Voxel3D.invalidate()
|
||||||
OverworldBattle.invalidate()
|
OverworldBattle.invalidate()
|
||||||
|
AntiAlias.invalidate()
|
||||||
ChunkMesher.invalidate() -- no map id = every cached mesh
|
ChunkMesher.invalidate() -- no map id = every cached mesh
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
@@ -361,6 +376,21 @@ local SETTINGS = {
|
|||||||
.. "let CYCLE run it -- ten minutes of sun, ten of moon, with the "
|
.. "let CYCLE run it -- ten minutes of sun, ten of moon, with the "
|
||||||
.. "shadows, the sky and the light following -- or SYNC it to the "
|
.. "shadows, the sky and the light following -- or SYNC it to the "
|
||||||
.. "clock on the wall, so Kanto's evening falls when yours does." },
|
.. "clock on the wall, so Kanto's evening falls when yours does." },
|
||||||
|
-- Marked `full` for the opposite reason the battle rows are: this is not a
|
||||||
|
-- knob on the look at all, it is what the look COSTS. FULL is a preset for
|
||||||
|
-- the diorama, not a licence to spend four times the fill rate on the
|
||||||
|
-- machine it happens to be running on, so it neither sets this nor takes
|
||||||
|
-- the row away -- the player decides what their hardware can carry, from
|
||||||
|
-- inside FULL like anywhere else.
|
||||||
|
{ AntiAlias.setting,
|
||||||
|
"Smooth the stair-stepped edges of the 3D world -- roof ridges, ledge "
|
||||||
|
.. "lips, a tree against the sky -- by rendering the diorama larger than "
|
||||||
|
.. "the window and folding it back down. Every edge in the picture "
|
||||||
|
.. "softens with them, the tileset's own texels included, so the diorama "
|
||||||
|
.. "reads smoother rather than sharper. 2X costs half again as many "
|
||||||
|
.. "pixels in each direction and 4X twice, which makes this the most "
|
||||||
|
.. "expensive row in the mod.",
|
||||||
|
full = true },
|
||||||
}
|
}
|
||||||
|
|
||||||
local schema = {}
|
local schema = {}
|
||||||
@@ -860,7 +890,7 @@ mod.hooks:wrap("world.tod", function(next, tod, ctx)
|
|||||||
return DayNight.tod()
|
return DayNight.tod()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
mod.exports.version = "1.4.0"
|
mod.exports.version = "1.4.1"
|
||||||
-- exposed so a companion mod can pin its own tiles' shapes or read the
|
-- exposed so a companion mod can pin its own tiles' shapes or read the
|
||||||
-- camera without reaching into this mod's file layout
|
-- camera without reaching into this mod's file layout
|
||||||
mod.exports.lib = V
|
mod.exports.lib = V
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "DRAMATIC_SHAPE",
|
"id": "DRAMATIC_SHAPE",
|
||||||
"name": "Dramatic Shape Voxel Mod",
|
"name": "Dramatic Shape Voxel Mod",
|
||||||
"version": "1.4.0",
|
"version": "1.4.1",
|
||||||
"api": 2,
|
"api": 2,
|
||||||
"entry": "main.lua",
|
"entry": "main.lua",
|
||||||
"profile": "content",
|
"profile": "content",
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
-- Driver: one scene, once per rung of the AA row.
|
||||||
|
--
|
||||||
|
-- The AA row is the one setting in this mod whose whole effect is a pixel
|
||||||
|
-- wide, so it is also the one that cannot be judged from a description. This
|
||||||
|
-- renders the SAME frame at each rung and writes one PNG per rung; put two of
|
||||||
|
-- them side by side, magnified, and the row is either doing something or it
|
||||||
|
-- is not.
|
||||||
|
--
|
||||||
|
-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/aa_shots.lua \
|
||||||
|
-- SHOT_DIR=<dir> lovec.exe .
|
||||||
|
--
|
||||||
|
-- knobs (env):
|
||||||
|
-- SHOT_DIR output directory (created if missing) (default "shots/aa")
|
||||||
|
-- AA_MAP map id (default VIRIDIAN_CITY)
|
||||||
|
-- AA_SPOT "x,y[,facing]" (default 20,26,up)
|
||||||
|
-- AA_RUNG the voxel camera rung (default 5, the 75 one)
|
||||||
|
--
|
||||||
|
-- The scene defaults to a town at the LOW camera on purpose: roof ridges, the
|
||||||
|
-- diagonal of a fence and a tree's silhouette against the sky are the edges
|
||||||
|
-- that stair-step, and 75 degrees is the rung that puts the most of them at an
|
||||||
|
-- angle to the pixel grid.
|
||||||
|
--
|
||||||
|
-- Determinism matters here for the same reason it does in voxel_shots_ab: the
|
||||||
|
-- three shots differ ONLY by the row under test, or comparing them means
|
||||||
|
-- nothing. The clock is pinned, the animated tile slots are frozen, the
|
||||||
|
-- townsfolk are stopped where they stand, and the tilt-shift is held at zero
|
||||||
|
-- (a gaussian over the frame would smear away the very edges being looked at).
|
||||||
|
--
|
||||||
|
-- Nothing here writes the player's options: the row is moved with
|
||||||
|
-- ModSetting:sync, which moves the cached index and persists nothing.
|
||||||
|
return function(game)
|
||||||
|
local U = dofile("tests/drivers/util.lua")
|
||||||
|
local Pipelines = require("src.render.Pipelines")
|
||||||
|
local OverworldState = require("src.world.OverworldController")
|
||||||
|
|
||||||
|
local ROOT = os.getenv("SHOT_DIR") or "shots/aa"
|
||||||
|
|
||||||
|
local handle = game.mods.exports["DRAMATIC_SHAPE"]
|
||||||
|
if not (handle and handle.lib) then
|
||||||
|
print("[aa] DRAMATIC_SHAPE mod not loaded -- nothing to shoot")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local V = handle.lib
|
||||||
|
local DayNight = V.require("DayNight")
|
||||||
|
local AntiAlias = V.require("AntiAlias")
|
||||||
|
local ChunkMesher = V.require("ChunkMesher")
|
||||||
|
local Voxel = V.require("VoxelState")
|
||||||
|
local ShadowMap = V.require("ShadowMap")
|
||||||
|
|
||||||
|
local MAP = os.getenv("AA_MAP") or "VIRIDIAN_CITY"
|
||||||
|
local SPOT = os.getenv("AA_SPOT") or "20,26,up"
|
||||||
|
local RUNG = math.floor(tonumber(os.getenv("AA_RUNG")) or 5)
|
||||||
|
local sx, sy, sf = SPOT:match("^(%-?%d+),%s*(%-?%d+),?%s*(%a*)$")
|
||||||
|
sx, sy = tonumber(sx) or 20, tonumber(sy) or 26
|
||||||
|
if sf == "" then sf = "up" end
|
||||||
|
|
||||||
|
OverworldState.rollEncounter = function() return nil end
|
||||||
|
|
||||||
|
local NPC = require("src.world.NPC")
|
||||||
|
if not NPC.dramaticShapeAaFreeze then
|
||||||
|
local inner = NPC.update
|
||||||
|
function NPC:update(...)
|
||||||
|
self.frozen = true
|
||||||
|
return inner(self, ...)
|
||||||
|
end
|
||||||
|
NPC.dramaticShapeAaFreeze = true
|
||||||
|
end
|
||||||
|
pcall(love.math.setRandomSeed, 20260801)
|
||||||
|
|
||||||
|
local TileRenderer = require("src.render.TileRenderer")
|
||||||
|
TileRenderer.tick = function() end
|
||||||
|
TileRenderer.animFrame = function() return 0 end
|
||||||
|
|
||||||
|
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 cameraStill()
|
||||||
|
local o = game.overworld
|
||||||
|
local c = o and o.camera
|
||||||
|
if not c then return true end
|
||||||
|
local lx, ly, held = nil, nil, 0
|
||||||
|
for _ = 1, 300 do
|
||||||
|
if c.x == lx and c.y == ly then
|
||||||
|
held = held + 1
|
||||||
|
if held >= 10 then return true end
|
||||||
|
else
|
||||||
|
held = 0
|
||||||
|
lx, ly = c.x, c.y
|
||||||
|
end
|
||||||
|
U.wait(1)
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The camera PITCH, which is the one that caught this driver out. The tween
|
||||||
|
-- runs on wall-clock dt and Voxel.t reaching 1 is not the same instant the
|
||||||
|
-- angle stops moving, so the first shot of a run came out at 67 degrees
|
||||||
|
-- while the two after it were at 75 -- three frames that differ by the
|
||||||
|
-- camera, in a comparison whose entire subject is a pixel.
|
||||||
|
local function angleStill()
|
||||||
|
local last, held = nil, 0
|
||||||
|
for _ = 1, 600 do
|
||||||
|
if Voxel.angle == last then
|
||||||
|
held = held + 1
|
||||||
|
if held >= 10 then return true end
|
||||||
|
else
|
||||||
|
held = 0
|
||||||
|
last = Voxel.angle
|
||||||
|
end
|
||||||
|
U.wait(1)
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
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
|
||||||
|
angleStill()
|
||||||
|
cameraStill()
|
||||||
|
-- the sun map is only redrawn when its inputs move, and the AA row is not
|
||||||
|
-- one of them -- so force one pass at the settled camera rather than
|
||||||
|
-- comparing a frame against a map fitted a few hundredths of a pixel ago
|
||||||
|
if ShadowMap.forget then ShadowMap.forget() end
|
||||||
|
U.wait(20)
|
||||||
|
end
|
||||||
|
|
||||||
|
DayNight.setting:sync("day")
|
||||||
|
U.teleport(game, MAP, sx, sy, sf)
|
||||||
|
Pipelines.setLevel("voxel", RUNG)
|
||||||
|
Pipelines.setLevel("tiltshift", 0)
|
||||||
|
|
||||||
|
-- Warm up before the FIRST shot, not just between them.
|
||||||
|
--
|
||||||
|
-- Neighbour maps are requested from inside the render itself
|
||||||
|
-- (VoxelScene.prefetch), so an empty build queue right after a teleport
|
||||||
|
-- means "nothing has been asked for yet", not "everything is here". The
|
||||||
|
-- first capture of a run came out with the map beyond Viridian missing --
|
||||||
|
-- a whole tree line absent from one frame of a three-way comparison, which
|
||||||
|
-- looks exactly like the row under test doing something enormous. Settling
|
||||||
|
-- twice lets the first render request the neighbourhood and the second
|
||||||
|
-- drain it.
|
||||||
|
settle()
|
||||||
|
settle()
|
||||||
|
|
||||||
|
local shots, missed = 0, 0
|
||||||
|
for _, samples in ipairs({ 0, 2, 4 }) do
|
||||||
|
AntiAlias.setting:sync(samples)
|
||||||
|
settle()
|
||||||
|
-- AA_TRACE=1 prints the state each shot was taken in. When two shots of a
|
||||||
|
-- run disagree by more than the row could account for, this is what says
|
||||||
|
-- which input moved -- it is how the camera-tween and the neighbour-mesh
|
||||||
|
-- settles above were both found.
|
||||||
|
if os.getenv("AA_TRACE") == "1" then
|
||||||
|
local Voxel3D = V.require("Voxel3D")
|
||||||
|
local o = game.overworld
|
||||||
|
local cw, chh = Voxel3D.size()
|
||||||
|
print(("[aa] trace samples=%d angle=%.6f fov=%.6f cell=%.4f canvas=%dx%d cam=(%.3f,%.3f) eye=(%.2f,%.2f,%.2f) factor=%.4f")
|
||||||
|
:format(samples, Voxel.angle or -1, Voxel3D.fovY or -1,
|
||||||
|
Voxel3D.cell or -1, cw or 0, chh or 0,
|
||||||
|
o and o.camera and o.camera.x or -1,
|
||||||
|
o and o.camera and o.camera.y or -1,
|
||||||
|
(Voxel3D.eye or {})[1] or 0, (Voxel3D.eye or {})[2] or 0,
|
||||||
|
(Voxel3D.eye or {})[3] or 0, AntiAlias.factor()))
|
||||||
|
end
|
||||||
|
local path = ("%s/aa_%d.png"):format(ROOT, samples)
|
||||||
|
game.capturePath = path
|
||||||
|
U.wait(6)
|
||||||
|
local f = io.open(path, "rb")
|
||||||
|
if f then
|
||||||
|
f:close()
|
||||||
|
shots = shots + 1
|
||||||
|
print(("[aa] %s samples=%d"):format(path, samples))
|
||||||
|
else
|
||||||
|
missed = missed + 1
|
||||||
|
print("[aa] capture did not reach disk: " .. path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- left where it was found, so a run cannot leak a rung into the next one
|
||||||
|
AntiAlias.setting:sync(0)
|
||||||
|
|
||||||
|
print(("[aa] %d shots into %s (%d failed to reach disk)")
|
||||||
|
:format(shots, ROOT, missed))
|
||||||
|
end
|
||||||
+199
-11
@@ -145,6 +145,9 @@ T.check(not fullIds["DRAMATIC_SHAPE:daytime"], "and DAYTIME")
|
|||||||
-- back sprite (or no staged fights at all) can still say so from inside FULL
|
-- back sprite (or no staged fights at all) can still say so from inside FULL
|
||||||
T.check(fullIds["DRAMATIC_SHAPE:battles"], "3D-BTL is still on the menu under FULL")
|
T.check(fullIds["DRAMATIC_SHAPE:battles"], "3D-BTL is still on the menu under FULL")
|
||||||
T.check(fullIds["DRAMATIC_SHAPE:battleBack"], "and BACK SPRITES with it")
|
T.check(fullIds["DRAMATIC_SHAPE:battleBack"], "and BACK SPRITES with it")
|
||||||
|
-- and AA, for the opposite reason: it is not a knob on the look at all, it is
|
||||||
|
-- what the look COSTS, and only the player knows what their machine can carry
|
||||||
|
T.check(fullIds["DRAMATIC_SHAPE:aa"], "and AA, which FULL neither sets nor owns")
|
||||||
|
|
||||||
-- DAYTIME is not only hidden under FULL, it is HELD at SYNC: the row cannot
|
-- DAYTIME is not only hidden under FULL, it is HELD at SYNC: the row cannot
|
||||||
-- be reached while FULL owns it, so a value changed underneath (the mod
|
-- be reached while FULL owns it, so a value changed underneath (the mod
|
||||||
@@ -379,9 +382,11 @@ end
|
|||||||
Pipelines.setLevel("voxel", 2)
|
Pipelines.setLevel("voxel", 2)
|
||||||
local hookedRows = Runtime.call("ui.options.rows", function(_, r) return r end,
|
local hookedRows = Runtime.call("ui.options.rows", function(_, r) return r end,
|
||||||
{ data = Data }, { { id = "text_speed" } })
|
{ data = Data }, { { id = "text_speed" } })
|
||||||
T.eq(#hookedRows, 7, "the options hook added a row per setting")
|
T.eq(#hookedRows, 8, "the options hook added a row per setting")
|
||||||
local grid, curve, water = hookedRows[2], hookedRows[3], hookedRows[4]
|
local grid, curve, water = hookedRows[2], hookedRows[3], hookedRows[4]
|
||||||
local battles, backRow, daytime = hookedRows[5], hookedRows[6], hookedRows[7]
|
local battles, backRow, daytime = hookedRows[5], hookedRows[6], hookedRows[7]
|
||||||
|
-- the AA row is hookedRows[8]; it is read in its own block below, because
|
||||||
|
-- this chunk is one main function and has 200 local slots to spend
|
||||||
T.eq(water.label, "WATER", "the water row carries its label")
|
T.eq(water.label, "WATER", "the water row carries its label")
|
||||||
T.eq(water.value(), "FULL",
|
T.eq(water.value(), "FULL",
|
||||||
"and defaults to FULL -- reflections are the point of having the row")
|
"and defaults to FULL -- reflections are the point of having the row")
|
||||||
@@ -459,6 +464,57 @@ curve.step(settingGame, 1)
|
|||||||
curve.step(settingGame, 1)
|
curve.step(settingGame, 1)
|
||||||
T.eq(curve.value(), "OFF", "the curve is left off for the rows below")
|
T.eq(curve.value(), "OFF", "the curve is left off for the rows below")
|
||||||
|
|
||||||
|
-- ------- AA renders the pass larger and folds it back down
|
||||||
|
--
|
||||||
|
-- The ladder is SAMPLES per display pixel, so the canvas scale each rung asks
|
||||||
|
-- for is its square root -- and the scale in force has to be readable after
|
||||||
|
-- the fact, because two things are quoted in DISPLAY pixels and have to be
|
||||||
|
-- multiplied up into the canvas the pass actually opened: the wireframe's
|
||||||
|
-- line width and the FX overlay's sprite scale.
|
||||||
|
do
|
||||||
|
local AntiAlias = run.loader.exports.DRAMATIC_SHAPE.lib.require("AntiAlias")
|
||||||
|
local VoxelGrid = run.loader.exports.DRAMATIC_SHAPE.lib.require("VoxelGrid")
|
||||||
|
local aaGame = { save = { options = {} }, mods = { modOptions = {} } }
|
||||||
|
local aa = hookedRows[8]
|
||||||
|
T.eq(aa.label, "AA", "the anti-aliasing row carries its label")
|
||||||
|
T.eq(aa.value(), "OFF",
|
||||||
|
"and starts off -- supersampling is a cost knob, and a mod must not spend "
|
||||||
|
.. "four times the fill rate of the machine it lands on unasked")
|
||||||
|
|
||||||
|
T.eq(AntiAlias.samples(), 0, "AA is off by default")
|
||||||
|
local w, h = AntiAlias.expand(320, 200)
|
||||||
|
T.eq(w, 320, "an OFF row renders at the window's own width")
|
||||||
|
T.eq(h, 200, "and its height")
|
||||||
|
T.eq(AntiAlias.factor(), 1, "with nothing to multiply display pixels by")
|
||||||
|
T.eq(VoxelGrid.width(), VoxelGrid.WIDTH,
|
||||||
|
"so the wireframe is the one-display-pixel line it has always been")
|
||||||
|
|
||||||
|
aa.step(aaGame, 1)
|
||||||
|
T.eq(aa.value(), "2X", "stepping the row climbs to two samples a pixel")
|
||||||
|
T.eq(aaGame.save.options.modOptions.DRAMATIC_SHAPE.aa, 2,
|
||||||
|
"the sample count persists beside the other settings, not over them")
|
||||||
|
w, h = AntiAlias.expand(320, 200)
|
||||||
|
T.eq(w, 453, "two samples a pixel is a canvas root-two wider")
|
||||||
|
T.eq(h, 283, "and root-two taller")
|
||||||
|
T.check(math.abs(AntiAlias.factor() - 453 / 320) < 1e-9,
|
||||||
|
"and the factor is what it MEASURED, not what the row asked for")
|
||||||
|
|
||||||
|
aa.step(aaGame, 1)
|
||||||
|
T.eq(aa.value(), "4X", "and again to four")
|
||||||
|
w, h = AntiAlias.expand(320, 200)
|
||||||
|
T.eq(w, 640, "four samples a pixel is a canvas exactly twice the size")
|
||||||
|
T.eq(h, 400, "in each direction, which is the 2x2 box the fold reads")
|
||||||
|
T.eq(AntiAlias.factor(), 2, "with everything in display pixels doubled")
|
||||||
|
T.eq(VoxelGrid.width(), VoxelGrid.WIDTH * 2,
|
||||||
|
"the wireframe among them -- a seam left at 1.0 would fold down to half a "
|
||||||
|
.. "line, so the smoothing row would appear to fade the grid row out")
|
||||||
|
|
||||||
|
aa.step(aaGame, 1)
|
||||||
|
T.eq(aa.value(), "OFF", "and the ladder wraps back to off")
|
||||||
|
AntiAlias.expand(320, 200)
|
||||||
|
T.eq(AntiAlias.factor(), 1, "leaving nothing behind for the next pass")
|
||||||
|
end
|
||||||
|
|
||||||
-- ------- the animated terrain atlas survives an engine without its seams
|
-- ------- the animated terrain atlas survives an engine without its seams
|
||||||
--
|
--
|
||||||
-- Regression: cycling palette modes with voxel mode on eventually killed
|
-- Regression: cycling palette modes with voxel mode on eventually killed
|
||||||
@@ -2476,7 +2532,35 @@ T.check(onAt["DRAMATIC_SHAPE:battleBack"], "switched back on, so is the row")
|
|||||||
T.eq(onAt["DRAMATIC_SHAPE:battleBack"] - onAt["DRAMATIC_SHAPE:battles"], 1,
|
T.eq(onAt["DRAMATIC_SHAPE:battleBack"] - onAt["DRAMATIC_SHAPE:battles"], 1,
|
||||||
"directly under the row it belongs to")
|
"directly under the row it belongs to")
|
||||||
|
|
||||||
|
-- ------- and which pic is the pinned one is asked with the other side BLANKED
|
||||||
|
--
|
||||||
|
-- picImage asks this so BattlePics knows whether a pic's feet are on the text
|
||||||
|
-- box -- where its bottom edge seals, and its belly stops being see-through --
|
||||||
|
-- and it is asked DURING the billboard render, inside which sideTexture has
|
||||||
|
-- switched the side it is not drawing off by setting the field to FALSE rather
|
||||||
|
-- than to nil (see OFF).
|
||||||
|
--
|
||||||
|
-- So the read has to be by truthiness. A test against nil passes that `false`
|
||||||
|
-- through to the index below it, the error comes back out of sideTexture into
|
||||||
|
-- the pcall that calls it, textures() reports no card for the side -- and the
|
||||||
|
-- foe is simply not on the field. Which is the whole bug: fixing the player's
|
||||||
|
-- back pic took the enemy's billboard out.
|
||||||
|
local mine, theirs = {}, {}
|
||||||
|
local live = { player = { sprite = mine }, enemy = { sprite = theirs } }
|
||||||
|
T.eq(Battles.pinnedPic(live, mine), true,
|
||||||
|
"the player's own mon is the pic on the box")
|
||||||
|
T.eq(Battles.pinnedPic(live, theirs), false,
|
||||||
|
"and the foe is geometry out on the map, whatever the mode")
|
||||||
|
T.eq(Battles.pinnedPic({ player = false, enemy = { sprite = theirs } }, theirs),
|
||||||
|
false, "asking about the foe while the player is blanked answers, not throws")
|
||||||
|
T.eq(Battles.pinnedPic({ playerBackPic = mine }, mine), true,
|
||||||
|
"the trainer back holds the slot until Go!, on the box like the mon")
|
||||||
|
T.eq(Battles.pinnedPic({ player = false, playerBackPic = false }, mine), false,
|
||||||
|
"and with the side blanked outright nothing of it is pinned")
|
||||||
|
|
||||||
Battles.backSetting:setIndex(1, backGame) -- and off for the rows below
|
Battles.backSetting:setIndex(1, backGame) -- and off for the rows below
|
||||||
|
T.eq(Battles.pinnedPic(live, mine), false,
|
||||||
|
"with BACK SPRITES off the player's mon is out on the map with the foe")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ------- the hour reaches the FLAT world too
|
-- ------- the hour reaches the FLAT world too
|
||||||
@@ -2611,21 +2695,35 @@ local BattlePics = run.loader.exports.DRAMATIC_SHAPE.lib.require("BattlePics")
|
|||||||
-- reader over what came out. The pic is faked at the readback seam, which is
|
-- reader over what came out. The pic is faked at the readback seam, which is
|
||||||
-- the only thing between this and the pixels the engine would have blitted.
|
-- the only thing between this and the pixels the engine would have blitted.
|
||||||
local lastCanvas = nil -- what the readback asked newCanvas for
|
local lastCanvas = nil -- what the readback asked newCanvas for
|
||||||
local function fill(rows)
|
-- '#' is ink and '.' the keyed-out nothing. 'W' is ink too, of the pic's
|
||||||
|
-- LIGHTEST shade -- a highlight the decoder happened not to key -- which is
|
||||||
|
-- what the paper a hole gets filled with is read off.
|
||||||
|
local SHADE = { ["#"] = 0.25, ["W"] = 0.75 }
|
||||||
|
-- reuse hands the SAME pic back through, which is how the two bottom rules
|
||||||
|
-- can be asked of one image the way a running battle would ask them
|
||||||
|
local function fill(rows, sealBottom, reuse)
|
||||||
local W, H = #rows[1], #rows
|
local W, H = #rows[1], #rows
|
||||||
local built = nil
|
local built = nil
|
||||||
local function fakeData()
|
local function fakeData()
|
||||||
local px = {}
|
local px, sh = {}, {}
|
||||||
for y = 0, H - 1 do
|
for y = 0, H - 1 do
|
||||||
for x = 0, W - 1 do
|
for x = 0, W - 1 do
|
||||||
px[y * W + x] = rows[y + 1]:sub(x + 1, x + 1) == "#" and 1 or 0
|
local shade = SHADE[rows[y + 1]:sub(x + 1, x + 1)]
|
||||||
|
px[y * W + x] = shade and 1 or 0
|
||||||
|
sh[y * W + x] = shade or 0
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return {
|
return {
|
||||||
px = px,
|
px = px, sh = sh,
|
||||||
getDimensions = function() return W, H end,
|
getDimensions = function() return W, H end,
|
||||||
getPixel = function(self, x, y) return 0, 0, 0, self.px[y * W + x] end,
|
getPixel = function(self, x, y)
|
||||||
setPixel = function(self, x, y, r, g, b, a) self.px[y * W + x] = a end,
|
local k = y * W + x
|
||||||
|
return self.sh[k], self.sh[k], self.sh[k], self.px[k]
|
||||||
|
end,
|
||||||
|
setPixel = function(self, x, y, r, g, b, a)
|
||||||
|
self.px[y * W + x] = a
|
||||||
|
self.sh[y * W + x] = r
|
||||||
|
end,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2639,12 +2737,14 @@ local function fill(rows)
|
|||||||
built = data
|
built = data
|
||||||
return { setFilter = function() end }
|
return { setFilter = function() end }
|
||||||
end
|
end
|
||||||
local pic = { getDimensions = function() return W, H end }
|
local pic = reuse or { getDimensions = function() return W, H end }
|
||||||
local out = BattlePics.filled(pic)
|
local out = BattlePics.filled(pic, sealBottom)
|
||||||
love.graphics.newCanvas, love.graphics.newImage = realNewCanvas, realNewImage
|
love.graphics.newCanvas, love.graphics.newImage = realNewCanvas, realNewImage
|
||||||
-- deliberately NOT invalidated: each figure brings its own pic, and the
|
-- deliberately NOT invalidated: each figure brings its own pic, and the
|
||||||
-- cache check at the bottom needs one of them still in there
|
-- cache check at the bottom needs one of them still in there
|
||||||
return out, pic, built and function(x, y) return built.px[y * W + x] > 0.5 end
|
return out, pic,
|
||||||
|
built and function(x, y) return built.px[y * W + x] > 0.5 end,
|
||||||
|
built and function(x, y) return built.sh[y * W + x] end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ------- the cut at the feet, which is what the closed bottom edge is for
|
-- ------- the cut at the feet, which is what the closed bottom edge is for
|
||||||
@@ -2727,6 +2827,75 @@ local drainOut, drainPic, drain = fill({
|
|||||||
T.check(drainOut ~= drainPic and drain and drain(8, 4),
|
T.check(drainOut ~= drainPic and drain and drain(8, 4),
|
||||||
"a narrow one is where the drawing ran out, and is paper")
|
"a narrow one is where the drawing ran out, and is paper")
|
||||||
|
|
||||||
|
-- ------- but a pic ON THE MENU has no mouth at all
|
||||||
|
--
|
||||||
|
-- The drain/mouth cut is for a pic standing on the MAP, where a wide opening
|
||||||
|
-- along the bottom is a stride with real ground behind it. Under BACK SPRITES
|
||||||
|
-- the player's mon is drawn in the GB's own slot with its feet flush on the
|
||||||
|
-- text box, and the only thing under its lowest row is white box -- so nothing
|
||||||
|
-- reaches it from below, whatever the opening's width, and the rule stops
|
||||||
|
-- being a heuristic: paper is whatever the background cannot walk to from the
|
||||||
|
-- left, the right or the top.
|
||||||
|
--
|
||||||
|
-- Which is the difference between a Pikachu and a wireframe. The pale-bodied
|
||||||
|
-- back pics -- Pikachu, Seel, Dewgong, Chansey, Jigglypuff -- are drawn as
|
||||||
|
-- OUTLINES, every shade-0 pixel inside the ink keyed away, and each of them
|
||||||
|
-- leaks out through a bottom opening far too wide to read as a drain. On the
|
||||||
|
-- map that reading is right; on the box it left the mon a rim with the arena
|
||||||
|
-- showing through it.
|
||||||
|
--
|
||||||
|
-- The same stride figure the map rule leaves open, now standing on the box.
|
||||||
|
local boxOut, boxPic, boxOpaque = fill({
|
||||||
|
"..##############..",
|
||||||
|
"..##############..",
|
||||||
|
"..##############..",
|
||||||
|
"..###........###..",
|
||||||
|
"..###........###..",
|
||||||
|
"..###........###..",
|
||||||
|
}, true)
|
||||||
|
T.check(boxOut ~= boxPic and boxOpaque,
|
||||||
|
"the gap a stride would have shown the world through is paper on the box")
|
||||||
|
T.check(boxOpaque(8, 3) and boxOpaque(8, 5),
|
||||||
|
"and it fills right down to the row the feet are on")
|
||||||
|
|
||||||
|
-- the seal is the BOTTOM alone: the sides and the top still let the background
|
||||||
|
-- in, which is what keeps the silhouette cutting against the arena instead of
|
||||||
|
-- standing the mon in a white block
|
||||||
|
local boxGapOut, boxGapPic = fill({
|
||||||
|
"..#####.",
|
||||||
|
"..#...#.",
|
||||||
|
"..#...#.",
|
||||||
|
"....###.", -- opens at the left, and drains out that way
|
||||||
|
"..#####.",
|
||||||
|
"..#####.",
|
||||||
|
}, true)
|
||||||
|
T.eq(boxGapOut, boxGapPic,
|
||||||
|
"a pocket that drains out to the side is background on the box too")
|
||||||
|
|
||||||
|
-- ------- and a hole is filled with the pic's OWN paper, not with white
|
||||||
|
--
|
||||||
|
-- Shade 0 is white only while the pic is still grays, and by the time one
|
||||||
|
-- reaches here it usually is not: picImage hands it over after the bake -- a
|
||||||
|
-- species SGB colour, a BGP fade mid-animation, PAL_BLACK across the whole
|
||||||
|
-- screen while the blackout text is up -- and shade 0 travels with the rest.
|
||||||
|
-- A hardcoded white belly would be the one lit thing on a blacked-out mon.
|
||||||
|
--
|
||||||
|
-- So the paper is read off the pic: the lightest shade still standing in it,
|
||||||
|
-- which is shade 0 wherever the decoder could not reach one. It never has to
|
||||||
|
-- guess -- all 151 of this game's back pics keep at least one, an eye or a
|
||||||
|
-- highlight down a cheek.
|
||||||
|
local _, _, _, paperShade = fill({
|
||||||
|
"..####..",
|
||||||
|
"..#WW#..", -- a highlight the decoder did not key: this is the paper
|
||||||
|
"..#..#..",
|
||||||
|
"..#..#..",
|
||||||
|
"..####..",
|
||||||
|
})
|
||||||
|
T.eq(paperShade(3, 2), paperShade(3, 1),
|
||||||
|
"the hole takes the lightest shade the pic still has")
|
||||||
|
T.check(paperShade(3, 2) ~= paperShade(2, 2),
|
||||||
|
"which is not the ink beside it")
|
||||||
|
|
||||||
-- ------- and the readback is measured in PIXELS, which is what kept the mons
|
-- ------- and the readback is measured in PIXELS, which is what kept the mons
|
||||||
-- the size of the squares they stand on
|
-- the size of the squares they stand on
|
||||||
--
|
--
|
||||||
@@ -2743,12 +2912,31 @@ T.check(drainOut ~= drainPic and drain and drain(8, 4),
|
|||||||
-- scale that was wrong everywhere.
|
-- scale that was wrong everywhere.
|
||||||
T.check(lastCanvas and lastCanvas.opts and lastCanvas.opts.dpiscale == 1,
|
T.check(lastCanvas and lastCanvas.opts and lastCanvas.opts.dpiscale == 1,
|
||||||
"the readback canvas is one texel per pic pixel, on a highdpi phone too")
|
"the readback canvas is one texel per pic pixel, on a highdpi phone too")
|
||||||
T.eq(lastCanvas.w, 18, "and it is the size of the pic, in those pixels")
|
T.eq(lastCanvas.w, 8, "and it is the size of the pic, in those pixels")
|
||||||
|
|
||||||
-- the answer is cached on the image, so a pic costs one readback a session
|
-- the answer is cached on the image, so a pic costs one readback a session
|
||||||
-- rather than one a frame -- checked on the first figure, which is still in
|
-- rather than one a frame -- checked on the first figure, which is still in
|
||||||
-- there because fill() does not clear it
|
-- there because fill() does not clear it
|
||||||
T.eq(BattlePics.filled(pic), out, "the rebuilt pic is cached on the original")
|
T.eq(BattlePics.filled(pic), out, "the rebuilt pic is cached on the original")
|
||||||
|
-- and cached PER BOTTOM RULE, because one image answers differently on the map
|
||||||
|
-- and on the box. A single table for both would hand whichever caller asked
|
||||||
|
-- second the other one's answer -- the map's stencil to the menu, or a menu
|
||||||
|
-- fill to a mon standing on grass -- which is this section's bug arriving
|
||||||
|
-- through the cache rather than through the flood.
|
||||||
|
--
|
||||||
|
-- The stride figure again, on the SAME pic the map rule already answered for.
|
||||||
|
local reOut = fill({
|
||||||
|
"..##############..",
|
||||||
|
"..##############..",
|
||||||
|
"..##############..",
|
||||||
|
"..###........###..",
|
||||||
|
"..###........###..",
|
||||||
|
"..###........###..",
|
||||||
|
}, true, stridePic)
|
||||||
|
T.check(reOut ~= stridePic,
|
||||||
|
"the pic the map left open still fills when the box asks for it")
|
||||||
|
T.eq(BattlePics.filled(stridePic), stridePic,
|
||||||
|
"and the map's own answer for it is still the pic itself")
|
||||||
BattlePics.invalidate()
|
BattlePics.invalidate()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+304
-11
@@ -547,6 +547,98 @@ TEMPLATES = {
|
|||||||
roof_rows=17, roof_back=5, roof_front=3, roof_cycle=(5, 9),
|
roof_rows=17, roof_back=5, roof_front=3, roof_cycle=(5, 9),
|
||||||
slab=4, front_eave=4, ledge=None, tileset="forest",
|
slab=4, front_eave=4, ledge=None, tileset="forest",
|
||||||
),
|
),
|
||||||
|
# F01: the starter-ball table in Oak's lab -- the first FURNITURE
|
||||||
|
# through the pipeline, and the first drawing whose plot is smaller
|
||||||
|
# than its grid. Rows 0-15 are the tabletop seen from above; 16-18
|
||||||
|
# are the top slab's own front edge (black/#555/black -- exactly
|
||||||
|
# what the rim treatment paints, so slab=3 and those rows fold into
|
||||||
|
# the roof band instead of extruding); 19-21 are the base: corner
|
||||||
|
# feet and the inset dark panel between them. The legs stand on
|
||||||
|
# open FLOOR, so the measured ground line lands two rows short of
|
||||||
|
# the grid; `depth` keeps the plot to the blocked cell row -- the
|
||||||
|
# legs row of the grid is the walkable cell the player faces the
|
||||||
|
# table from, and D = len(tiles) would stand the model in their
|
||||||
|
# path. 16 top rows onto a 16px plot map 1:1: no cycling, and
|
||||||
|
# roof_cycle is unreachable behind roof_back=16.
|
||||||
|
"lab_table": dict(
|
||||||
|
tiles=[
|
||||||
|
[41, 59, 59, 59, 59, 42],
|
||||||
|
[78, 57, 57, 57, 57, 79],
|
||||||
|
[88, 89, 89, 89, 89, 90],
|
||||||
|
],
|
||||||
|
roof_rows=19, roof_back=16, roof_front=0, roof_cycle=(2, 13),
|
||||||
|
slab=3, front_eave=0, ledge=None, tileset="gym", depth=2,
|
||||||
|
),
|
||||||
|
# F02: the computer desk in Oak's lab -- and the Hall of Fame's
|
||||||
|
# recording machine, the same drawing on the GYM atlas (one
|
||||||
|
# placement each; both registered in voxel_heights). The one
|
||||||
|
# DESK-SET template: the methodology's region classification at
|
||||||
|
# part granularity. The desk is the sibling lab table (fascia rows
|
||||||
|
# 16-18, base 19-21); on it stand a monitor over its keyboard
|
||||||
|
# (left), a computer tower over a keyboard and mouse (middle), and
|
||||||
|
# a sheet of paper LYING FLAT (right). Upright parts anchor their
|
||||||
|
# drawn bottom row to the desk's top plane and wear their own drawn
|
||||||
|
# tops as lids; flat parts lie one voxel proud with drawn row =
|
||||||
|
# depth row -- the same 1:1 the tabletop itself is drawn with, so
|
||||||
|
# an object's height ON the drawing is its position ON the desk.
|
||||||
|
# The desk's own top is the one synthesized surface (the objects
|
||||||
|
# cover every pixel of it), continued from the sibling tables'
|
||||||
|
# pattern in the drawing's own shades.
|
||||||
|
"lab_computers": dict(
|
||||||
|
tiles=[
|
||||||
|
[91, 92, 93, 94],
|
||||||
|
[54, 55, 85, 95],
|
||||||
|
[88, 89, 89, 90],
|
||||||
|
],
|
||||||
|
roof_rows=0, roof_back=0, roof_front=0, roof_cycle=(0, 0),
|
||||||
|
slab=0, front_eave=0, ledge=None, tileset="gym", depth=2,
|
||||||
|
desk=dict(fascia=(16, 18), base=(19, 21)),
|
||||||
|
parts=[
|
||||||
|
dict(kind="upright", x=(2, 13), top=(0, 2), facade=(3, 10),
|
||||||
|
depth=4), # the monitor
|
||||||
|
dict(kind="flat", x=(1, 13), rows=(11, 14)), # its keyboard
|
||||||
|
dict(kind="upright", x=(14, 21), top=(0, 3), facade=(4, 10),
|
||||||
|
depth=6), # the tower
|
||||||
|
dict(kind="flat", x=(14, 21), rows=(11, 14)), # keys + mouse
|
||||||
|
dict(kind="flat", x=(22, 30), rows=(1, 14)), # the paper
|
||||||
|
],
|
||||||
|
),
|
||||||
|
# F03: the empty north table beside it -- the starter table's band
|
||||||
|
# table verbatim on a grid two tiles narrower (one placement).
|
||||||
|
"lab_table_small": dict(
|
||||||
|
tiles=[
|
||||||
|
[41, 59, 59, 42],
|
||||||
|
[78, 57, 57, 79],
|
||||||
|
[88, 89, 89, 90],
|
||||||
|
],
|
||||||
|
roof_rows=19, roof_back=16, roof_front=0, roof_cycle=(2, 13),
|
||||||
|
slab=3, front_eave=0, ledge=None, tileset="gym", depth=2,
|
||||||
|
),
|
||||||
|
# F04: the Pokemon Center's PC -- every Center's northeast corner
|
||||||
|
# (11 placements) plus the Indigo Plateau lobby, whose MART tileset
|
||||||
|
# shares this atlas. The lab desk-set read again: a
|
||||||
|
# Mac-style unit drawn face-on -- white top band (rows 0-3), bezel,
|
||||||
|
# screen and drive slot (4-14) -- standing at the back of a low
|
||||||
|
# desk whose front face is rows 20-23 and whose drawn top (the
|
||||||
|
# white sliver of row 15 and the margins beside the unit) names the
|
||||||
|
# lid shade. The keyboard rows 17-19 lie BELOW the desk's 16px top
|
||||||
|
# span, so the flat part carries an authored z origin: it lies at
|
||||||
|
# the desk's front edge, in front of the unit.
|
||||||
|
"center_pc": dict(
|
||||||
|
tiles=[
|
||||||
|
[66, 70],
|
||||||
|
[82, 86],
|
||||||
|
[9, 88],
|
||||||
|
],
|
||||||
|
roof_rows=0, roof_back=0, roof_front=0, roof_cycle=(0, 0),
|
||||||
|
slab=0, front_eave=0, ledge=None, tileset="pokecenter", depth=2,
|
||||||
|
desk=dict(fascia=(20, 21), base=(22, 23), lid="white"),
|
||||||
|
parts=[
|
||||||
|
dict(kind="upright", x=(2, 13), top=(0, 3), facade=(4, 14),
|
||||||
|
depth=6), # the unit
|
||||||
|
dict(kind="flat", x=(2, 13), rows=(17, 19), z=13), # keyboard
|
||||||
|
],
|
||||||
|
),
|
||||||
# B23: the Victory Road entrance on Route 23: a rock face with two
|
# B23: the Victory Road entrance on Route 23: a rock face with two
|
||||||
# barred doors. The roof band is the pale cliff top seen from above.
|
# barred doors. The roof band is the pale cliff top seen from above.
|
||||||
"victory_road_gate": dict(
|
"victory_road_gate": dict(
|
||||||
@@ -642,7 +734,14 @@ def profile(sp, t):
|
|||||||
r = next((y for y in range(H) if inside(x, y)), t["roof_rows"])
|
r = next((y for y in range(H) if inside(x, y)), t["roof_rows"])
|
||||||
top.append(min(r, t["roof_rows"]))
|
top.append(min(r, t["roof_rows"]))
|
||||||
|
|
||||||
wall_h = H - t["roof_rows"]
|
# The drawing's own ground line: the row after the last drawn one. A
|
||||||
|
# building ends on the black threshold row it stands on (ground == H),
|
||||||
|
# but furniture is drawn standing on open floor -- the lab table's
|
||||||
|
# legs stop two rows short of its grid -- and extruding against H
|
||||||
|
# would float it that far above its own plot.
|
||||||
|
ground = max((y for y in range(H) for x in range(W) if inside(x, y)),
|
||||||
|
default=H - 1) + 1
|
||||||
|
wall_h = ground - t["roof_rows"]
|
||||||
ytop = wall_h - 1 + t["slab"]
|
ytop = wall_h - 1 + t["slab"]
|
||||||
|
|
||||||
# Recesses: the panes the art seals behind a black frame. Non-black
|
# Recesses: the panes the art seals behind a black frame. Non-black
|
||||||
@@ -674,15 +773,136 @@ def profile(sp, t):
|
|||||||
if x1 - x0 + 1 <= RECESS_MAX and y1 - y0 + 1 <= RECESS_MAX:
|
if x1 - x0 + 1 <= RECESS_MAX and y1 - y0 + 1 <= RECESS_MAX:
|
||||||
recess.update(cells)
|
recess.update(cells)
|
||||||
|
|
||||||
|
# Depth is the PLOT. For a whole-drawing building that is the grid
|
||||||
|
# itself; `depth` (in tile rows) names it when the grid runs past the
|
||||||
|
# plot onto ground the drawing merely stands its legs on.
|
||||||
return dict(top=top, wall_h=wall_h, ytop=ytop, recess=recess,
|
return dict(top=top, wall_h=wall_h, ytop=ytop, recess=recess,
|
||||||
inside=inside, D=H, W=W, H=H)
|
inside=inside, D=t.get("depth", len(t["tiles"])) * 8,
|
||||||
|
ground=ground, W=W, H=H)
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------- stage 3 --
|
# --------------------------------------------------------------- stage 3 --
|
||||||
|
def build_desk_set(sp, pr, t):
|
||||||
|
"""A desk with separately-classified objects on it (the `parts` list):
|
||||||
|
upright parts stand on the desk's top plane wearing their own drawn
|
||||||
|
tops as lids, flat parts lie one voxel proud at drawn row = depth row,
|
||||||
|
and the desk itself is the lab-table slab + base with a synthesized
|
||||||
|
lid (the objects cover every drawn pixel of the tabletop)."""
|
||||||
|
W, H, D = pr["W"], pr["H"], pr["D"]
|
||||||
|
inside = pr["inside"]
|
||||||
|
col, src = sp["col"], sp["src"]
|
||||||
|
ground = pr["ground"]
|
||||||
|
vox = {}
|
||||||
|
|
||||||
|
def put(x, y, z, sx, sy):
|
||||||
|
vox[(x, y, z)] = (col[sy][sx], src[sy][sx])
|
||||||
|
|
||||||
|
shade_px = {}
|
||||||
|
for sy in range(H):
|
||||||
|
for sx in range(W):
|
||||||
|
if inside(sx, sy):
|
||||||
|
shade_px.setdefault(col[sy][sx], (sx, sy))
|
||||||
|
|
||||||
|
def interior(sx, sy, lo, hi):
|
||||||
|
if col[sy][sx] != BLACK:
|
||||||
|
return sx
|
||||||
|
step = 1 if sx < (lo + hi) // 2 else -1
|
||||||
|
for d in range(1, 4):
|
||||||
|
nx = sx + step * d
|
||||||
|
if lo <= nx <= hi and inside(nx, sy) and col[sy][nx] != BLACK:
|
||||||
|
return nx
|
||||||
|
return sx
|
||||||
|
|
||||||
|
f0, f1 = t["desk"]["fascia"]
|
||||||
|
b0, b1 = t["desk"]["base"]
|
||||||
|
plane = (b1 - b0 + 1) + (f1 - f0 + 1) # the desk's top plane
|
||||||
|
|
||||||
|
# the base band, extruded exactly like every lab table's
|
||||||
|
for sy in range(b0, b1 + 1):
|
||||||
|
y = ground - 1 - sy
|
||||||
|
for sx in range(W):
|
||||||
|
if not inside(sx, sy):
|
||||||
|
continue
|
||||||
|
ix = interior(sx, sy, 0, W - 1)
|
||||||
|
for z in range(D):
|
||||||
|
put(sx, y, z, sx if z in (0, D - 1) else ix, sy)
|
||||||
|
for sx, sy in pr["recess"]:
|
||||||
|
if b0 <= sy <= b1:
|
||||||
|
vox.pop((sx, ground - 1 - sy, D - 1), None)
|
||||||
|
|
||||||
|
# the slab: the fascia rows wrap every side; the lid is the one
|
||||||
|
# synthesized surface in the model -- the drawing never paints the
|
||||||
|
# tabletop (its objects cover it), so the lid continues the sibling
|
||||||
|
# tables' pattern in the drawing's own shades: black rim, white
|
||||||
|
# highlight courses along the north and west, grey field
|
||||||
|
for i, sy in enumerate(range(f0, f1 + 1)):
|
||||||
|
y = plane - 1 - i
|
||||||
|
for sx in range(W):
|
||||||
|
for z in range(D):
|
||||||
|
put(sx, y, z, sx, sy)
|
||||||
|
field = WHITE if t["desk"].get("lid") == "white" else GREY
|
||||||
|
for sx in range(W):
|
||||||
|
for z in range(D):
|
||||||
|
if sx in (0, W - 1) or z in (0, D - 1):
|
||||||
|
shade = BLACK
|
||||||
|
elif sx == 1 or z == 1:
|
||||||
|
shade = WHITE
|
||||||
|
else:
|
||||||
|
shade = field
|
||||||
|
px = shade_px.get(shade) or shade_px[BLACK]
|
||||||
|
put(sx, plane - 1, z, px[0], px[1])
|
||||||
|
|
||||||
|
for p in t["parts"]:
|
||||||
|
x0, x1 = p["x"]
|
||||||
|
if p["kind"] == "flat":
|
||||||
|
r0, r1 = p["rows"]
|
||||||
|
for sy in range(r0, r1 + 1):
|
||||||
|
# drawn row = depth row by default; `z` renames the
|
||||||
|
# origin when the flat sits below the desk's own drawn
|
||||||
|
# top span (the Center PC's keyboard)
|
||||||
|
z = p.get("z", r0) + (sy - r0)
|
||||||
|
if not 0 <= z < D:
|
||||||
|
continue
|
||||||
|
for sx in range(x0, x1 + 1):
|
||||||
|
if inside(sx, sy):
|
||||||
|
put(sx, plane, z, sx, sy)
|
||||||
|
continue
|
||||||
|
tr0, tr1 = p["top"]
|
||||||
|
fr0, fr1 = p["facade"]
|
||||||
|
pd = p["depth"]
|
||||||
|
ytp = plane + (fr1 - fr0)
|
||||||
|
for sx in range(x0, x1 + 1):
|
||||||
|
# the lid: the part's drawn top laid across its depth from
|
||||||
|
# the back, last row continuing forward; the front lid row
|
||||||
|
# is the facade's own top row -- the drawn front-top edge
|
||||||
|
for z in range(pd):
|
||||||
|
sy = fr0 if z == pd - 1 else min(tr0 + z, tr1)
|
||||||
|
while sy <= tr1 and not inside(sx, sy):
|
||||||
|
sy += 1
|
||||||
|
if sy > tr1 and not (z == pd - 1 and inside(sx, fr0)):
|
||||||
|
continue
|
||||||
|
put(sx, ytp, z, sx, fr0 if z == pd - 1 else sy)
|
||||||
|
# the body: facade rows anchored to the desk's top plane
|
||||||
|
for sy in range(fr0 + 1, fr1 + 1):
|
||||||
|
y = plane + (fr1 - sy)
|
||||||
|
if not inside(sx, sy):
|
||||||
|
continue
|
||||||
|
ix = interior(sx, sy, x0, x1)
|
||||||
|
for z in range(pd):
|
||||||
|
if z == pd - 1:
|
||||||
|
if (sx, sy) not in pr["recess"]:
|
||||||
|
put(sx, y, z, sx, sy)
|
||||||
|
else:
|
||||||
|
put(sx, y, z, sx if z == 0 else ix, sy)
|
||||||
|
return vox
|
||||||
|
|
||||||
|
|
||||||
def build(sp, pr, t):
|
def build(sp, pr, t):
|
||||||
"""The voxel model. Order is load-bearing: walls, ledge, recesses, then
|
"""The voxel model. Order is load-bearing: walls, ledge, recesses, then
|
||||||
the roof solid overwrites what it intersects and the walls are trimmed
|
the roof solid overwrites what it intersects and the walls are trimmed
|
||||||
to the roof's underside."""
|
to the roof's underside."""
|
||||||
|
if t.get("parts"):
|
||||||
|
return build_desk_set(sp, pr, t)
|
||||||
W, H, D = pr["W"], pr["H"], pr["D"]
|
W, H, D = pr["W"], pr["H"], pr["D"]
|
||||||
inside, top, ytop = pr["inside"], pr["top"], pr["ytop"]
|
inside, top, ytop = pr["inside"], pr["top"], pr["ytop"]
|
||||||
col, src = sp["col"], sp["src"]
|
col, src = sp["col"], sp["src"]
|
||||||
@@ -720,7 +940,9 @@ def build(sp, pr, t):
|
|||||||
|
|
||||||
# ---- walls: the facade rows extruded straight back, trimmed under the roof
|
# ---- walls: the facade rows extruded straight back, trimmed under the roof
|
||||||
for sy in range(t["roof_rows"], H):
|
for sy in range(t["roof_rows"], H):
|
||||||
y = H - 1 - sy
|
y = pr["ground"] - 1 - sy # rows below the ground line are floor
|
||||||
|
if y < 0:
|
||||||
|
continue
|
||||||
for sx in range(W):
|
for sx in range(W):
|
||||||
if not inside(sx, sy) or trimmed(sx, y):
|
if not inside(sx, sy) or trimmed(sx, y):
|
||||||
continue
|
continue
|
||||||
@@ -742,7 +964,7 @@ def build(sp, pr, t):
|
|||||||
if t["ledge"]:
|
if t["ledge"]:
|
||||||
l0, l1 = t["ledge"]
|
l0, l1 = t["ledge"]
|
||||||
for sy in range(l0, l1 + 1):
|
for sy in range(l0, l1 + 1):
|
||||||
y = H - 1 - sy
|
y = pr["ground"] - 1 - sy
|
||||||
for sx in range(W):
|
for sx in range(W):
|
||||||
if inside(sx, sy) and not trimmed(sx, y):
|
if inside(sx, sy) and not trimmed(sx, y):
|
||||||
for z in (-2, -1, D, D + 1):
|
for z in (-2, -1, D, D + 1):
|
||||||
@@ -750,7 +972,7 @@ def build(sp, pr, t):
|
|||||||
|
|
||||||
# ---- recesses: the front voxel of every pane sinks, its frame stays proud
|
# ---- recesses: the front voxel of every pane sinks, its frame stays proud
|
||||||
for sx, sy in pr["recess"]:
|
for sx, sy in pr["recess"]:
|
||||||
vox.pop((sx, H - 1 - sy, D - 1), None)
|
vox.pop((sx, pr["ground"] - 1 - sy, D - 1), None)
|
||||||
|
|
||||||
# ---- roof: flat top over the plateau, stepped diagonal ends
|
# ---- roof: flat top over the plateau, stepped diagonal ends
|
||||||
z0, z1 = 0, D - 1 + t["front_eave"]
|
z0, z1 = 0, D - 1 + t["front_eave"]
|
||||||
@@ -805,6 +1027,83 @@ def verify(vox, sp, pr, t):
|
|||||||
for (x, y, z), _ in vox.items():
|
for (x, y, z), _ in vox.items():
|
||||||
assert y <= T(x), f"voxel pokes through the roof at {x},{y},{z}"
|
assert y <= T(x), f"voxel pokes through the roof at {x},{y},{z}"
|
||||||
|
|
||||||
|
# A desk set answers its own asserts; a facade-only template (roof_rows
|
||||||
|
# == 0) has no roof band, so there is no surface to hold level or slope
|
||||||
|
# and no coverage to demand of it; everything with a roof band answers
|
||||||
|
# the full set.
|
||||||
|
if t.get("parts"):
|
||||||
|
verify_desk_set(vox, pr, t)
|
||||||
|
elif t["roof_rows"] == 0:
|
||||||
|
# its one geometric intent: a straight extrusion, so each column
|
||||||
|
# of the drawing is solid from its lowest voxel to its top. NOT
|
||||||
|
# from the ground -- the drawing's base band is inset like every
|
||||||
|
# lab table's, and the body's outer columns legitimately
|
||||||
|
# overhang it -- and not on the recess layer, which sinks panes.
|
||||||
|
lo, hi = {}, {}
|
||||||
|
for (x, y, z) in vox:
|
||||||
|
lo[(x, z)] = min(lo.get((x, z), y), y)
|
||||||
|
hi[(x, z)] = max(hi.get((x, z), y), y)
|
||||||
|
for (x, z), h in hi.items():
|
||||||
|
if z == D - 1:
|
||||||
|
continue
|
||||||
|
assert all((x, y, z) in vox for y in range(lo[(x, z)], h + 1)), \
|
||||||
|
f"facade column has a hole at {x},{z}"
|
||||||
|
else:
|
||||||
|
verify_roof(vox, pr, t)
|
||||||
|
|
||||||
|
shell = [k for k in vox if not all(
|
||||||
|
(k[0] + d[0], k[1] + d[1], k[2] + d[2]) in vox
|
||||||
|
for d in ((1, 0, 0), (-1, 0, 0), (0, 1, 0),
|
||||||
|
(0, -1, 0), (0, 0, 1), (0, 0, -1)))]
|
||||||
|
return shell
|
||||||
|
|
||||||
|
|
||||||
|
def verify_desk_set(vox, pr, t):
|
||||||
|
W, D = pr["W"], pr["D"]
|
||||||
|
f0, f1 = t["desk"]["fascia"]
|
||||||
|
b0, b1 = t["desk"]["base"]
|
||||||
|
plane = (b1 - b0 + 1) + (f1 - f0 + 1)
|
||||||
|
|
||||||
|
# the slab is a full solid under everything on it
|
||||||
|
for x in range(W):
|
||||||
|
for z in range(D):
|
||||||
|
for y in range(b1 - b0 + 1, plane):
|
||||||
|
assert (x, y, z) in vox, f"slab hole at {x},{y},{z}"
|
||||||
|
|
||||||
|
# each part stands where authored -- upright bodies solid from the
|
||||||
|
# desk to their own drawn top (bar the front layer, whose panes
|
||||||
|
# sink) -- and nothing stands anywhere else
|
||||||
|
tops = {}
|
||||||
|
for p in t["parts"]:
|
||||||
|
x0, x1 = p["x"]
|
||||||
|
if p["kind"] == "flat":
|
||||||
|
r0, r1 = p["rows"]
|
||||||
|
z0 = p.get("z", r0)
|
||||||
|
for x in range(x0, x1 + 1):
|
||||||
|
for z in range(max(z0, 0), min(z0 + r1 - r0, D - 1) + 1):
|
||||||
|
tops[(x, z)] = max(tops.get((x, z), 0), plane)
|
||||||
|
else:
|
||||||
|
fr0, fr1 = p["facade"]
|
||||||
|
ytp = plane + (fr1 - fr0)
|
||||||
|
for x in range(x0, x1 + 1):
|
||||||
|
for z in range(p["depth"]):
|
||||||
|
tops[(x, z)] = max(tops.get((x, z), 0), ytp)
|
||||||
|
if z == p["depth"] - 1:
|
||||||
|
continue
|
||||||
|
ys = [y for y in range(plane, ytp + 1)
|
||||||
|
if (x, y, z) in vox]
|
||||||
|
assert ys == list(range(ys[0], ys[0] + len(ys))) \
|
||||||
|
if ys else True, f"part column hole at {x},{z}"
|
||||||
|
for (x, y, z) in vox:
|
||||||
|
assert y <= tops.get((x, z), plane - 1), \
|
||||||
|
f"voxel above its part at {x},{y},{z}"
|
||||||
|
|
||||||
|
|
||||||
|
def verify_roof(vox, pr, t):
|
||||||
|
W, H, D = pr["W"], pr["H"], pr["D"]
|
||||||
|
ytop, top, slab = pr["ytop"], pr["top"], t["slab"]
|
||||||
|
T = lambda x: ytop - top[max(0, min(W - 1, x))]
|
||||||
|
|
||||||
# The roof surface reads over the columns the drawing actually paints:
|
# The roof surface reads over the columns the drawing actually paints:
|
||||||
# a sprite inset from its box says nothing about the rest.
|
# a sprite inset from its box says nothing about the rest.
|
||||||
roofed = [x for x in range(W) if top[x] < t["roof_rows"]]
|
roofed = [x for x in range(W) if top[x] < t["roof_rows"]]
|
||||||
@@ -845,12 +1144,6 @@ def verify(vox, sp, pr, t):
|
|||||||
elif any((x, y, z) in vox for y in range(0, T(x) - slab + 1)):
|
elif any((x, y, z) in vox for y in range(0, T(x) - slab + 1)):
|
||||||
assert (x, T(x), z) in vox, f"wall uncovered at {x},{z}"
|
assert (x, T(x), z) in vox, f"wall uncovered at {x},{z}"
|
||||||
|
|
||||||
shell = [k for k in vox if not all(
|
|
||||||
(k[0] + d[0], k[1] + d[1], k[2] + d[2]) in vox
|
|
||||||
for d in ((1, 0, 0), (-1, 0, 0), (0, 1, 0),
|
|
||||||
(0, -1, 0), (0, 0, 1), (0, 0, -1)))]
|
|
||||||
return shell
|
|
||||||
|
|
||||||
|
|
||||||
def preview(shell, vox, sp, name, out, flip, canvas=(1700, 900), pad=20):
|
def preview(shell, vox, sp, name, out, flip, canvas=(1700, 900), pad=20):
|
||||||
# Mirroring depth swings the camera round to the other side. Mirror
|
# Mirroring depth swings the camera round to the other side. Mirror
|
||||||
|
|||||||
Reference in New Issue
Block a user