Files
DramaticShapeVoxelMod/CHANGELOG.md
T
2026-08-08 14:21:41 -04:00

3751 lines
218 KiB
Markdown

# Changelog
## Unreleased
### Added
- **SHINY POKEMON.** On by default, with no row to switch it off:
shininess is a property of a Pokemon, not a display mode, and one that
differed between two players' saves would be a setting rather than a
Pokemon.
**It was always there.** Gen 1 has no shininess of its own, but it has
the four DVs Gen 2 later reads to decide it, and the engine already
ships that reading (`src/pokemon/Stats.lua`, `isShiny` -- its own
comment calls it "the RBY virtual shiny"). So nothing new is stored on
a Pokemon and nothing migrates: every save ever made already contains
the answer, and this release starts drawing it. A mon is shiny when
Defense, Speed and Special are all exactly 10 and Attack is one of
2/3/6/7/10/11/14/15 -- which random DVs land on 1 time in 8192, the
classic rate, and the default the odds dial ships at. Deriving rather
than storing is what makes it survive a save, a box, a trade and an
evolution without a second copy of the truth to drift out of step; a
shiny Bulbasaur is a shiny Venusaur without being told. `mon.shiny` is
maintained as a cache beside it, written from the DVs and never read
as the source.
The roll happens in `Pokemon.new`, which is where every wild, gift,
starter and traded mon is built -- before the battle bakes its sprite,
which `battle.started` is already too late for. It draws from the mod's
OWN random stream rather than the game's, so installing this does not
shift the sequence every damage roll and encounter slot comes out of.
Trainers' Pokemon come out ordinary by themselves, because the engine
pins their DVs to a fixed set -- which is also what the real games do.
**The models are genuinely recoloured**, not tinted. The recolour runs
as part of the Stadium extraction: each species' textures are decoded
once, packed as usual, then recoloured and packed again beside it as
`NNNs.dsm`. The colours are Stadium's own -- it slides a model in HSL
rather than shipping second textures, a hue rotation in degrees plus
saturation and lightness on a quantized -8..+8 scale at 12.5% a step --
and all 151 sets of values are shipped in `data/shiny_colors.lua`.
Five species get an explicit colour table instead, because Stadium
gives THEM a real alternate texture and no single slide can reproduce
it: Clefairy, Clefable, Jigglypuff, Wigglytuff and Gyarados, whose
bodies must stay put while a small region rotates a long way.
Generated effect frames -- flames, beams, sparks -- are excluded, so a
shiny Charizard has a shiny hide and an ordinary fire.
Doing this at extraction rather than at load is what makes that
exclusion exact: `StadiumFx` marks its generated frames and the packer
drops the marker, so extraction is the last moment a flame is
distinguishable from a hide. The normal packs come out byte-identical
either way -- the shiny pass runs after they are written -- so
`tests/stadium_extract_test.lua` still diffs all 151 against the Python
oracle unchanged, and the format did not move. The install marker's REV
goes to 3 so an existing cache rebuilds rather than quietly showing
every shiny in its ordinary colours.
**Flat art is tinted** rather than recoloured, because the engine bakes
a species palette into an image cache that has no idea which individual
is being drawn. The tint is derived from that species' own shiny slide,
so a shiny Golbat leans green and a shiny Charizard goes dusky. On the
3D path each side is tinted separately, which is the only place the two
sides can differ. A multiply can only darken, so species whose shiny is
LIGHTER than their normal read quieter on the flat art than on the
model.
**A sparkle** on arrival: a ring of additive stars that springs from
the mon's chest and fades over three quarters of a second, armed on the
frame a side's occupant changes -- which covers a send-out, a switch
and a wild foe alike. A wild Pokemon never grows out of a ball, so the
grow was the wrong edge to hang it on.
**A star on the status page**, beside the level on page 1, drawn in the
engine's own pixel grid so it is palette-processed like every other
pixel rather than floating over the finished frame. Page 1 only: page 2
clears that block itself.
**SHINY ODDS**, on the DRAMATIC SHAPE menu itself rather than in one of
its four categories -- those are the diorama, the fights, what the look
costs and the headset, and an encounter rate is none of them. The row
reads `1:8192` and halves down to `1:1`, so every rung is exactly twice
as often as the one above it, and `1:8192` is both the default and the
fallback for an unreadable options file: the mod's default is the games'
own rate, not a buff. The number is the truth rather than an
approximation of it, because a missed roll also clears a mon that landed
on the pattern by luck -- without that, every setting would be itself
and 1/8192 in parallel, and no setting could ever be rarer than 8192.
## 1.8.0
### Added
- **LET'S GO: Pokemon GO-style catching, staged in the 3D battle.** A new
three-rung row. CATCH ONLY changes nothing about the game except the
throw: picking a Poke/Great/Ultra/Master Ball in a wild battle (or the
BALL row of the safari menu) opens capture mode instead of the automatic
toss. FULL makes wild encounters the real Let's Go article: the
encounter IS the catch -- it opens in throwing mode and stays there,
the foe never takes a turn, your own Pokemon is never sent out or
shown (no back pic, no model, no HUD), and B runs, which from a catch
encounter always works. Poke/Great/Ultra Balls are half price at every
mart, and EXPERIENCE works the way that game's does: every healthy
party member gains from every catch AND every trainer knockout, each
one measured against its OWN level through the Gen VII scaled formula
-- which is why Let's Go ships no EXP.ALL, and why a level 5 party
member takes several times what a level 45 one does from the very same
fight. A catch adds the throw stack on top: grade, first ball of the
encounter, new species, and a persistent catch combo. CATCH ONLY
leaves experience exactly as the original game had it.
**The throw.** The camera locks HEAD ON with the wild Pokemon -- its
own seat on the arena's axis, no drift, no steer -- and a real 3D Poke
Ball (modelled and animated for this: hinged lid, capture beam,
squash-click, decaying wobble, caught stars, breakout burst; GREAT
blue, ULTRA's yellow band, MASTER purple, SAFARI olive) hangs at the
bottom of the frame. The ball rides UNDER the finger -- mouse, touch,
or the right stick -- and releasing throws it with the swipe's own
velocity: forward from how hard, height from its rise, side from its
slant, gravity and collision deciding the rest, with a bearing-and-
range assist trimming honest errors. Circling the ball WINDS it -- the
spin visibly builds with the gesture to a cap and bleeds off when the
hand pauses -- and only a ball at the cap flies with the late-biting
curve. Contact is against the creature's own GEOMETRY: a pic foe is
its sprite's opaque pixels (a ball through the gap under a wing flies
on), a STADIUM foe its model's measured height, girth and hover. The
timing ring pulses on the creature, coloured by the Gen 1 odds, and is
judged AT the moment of contact: inside earns NICE / GREAT /
EXCELLENT, which multiplies the engine's own Gen 1 catch roll; the
shakes the roll answers are the rocks the ball plays on the ground.
**Running out, and staying out of the way.** Under FULL an empty bag
does not hand the fight back to the classic menu -- there is no fight to
hand back, since a Let's Go wild has no Pokemon of yours in it and a foe
that never takes a turn, so that menu would offer a FIGHT that cannot
happen. The encounter keeps its own screen: the seat holds, the Pokemon
stands there, the readout says NO BALLS LEFT, and RUN is the way out.
Throwing your last ball lands in the same place rather than ending the
session. And the scripted catch tutorials -- the VIRIDIAN CITY old man,
and Yellow's PROF.OAK catching the PIKACHU -- are left alone at every
rung: they are cutscenes wearing a battle's clothes, where the cursor,
the bag and the throw are all scripted and nobody keeps the Pokemon, so
they play exactly as the original does with no capture screen, no held
camera and no experience.
**What it stands on.** The outcome is exactly a Gen 1 ball throw: same
catch math (status, HP and ball factors intact), same outcome texts,
same caught flow -- dex page, nickname, box overflow -- and a missed
ball is a spent ball. Outside FULL, a failed throw still costs the
turn it always did. Needs the staged 3D battle standing (3D-BTL on, a
depth-capable driver, no headset); anywhere it cannot stand, balls
quietly take the engine's classic toss.
- **SHADOWS: a row that stands the sun's pass down.** Cast shadows are the
most expensive thing the mode draws after the geometry -- the whole world
rendered a second time from the light, every time the view or anybody in
it moves -- and on a phone or an old laptop that is the difference between
the diorama running and the diorama stuttering. ON by default, because a
world where a building throws nothing reads as flat however many voxels it
is made of. OFF means off rather than "fall back": the flat decal drop
shadows are the stand-in for a machine that WANTED shadows and could not
have them, so they stay down too, and the forest's light shafts go with
them (the beams are lit by the sun's own map). FULL neither sets the row
nor takes it away, on the same reasoning as AA -- what the look costs is
the player's question, not a preset's.
### Added
- **SELECT on any row of the mod's menus explains what it does.** Every
setting here has carried a paragraph of help since it was written -- it is
handed to the mod manager with the rest of the schema -- and nothing in the
engine has ever drawn one. It could not: a row is a label and a value, and
no options row anywhere has room for a third thing. So a row says what it
IS on one line and what it is SET TO on the next, and SELECT says what that
MEANS, which is the question RENDER DIST or 2D-3D B cannot answer in
eighteen characters however the label is worded.
It opens the game's own dialogue box -- drawn with the ROM's own border
glyphs, anchored to the bottom of the screen where this game has always put
text, and only as tall as the sentence it holds, so the row being asked
about is still visible above it. A, B, START and SELECT all close it, SELECT
included: it is the button somebody who just pressed it will reach for. The
bottom line of every one of the mod's menus now reads `B BACK SEL HELP`,
because a binding nobody knows about is worth nothing.
Every description is ONE SENTENCE, and the whole of it is on screen at once.
The long paragraphs these grew from were written for a reader that never
existed, and they read as documentation rather than as an answer; a box you
have to scroll is a worse reply to "what does this do" than a shorter
sentence is. Both properties are tested rather than trusted -- a description
that gains a second sentence, or that outgrows its box, fails the suite.
VOXEL, T-SHIFT and STADIUM ROM got sentences of their own to go with the
thirteen settings: the first two are the engine's row descriptors with
nowhere to keep one, and the third is an action rather than a setting.
The suite also checks every character of every description against the ROM's
real charmap, because Font.encode answers a glyph it does not have with a
SPACE and a one-time console warning -- so a curly quote pasted in from
somewhere would blank a word on screen and say nothing about it.
### Changed
- **The settings live on menus of their own now, behind one red row at the
top of OPTIONS.** This mod had grown to fourteen rows on the engine's
list, spliced in as one block. OPTIONS shows four boxes at a time, so that
was four screens of scrolling inside a list that already carried twenty
engine rows, and finding SHADOWS meant knowing it was in there somewhere
past the wireframe and the horizon bend.
What is on OPTIONS now is `DRAMATIC SHAPE..`, and it leads the list --
a mod that replaces the look of the whole game should not make the player
scroll to find out where its settings went, least of all past the engine
rows it has quietly taken away. It opens VOXEL and T-SHIFT, which came off
the engine's list with it, and four categories: **3D WORLD** (V-GRID,
V-CURVE, RENDER DIST, WATER, DAYTIME), **BATTLES** (3D-BTL, BACK SPRITES,
LET'S GO), **PERFORMANCE** (FOREST FX, SHADOWS, AA) and **VR** (VR,
SMOOTH TURN). STADIUM ROM stays on that top-level menu, last: it is
one-time setup rather than a setting, and somebody who has been told to
import a cartridge should find the row where the mod begins, not two
levels down a category they have no reason to open until it has worked.
The split is not a new opinion: it is the `full` flag each row already
carried. `full` marks a row the FULL preset does not take away, and the
reason written beside each one was always the same -- this is a question
about the HARDWARE, or about the GAME, not a knob on the diorama FULL is a
preset for. So 3D WORLD is exactly the rows FULL owns, and needs no rule
to disappear under it: every child filters itself out and an empty category
is not offered. Under FULL the menu is four rows on one screen with no
scroll arrow. The same rule retires VR where there is no VR to have.
**Nothing you had set has moved.** Every setting keeps its stored key, its
ladder and its row id, so `options.lua` is byte-identical across the
upgrade for a player who changes nothing -- and the hotkeys are untouched,
which is what makes the nesting affordable: 3, 5, 6, 7, 8 and 9 still put
every buried row one keypress away. The mod manager's own page still lists
all thirteen settings flat, now in category order.
The row is drawn in red, which is a palette zone rather than a color:
`setColor` cannot tint this text, because the glyph atlas is black ink and
LOVE tints multiplicatively, and because the palette shader keys on the red
channel alone and would send a red pixel to the lightest slot. What the
zone changes is which color the shade the text was drawn in comes out as.
It is MEWMON -- the palette the OPTIONS menu already wears -- copied with
only the ink slot replaced, so the paper under the row is the same white as
the row above it in all three ROMs, and the band covers the two text lines
alone rather than the cursor and the box borders beside them. SGB INV
reverses a palette, so there the red starts in the other slot and still
lands on the ink; OG, OG INV and CLASSIC substitute their own tables
outright, and the row simply draws monochrome, which is what asking for a
screen with no colors in it should get.
### Fixed
- **A setting that pins another one now pins it from wherever it was
changed.** 3D-BTL holds BATTLE LAYOUT at OG while a fight can be staged on
the map, and FULL holds DAYTIME at SYNC while it owns that row. Both pins
used to be a side effect of the options-rows hook, which every step on the
OPTIONS menu happened to rerun -- so they fired whether or not the step was
the one that mattered, and nothing had to name them. A step made on the
mod's own menus reruns no hook, so the pinning is a function now, and the
hook, the menus and the mod manager's page all ask for it.
- **An open OPTIONS menu notices a change made on a menu pushed over it.**
The rebuild that keeps the row list honest compared the voxel level and the
two battle switches across one call of `update`. The stack ticks its top
state only, so a step taken on one of the mod's own menus happens while
OPTIONS is suspended: both halves of that comparison were read after the
fact and always agreed, and OPTIONS came back still showing a BATTLE LAYOUT
row that no longer belonged there. The signature is held on the menu and
stamped where the rows are built, which is the thing it is a signature of.
- **A building's back no longer wears its own front door.** Every voxelized
building is its drawing extruded straight through the footprint, so the
far wall is the facade again -- and read from behind, the facade mirrored:
a door on the back of every house, a POKe sign readable backwards on
every Center, MART on every mart and GYM painted across the back of every
gym. Those tiles are now named per tileset (`frontOnly` in
`data/voxel_heights.lua` -- the doorways, the hanging shop signs and the
gyms' lettering) and every cell wearing one takes the art of the nearest
ordinary cell beside it in the same tile row instead. The donor is picked
per RUN, so a two-tile doorway comes out as two tiles of the same wall
rather than borrowing left from one side and right from the other, and
between the two neighbours the one that row uses more often wins -- which
is what reaches past a gable's sloped corner for the wall behind it. At
the base course the donor lifts one row with the model, because the
drawing's last row is the black threshold a door stands on and the wall
beside it does not paint; without that the doorway kept its own foot and
the back's bottom course had a notch in it. Windows are deliberately left
alone: a back wall with windows is right. The generic volume path folds
the same drawing up all four sides and had the same bug on its back AND
its flanks, so it takes the same substitution -- only the south face,
which IS the drawing, keeps every tile of it.
- **B now actually runs from capture mode -- and A throws, and L/R switch
balls.** The capture session read its button presses on the RENDER clock,
along with everything else it does per frame. Button edges do not survive
there: the engine rebuilds the edge table once per fixed logic step and
runs all of a frame's steps BEFORE the render-clock hooks, so any frame
carrying more than one step had already thrown the press away before
anything looked at it. That is not a rare race -- it is every press below
60fps, which is exactly where a 3D battle lives, so these buttons were
reliably dead on the machines that most needed them and fine on a 144Hz
one. They are read on the logic step now, through the engine's own
input.step seam, and taken rather than peeked so a press the capture used
does not also page the message it just queued.
- **No more grass smeared across the top of a LET'S GO throw.** The capture
seat handed BattleScene its pitch as the DEPRESSION below level, and the
one thing that reads it -- the camera-ward pull the grass and flowers are
drawn with -- measures angles off STRAIGHT DOWN, the complement. So a seat
looking nearly level read as the top-down end of the ladder, where the pull
is longest: 46 world pixels of bias, handed to a camera standing 46 world
pixels behind the player. The pull is a shove along each vertex's own eye
ray -- a pure depth bias while it is shorter than the range, and past that
it carries geometry THROUGH the lens, where the projection turns inside out
and a single tuft at the eye lands smeared across the frame. That was the
greenery hanging over the top of a capture shot on any route or street with
grass rows beside it. The seat now speaks the same convention the battle's
own rig does, and the vertex stage clamps the pull to half the range to the
eye besides -- so no camera standing this close can be smeared by a bias
again, in a capture, a fight, or first person.
- **The grass moves during a staged battle.** The wind is switched on around
the free-roam pass's grass draws and off again after them, and the battle
pass -- which draws the same tufts, on the same map, from its own camera --
never switched it on: the uniform sat at the per-frame default, which means
no wind, so a field that was moving one frame before the encounter went dead
still for the whole fight and started again when it ended. A fight is staged
on the MAP, in that place's own weather and light; a frozen field was the one
thing reading as a photograph of it rather than the place. No walker-contact
push comes with it -- that is somebody stepping through the grass, and the
two mons stand still on their own tiles.
- **The bottom of the frame no longer bites a row out of the scenery.**
RENDER DIST cut the world to where the frame's rays land on the GROUND,
and the ground is not what the picture is made of: a tree at the bottom of
the screen has its feet south of the row its top is seen on, because the
bottom edge's ray is still coming down as it passes them. The cut is by
column -- deliberately, so it never takes the tops off trees -- so a tree
whose base fell one pixel outside lost its whole height at once, and the
last row of forest along the bottom of the frame was cut through with the
ground behind it showing. The south edge is now walked back down that same
ray by the tallest thing that can stand on it (about a tile and a half at
35 degrees, four tiles at 50, eleven at 75), plus a tile of slack so a hard
edge is never decided by a rounding. FIT carries it too: it is a correction
to the honest answer, not margin around it.
## 1.7.1
### Added
- **RENDER DIST: stop drawing the map you cannot see.** The orbit rungs now
cut the world to the ground the camera actually frames, so a connected
map that falls entirely outside it is skipped before it is drawn --
terrain, water, grass, flowers and its whole shadow pass. At the high
rungs, where the camera is nearly overhead, that is most of the frame's
geometry never submitted.
**The footprint is not the window.** Tilt the camera and the ground it
frames stops being the flat game's own rectangle and becomes a
trapezoid: reaching much further north, flaring much wider out there,
and pulling in at the near edge. At 35 degrees a 320x288 view reaches
270 world pixels north where the window reaches 144, and 246 to each
side where the window reaches 160 -- so a window-sized cut takes a bite
out of a world plainly on screen, with sky showing through the top and
both sides. It is derived from the orbit's own basis rather than guessed
-- the frame's corner rays dropped on the ground plane, in closed form,
cross-checked against a ray cast in the suite -- and the stored
rectangle sits north of the view centre, because the trapezoid does.
**The row is a real render distance at 75.** Past about 63 degrees
(exactly `atan(2*FOCAL)`) the horizon is inside the frame and "all the
ground on screen" is an infinite answer, so something has to name a
distance. FIT is the closest of the four, WIDE through WIDEST push the
world's edge out, OFF stops cutting. Below that pitch the honest
footprint is already inside the reach and the row does nothing to the
picture at all.
The cut reaches the shader as the same box the headset's DIORAMA uses --
rectangular now, with two half-extents, and the diorama passes the same
number twice. Under V-CURVE the rim dissolves rather than cutting,
because a bent world has no straight sides. The sun and the eye ask the
same question about the same maps, so the light can never record a map
the camera did not draw.
Not on 1ST or 3RD -- the player is standing in the world there -- and
the box opens out and away over the rung tween rather than vanishing on
the frame the rung changed. FULL sets it to FIT.
## 1.7.0
### Added
- **The DIORAMA modes: Kanto as a model you can pick up.** The VR row is a
ladder now -- OFF / STANDARD / DIORAMA / DIORAMA-MR. STANDARD is what the
mod already did (the headset follows the VOXEL ladder, orbit rungs a
tabletop and 1ST life size). DIORAMA is one presentation instead of a
ladder: the world is always the model on the table, and the model is a
thing in the room.
Everything outside an invisible **box** centred on the view is simply
not drawn -- the Final Fantasy Tactics read, a square slab of the world
sitting in the air rather than a map running off to a horizon, cut with
a hard edge because a flat world is a thing with sides. The cut reaches
every pass the world is made of -- terrain, characters, grass, water and
the forest's beams and motes.
**V-CURVE changes its shape.** With the bend on the world is not flat any
more -- it is a little globe curling away over its own horizon -- and a
square cut through that is a lie about what is being looked at. So the
box becomes a **ball**, and its edge becomes a **gradient** dissolving
into the sky (the same sky the flat screen has). One click of the left
stick throws the row and swaps the whole reading of the model.
**A staged fight** ignores both shapes and cuts a vertical **pillar**
about the arena, always dissolved at the rim, framing the model to it:
the fight lifted out of the map as a floating disc.
**The grips** take hold of the whole thing: one hand carries the model
anywhere in the room, both hands turn it and open the viewport out to
whatever you spread your hands to. The **left stick's click** throws
V-CURVE to its top rung and back rather than stepping views -- there is
no 2D diorama and no first-person one, so the ladder is held on an orbit
rung for as long as the mode runs, and the Pokedex stays away.
**DIORAMA-MR** is the same mode with the background keyed pure green --
no bands, no sun, no haze, because every one of those is a colour a
keyer would have to survive -- for a mixed-reality capture that
composites the model into the player's own room.
A save that stored the old VR toggle as `true` comes back on STANDARD
rather than falling to OFF. The viewport is compiled into the scene
shader as its own variant, so a flat frame -- and a phone above all --
builds and binds exactly what it always did.
## 1.6.2
### Added
- **The air of Viridian Forest: volumetric god rays, ground fog, and a
FOREST FX row.** An invisible jungle canopy now hangs above the forest's
real trees, and light comes down through it as true volumetric beams: a
per-pixel march reads the frame's own depth buffer and the sun's own
shadow map, so shafts stand exactly where light really breaks between
the tree hulls, trunks and passing characters carve dark columns
through them, and a wind-blown leaf field at the canopy plane opens and
closes the beams like foliage moving overhead. The beams are alpha zero
at the canopy and fade in as they descend -- light below the leaves,
never a lid above them -- and a forward-scattering term blooms them for
a camera looking up into the light, first person especially.
The scene shader gains a height-and-distance fog every surface sinks
into, and the rays are that fog lit: one shared ramp off the day/night
clock colours both, gold spears of sun by day, silver moon rays after
dark, dying back through the twilights as one hands over to the other.
Pollen drifts through the day's beams and fireflies blink low over the
floor at night, all shader-animated and deterministic. A fight staged
on the forest floor sits in the same haze at half density.
The direction never moves: a canopy map's light is pinned to noon (see
DayNight.CANOPY), so the beams always agree with the shadows on the
floor. Everything is authored per map in `data/map_atmosphere.lua` --
a map with no entry spends nothing -- and the **FOREST FX** row (FULL /
LOW / OFF, FULL by default) governs the cost: LOW halves the march and
stands the particles down. On Android the row offers LOW / OFF only --
no mobile driver grants the readable depth the march needs -- so the
forest keeps its haze there and loses the beams.
## 1.6.1
### Fixed
- **Exeggutor, Tangela and Magmar stand as models in STADIUM battles, and
Pidgeot and Dodrio stop animating garbled.** Five species animate with
hermite keyframes rather than packed per-frame streams, and the extractor
read the animation flags byte from the wrong half of its u16 -- the half
that is always zero -- so it decoded their keyframe tables as streams.
For Exeggutor, Tangela and Magmar the result exploded so hard the packer
declined them to flat battle pics; Pidgeot and Dodrio stayed models but
played the garbage. All five now decode the way the game's own sampler
(src/17300.c) does, and no species is held off the field any more.
Model packs built by an older version of the mod are detected by a
revision stamp in the install marker and rebuilt from the ROM on the next
launch (or shadowed by a checkout's freshly packed set) rather than
trusted.
## 1.6.0
### Added
- **STADIUM battles, and a disc stage: three new rungs on the 3D-BTL row.**
The row that used to read ON / OFF now reads **2D-3D A / 2D-3D B /
STADIUM A / STADIUM B / OFF**, which is two independent choices laid out
as one ladder -- WHAT is standing there, and WHERE:
| | on the MAP | on two DISCS |
| ---------- | ----------- | ------------ |
| **pics** | 2D-3D A | 2D-3D B |
| **models** | STADIUM A | STADIUM B |
2D-3D A is what ON always was -- the fight staged on the map with the Game
Boy's own pics stood up on their tiles as quads. The STADIUM rungs keep
the whole staging and replace those quads with the **Pokemon Stadium
battle models**: all 151 species, skinned, lit and animated, playing the
animation the move being used actually calls for.
**A** stands the fight on the map, in the world's own light and weather.
**B** stands it on two discs against the sky and draws no map at all --
the Game Boy's own framing, staged rather than found.
All four combinations are reachable rather than only the diagonal. The
discs do not know what is on them -- they are two platforms at two cells,
drawn off the arena alone -- so **2D-3D B** costs one value on the ladder
and gives the disc framing to a player who has no Pokemon Stadium ROM and
would rather not go and find one. It needs nothing the base game did not
ship: the stage is generated in Lua and the Pokemon on it are the game's
own art.
B exists because the map does not always cooperate. Half of Kanto's
interiors are furniture, a cave floor can be nothing but two-cell
corridors, and some maps have nowhere a fight can be SEEN from a low
camera and are declined outright -- which drops the player back to the
flat battle screen with no warning. A carried stage has none of those
problems: it works on every map, at every step, and the framing is the
same every time. What it gives up is the thing A is for, which is
fighting somewhere real.
It is abstracted from the GROUND, not from the world. The sky behind the
discs is the hour's own -- gold at dusk, navy at midnight -- and a fight
in a cave or a shop is under that place's void and its own flat light, so
walking into Mt. Moon at night and starting a battle looks like Mt. Moon
at night.
The discs themselves are flat: one quad apiece, wearing a painted circle
that fades out at its rim, sized to the Pokemon standing on it. The fade
is an ordered dither baked into the texture's alpha rather than a smooth
ramp -- the scene shader discards under half alpha outright, so a ramp
would come out as a hard-edged circle -- which is the same trick the sky's
own bands use and reads as intended on a mode built out of visible texels.
Nothing else about the fight moves. The arena is picked the same way, the
camera is solved the same way, and the HUDs, the frosted text box, the
move animations and the depth of field are all exactly what 2D-3D draws
-- because every one of those is hung off the arena's CELLS rather than
off the pics. Swapping what stands on a cell changes nothing about where
the cell projects to, which is why this is a rung on the mode rather than
a second mode.
The animations are driven from the fight itself: a move plays the animation
that species' own battle table names for that move (the Stadium ROM's
per-species move table, packed into the assets, keyed by the same Gen 1
move id the engine's move defs already carry -- so DIG really does put
Diglett into the ground), fainting plays the faint and holds on its last
frame, and a send-out grows the Pokemon out of the ball as it opens and
plays the entrance with it. Between all of that, the standby loop. The eyes blink and go
dizzy on their own counter, which is a texture animation glTF has no
channel for and the pack carries anyway. Charmander's tail flame and
Weezing's gas are there too, drawn additively over the body.
The models go through the mod's OWN vertex format and shader rather than
a path of their own, which is what earns them everything the rest of the
diorama has: the depth buffer decides what is in front of what, the sun
pass throws a shadow of the actual pose, the hour's tint lands on them,
the hit flash flattens them and the tilt-shift and depth of field see
them as part of the picture. Skinning is done on the CPU -- these are
674-vertex models and exactly two are ever on screen -- and once per
frame, so the sun, the camera and both VR eyes draw the same posed mesh.
It declines per POKEMON rather than per battle: a species whose pack is
missing, a substitute doll, or the trainer's own pic before the send-out
all fall back to the flat card, on that side alone, with the other side
keeping its model.
Stored as new values on the same key, with 2D-3D A still first -- so a
save written before this update reads back as exactly the mode it was
written for.
- **The models are built on your machine, from your own cartridge.** The
mod ships no Pokemon Stadium data and cannot: it is that game's. What it
ships is the READER.
**Press STADIUM ROM on the OPTIONS menu and pick the file.** The row opens
the host's own file dialog -- osascript on macOS, PowerShell's
OpenFileDialog on Windows, zenity then kdialog on Linux, which are the same
four the engine's own Game Boy importer uses -- and the models are built
from whatever comes back. `.z64`, `.n64` and `.v64` all work; the byte
order is detected. The row reads IMPORT before and READY after, and
pressing it again imports a different cartridge.
The ROM is **not kept**: it is read, built from, and forgotten. A Stadium
cartridge is 32 MB and the models built out of it are 34, so keeping both
would double the cost of the feature for a file with no further use -- the
marker still records its md5, so a swapped cartridge is noticed.
The wrong file is refused with a reason on the loading screen rather than
half-built, and refused BEFORE anything is written -- which matters more
than it sounds, because the marker is the only thing that makes 151 files
on disk count as installed, so a refusal that wrote one anyway would
uninstall a working set.
The original route still works everywhere, and is the answer on the
platforms with no dialog (Android; a Linux install with neither zenity nor
kdialog): drop a Pokemon Stadium (US) ROM into a `baseroms/` folder beside
the game, straight in it rather than in a revision subfolder under it, and
the first time it runs
the 151 battle models are built out of it on a loading screen, in about ten
seconds, one species a frame. The screen says what it is doing in those
words -- **ONE-TIME EXTRACTION OF STADIUM ASSETS** -- carries a progress bar
filled by species written rather than by elapsed time, and names the
Pokemon it is on, which is the difference between a bar that is trusted and
one that is suspected of having hung. After that they sit in
the save directory and are read like any other asset. Until then the two
STADIUM rungs simply are not on the row: they are skipped rather than
shown and refused, because a setting that can be selected and then does
nothing is indistinguishable from a broken mod.
This is the same arrangement the engine already has for the Game Boy ROM
it is a recompilation of, and it is why nothing ROM-derived is in this
repository.
Doing it needed the whole extraction pipeline in Lua: the archive and its
Yay0 streams (`StadiumRom`), a geo-layout walk and an F3DEX2 interpreter
with six texture codecs and a bit-packed animation sampler
(`StadiumFragment`), the generated flame and gas stand-ins (`StadiumFx`),
and the bind-pose measurement and packer (`StadiumBuild`).
**Pure Lua, and deliberately so: it runs anywhere LOVE does, phones
included.** No FFI, no native helper, no second process, nothing outside
the standard library and stock LOVE calls. On desktop it is about seven
seconds and peaks at 68 MB of Lua heap, 32 MB of which is the cartridge
itself; the working set does not grow across the run.
`tests/stadium_budget_test.lua` measures both and fails if either stops
being bounded, because a transient peak a desktop shrugs off is what gets
a process killed on a phone. On Android the save directory is the app's
external-files folder, so `baseroms/` there is reachable over USB or a
file manager without root.
- **Acknowledgement for [pret/pokestadium](https://github.com/pret/pokestadium)**,
in README.md, model_extract/README.md and mod.card. The STADIUM extractor is
original code, but the decompilation is what it was written against -- the
bone matrix chain and the fact that scale is kept out of it, the rotation
basis, the animation and texture-animation samplers, the battle context
slots and the move-id constants all came from reading that project. None of
its code or data is vendored here or needed to run this, and the credit says
so as plainly as it says what was owed.
- `tools/stadium_pack.py` now reads the ROM directly rather than a
pre-extracted tree, which makes it the ORACLE the Lua port is verified
against: `tests/stadium_extract_test.lua` runs both over the same
cartridge and requires all 151 packed files to come out **byte for byte
identical**. That is the only honest test of a port of that much numeric
code -- every rounding mode, iteration order and off-by-one shows up as a
differing byte, and there are thirty-four megabytes of them.
- `ModSetting:setValue`, which sets a setting by its stored value rather
than by its place on the ladder, and `ModSetting:setGate`, which lets a
rung exist only when there is something behind it. 3D-BTL grew two rungs
in the middle of itself, and every caller that had counted to two would
otherwise have quietly meant something else afterwards.
- `tests/stadium_shots.lua`, a shot driver for both rungs, with a control
mode and a pinned clock so two runs of it can be compared.
- **`tests/stadium_anim_qa.lua`: every Pokemon, every animation, every
frame.** A battle asks a species for one of its animations and then poses,
skins, re-textures and draws it sixty times a second, and nothing exercised
that chain -- the pack probe reads the format and walks a bind pose, the
shot drivers show one species in one animation at a time. So the failures
only some species have had no way of being found except by a player calling
that Pokemon out, which is exactly how the eviction crash above was found.
This is that sweep, headless: the real StadiumPack, StadiumRig and
StadiumMon over stubs for the three things a graphics context provides, and
the stubs are not lenient -- a released object throws with LOVE's own
message, because a stub that quietly accepted one would hide the class of
bug the sweep exists to find. 151 species, 403,716 posed frames, 25
seconds. It also drives the state machine over all 165 move slots and
reproduces the cache eviction directly, which no amount of playing one
species' animations can reach.
What it found, and what happened to each:
| finding | count | outcome |
| --- | --- | --- |
| pack cache evicted a model still on the field | 2 | **fixed** -- see above |
| animation walks the Pokemon off its tile | 65 entrances, 36 hits, 35 faints | **fixed** -- see above |
| part has no texture | 104,728 | **not a defect.** 39 primitives across 37 species, 1.6% of the set's vertices, every one with all-zero UVs: they are flat-shaded geometry in the original. The pack stores texture indices one-based, so the packer's `0xFFFF` "untextured" sentinel arrives as 65,536 and resolves to nothing, which is correct. Counted rather than reported now |
| pose flies apart | 250 | **understood, left alone.** Two species in one animation each: Farfetch'd's entrance at 6.0x its bind height and Dewgong's at 6.1x, both just over the threshold, and both because the measurement is a bounding box that includes an authored trail -- Farfetch'd's is a dedicated 30-vertex primitive on a five-bone chain. The bodies are intact and, since the anchor, in frame |
| NaN or infinite vertex, rig would not build, loopStart out of range, missing aux animation, track indexed off its end, move slot with no animation | 0 | none |
The three species the packer already declines (Exeggutor, Tangela, Magmar)
are skipped rather than swept: they are never posed in a game, and sweeping
them anyway produced 356 of the 362 original "flies apart" findings.
### Fixed
- **Idle animations snapped, and then ran at half the frame rate.** Two
bugs with the same cause, fixed in opposite directions.
The animation streams are not keyframes: they carry one value per frame at
30 Hz and the game steps them. Blended naively against a 60 Hz camera
that produced the reported glitch -- Rattata's idle snapping almost upside
down for a frame, Charmander's arms turning inside out for a few --
because rotations here are **Euler triples**, and two triples can describe
nearly the same orientation while being nowhere near each other component
by component. `(0, 20976, 32736)` and `(0, -19936, -5904)` are a real
consecutive pair out of the set. Walking from one to the other passes
through orientations that are nothing like either end.
Dropping the blend fixed that and cost the smoothness: every pose then
held for two frames, and a set of models moving at half the rate of
everything around them reads as a stutter. So the blend is back, and the
snaps are **detected** rather than smoothed. A bone whose rotation moves
more than a quarter turn inside one 30 Hz frame is not being animated, it
is being re-expressed, and it holds its frame instead of blending -- all
three components together, because they are one rotation. The same guard
covers a translation that teleports more than half the Pokemon's own
height in a frame. Angles blend the short way round the +-pi seam, and
texture animations still step whole frames, because an eye is open or shut
and there is no halfway swap to draw.
Measured, not eyeballed: walking every species' standby loop in quarter
frames and differencing each bone's world origin against itself, the
number of species that jump more than a quarter of their own height in a
quarter frame goes **42 -> 11 -> 3**: naive blend, stepped, guarded blend.
The three that remain (Pidgeot, Dodrio, Grimer) have erratic rotation data
at source and are held rather than smoothed, which is exactly the guard
doing its job.
- **Exeggutor, Tangela and Magmar are drawn as battle sprites instead of
models.** Those three come out of the ROM with standby loops that throw
bones hundreds of units off the body -- the game's own index arithmetic
evidently reads their channel streams differently from the way this does.
They used to stand still in their **bind pose** instead, on the reasoning
that a Pokemon looking like itself beats one coming apart.
It does, but only just: a bind pose is a rigging pose, not a portrait, and
three species holding a T-pose among a hundred and forty-eight that
breathe read as broken rather than as still. So they now decline the model
outright and the Game Boy's own battle pic stands on the tile instead --
the same per-Pokemon fallback a species with no pack at all already took,
in the same arena, under the same light.
Keyed on the packer's own measurement rather than on a list of dex
numbers, so a re-extraction that fixes those streams -- or breaks a fourth
species -- moves this with it.
- **The eyes blinked several times a second.** A texture animation was
running on a clock of its own and wrapping on its own stream's length.
Rattata's standby loop is forty frames and its blink is five -- `6 8 7 8
6`, open through shut and back -- so that played it six times a second,
and every species with a short blink twitched the same way.
Two things were wrong, and the data says so plainly: 507 of the 691
animation/texture-animation pairs in the set are exactly the same length
as each other, which is what one shared frame counter looks like from the
outside. So the texture animation now rides the SKELETAL animation's own
frame, and it HOLDS its last entry past the end of its stream rather than
looping -- which is what the game's own sampler does (`func_80017540`).
Rattata now blinks once at the top of each idle loop and keeps its eyes
open for the other thirty-five frames.
The frame is not recomputed for the textures; `pose` stashes the one it
resolved and `textures` reads it, so the two cannot drift apart.
- **The faint animation no longer plays while the HP bar is still
draining.** `onFaint` runs the instant HP reaches zero, but the engine
queues the visible collapse behind the move animation and the bar drain
-- and that drain takes real time, some two and a half seconds on a
full-health Pokemon. The model was therefore lying down while its own
health went on emptying above it.
The request is now recorded when HP hits zero and played when the bar
reaches zero, off the engine's own `shownHP` -- the same number the bar is
drawn from, so the two cannot drift. A faint that stops being owed in the
meantime (a revive, a battler replaced under us) is dropped rather than
fired late at whoever is standing there. Measured rather than eyeballed:
the shot driver's `DS_FAINT` case prints the frame the bar empties against
the frame the animation starts, and they are the same frame.
- **Being hit played the ATTACK animation.** The context slot the mod called
`hit` is not a damage reaction: read against the move table, Bulbasaur's is a
95-frame animation that 66 of its moves play, and Pidgey's is 138 frames --
four and a half seconds -- shared by 111 of its moves. That is the species'
DEFAULT ATTACK, which is why taking damage looked exactly like swinging: it
was the swing. Slots 173, 178, 179, 180 and 181 all point at the same one.
Nor is a reaction hiding elsewhere. Exactly one animation per species is
claimed by no slot and no move, and it is the same length as that species'
idle for essentially all of them -- 48/48, 56/56, 60/60, 84/84 -- so it is a
second standby loop, not a recoil. **This set has no damage reaction in it.**
So damage now plays nothing and the Pokemon carries on with what it was
doing, which is what the engine already communicates through its own screen
flash, pic blink and HP drain. The slot is renamed `attack_default`
throughout (a label on a position -- the file format is the ORDER, so no
bytes changed), and the generic swing a move with no table entry falls back
to now uses it rather than resolving to the standby loop.
- **The battle menu no longer changes colour with the scenery.** There was a
pass that measured each frosted panel's average brightness and flipped the
glyphs to white over a dark one, with hysteresis so a drifting camera could
not strobe them. It worked, and it was still wrong: the battle menu is the
part of the frame the player reads constantly, and having its colour depend
on where the camera happens to point makes it unreliable furniture. Gen 1's
battle ink is black, so it is black -- on a cave floor as much as on a
meadow -- and the panel's tint, which always pushes toward white, is what
earns it its contrast. Removing it also took out a one-pixel GPU readback
that ran several times a second to answer a question nothing asks any more.
- **The required ROM is now named everywhere: Pokemon Stadium (US) 1.0.** It
always was the only one that works -- every offset in the reader was
measured against that cartridge -- but the docs and the file picker just
said "Pokemon Stadium (US)", which is three different ROMs. The picker's
title, the OPTIONS help, the README, mod.card and the manifest all name the
revision now, the README carries the reference md5
(`ed1378bc12115f71209a77844965ba50`) so a player can check their own file,
and a build from anything else says so on the loading screen as well as the
console rather than quietly producing wrong models.
- **The extraction screen just says STADIUM EXTRACTION now**, with the
progress bar and the species name under it; the "this runs once" line is
gone.
- **Pokemon now grow out of the ball, instead of appearing beside it.**
Measured, the send-out is: the ball is thrown, the POOF animation runs for
27 frames, and `startGrowIn` fires on the frame AFTER it ends. So the model
did not begin to exist until the ball had finished opening -- the ball came
apart, and then a Pokemon was switched on next to it.
The engine's own ramp is the Game Boy's three steps (0, 3/7, 5/7, full)
across the twelve frames after that, which on a 56-pixel sprite is a chunky
pop and on a smooth 3D model is just a pop.
The model now runs its own ramp, started when the POOF BEGINS and
continuous: it grows out of nothing while the ball is coming apart and
reaches full size exactly as the engine's own grow finishes -- 39 frames
end to end, which is the poof's 27 plus the engine's 12. Smoothstep rather
than linear or ease-out, because the ball is still opening through the first
half and a curve that was already near full size by then would have the
Pokemon standing about waiting for it. The entrance animation starts with
the grow, so the arrival is one performance rather than a grow followed by
a flourish.
Its own ramp OWNS the arrival once it starts: falling back to the engine's
afterwards shrank the Pokemon from 0.96 back to 0.71 and then snapped it to
full, because the two finish a few frames apart -- a visible hitch at the
end of the one animation that exists to not have one.
- **The first Pokemon of a battle arrived, left, and arrived again.** Every
guard deciding whether the player's Pokemon is on the field is a field the
engine sets once the battle is RUNNING, and during the opening none of them
is set yet: `showPlayerBack` is still nil (BattleState assigns it further
in), `playerBackPic` is nil with it, and `sendingOut` does not go true until
the ball is thrown. So the whole intro read as "this Pokemon is standing on
the field" -- two and a half seconds of it, on its tile, playing its standby
loop, before the trainer sprite it is meant to be hiding behind had even
appeared. It then vanished when that sprite arrived and came back with its
entrance when the ball opened. A switch has no intro, which is why a switch
always looked right and was the thing worth comparing against. The player's
side is now simply not on the field during the intro phase.
- **The anchor made birds shake, and the shake read as fast flapping.** The
body estimate it corrects toward was the MEDIAN bone origin -- robust to a
few bones flung out, which is what it was chosen for, and wrong in a way
that only shows on a flapping model: a median is a RANK, and on a bird most
of the skeleton is wing, so which bone sits at the middle of the sorted list
swaps between the up cluster and the down one every beat. Measured, the
estimate moved a tenth of a body-height between adjacent half-frames on
Pidgey and three whole body-heights on Pidgeot, and the anchor turned that
into a translation of the entire Pokemon -- the body counter-shaking against
its own wings, which reads as flapping at twice the real speed.
The centre is now the bone origins averaged and **weighted by how many
vertices each bone moves**. The weights are a property of the mesh, computed
once, so there is no rank to flip -- and a bone with little geometry barely
counts, which is the robustness the median was for in the first place
(Farfetch'd's trail is 30 vertices on five bones). On top of that the
correction is low-passed, so what survives is where the Pokemon has drifted
to and never how it is shaking on the way. Pidgey's shake goes from 0.24 to
**0.10 pixels a frame** on a fourteen-pixel model, and travel correction
improved with it.
Two species -- Pidgeot and Dodrio -- have standby loops with genuinely junk
rotation frames, which move the estimate three and two body-heights in a
single frame against a fifth of a body-height for the fastest real motion in
the set. No filter separates those: rate-limiting the correction bounded the
shake but put 33 of the 148 entrances back outside the frame, and
rate-limiting the measurement could not tell a spike from an excursion
because they are only a factor of fifteen apart. So each species' own
standby loop is walked once, at rig construction, and one whose estimate is
that unsteady is **not anchored at all**: it travels as far as its animation
says and does not vibrate, which is exactly how it behaved before the anchor
existed. One species trading a framing problem for no problem beats 147
trading a solved framing problem for a shake.
- **A Pokemon calling out a fifth species killed every model in the fight.**
Reported as `Cannot use object after it has been released` out of
`Voxel3D.draw`, after which nothing 3D drew for the rest of the battle.
The pack cache holds four models and evicts the least recently *loaded* --
and `load` only runs when a side's species CHANGES, so a Pokemon that has
been standing there for a few turns is the oldest entry in the cache. Send
out a fifth species and it was evicted mid-fight and **its textures
released**, while it was still being drawn sixty times a second.
Two things were wrong. The eviction released each texture but left the dead
object in its slot -- and a released Image is still a truthy value, so the
next `image()` handed the corpse straight back out to `mesh:setTexture`.
Slots are now cleared, so an evicted model simply decodes its textures
again. And the mode now says, every frame, which two species are actually
standing there (`StadiumPack.keep`), so the two in use are always the two
most recent and cannot reach the front of the queue at all.
- **And a model that does fail now fails gracefully.** Both draws were a bare
loop inside the caller's single pcall, so a throw on the first side skipped
the second -- one broken Pokemon took its opponent off the screen with it
-- and nothing recorded that it had happened, so the same throw came back
every frame forever. Each side is now drawn, cast and posed inside its own
guard, and a side that throws is RETIRED: its rig is released and
OverworldBattle renders its flat battle pic from the next frame on, which
is the fallback a species with no pack has always had. The fight carries on
with a flat Pokemon instead of a missing one, and its opponent is
untouched.
- **Half the set's animations walked the Pokemon out of the shot.** Found by
the sweep below, not by a bug report, and it is the biggest of them: 65 of
the 148 send-out entrances carry the body more than its own height off the
spot it started on -- Dewgong's reaches seven and a half, and its faint
nearly ten. Every one returns to exactly where it began, because Pokemon
Stadium framed each Pokemon with a camera of its OWN that followed the
performance around a stage. This mode has one camera, solved to hold two
fixed map cells, so a Pokemon that travels seven body-heights is simply
gone: sending out a Farfetch'd left an empty tile for three and a half
seconds while its animation played somewhere off to the left of the frame.
`StadiumRig.anchor` now takes the excess back out -- the pose is measured
against where the bind pose put the body, and whatever has carried it
further than `StadiumMon.TRAVEL` (three quarters of a body-height, which is
what the frame holds) is subtracted from every bone. The EXCESS only: the
83 species that never reach the limit are bit-for-bit what they were, and a
lunge, a hop or a collapse still reads as big and still comes back to the
tile it left. The centre is the median bone origin rather than the mean or
the root, so Farfetch'd's five-bone trail streaking three thousand units
out cannot drag the bird with it.
- **FLY and DIG now take the model off the field.** The charging turn of a
two-turn move puts the Pokemon out of reach, and the engine says so through
`picFx[battler].hidden` -- FLY runs `SE_SLIDE_MON_OFF` and DIG
`SE_SLIDE_MON_DOWN`, each a 19-24 frame slide that ends by setting that
flag, and the release turn puts the pic back. The model was reading
`fxHidden`, which is the damage BLINK and nothing else, so it stood on its
tile while the game insisted it was underground -- and insisted in the
strongest way it has, by making every attack aimed at it miss.
It now reads the same field the pic does, which also covers every other
vanishing act on that seam: the user of Explosion, a Pokemon Teleported
away. Read as the engine's own answer rather than as a list of move ids,
so a mod that adds a third two-turn move gets it for free.
It is deliberately NOT held to the end of its own animation the way a
collapse is. The Stadium animations are authored as the WHOLE move --
Charizard's DIG is 3.83 seconds of burrow, emerge and strike, because
Stadium plays it in one turn -- so cutting at the engine's own hide shows
the burrowing and holds the strike back for the turn it actually lands on.
Verified as a timeline (`DS_FLY`, and `DS_MOVE=DIG`): the pic hid at frame
67, the model went with it, and both came back at 264.
- **And the faint animation now gets to finish.** A fainted Pokemon left the
field when its PIC did, and the engine's pic slide is fourteen frames of a
60 Hz clock -- `SlideDownFaintedMonPic`, seven rows two frames apart, under
a quarter of a second. The Stadium faint animations are nothing like that
short: the briefest in the set is 49 frames of a 30 Hz clock, the median is
110 and the longest 230. Every model was therefore cut off inside the first
fifth of its own collapse -- the Pokemon began to fall and then vanished
mid-fall, which is worse than not animating at all.
A model that is collapsing now stays until it has finished collapsing, and
the two timings are simply not tied to each other any more: how long a flat
pic takes to slide off the bottom of a 160x144 frame has nothing to say
about how long it takes a Gyarados to fall over. Bounded at both ends --
it ends when the animation does, so nothing is left lying on the field for
the rest of the fight, and the side is reset outright the moment a
different battler stands in that slot, which is what stops the next
Pokemon out of the ball arriving face down (that one bites when a trainer
leads with two of the same species, where the dex number never changes and
nothing downstream would otherwise notice the swap).
`DS_FAINT` now prints the span as well as the moment: 127 frames against
the pic's 14.
- Two ordering bugs in the extraction pipeline that made its output
**non-reproducible**. The generated flame nodes were deduplicated through
a Python `set` of tuples containing strings, so their order moved with
`PYTHONHASHSEED` and Ponyta, Rapidash and Moltres got differently-seeded
flames on different runs of the same build; and texture registration
order came from iterating a `set` of ints, which is stable across runs
but is a CPython implementation detail rather than a fact about the data.
Both now go in order of appearance, which is the game's own order and the
one a Lua port can reproduce.
### Changed
- The packed format is now **DSM3**: textures are stored as raw RGBA rather
than PNG. An ImageData over those bytes costs nothing where a PNG costs a
decode on the frame a battle starts -- but the reason it was done is that
PNG means zlib, and Python's deflate and LOVE's need not agree byte for
byte, which would have made the extractor impossible to check against the
packer. Uncompressed pixels are the same pixels whoever wrote them. The
set is 34 MB rather than 24 MB, and it is generated locally rather than
shipped, so that is a trade worth making.
### Known
- Three species -- Exeggutor, Tangela and Magmar -- have standby loops that
are corrupt in the source extraction, and fall back to their battle
sprites on the STADIUM rungs (see above). 148 of the 151 have models.
- Pidgeot, Dodrio and Grimer have a handful of erratic rotation frames in
their standby loops -- a few frames of junk at the top of the loop rather
than a corrupt stream. The blend guard holds those frames rather than
smoothing through them, so they step where the source steps, and Pidgeot
and Dodrio are additionally left unanchored because those frames move the
body estimate too far to measure against (see above). Not chased
further: the extraction is byte-identical to a reference pipeline whose
sampling was validated against the decompilation's own arithmetic, and
guessing at the format to fix three species risks the other 145.
## 1.5.5
### Added
- **A third-person camera: the 3RD rung.** The VOXEL ladder's eighth rung
(`3` / SELECT walk onto it after 1ST, and it is on the OPTIONS row)
stands the camera on a boom behind the player's shoulder. It is the
first-person rig with one number added, so everything 1ST already did
it does: the look steers on a mouse, the right stick or a touch drag,
and the grid walk is replaced by continuous camera-relative movement --
push in any direction and you go there, at any angle, with collision,
warps, ledges, encounters and scripts still running through the
engine's own machinery.
The boom **collides**: it marches back through the terrain height field
and the map's own walkability, so backing into a wall walks the camera
in to your shoulders instead of through it, and rounding a corner eases
it back out rather than snapping. Squeezed all the way into the head it
simply draws as 1ST until you step clear. It also carries a small
over-the-shoulder rail offset, which fades out as the boom shortens.
Every sprite in the world **turns to face it** -- yours, the NPCs', the
figures drawn into the furniture -- and shows the frame it would look
like from where the camera actually stands, so walking behind someone
shows you their back. Your own character is drawn (with the
through-the-wall silhouette 1ST had no use for) and **turns to face
where they are walking** rather than where the camera looks, so a strafe
reads as one; standing still they come back round to the camera's
bearing, which is the one A talks along.
Your card's frame is chosen from your body's **continuous** bearing
rather than from the compass direction the grid game stores. An NPC's
facing really is one of four directions, but yours is the angle the
camera itself is hung off, and quantising it before measuring it against
the eye leaves no margin for the shoulder rail's few degrees of offset:
in a band just short of each 45-degree boundary the pair read as 135
degrees apart and picked the mirrored PROFILE frame. Standing still.
Spinning the camera swept four of those bands a revolution, which showed
up as the character flicking sideways for a split second.
In VR the boom is declined outright and 3RD presents as 1ST does: a
headset that seats its wearer three cells behind their own body is a
well-known way to make people ill. The rung still changes the walk and
the sprites the same way.
- **Every camera zooms, on whatever the machine has.** The mouse wheel,
`Q`/`E`, a two-finger pinch and the pad's two stick clicks all reach
whichever camera is actually in front of you -- the third-person boom,
the staged battle's lens, or the engine's own survey zoom on an orbit
rung. One module (`lib/CamControl.lua`) answers "which camera is this
aimed at" so the four cameras never race each other for an event, and
forwards everything it does not claim. 1ST claims nothing: the eye is in
the player's head, and a pinch there would only wind the survey zoom for
whenever they stepped back out.
- **The battle camera is yours to steer.** The right stick, a drag across
the screen or the mouse walks the staged shot around the arena and raises
the seat; the wheel, `Q`/`E`, a pinch or a stick click work the lens.
Both axes stop where the composition does. LEFT stops at the shot the rig
was solved for, because there is nothing to the left of it. RIGHT ends
SIDE-ON -- the eye square to the arena's axis, both Pokemon at the same
distance instead of one behind the other -- computed from each rig's own
stance rather than written down. DOWN stops at the rig's low stance and
UP is 45 degrees above it, raised about the focus at a constant radius so
climbing never doubles as zooming. Input accumulates into a goal the eye
eases after, so a flick reads as the camera being pushed rather than
dragged.
The lens **opens by exactly the amount the pair spreads**: the solved
shot looks along the arena's axis at a shallow angle, which foreshortens
the gap between the two mons to less than half its length, and swinging
round or climbing un-foreshortens it. Left alone that threw both Pokemon
off the edges of the frame at the far end of either range, which made the
whole far end unusable.
Where you leave the camera is where the next battle opens. An angle and a
lens you chose are how you want to watch battles, not a fact about one
encounter.
- **Move animations track the camera.** They already slid to follow the
pair's midpoint; now they follow its SEPARATION too. Both mons are
geometry standing on the map, so the camera sizes them -- and an effects
layer that kept the authored 106-pixel spacing through a zoom and a
60-degree swing fired its beams into the air beside the Pokemon they were
aimed at.
- **BACK SPRITES locks the battle camera.** That setting pins your own mon
to the GB's slot on the menu while the foe stands out on the map, and no
angle holds a composition that is half frame and half world. The steer,
the climb and the lens all stand down -- in the rig as well as at the
inputs, so an angle stored from before the row was switched on cannot
leave it steered anyway. The slow drift stays: it was always there under
BACK SPRITES and two degrees is not a composition problem.
- **BATTLE BG is pinned to WHITE and its row comes off the menu.** The row
picks what fills the screen AROUND the battle, and this mode fills the
window with the map the fight is standing on -- there are no voids left
for it to be about. WORLD was actively wrong under it: it makes the
battle non-opaque so the engine draws a second, dimmed copy of the
overworld beneath the arena pass's own. Pinned rather than merely hidden,
so a save written before the mod was installed cannot carry a value the
menu can no longer reach. Uninstall and the row is back.
### Fixed
- **Grass and flowers are closed off at the sides.** Both stand as
per-pixel slabs built from runs of lit pixels, and only the front, the
back and a lid were ever emitted -- so from any angle off square you
looked straight in through the open end of every run and out the far
side. At the low cameras this release adds, that is most of the time.
Each run now wears end walls in the colour of the pixel they close off.
Flowers needed more than that, because a flower SWAYS: the mesh spans the
union of every animation frame and each frame is cut back out in texture
space, so a pixel that drops out of a frame takes the union's wall with
it and leaves an interior boundary that never had one. The first cut of
this looked solid on the base frame and still had gaps on every other.
Every pixel of a flower now carries a cap on all four of its remaining
faces: enclosed and invisible while its neighbour is there, and already
in place the moment the animation takes that neighbour away.
- **No more machine-gun bonking in 1ST and 3RD.** The grid walk's collision
sound marks a discrete event -- a direction pressed, a step refused. A
free walk has no such moment: the body slides along every wall it grazes,
continuously, so a corridor taken at a slight angle rang the bonk twice a
second from end to end. The wall stopping you is the feedback.
## 1.5.2
### Added
- **The Pokédex in hand is a quarter larger.** Its voxel pitch went from
1.1 cm to 1.375 cm (the body from about 10x15 cm to about 12x19 cm),
because the screen carries every menu in first person and was
squint-small at the old size. The attachment -- flush along the left
controller -- is unchanged.
- **Left stick click steps the VOXEL ladder.** In VR the click now makes
exactly the step the "3" key (and the pad's SELECT) makes -- the same
function, handed across, so the ladder walk, the FULL step-over and
the TILT/GBC FX clearing can never drift from the key's. It used to
toggle first/third person against a remembered return rung.
- **The floating panel shows the same picture at every window size.**
The GB-frame region used to be copied into the headset's panel
pixel-for-pixel, and the panel's swapchain image has a fixed size --
so a window scaled past it (fullscreen above all) ran the frame off
the copy's edge and cut the START menu out of the panel. The region
is now blitted OUT of the window and SCALED into the swapchain image
at the frame's own aspect: identical picture, identical near-square
ratio, whatever size or shape the window takes.
- **Menus stay inside the GB frame while a headset is live.** The
engine's new zoom-aware anchoring docks the START menu to the
WINDOW's edge -- and both VR screens (the floating panel and the
Pokédex) crop the window to the near-square GB frame, so a docked
menu was cropped away with the border it hugged. While a headset is
live the mod answers the engine's own "hold the anchors" predicate
with yes, and every menu blits where it was drawn: the START menu's
classic slot, flush with the frame's right edge, which is the right
edge of everything the headset shows.
- **The sky's dither is glued to the sky.** The gradient's bands were
already read by true elevation, but the GBC checker between them kept
SCREEN-cell parity -- so a head's pitch or roll slid the world-fixed
band edges over a screen-fixed checkerboard and the whole gradient
shimmered as the pattern recomputed. The ray path is now a computed
SKYBOX: each pixel's ray lands in a cell of the sky's own angular
grid (azimuth columns, elevation rows, sized to match the diorama's
pixel grid on screen), and the band, the checker's parity and the
twilight glow -- now measured by the angle to the sun's own direction
-- are all answered from that cell's centre. The screen grid
quantises nothing, so the picture behaves exactly like a
nearest-filtered texture on a dome: its cells slide smoothly with the
world, no motion of the head recomputes the pattern, and only the
clock moves the sky.
- **VR works from an installed release.** The OpenXR loader used to be
looked for only against the working directory and the game's source --
right for the dev tree, wrong for a release install, where importing
the mod lands it in the game's SAVE DIRECTORY (or keeps it zipped) and
the VR row silently failed to start. The search now also asks the
mount that actually holds the mod and the save directory itself; and
if nothing on disk answers -- the mod imported as an archive -- the
DLL is copied once out of the mod into the save directory and loaded
from there, so every install shape reaches the headset.
- **SELECT walks the VOXEL ladder.** In free roam, the pad's SELECT
button makes exactly the step hotkey 3 makes -- OFF through the angle
rungs to 1ST and round, stepping over FULL, clearing TILT and GBC FX
on every press like the key does. For the machines with no number row:
the phone's touch pad and a controller. SELECT has no overworld job in
Gen 1 -- its work is all in-menu, and menus keep it untouched.
- **PCVR, in-process, through OpenXR -- with no change to the parent
app.** A new **VR** row (OFF / ON, off by default; on the OPTIONS menu
and the mod manager's page). The whole stack rides LuaJIT's FFI from
inside the mod: the Khronos OpenXR loader ships in `assets/vr/` (with
its Apache-2.0 license alongside), the session binds to LOVE's own
OpenGL context, each eye is rendered by the mod's existing scene pass
under a placed camera built from the tracked pose, and the finished
canvases are blitted straight into the runtime's swapchain images.
Works with any Windows OpenXR runtime (SteamVR, Oculus, WMR).
- **What you see mirrors the VOXEL ladder.** On the orbit rungs the
world is a TABLETOP DIORAMA hung at the RUNG'S own viewing angle and
at the scale that reproduces the flat screen's framing -- step onto
35 and the model presents at 35 degrees, onto 75 and it rises toward
eye level, easing between rungs; lean in and the town grows, walk
around the table and honest occlusion shows you the far side of the
buildings. On **1ST** you stand inside the world at life size (a
tile is a stride), the headset steers the same yaw and pitch the
flat screen's mouse does, and FreeMove walks where you look.
- **VR controllers.** One OpenXR action set, suggested onto Touch,
Index and WMR (plus the khr/simple fallback), rebindable in the
runtime's own UI. In both modes: **left stick** moves (through the
engine's own stick path, so it grid-walks the diorama and free-walks
1ST), **A/B** are A/B, **either trigger** is START, and **clicking
the left stick** steps the VOXEL angle ladder exactly as the "3"
key does. In the diorama: **right stick up/down** zooms the
model, and **squeezing a grip** while moving that hand up or down
drags the whole table with it. No controller button leaves VR --
both directions belong to the VR row alone, so no mid-fight click
can eject you from the headset.
- **Battles happen ON the world -- from the flat game's own seat.**
When a staged fight starts, the headset fades to black and comes
back seated in the flat battle's over-the-shoulder shot: on the same
camera line (your mon near-left, the foe far-right), pulled in to
the wide rig's standing distance at life scale, turned to face the
arena -- and fades back to wherever you were when the fight ends.
Both mons stand on their arena cells in the VR eyes' own view, yawed
per eye, casting real shadows, wearing the hit flash -- and the MOVE
ANIMATIONS play out there with them: the engine's own effects layer,
caught on a canvas and stood on a billboard that faces the eye the
way the mon cards do (effects are 2D drawings, and a drawing must
face the eye that is looking), with the classic layout's two slot
marks pinned where each arena cell lands on that plane along the
eye's own ray -- so a burst authored at a slot sits exactly over the
mon standing in for it, per eye, and a projectile crossing the frame
crosses the arena. (The flat screen keeps its
composed battle shot, untouched.) And the battle camera's parallax
drift holds still while a headset is watching: the sway is a flat
screen's depth cue, and inside VR it read as the world lurching.
- **A voxel POKEDEX along your left controller.** A hand-authored
voxel model of the series' own field guide -- red slab, lens, LEDs,
hinge, d-pad, dark screen bezel -- laid flush along the left
controller's grip pose (a full quarter turn forward, so holding the
controller is holding the device) through the same XR-to-world
mapping as the eyes, at its real hand size wherever the camera is.
It rides in FIRST PERSON and in the BATTLE seat; the diorama does
without it -- a hand-sized device hovering over a tabletop town is
clutter, and the floating panel serves there. In first person its
screen carries EVERYTHING the flat screen shows -- menus, dialogs,
shops, wipes -- and the floating billboard is retired outright:
raise your hand to read, lower it to play. In a staged fight the
screen is the 2D battle -- text, menus, HP bars, and any party or
bag screen opened over it -- and there too the floating billboard is
gone entirely: the fight owns the view, the reading is in the hand.
(No tracked left controller still gets the floating panel -- the UI
must be readable somewhere.) Drawn by the scene's own pass with real
depth, per eye; dark when nothing is showing.
- **The floating panel wears the GB frame.** The quad used to show
the whole window -- monitor-wide, mostly mirror -- and now crops to
the 160x144 letterbox where everything the flat screen has to say
actually lives, so the panel presents near-square (10:9) at 1:1
pixel aspect. To keep a battle's HUDs inside that frame, the HUD
blocks stay in their classic GB slots for as long as a headset is
live instead of snapping out to the window's edges.
- **The sky is anchored in space.** On the flat screen the band
gradient hangs off the frame; inside a headset that meant the sky
(and the sun and moon with it) rode the player's head. The VR eyes
now hang the gradient over a fixed slice of elevation above the
world's real horizon and drop the fixed-slice fallback entirely, so
tilting your head slides the frame across a sky that stays put --
and the discs, already projected through each eye's true camera,
stand still over their own azimuth. The gradient is a true SKYBOX:
each eye hands the sky shader its own ray fan (head rotation plus
frustum tangents), and every pixel takes its band -- and its GBC
checker dither -- from the TRUE elevation of its own view ray, so
no motion of the head, pitch, yaw or roll, in the diorama or in
first person, moves a band by a pixel; only the clock recolours
them. The FLAT screen's first person gets the very same treatment:
the placed rig builds its own ray fan from the basis its view is
made of, so mouse-look pitch slides the frame over a sky that
stays put there too, dither and all -- while the orbit rungs keep
their classic frame-hung painting. And the SUN AND MOON are no
longer painted on the frame at all (in first person on the flat
screen included): the cell art is baked to a texture once per
palette and hung on a quad IN THE WORLD, projected through the
camera like any geometry -- no per-frame cell snapping (the
jitter), no pattern squared to the canvas (the face that turned
with the head).
- **The VR row exists only where VR can.** Off Windows -- the Android
build above all -- the row is absent from the OPTIONS menu and the
mod manager's page both (the loader and the GL interop are Win32),
and a stored vr=true that migrated over in a save is ignored instead
of read, so a phone never tries to start a session or force the
battle rows.
- **VR owns the battle rows.** While the VR row is ON, staged battles
are REQUIRED (3D-BTL answers ON whatever it was set to) and BACK
SPRITES is held OFF -- the battle seat, the pokedex screen and the
effects plane all assume both mons standing on the world -- and both
rows leave the OPTIONS menu for the duration, because a switch that
decides nothing reads as broken. Both come back, at their stored
values, the moment VR goes off.
- **First person snap-turns.** Flicking the right stick left or right
steps the view 45 degrees, once per flick (it re-arms at centre) --
a snap rather than a smooth spin, because smooth software yaw is
the classic VR comfort mistake. The turn steps the XR-to-world
mapping itself, so the eyes, the walk direction and the pokedex in
your hand all agree about which way the world now faces.
- **The cast holds one pose for the headset.** Sprite cards lean back
by the camera pitch on the flat screen; a head that roams the table
has no one pitch to match, so every VR frame leans the cards at the
top rung's near-upright 75 degrees instead -- whatever orbit rung
the ladder is on -- and the flat screen keeps leaning with the rung.
- **Menus, dialogs, battles and wipes float on a panel** (an OpenXR
quad layer fed from the window), so everything the flat screen can
show, the headset can read. The window itself becomes the VR mirror
-- the left eye, fitted to the window -- and every existing
keyboard, mouse and pad input keeps working alongside the XR
controllers.
- The two eyes share one shadow map, one pose capture and one glint
step per frame (VoxelScene.render's `eyes` path), so they can never
disagree about anything but their viewpoint. xrWaitFrame paces the
app at headset rate while FixedStep keeps game logic at its own 60
Hz; vsync is handed off while the session runs and put back after.
- Failure is a status, never a crash: no runtime, no headset, no GL
interop, or a session lost mid-play all land back on the flat screen
with the reason printed (`XR_ERROR_FORM_FACTOR_UNAVAILABLE` means
"plug the headset in, then toggle the row"). Needs the mod running
from a real folder (the FFI cannot load a DLL out of an archive).
## 1.5.0
### Added
- **1ST: a first-person camera, played like a modern one.** A seventh
rung on the VOXEL ladder (hotkey 3 walks it; the OPTIONS row carries
it). Stepping onto it dives the camera from wherever the orbit was
into the player's own head over half a second, and stepping off flies
it back out. The rig rides the same placed-camera seam the staged
battle proved out, so the sky's bands meet the horizon, the sun and
moon hang where their shadows say, and the water reflects at eye
level -- all through math that was already there.
- **Free look.** Relative mouse motion (the cursor is captured while
the rung is on; left click is A, right click is B), the right
stick at a rate with a squared response curve, or a touch dragged
across any open screen -- the overlay's d-pad and buttons still
work, and a second finger can drag the view while the first
walks. Pitch clamps short of straight up and straight down.
- **Free movement.** While 1ST drives, the grid walk is replaced by
a continuous, camera-relative one: push forward and you go where
you look, at any angle, sliding along whatever you graze. The left
stick's raw deflection, the touch d-pad's true vector, or the held
keys (forward / backpedal / strafe) all steer it. The grid is
still the game: the walk asks the engine's own collision the same
per-cell questions a grid step asks, the logical cell tracks the
body, and every cell crossed runs the engine's own landing
pipeline -- warps, encounters, spinners, gates, poison, repel, the
step counters. Walking off the map edge, into a ledge or into a
boulder hands the push to the engine's own handlers, so
connections cross, ledges hop and boulders shove exactly as
themselves. Speed is the grid walker's own (bike included), so
distance per second and encounters per tile are unchanged.
- **Billboards seen from inside the world.** Character cards stop
leaning and start turning: upright, yawed about their feet to face
the eye, wearing the frame their pose shows *this* viewer -- walk
behind an NPC and you see their back, circle to a flank and you
get the profile, exactly the four frames Gen 1 drew. The authored
figures (the couch sitters) turn the same way, about their own
middle. The sun pass swaps frames in step, so a card never reads
its own shadow through a mirror-flipped record of itself. The
player's own card is left out of the camera draw -- the eye stands
in it -- but still casts its shadow on the ground ahead.
- The shadow map's box follows the look (the orbit's fit reaches far
north and barely south, which is wrong for a head facing south);
the world curve is declined outright while the head owns the
camera; and the whole rung falls back to the 75-degree orbit on
hardware without the 3D pass.
## 1.4.3
### Added
- **The furniture of the whole game goes through the building
pipeline.** 1.4.1 put four drawings through it; this is the rest of
the rooms. Every one of them is the same read -- the drawing's own
bands say what is a top seen from above, what is a face seen head-on,
and where the thing ends on the floor -- and every one of them
replaces a pinned box that wore its drawing as a decal. The pins all
stay as the degradation path, neutralized wherever a template stamps.
- **The bookcase, the commonest piece of furniture in the game** --
58 placements across two drawings on the town-house atlas (books
and a bowl on each shelf at the west end of eighteen homes, books
on both at the east), plus Red's and the Copycat's pair. Pinned
`desk` it was a 24px box with the books painted on its flat front.
Modelled it is 23 voxels of cabinet with its top seen from above,
and every book, bowl and door panel sunk a voxel behind the frame
the drawing seals it in.
- **Celadon's display cabinets** -- the tall one with the trophy
behind its glass and the short one beside it, band for band the
same object as the town house's on another atlas, which is what
makes the pair read as one line of furniture: 23 voxels and 15,
exactly the 8 rows of drawing between them.
- **The dining table, everywhere it is drawn** -- the generic town
house's at 18 placements, Red's and the Copycat's, and the chief's
long table at four cells wide. All of them the lab table's read at
a different width, all of them 6 voxels, all of them standing on
the ground line their legs are drawn stopping at rather than on
the grid's floor.
- **The stool at every one of those tables** -- 94 placements on the
house atlas alone, ten more in Red's and the Copycat's, and the Fan
Club's four members' chairs, a different drawing that is
pixel-identical from the seat down. The first template with no base
piece at all: a stool is drawn mid-cell over its own floor, so it
is a desk-set of exactly one part, seat lid over legs with the
floor showing between them.
- **The Pokemon Center's healing machine** -- two variants, 24
placements, plus the Indigo Plateau lobby's pair. A wall-height
cabinet with its monitor perched on the front of its top face,
drawn across two map rows because it towers over the 16px band
behind it, which the volume path could only read as more wall. The
hoses leaving its side are modelled as hoses, at the elevation and
the depth the two stacked motifs put them; the west machine's
keyboard is a shelf at counter height wearing its own top-view art.
- **Bill's desk, and the Silph president's** -- the same drawing in
both rooms. Its terminal is drawn in 2:1 isometric, turned 45
degrees to the map, and builds as a cube rather than the slab a 2:1
reading gives; the kinked dark run between keyboard and computer is
raised to the keyboard's height and reads as the cable it is. The
desk stops at its own two cells because the artist drew its apron
into the walkable cell in front, sharing tiles with the chair
pushed up to it -- so the chair is modelled as a part of the desk.
- **The Bike Shop's open toolbox.** The drawing looks down INTO the
tray, which is why every solid treatment failed it -- as a
`billboard` the whole cell went up as one 10-voxel slab wearing the
drawing as a decal. `tray` builds four walls, a floor and air
between them, with the lid standing open on its hinge.
What the template language grew to carry them: `tray`; a `desk` band
that lays its top face flat as a lid; the `box`, `flat` and `iso`
part kinds; `stretch` for a band mapped over a deeper plot than it
was drawn on; `inset` for a pane sunk by hand; `panes = false` where
the global recess pass has the polarity backwards; a `wall` element
so a template can keep the band behind it solid; `plane` for a height
the drawing states elsewhere; and `scrub`/`keep`/`support`, which let
a template model a surface while leaving an object standing on it to
its own standee -- Red's potted plant on the dining table.
- **Round bins: the `can` class.** Vermilion Gym's switch puzzle stands
fifteen galvanised trash cans in a row, and the S.S. Anne redraws the
same object pixel for pixel as its galley barrels. Left to the thin
standee pool they were flat discs on edge -- fifteen coins standing
in a row; pinned a plain `cylinder` the drawing's base arc revolves
too and they came out as barrels balanced on a three-voxel stem.
`can` is the round hull cut at both ends, hollowed and tapered: the
drawn mouth ellipse projects across the top and down the well so you
look into the bin, the drawn base ellipse is ground contact rather
than body, and the plan narrows toward the floor. The two ellipses
are measured off the pixels; the height, the well and the taper are
authored, and the entry says why.
- **The rock gyms' boulders are round.** 87 placements over Pewter's
walls and maze and Bruno's clusters, and every one of them was a
square bar wearing a boulder texture in relief -- the repeat-aware
scenery path extruding the whole drawing as one course. Each cell is
now a hull whose plan is its own drawn width profile turned in depth:
a dome full-width from the drawn shoulder down, tapering over the top
five rows exactly where the art tapers, with the floor's corner
diamonds opening between them the way the drawing has them. Still
16px, so nothing standing on or beside a rock moves.
- **The potted plant stands as a plant.** The most repeated interior
prop in the game -- 78 placements over 13 maps, six per Pokemon
Center -- and its urn was rendering as a hollow black frame, because
the drawing's foot lies flush on the block's bottom edge and the
background vote took the plant's own darks away with the floor. Named
outright as light and white instead, it stands as one organic
silhouette 32px tall over its two stacked cells, crown overhanging
the stem. `planter` carries the same reading for a round drawing
stacked two cells high on one cell of plot.
- **Bicycles, in both places the Bike Shop draws them.** The six on the
showroom floor get their own pool at two voxels rather than the thin
pool's five: a bike is a line drawing, and at five voxels every
stroke closes the gap to its neighbour with its own side faces, so
from any angle but dead-on the air inside the frames filled in and
the six came out as one dark lump. And the two against the north wall
get `mounted`, a new authored-mask escape for a thing drawn INTO a
wall band: it holds the wall's plane as a thin per-pixel slab instead
of standing up as a sprite card, and it keeps its drawn elevation, so
a bicycle hung clear of the floor stays hung. Its mask is measured
rather than hand-drawn -- the plain panel tile composited across the
same grid and the background flooded in through the pixels that still
match it, which separates bicycle from stripe exactly.
- **The Marts' cash register is a machine, not a decal.** An authored
figure may now state a `depth`, which makes it an object rather than
a person: a per-pixel solid standing on the counter instead of the
flat card that turned edge-on with the camera. And the drawing is not
a box -- its black linework packs two facings, an L of base and arm
around a keypad that is the machine's deck seen from above. `flat`
lays that rect horizontal in the notch of the L, and `thin` gives the
receipt curl a paper's thickness where the body's would have made it
a wedge.
- **Shelf fronts have relief.** Everything the `bookcase` collapse is
used for is a shelf, a rack or a display case, and all of them seal
their contents behind the drawing's own black frame -- so those
regions now sink a voxel, the same rule a facade's window panes are
recessed by, and the books stand in the shelf instead of being
painted on it. A tileset that borrows the collapse for something that
is not a shelf says `bookcase_relief = false`: the League's masonry
and pilasters, whose courses are the wall itself, and Bill's
transporter drums, whose light regions are a lit barrel.
### Changed
- **Class heights now follow the models under them.** A tileset's
`heights` gets stools at 5 and tables at 6 in the houses, Bill's desk
at 8, and cans at 9 -- each of them the drawn elevation the new
template or hull stands at, so whoever sits on a stool sits on the
seat, and whatever object sprite stands on a table lands on the
modelled top rather than three voxels over it or under it.
- The healing machines' two flanks leave the `wall` pin for the thin
standee pool. They are equipment standing beside the console -- a
pair of pipes and a keyboard -- and as wall each was boxed into a
solid 16px half-cell wearing its drawing in relief.
### Fixed
- **Water no longer hides behind water.** The reflective pass writes no
depth -- the depth canvas is detached for the length of it so the
shader can read it -- so nothing put a lake in the buffer and no lake
could occlude another; the sheets were simply painted in mesh order.
Flat water never showed it, one plane, a farther sheet always landing
farther down the screen. The world curve ends that: it drops the far
side of the map into the near field of view, and a sea a hundred and
fifty tiles away came out rasterised on top of the pond at the
player's feet, tall grass and all -- water and terrain "from the
other side of the map", not reflected but there. The water meshes now
go down flat first, through the ordinary scene shader with depth
writes on, and the reflective pass draws over what survived. The
buffer holds the surface, so the pass's own test throws the far sheet
away; the reflection copy holds it too, so a ray grazing another part
of the lake reads water rather than the void behind it; and a frame
that cannot run the pass at all is unchanged, because the flat draw
is the fallback that was already there.
- **Reflections under the world curve.** The bend tips the world away
and the things standing on it do not lean with it -- and a lake is
one of those things. Reflected off the bowl the bend makes, the far
half of a pond was a mirror tilted twenty degrees: it threw the ray
past the vertical, where the sky ramp's own measure swings from one
end to the other across a single column, and hard-edged patches of
the wrong sky stamped into the water; the same tilt sent the
screen-space march grazing along the bank rather than over it, which
is what smeared the dock and the roofs across the harbour. What the
water reflects is now worked out in the flat world, exactly as it
would be with the curve off, and every marched sample is bent on its
way to the screen by the vertex stage's own displacement -- so the
ray is straight where it should be and lands where the geometry did.
The wave columns are read on the flat sheet too: the relief walk is
built on an even slab over a level plane, and in the curved world
that slab is a bowl, which handed back a column a pixel or three off
per fragment -- a patch of noise in the middle of a pond.
- **Merged runs tore open under the curve.** A quad's interior is the
chord of a parabola its neighbours draw the arc of, so a long run
hangs below the short quads butted against it. Nothing bounded a
run's length, and the ones that ran away were those wearing a
constant texel -- a roof's black eave outline, its fascia, its shaded
underside -- because a flat run has no art to break it. At 102px
across a gym the eave tore off the roof and the slot showed the
building's dark interior through it. Runs now stop at the next 8px
lattice line, which is the lattice buildings are stamped on and the
one every other quad in the scene already ends on, so every join is
vertex-for-vertex and the bend carries them together. It costs quads
whether the curve is on or not -- Cerulean's object stream goes from
35.7k to 41.6k -- and that is deliberate: the mesh is cached per map
and built over seconds, so meshing for the curve's sake only when the
curve is on would mean rebuilding every live map on a keypress.
## 1.4.1
### Added
- **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
### Added
- **WATER, a new row on hotkey 9: water reflects the world, the sky, the sun
and the moon.** Every lake, sea and pond in Kanto was a flat animated
texture lying in a hole in the ground. It is now a surface, and it is
reflective.
What it reflects, in the order the shader resolves them:
- **The sky.** The reflected direction goes through the very matrix the
frame is drawn with, as a point at infinity, and the canvas row that
lands on is looked up on Sky's own band ramp -- the identical texture,
the identical checkerboard dither, the identical display-mode transform.
So the sky in the lake is the sky over it, and the two meet at the
waterline with no seam at any pitch, field of view, window shape or zoom.
Blue at noon, gold at dusk, navy under the moon; GRAY gets a grey lake
and CLASSIC a green one, for nothing.
- **The sun and the moon**, hung by ANGLE rather than by screen position,
because a reflected body is usually off the top of the frame entirely
and a projected point stops meaning anything out there. The angular
radius is the painted disc's own radius run back through the camera's
field of view, so the two are the same size -- craters, dithered rim,
the sunset's loom and all, off one shared list. This is also the
specular: a low sun lays a broken gold path across the water on its own,
out of the reflection rather than out of a highlight term nailed on
beside it.
- **The world, in screen space.** The reflected ray is walked forward in
world space, each step projected through the same matrix, looking for
where it passes behind what the depth buffer holds -- then binary-refined
onto the contact and read out of a copy of the frame as it stood before
the water went down. Shore trees, buildings, ledges and cliffs land in
the water because they are on screen; where the ray leaves the frame or
finds nothing, the sky above answers instead, which is what makes the far
half of a lake sky and the near half scenery with no seam between them.
Fresnel decides how much of it shows: almost nothing looked straight down
at, almost everything looked along -- so the 15-degree rung is a pond and
the 75-degree rung is a mirror, off the same surface.
Every rung gets one, though, which took a lean. A reflection off flat water
points as far above the horizon as the eye is above the water: 15 degrees
at the top rung -- grazing the sky's pale end, sweeping the sun's own path,
travelling far enough across the screen for the march to find the shoreline
-- and 75 degrees, straight up, at the steepest. Up there the bands are at
their darkest, the sun and moon sit at about 6 degrees of squashed
elevation and are nowhere near it, and the screen-space ray leaves the top
of the frame in two steps. All three are correct, and together they are a
lake with nothing in it.
So the reflection now LEANS toward the elevation the top rung reflects at,
by however far the camera is from having a horizon in frame -- **zero** at
the rung where the horizon IS in frame, so the one place the join can be
seen, the waterline, is still the exact reflection it was. Toward an
elevation rather than by a weight, because the ray it starts from differs
at every rung and a fixed fraction lands them all somewhere different: the
middle rungs came out further from the sun than the steepest one. And it
leans the LEVEL reflection with each column's own deflection added back on
top -- leaning the perturbed ray sets its elevation outright, which at full
lean gave every column on the lake the same one, flattened the sky to a
single band and removed the moon entirely.
Three rungs rather than a toggle. FULL is the whole thing; SKY drops the
ray march and keeps the sky, sun and moon, which is most of the look for a
handful of instructions; OFF is the flat water this mode always drew. The
FULL preset sets it to FULL.
- **The water surface is a field of pixel-tall columns, and they are real.**
Not a normal map: a heightfield of one-world-pixel bars -- the same unit
every other voxel in this mode is built from, and exactly one texel of the
water tile -- each standing a WHOLE number of pixels high and rising and
falling on its own.
Three travelling wave trains, and one of them dominates: a wave has a
DIRECTION, and its crest is a line running across it for as far as the
water goes. Three trains of equal weight cancel and reinforce in patches
instead, and the surface comes out as round islands of raised pixels with
no travel to them -- blobs rather than waves. The dominant train's
wavelength is about forty world pixels, five tiles, so a crest is a long
run of columns at one height with a step down either side.
Drawn with no extra geometry at all: the mesh is still one flat quad per
tile, and the columns are found by walking the view ray down through the
slab in the pixel shader. That is what makes them read as solid -- a tall
bar hides the shorter ones behind it, you see the SIDE of the ones facing
you (wearing the mesh's own direction shading, so a crest is lit like every
other voxel in the world), and the whole field parallaxes against the plane
as the camera moves. The water's art is read at the column the ray landed
on rather than at the flat quad underneath, so the pixels travel with the
bars they are made of.
The columns are what you SEE; the normal they reflect with is read off the
smooth surface they are a quantisation of. That distinction is the whole
difference between a moon on the water and confetti: whole-pixel heights
have whole-pixel differences, so a normal built from them can only point in
about five directions, and a sun or moon barely two degrees across falls
between them. Still one normal per column, so the surface stays
pixel-quantised in space while the value it reflects with is continuous.
Crests stand up to five world pixels, well past the 2px recess water sits
in -- deliberately, because the columns are relief drawn inside the water
quad's own footprint, so a bar that reaches above the bank is clipped at
the water's edge rather than spilling over it. What it buys is a surface
with real swell in it instead of a two-rung terrace.
And it moves in STEPS, at **15 a second** -- the cadence hand-drawn pixel
art is animated at. A surface built out of whole pixels that crawls
smoothly between them gives away that the quantisation is only skin deep.
Each step advances the dominant wave by exactly one world pixel, derived
from that train's own wavelength rather than tuned beside it, so nothing
ever lands half-way between two pixels and changing a wavelength moves the
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
- **The water surface is its own mesh, and its own pass.** A mirror cannot be
drawn until what it reflects exists, so water is lifted out of the terrain
mesh at build time and drawn between the world and the characters. The
shoreline faces around it are untouched -- they belong to the GROUND that
exposes them -- and the sun still sees the surface, so a tree at the water's
edge still throws its shadow onto the lake.
- **The scene's depth buffer is a readable canvas.** It was an internal buffer
that could be written and tested and never sampled; it is now the same
buffer with a texture handle on it, at the same cost. Drivers that will not
make one fall straight back to the old buffer and lose the reflections and
nothing else.
- **The cast is reflected too -- by being drawn twice.** Gen 1 draws people
over the world and water is world, so a surfing player has to composite
OVER the water they are sitting on, which puts them after it; and a
reflection can only hold what came before it. So the walkers, the NPCs and
the authored figures are painted into the reflection COPY alone, where they
are in the picture the water reflects and not yet in the picture the water
is drawn into. Both draws go through one function, so they cannot come out
different. The staged battle does the same with its two Pokemon.
The ray march finds them the honest way round: a sprite is not in the depth
buffer at that point, so a ray aimed at one passes through to the terrain
standing behind it and reads the copy there -- where the sprite is already
painted. The reflection lands a hair off the sprite's own depth and exactly
on its colour, which at a lake's worth of wave is the same picture.
### Changed
- **The waves arrive in sets now, and a little slower.** Three fixed trains
are an exactly periodic field -- every forty-odd pixels of sea wore the
same crest at the same height, which reads as wallpaper the moment a lake
is bigger than the repeat. Two long-wavelength fields now ride the
dominant train, four to five carrier wavelengths apiece so neither reads
as a wave itself: a SWELL that breathes its amplitude, so a few tall
crests march through and hand over to a lull that is itself moving, and a
BEND that bows its phase, so a crest line curves across the surface
instead of ruling itself over all of it. The two lesser trains stay
plain: they are texture rather than structure, and a third modulator is
the soup the train weights exist to avoid. The step beat comes down from
15 to 12 a second -- the crests were hurrying, and a big wave is slower
than a walk cycle -- still a clean divisor of the engine's 60, and still
exactly one world pixel of dominant-crest travel per step.
- **Staged battles draw their water plain, whatever the WATER row says.**
The reflective pass is tuned for the overworld's ladder of cameras; a
battle's camera is PLACED -- low, tilted, framed like a picture -- and
under it the pass read wrong: Fresnel opened all the way up, the leaned
sky landed on bands the framing never shows, and a lake-sized arena came
out as murk wearing the tile art. The battle is a stage set, and stage
water is painted: the flat animated tiles the mode always drew, with the
mons compositing over them like everything else on the set.
### Fixed
- **On Android the water stayed flat, as if the row were off -- and once it
did draw, it came up in blocks with the haze showing through the holes.**
Three separate faults, every one of them invisible on desktop GL, run down
on a Galaxy Z Fold 7 with the driver's own compiler errors in logcat:
**The shader would not build.** Fragment floats default to **mediump** on
GLSL ES while the vertex stage's default is highp, and the water shader is
the mod's first to declare the same uniform -- the frame's `vp` matrix --
in BOTH stages, one on each default; GLSL ES refuses to link that, and the
pass fell back, quietly and by design, to the flat water the mode always
drew. The pixel stage now lifts its float default to highp (guarded, so a
GPU without fragment highp still compiles and falls back flat), which
settles the link and is also simply needed: the march works in world
coordinates that run to a few thousand, where fp16 has no fraction left.
The world-position varying is qualified highp for the same reason the
wireframe's always was, and the depth sampler too -- samplers default to
**lowp** whatever the floats are set to, and eight bits of depth is a
march with nothing to land on. One wrinkle inside the fix: LOVE's header
forward-declares `effect()` under ITS default, and Samsung's Xclipse
compiler treats a definition whose parameter precisions have drifted from
the prototype's as an illegal overload -- so effect()'s own float
parameters stay pinned to mediump, matching the declaration, and the
maths above them runs highp regardless.
**The depth test read the wrong texels.** The shader's own depth test
normalised LOVE's pixel coordinate by the `screen` uniform, which counts
canvas UNITS -- and on a highdpi phone (Android's density here is 2.625)
a canvas holds that many PIXELS per unit, so the lookup ran to 2.6,
clamped, and read edge texels across two thirds of the frame. Water
discarded itself in blocks wherever the mis-read depth landed in front,
and the haze backdrop showed through the holes. The coordinate is now
normalised by `love_ScreenSize.xy` -- the bound canvas's own pixel size,
measured in the same units on every display.
**And the readable depth canvas** -- the one hardware requirement the
rest of the mode does not already have -- now tries four formats before
giving up: depth24, depth24 riding a stencil (a pairing some mobile
drivers will texture when they refuse the bare format), depth32f, and
depth16 as the floor every GLES3 device can read. Refused all four, the
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
- Screen-space reflections can only reflect what is in the frame. A tree just
off the top edge is not in the water below it, and a reflection whose ray
runs off the side of the screen fades into the sky rather than ending on a
hard line.
## 1.3.1
### Fixed
- **A staged battle on a phone stood some Pokémon three times the size of the
square they were on.** A Pidgey towered over the arena while the mon beside
it was the right size, which reads as a bug in one species and is not one.
Putting the paper back inside a battle pic (BattlePics, 1.3.0) needs the
pic's pixels, and a LOVE Image does not hand them back -- so the pic is drawn
into a canvas of its own size and the canvas is read. `newCanvas` takes the
SURFACE's dpi scale when it is not told otherwise, `conf.lua` turns highdpi
on for Android and iOS, and Android's display density is routinely 2.75. So
`newCanvas(56, 56)` allocated a 154x154 texture there, the pic was magnified
into it, and the readback came back at the magnified size. The rebuilt pic
was 2.75x the artwork, the engine's pics layer drew it 1:1 because it trusts
`getWidth()`, and the mon stood on its tile nearly three times too big.
Only a pic with an enclosed hole in it is rebuilt at all -- the rest are
handed straight back untouched -- which is why it hit some species and not
others, and why it never showed on desktop, where the dpi scale is already 1.
The readback now asks for one texel per pic pixel, the way the engine's own
`PixelCanvas` does for the same reason. The animated-tile atlas readback took
the same fix: on a phone it would have come back magnified too, and every
tile coordinate in it counts in eights from the top-left.
## 1.3.0
### Added
- **BACK SPRITES, a new row under 3D-BTL: your own Pokémon stays on the battle menu.**
The staged shot stands both mons on the map, which is the mode's whole claim
-- and it costs the framing Gen 1 is most recognisable by: your own Pokémon,
seen from behind, sitting on top of the battle menu with its feet on the box.
With BACK SPRITES on the foe is still geometry standing on its own tile at the far
end of the arena, and the player's side goes back to being the GB's own flat
back pic in the GB's own slot: same art, same 2x, same feet on row 96. It is
the engine's own pics layer that draws it, through the `onlySide` argument
that layer already takes, so every pic effect -- the grow-out-of-the-ball,
the faint slide, the damage blink, the send-out trainer pic -- comes along
unchanged and none of it is reimplemented.
Nothing else about the shot moves. The arena, the camera and the drift are
solved exactly as they were, so the foe stands where it always stood and the
player's cell is simply empty ground in the foreground. Two things follow the
setting: the `pokemon.sprite` hook stops asking for the front pic on the
player's side (it is a back view again, and the front art would be that mon
turned round to face the player it belongs to), and the move-animation offset
drops that side's contribution, because a pic that has not moved cannot have
moved the pair's centre.
OFF by default -- what the mode advertises is the two of them out there --
and only on the OPTIONS menu while 3D-BTL is on, since with staged battles
off the engine already draws exactly this.
### Fixed
- **Battle pics were see-through, and it took a back sprite on a tiled floor
to make it obvious.** Gen 1 pics are two-bit art whose lightest shade is
white, and the decoded PNGs key that shade to alpha 0 -- which cost nothing
when the field behind them was white too. Over a route, every belly, every
eye white and every highlight is a hole with the world showing through, and
the mon reads as a stencil.
`BattlePics` exists to put that paper back and, as written, put none of it
back. It flood-filled the outside from the border and filled what the flood
could not reach, which is exact and, on this game's art, empty: a Gen 1
figure is an open drawing, and its belly walks out to the border through the
gap between its legs. Read across all 305 of the game's battle pics, that
rule finds an enclosed hole in exactly none of them.
The fix is to start the flood somewhere else: at the edges of the ARTWORK'S
OWN BOUNDING BOX, and at three of them -- left, right and top. The bottom is
closed, because it is not a side the background is behind, it is where the
drawing was CUT. A pic is bottom-aligned in its slot with all the margin at
the top, so a mon's lowest row is the last row it was given and everything
below the belly simply stops. Treat that cut as open and the background
pours up inside the figure, which is the channel of world that used to show
through a Clefairy.
That is exact rather than a heuristic: nothing is filled because of what
surrounds it, only because the background provably cannot reach it. Which is
why it needs no idea whether it is holding a front pic or a back one -- the
sky between a pair of ears reaches the top edge and stays sky, the gap
between a body and a raised tail reaches the side and stays gap, the belly
reaches neither and is paper. The silhouette is untouched, so the mon still
cuts cleanly against the world.
It replaces the border flood outright rather than sitting beside it, since
anything the border could not reach the box edges cannot reach either.
The bottom edge needs one more distinction, because two different things
meet the underside of a figure. A DRAIN is where the drawing ran out -- a
belly whose white carries on down until the artist stopped, leaking out
through the inch between a body and a leg -- and is sealed. A MOUTH is the
space between two legs, background that happens to be enclosed on three
sides, and is left open so the world shows through a trainer's stride.
Width tells them apart, and on this game's art it is not a close call.
Measured along the bottom of every battle pic, the drains run 3 and 4 pixels
(Clefairy's back, Wartortle's back, Red's back) and the mouths run 10, 12, 14
and 17 (a Rattata's underbelly, Blue's stride, Brock's, a Pikachu's back).
Nothing lands between 4 and 10, so the cut is taken at 6 with room either
side rather than tuned to one sprite. Apart from that number the rule stays
exact.
Front pics come back untouched, and not by being special-cased: they are
near-solid silhouettes with almost nothing inside them to fill, so their own
shape is what says so.
Both mons were affected -- the cards in the arena as much as anything -- so
this lands wherever a battle pic is drawn over the world, not just under
BACK SPRITES.
- **The pinned back pic was lit at noon while the world behind it was not.**
Everything standing in the arena goes through the voxel shader, and that
shader multiplies by the hour's tint, so at dusk the diorama warms and at
night it goes blue -- the two mons' cards included, because they are drawn
in the same pass as the ground they stand on. A back pic pinned to the menu
is not in that pass; it is a flat blit over the finished shot, and it stayed
bright over a midnight route.
The same tint is now applied to that one draw, by multiplying every colour
the pics layer sets on its way past -- so the alpha, the faint slide's fade
and the damage blink all compose with it instead of being overwritten. What
it does not get is the sun: the cards are shadow-mapped and a pic pinned to
the menu has no position in the scene to be shadowed at, so it carries the
hour and not the weather.
### Added
- **The hour reaches the FLAT world too, not just the diorama.** DAYTIME drove
the 3D pass through the voxel shader's own tint uniform -- a uniform the 2D
tile path never runs -- so with VOXEL off, the same evening that fell on the
diorama left the flat world at permanent noon. One clock, two worlds, one of
them ignoring it. Outdoor maps now get the same multiply, painted as one
rectangle over the composited world.
The whole difficulty is WHERE, and it is worth writing down. Not on the world
canvas: in a colorized mode that canvas is grayscale art and the blit that
puts it on screen runs it through the palette shader, which classifies each
pixel into a shade BY ITS RED CHANNEL -- multiply a night blue over it first
and every pixel lands in the wrong bucket, so the world does not darken, it
changes colour. Not over the finished frame either, or the dialog boxes and
menus darken along with the world they are held up in front of, which is the
same reason the tilt-shift blur is a `worldPresent` and not a `present`.
Which leaves the instant between the world blit and the UI blit, and the
engine has no seam there -- `worldPresent` only runs when a PIPELINE produced
the world, which in flat mode is precisely what did not happen. So
`Renderer:endFrame` is wrapped and the UI canvas's own draw is watched for:
`blit` passes the canvas it is compositing as the first argument, so the
first draw of `Renderer.canvas` IS the boundary, by identity rather than by
counting. The shader and scissor that call arrives under belong to the UI
blit already in progress, so both are put aside for the rectangle and handed
straight back.
Skipped entirely when a pipeline drew the frame (it tinted itself, and twice
is wrong), indoors (a room has no sky to take its light from), and at midday
(a multiply by white) -- so a game with the clock at DAY issues not one extra
call.
### Changed
- **FULL no longer takes the two battle rows off the menu.** It still owns the
rows that describe the LOOK -- the wireframe, the horizon bend, the blur, the
hour -- because it is a preset for the diorama and a row that no longer
decides anything is worse than no row. 3D-BTL and BACK SPRITES are not that:
one decides what a fight is drawn OVER and the other how it is framed.
FULL still SETS both on arrival; it does not hold them, and leaving them
reachable is the difference between a preset and a lock.
This makes `stagedBattles()` honest as a side effect. It used to answer yes
under FULL as well, on the grounds that FULL owned the 3D-BTL row and
switched it on -- safe only while the row was hidden. With the row reachable
from inside FULL, that clause would have claimed staged battles for a preset
the player had just switched them off inside, pinning BATTLE LAYOUT to OG for
a fight that never gets staged. The row is the only thing that decides now,
which is what `OverworldBattle.begin` and `wantsFront` already believed.
- **TILT and GBC FX are off the OPTIONS menu entirely while this mod is
installed.** Both fight the diorama and both were already half-taken: the
mode's own key forces them off on every press, and the registry switches
TILT off whenever a world pipeline takes the pass. What was left was two
rows a player could set and watch get reverted -- TILT being the flat fake
of what this mode does for real, and GBC FX a full-screen present pass over
the top of the whole thing.
Dropped AND held at zero, which is the part that matters: hiding a live
setting is a trap, because a save written before the mod was installed can
carry TILT 3 and a row that is not there cannot turn it back off. Pinned
wherever the value could arrive from -- the menu opening, a save being
loaded or begun -- so there is no route by which either is on and
unreachable. Uninstalling the mod puts both rows back, at whatever they were
last set to.
- **The battle's text box and menus are frosted glass, like the HUDs.** The
HUD blocks got panels because black glyphs on grass are not readable. The box
at the bottom had the opposite problem and the same cause: it is drawn as an
opaque white slab with a black border, which was the field's own colour back
when the field was white and is a sheet of paper laid over the bottom third
of the diorama now that it is not.
It gets exactly what the HUDs get -- the world behind it, blurred to frosted
glass and laid back down translucent, at the same frost and the same tint --
and it is measured into the same brightness verdict, so the ink over the menu
flips white with the ink over the HUDs rather than against it. Only the FILL
is taken away: the border, the text, the cursor and the down arrow are the
engine's own glyphs in their own places. The move menu's TYPE/PP box and
Mimic's copy menu get their own panels, trimmed to the rows above the box
below them so no pixel is frosted twice.
## 1.2.1
### Fixed
- **On Android the sky went black below its first couple of bands.** A hard-edged
band of black ran from partway down the gradient to the horizon point, with the
moon still hanging correctly inside it. Desktop was unaffected.
What gave it away is that the same colour reached the screen by two routes and
only one of them was wrong. The haze filling the void UNDER the horizon is the
sky's palest band, and it is delivered by `love.graphics.clear` -- it landed
correctly. The bottom of the sky above it is that same band delivered by the
shader, and it was black. So the palette was not reaching the fragment shader,
and nothing was wrong with the palette, the layout or the camera.
The bands went in as `uniform vec3 bands[8]`, filled from Lua and read through
a loop counter, and on Android's GLSL ES the tail of that array arrived as
zero -- which is black. The likeliest reason is the fragment uniform budget:
ES 2.0 only guarantees sixteen uniform VECTORS, and eight band slots plus the
twilight glow plus LOVE's own built-ins is over it. A driver that truncates a
partly-filled array, or one that reflects `bands[0]` and nothing after it,
fails identically -- so the fix removes the whole class rather than the one
cause.
The bands are a one-texel-per-band TEXTURE now, sampled nearest, with the
band index clamped against the ramp's width. One texture unit replaces eight
uniform vectors, there is no array to index and no budget to overrun, and a
sample past the last band lands on the last band instead of on nothing. It is
still a palette and not a picture -- one texel per band on a single row -- so
the sky is still computed per pixel at the size it is displayed at, with
nothing resampled and nothing baked.
Also gone with it: `clamp(x, 0.0, 0.999999)`, which rounds its bound to 1.0 at
mediump -- the fragment default on GLSL ES -- and would have indexed one past
the last band on the sky's bottom row for the same black result.
## 1.1.1
### Fixed
- A move that shakes the screen no longer whites out the frame. The zone pass
fills each zone with its blank colour before drawing the shifted copy -- the
hardware showing empty BG in the strip the shake vacated -- and a shake
program alternates offset and no-offset frames, so over the map that read as
the whole battle screen, menu box included, flashing white a few times a
second. The fill is dropped while a battle is staged on the map; the shake
itself still moves the HUD.
## 1.2.0
### Added
- **A gradient sky behind the diorama, on every `VOXEL` rung.** The void behind
the world used to be a black plate at every rung but the top, where it became
one flat blue -- enough while that void was a sliver, and a wall of paint once
the horizon came into frame.
It is the 8-bit skybox recipe now: four blues painted as flat horizontal bands,
deepest overhead and palest at the bottom, with a CHECKERBOARD of the next band
dithered into the bottom 40% of each one. Alternating two colours on a pixel
grid is how a machine with four to a palette got a fifth, sixth and seventh out
of them, and it is what keeps four bands reading as a gradient rather than as
four stripes. Every channel of the palette is a multiple of 8 -- where a
five-bit GBC channel lands -- so no colour in it is one the hardware could not
have shown. No clouds, nothing moving.
Where the bands END is the camera's own answer. At `75` the ground plane's
vanishing line is genuinely in frame -- projected through the same matrix the
geometry is drawn with -- and the pale end meets it. At the steeper rungs that
line is above the top edge, and what shows up there is the ground running OUT
past the map edge instead, so the bands take a fixed slice of the frame and the
haze fills the rest. One sky across the whole ladder either way.
**Nothing is resampled**, which is why it is drawn the way it is: no baked
160x144 image scaled up to the window, no downsized buffer blown back up, no
texture at all. One rectangle through a shader answers every pixel from its own
canvas coordinate, so a pixel of sky is computed at the size it is displayed
at and there is nothing for a filter to soften. The band edges and the dither
cells are measured in the pass's own pixels-per-world-pixel, handed in fresh
every frame -- so a `ZOOM` keypress is reflected in the frame that follows it,
with nothing cached at the old scale, and the sky's grid is the same grid the
world's own texels sit on.
The palette goes through the display-mode transform like every other palette in
this mod, so GRAY gets four greys and CLASSIC four greens. Below the bands the
void is filled with the palest of them -- which is also what the bottom band
ends on -- so the join has no seam, and a driver that cannot compile the shader
gets flat bands and a logged line rather than a wrong sky.
The overworld only. A battle is a staged shot whose placed camera has the
horizon above the frame, so the arena keeps exactly the flat sky it had.
- **A day/night cycle**, on a new **DAYTIME** options row: `DAY`, `NIGHT`,
`DUSK`, `DAWN`, `SYNC`, `CYCLE`. One twenty-minute clock underneath all of
them -- ten minutes of sun, ten of moon -- where the four named settings
are PINS on that dial (noon, mid-night, sunset, sunrise), `CYCLE` lets it
run, picking up from whichever pin or SYNC sky the player was just looking
at, and `SYNC` -- the DEFAULT -- lays the machine's own clock onto the
dial: local noon is the DAY pin, midnight is NIGHT, six and eighteen the
twilights, an hour of the real day is fifty seconds of dial. Everything is
a pure function of the clock, so the pinned DUSK is exactly the running
cycle stopped at sunset. While **VOXEL** sits on `FULL` the DAYTIME row is
HELD at `SYNC` and taken off the menu with the other rows the preset owns
(DayNight.forceSync, enforced from the preset, the rows hook and the
manager's options_changed -- the same three places BATTLE LAYOUT's pin
lives): the full diorama runs on the real sky.
**The sun and the moon are in the sky**, and their positions are honest:
the disc is the light's own direction projected through the same matrix the
geometry is drawn with, so it stands over the point on the horizon its
shadows point away from, at every pitch, window shape and zoom. The sun's
noon is this mod's existing sun to the digit -- southeast, 45 degrees up,
overhead behind the north-facing camera and correctly out of frame -- and
its arc swings north at both ends, so the disc stands IN frame through dawn
and dusk, rising half-set on the horizon. The moon arcs the northern sky
all night, due north (screen centre) at mid-night, with scaled crater
cells. Both are cell art on the sky's own dither grid, sized by the frame
(a celestial body's apparent size is an angle, so zooming the ground does
not swell it), and both are SCISSORED to the sky's region: the horizon
point is where a setting body disappears -- it never hangs under the map.
**The sky follows the clock.** Phase palettes -- the daytime blues,
gold-to-violet dawn, a hotter gold-to-indigo dusk, moonlit navy -- six
bands each now, blended along the dial and re-quantised onto the 5-bit
lattice, so every mixed frame is still a colour the hardware could show.
The blends bend through designed WAYPOINTS rather than straight across --
a golden hour on the way into dusk, a violet civil twilight either side of
the night -- because day's blue and dusk's gold are near-complements, and
a straight lerp between complements bottoms out in dishwater grey. Through the twilights a posterised, checker-dithered GLOW
warms the bands around the low sun -- painted light, not an airbrush. The
blends are 75 seconds wide either side of each twilight and the pins land
on their phase palette unmixed.
**The shadows follow the sun and the moon.** The shear every shadow is
thrown by (direction opposite the body's bearing, length its elevation's
cotangent, clamped at twice the caster's height) comes off the clock, the
shadow map's signature carries it, and the light's press fades out over the
last twelve degrees before the horizon -- so sunset hands off to moonrise
through a soft shadowless gap, and moonlight presses at about two-thirds
the sun's weight. The scene shader also multiplies every surface by the
hour's tint: neutral at noon, warm at the twilights, dim blue at night.
**Outdoors only**, by the same `Map.isOutdoor` test the sky already rests
on: indoors keeps the noon rig, the neutral tint and no sky -- a cave at
midnight is exactly as dark as a cave at noon. Viridian Forest is the case
between, a CANOPY map (DayNight.CANOPY): there is no sky to paint and no
sun to see, so the shadow rig stays the mod's fixed noon light -- all that
ever filtered through the leaves -- but night still FALLS in a forest, so
of everything the clock does, exactly one thing reaches it: the hour's
tint, in free-roam and staged battles alike. A battle staged on an
outdoor map fights under the hour: the night sky behind the arena, the
tint on the mons, the sunset taking the arena's shadows with it; an indoor
arena is untouched. The engine's own `world.tod` hook is answered
(`MORNING`/`DAY`/`EVENING`/`NIGHT`), so palette or music packs keyed to the
period ride this clock for free.
**The clock rides the save slot.** On the engine's `save.writing` event the
cycle's time is written into the mod's own save-file bucket
(`save.modData.DRAMATIC_SHAPE`), and read back when a save is opened. A
save with no clock in it starts at day.
- **Window glass.** The panes in the overworld art -- the framed squares on
building fronts, the small lights in doors -- are found by SHAPE in the
tileset image (a black border row, four or five black-flanked glass rows,
a closing border), at pixel granularity because the door's pane straddles
a 2x2 tile block. No tile ids are hardcoded: a conversion that draws its
own windows in the same idiom gets glass for free. The scan yields a mask
texture aligned to the tileset atlas, which the scene shader samples with
the same coordinates the terrain does -- so the effect lands on any wall,
at any angle, in free-roam and staged battles alike, with no geometry
work.
By day a thin glint crosses the panes WHILE THE VIEW MOVES: the sweep's
phase is fed by the camera's own travel and its strength fades out within
a beat of standing still -- a reflection is something the viewpoint does,
so still camera means still glass. The sweep pattern lives in the pane's
OWN texels, not the screen's: a screen-anchored pattern has the world
sliding through it at zoom speed whenever the camera pans, which strobed
(worst walking against the sweep); anchored to the glass, panning moves
nothing and a step advances the glint a fraction of a texel, the same in
every direction. It lifts the texels toward sky-white and leaves the
shine art visible through it. The mask is consulted only by meshes
textured from the tileset atlas (Voxel3D.glass), never by sprite sheets,
whose coordinates would land on the panes' atlas positions by accident.
After dark the panes are LIT: the texel's own pattern carried into a warm
lamp colour, replacing the shaded answer entirely -- a lit window ignores
the sun, every shadow and the hour's tint, exactly as a window with a lamp
behind it does. The lamps follow the clock (DayNight.windowLight): on
through dusk, full all night, mostly out by dawn, and never lit indoors.
- **A fade out of a battle, where there used to be a hard cut.** The engine
wipes INTO a fight with one of the original's eight transitions and cuts
straight out of it: `BattleState:finish` pops itself and the map is simply
there on the next frame. Between a white field and a tile map the original got
away with that; between a placed camera looking across an arena and a diorama
looking down on a walking player it reads as a glitch. The battle now fades to
black, closes behind it, and the map fades up out of it -- twelve frames each
way, registered as a `voxel_battle_exit` transitions record so the timing is
retunable in data like the wipes it answers.
Only while voxel mode is on, and then for EVERY battle, including one that
found no arena and drew on the flat battle screen: what is being smoothed over
is the return to the map, and the map is a diorama either way. With the mode
off, the vanilla cut is untouched.
One black rectangle over the FINISHED composite does the fading, so the world,
the letterbox bars and the battle's own text box all darken by the same amount
-- the renderer's existing warp-fade overlay is painted between the world and
the UI, which would have left the text box bright over the black. A blackout's
own warp fade or an evolution prompt still owns the way out when it takes the
screen: the fade stops at the cut rather than fading in over the top of it.
- **A `FULL` rung on the VOXEL row**, directly after `OFF`. One choice that
puts the whole mode in its intended state -- the 35-degree camera, the
miniature blur at maximum, the horizon flat, the view fitted, and battles
on the map -- rather than making a player assemble it from four rows.
While it is selected, every row it owns comes OFF the menu: V-GRID,
V-CURVE, 3D-BTL and T-SHIFT. A row that no longer decides anything is
worse than no row. Stepping onto or off `FULL` rebuilds the open menu in
place, so the rows leave and return under the cursor instead of waiting
for the menu to be reopened.
It applies its settings when the row ARRIVES at `FULL`, not every frame:
holding them would make the zoom keys and the wheel dead while it was on.
Leaving it deliberately undoes nothing -- reverting would discard whatever
had been changed since.
### Fixed
- **The hit flash whited out the whole screen.** The engine draws it as a
full-screen white rectangle, which is a flash on a white battle field and
a whiteout of the map, the HUD and the text box over a world. It is now
dropped on the way past and put back where it was ever about: the two
Pokemon go solid white for those frames, silhouette and all, and nothing
else in the frame moves.
- **A scripted battle cut straight in with no transition** (an ENGINE seam,
fixed in `src/script/Commands.lua` rather than in this mod): the rival in
Oak's lab, and every `start_battle` script, pushed the BattleState bare --
no flash, no wipe, the theme starting late -- where the original wipes
into scripted fights like any other. `start_battle` now routes through the
overworld's own `pushBattle`, which is also the path this mod wraps, so a
scripted fight gets its arena staged and the cast culled BEFORE the wipe
instead of catching up behind it. A battle scripted with no overworld
under it still starts bare, and no music plays twice (BattleState's own
start is a same-song no-op).
- **A standing figure's shadow detached from its feet under a low sun.** The
shadow compare forgives `slack` world pixels so lit ground does not acne
against its own texels, and that same forgiveness lit the first `slack` of
every cast shadow -- so the shadow started a bias-width away from the feet,
further the lower the sun reached (the classic peter-panning, invisible at
the old fixed 45 degrees and plain at a day/night golden hour or under the
moon). Sprite cards -- characters, authored figures, flowers, battle mons
-- are now drawn into the shadow map snugged TOWARD the sun along their
own ray (`ShadowMap.snug`): moving along the ray changes nothing about
where a shadow falls, but storing the card shallower takes three quarters
of the forgiveness back for the shadow it throws -- and for nothing else:
no terrain moved, so the acne margin is untouched where it matters. The
obligation that comes with it: every snugged caster's LIT draw hands the
same snugged transform to its own shadow lookup (Voxel3D.draw's
`sunModel`), so stored and lookup agree exactly and the compare keeps its
full margin -- read un-snugged, the missing nine tenths showed up as
diagonal moire bands crawling across every sprite. The shadow root lands
back under the feet at every hour.
### Changed
- **Under `VOID FILL: TREES` the border wall is modelled trees or nothing.**
Only the first block past the map body gets carved into round trunks and
canopies; the two blocks past that were too far out to be worth the quads,
so they fell through to the mesher's plain box and came out as a flat-topped
slab of tree ART sitting beside the modelled forest -- a painted-on plateau,
and the more obvious the lower the camera got. Rather than pay to carve
hulls nobody walks near, the wall now simply STOPS where the carving does:
`Structures` does not build the ring past that distance (the same "nothing
out there" `BLACK` already produces), and the mesher drops any cell inside
it the 2x2 canopy grouping could not claim, so no strip of boxes survives at
a corner. `WATER` and every indoor border are untouched -- a flat sheet of
water is what water looks like from above anyway.
- **The two HP boxes snap to the window's edges during a staged battle.** The
battle screen is 160x144 in the middle of the window and the world is the
whole of it, which left both HUD blocks huddled together in the middle of the
frame with map showing on either side of them -- a Game Boy screenshot pasted
over a diorama rather than the diorama's own furniture. The foe's block now
sits against the left edge and the player's against the right, on the same
frosted glass, with the same tiles at the same size on the same rows. The
pokeball rows and the safari ball count travel with the block whose rows they
share. On a window shaped like the GB screen there is nowhere to go and
nothing moves.
The engine draws them into the 160x144 canvas, which clips at its own edges,
so the layer is rendered to a texture and composited into the world image --
the one surface here that covers the whole window. A driver that cannot do
that falls back to the HUD in the frame rather than to no HUD.
- **`BATTLE LAYOUT` is pinned to `OG` while battles are staged on the map**, and
the row comes off the OPTIONS menu with the rows `FULL` owns. The staged shot
is composed in the GB's own frame -- the arena camera is solved to put a cell
under each pic's feet, and the HUD rects and the intercepted background fill
are measured there too -- and `WIDE` re-lays that screen out on a 304x144
surface, moving every one of them. Set rather than worked around, on every
route in: the options row, hotkey `8`, the mod manager's page, `FULL`'s
preset, and a save that arrived with `WIDE` already on. Switching `3D-BTL`
off hands the row back with `WIDE` selectable again.
- **Hotkey `3` walks the angle rungs only and steps over `FULL`.** The key is
a display-mode cycler -- it should change the camera and nothing else --
and `FULL` reaches in and rewrites four other settings. Landing on it
mid-walk would silently push the blur to maximum and flatten the horizon
with nothing on screen saying a keypress had done it. `FULL` stays on the
OPTIONS row, where a preset that changes other rows belongs.
A press FROM `FULL` goes to `50`. `FULL` is already the 35-degree camera,
so stepping to the rung of that name would look like the key had done
nothing. Matched by angle, so it follows `FULL` if that is ever retuned.
- **The mode's four options are one block in the menu.** The engine splices
a pipeline row in beside TILT and lands a mod's own rows at the end of the
list, which had these four in two places with unrelated engine rows
between them. The settings now follow the pipeline rows directly.
## 1.1.0
### Added
- **Battles happen on the map you were standing on.** The battle screen's
white field is replaced by the world: the mod finds the nearest patch of
open ground, points a placed over-the-shoulder camera at it, and draws the
fight over that. New **3D-BTL** row and hotkey `8`, on by default.
The arena is a 3x6 clearing of cells the player could walk on, with the
two mons three cells apart down the middle column and a one-cell apron all
round so the camera looks across floor rather than into a wall. Where no
map has room for that -- a corridor, a cave, a shop -- the search relaxes
to a 1x4 corridor with the apron given up, and where even that will not
fit the battle draws exactly as it always did.
Everything else in the frame is the engine's own. The mon pics, HUDs, HP
bars, move animations, faint slides and text box are drawn by BattleState,
in its order, at its coordinates -- the GB's own layout, with the player's
mon low and left and the enemy's high and right, which is why the camera
is placed east of the arena axis rather than the layout being moved to
suit the camera. What changes is what is behind them.
Three things carry the shot. The overworld's cast is culled before the
wipe, so it plays over an empty map and no bystander is standing in the
arena. The camera drifts on a slow orbit about a point between the two
mons, which moves the near ground and the far ground by different amounts
-- parallax, not a sliding backdrop. And a depth-of-field pass holds the
band of frame the two mons stand in sharp and softens the middle distance
and the foreground; both mons are in focus by construction, because they
are drawn as the battle screen's own pics after the pass has run.
**Nobody moves.** The arena is where the CAMERA goes. Nothing here writes
a cell, a facing, a flag or a warp, so a trainer's post-battle dialogue is
still talking to someone standing in front of them, and the blackout path,
sight lines and every script find the player exactly where they left them.
The two HUD blocks gain the backing the white field used to be. Gen 1
draws them as black glyphs straight onto the background with no box round
them, and black-on-grass is not readable; the backing is painted inside
`drawHUDs`, so it lands in the same target and takes the same zone colour
as the HUD it sits under, in both the colorized and flat pipelines.
Declines cleanly at every step it cannot take: no depth support, no open
ground, the row switched off, or a terrain mesh still building all end at
the battle screen the engine has always drawn.
### Changed
- **Characters are flat sprite billboards, and nothing about a sprite is
voxelized any more.** Every figure -- the player, NPCs, the ghosts
standing on a neighbour map -- is now its current 2D frame on a single
flat quad, with the shader's alpha discard cutting the exact silhouette
out of it. It still faces south and leans back by the camera's pitch,
so it reads face-on at every tilt exactly as before.
Two things went away with that. The contoured slab, which gave each row
a thickness measured from the sheet's own side view; and the carved
visual-hull models (`lib/VoxelModels.lua`, `tools/build_voxels.py`, and
~70 generated files under `assets/voxels/`, 2 MB), which reconstructed
a figure from its three drawn views.
A sprite is a DRAWING, not an object seen from one side. Gen 1's
overworld figures are 16x16 icons with a fixed front-on reading, and
turning one into a solid invents a body the artist never drew and the
game never implied. The shipped models were also the one place this mod
carried a description of the ROM art -- a carve is a faithful record of
a sprite's silhouette, pixel for pixel -- which sat badly against a mod
that otherwise ships no game data at all.
The flat card is cheaper on every axis: no pixel access (only the
sheet's dimensions), one quad instead of hundreds of faces, and one
mesh shared by the solid draw, the sun pass and the player's occlusion
silhouette. That sharing is load-bearing rather than tidy -- the
silhouette draws with the depth test inverted, and any self-overlap in
the mesh would read as "behind something" and repaint the figure on
open ground.
A mod can still ship `overrides/voxels/<name>.lua`; that path is
unchanged and still wins where it exists.
## 1.0.6
### Added
- **Conditional pins.** A profile entry may now carry `when_above`:
tile id -> rules keyed on the tile drawn directly north of it,
resolved per POSITION in `TileShape.at`. A pin is per tile id and one
graphic can mean two things -- the route gates' `$32` is both the
wall's dark base course and every service counter's front, and it is
the bottom row of its cell either way. Pinned `wall` the counters
stood a full 16px; pinned `counter` the deep wall banks corrugated
16/8 for sixteen rows and the room read as crates. What separates the
two uses is what sits on top, so that is what the rule reads. The
gates now have half-height counters AND level walls.
- **The Pokemon Tower has an exterior.** It is the one catalogued
building drawing the map edge cuts off (no roof band is on the map at
all), and it had no `buildings` entry, so it fell to the volume path
-- which tops a run by repeating its first two rows, laying window
courses flat across the plateau. Sealing the silhouette on the north
alone closes it (88% fill, one piece, against 37% and 126 pieces
unsealed), and one row of roof band spent on the drawing's top margin
costs no window course. It stands as a real tower, panes recessed,
door on the ground.
- **The Indigo Plateau statues stand up**, built exactly like the gym
statues: plinth a solid 16px block, figure a per-pixel cutout riding
it. On the avenue the statues stack with no gap, so the flood joined
six of them into one 24-row region and the volume builder raised
ridges of boxes with the statue art folded on the front. The same
bird is drawn at the foot of every badge-check pillar, so those are
crowned too.
### Fixed
- **Tall grass: one clump per tile.** Each 8x8 tile is a whole clump,
but the template split every tile AGAIN into its top and bottom four
art rows and stood those at two different depths -- so any blade
running down a tile was cut in half, into two 4px stubs 4px apart.
One tile is now one full-height standing slab at its own depth; a
cell's 2x2 tiles still stand independently, so the player walks
between the north and south rows.
- **Ledge lines are continuous.** `$34` is the cliff slope's foot and
also the pillar between hop-down segments; pinned `wall` with the
rest of the slope chain (the Diglett's Cave fix) it stood those
pillars 16px beside a 6px lip. At ledge height the run reads as one
lip, and the mound is unchanged -- its foot row reads as the talus it
is drawn as.
- **A prop only stands on furniture when its own cell is blocked.**
"Is something drawn above me" is not "am I standing on it": a chair
drawn against the north side of a table is above the table's trim row
too, and was being lifted onto the tabletop, with its claimed cells
re-tiled as tabletop so the table marched two rows north. Three
chairs in Cinnabar's trade room and Fuchsia's meeting room, and the
Celadon diner's stools. The world already knows the difference: a
thing that sits ON furniture occupies a blocked cell, a seat you walk
up to is in a walkable one.
- **Caves: nothing below sea level, and the water is water.** Two tiles
were identified backwards in the first pass. `$14` -- the tile the
engine animates, that `Map.WATER_TILES` names and that Surf runs on
-- was pinned `wall`, so Cerulean Cave's lake and the Seafoam sea
stood up as rock slabs. And the pale dithered rock fill was pinned
`water`, cutting 612 tiles of two-cell-wide trench through four maps.
Both corrected; a sweep of all 19 cave maps now reports zero tiles
below the datum. The elevation scheme is documented in the entry and
derived from the game's own `tilePairs`: dark floor, water and drop
holes at 0, the lit shelf a 6px step above, rock at 16.
- **Cave ladders climb.** Which ladder graphic goes up and which goes
down is unanimous in the warp table -- 37 cells of one always warp
down, 40 of the other always up -- so they are real stepped flights
now, not painted plates.
- **Poke Mart's register stands on the counter.** The pin was on the
wrong tile: `$08` is the counter's own top band, not the register, so
the standee flood ate everything but two black lines. The register is
the keypad-and-receipt drawing one row up.
- **Celadon's televisions**, which were solid 16px boxes wearing the TV
art on one face, and the **Pokemon Tower reception desk** and the
**gate counters**, which stood at wall height, are all their drawn
heights now. The **Fan Club and Silph boardroom statues** were read
as seated chairmen and painted onto the tabletop; they are cutouts
standing on their pedestals, and the tables are cut to a true
octagonal footprint.
## 1.0.5
### Added
- **Every remaining interior is furnished.** The profile covered ten
tilesets; it now covers twenty-three -- 1,190 pinned tiles across
`GATE`, `FOREST_GATE`, `LOBBY`, `MUSEUM`, `LAB`, `MANSION`,
`INTERIOR`, `CLUB`, `SHIP`, `SHIP_PORT`, `FACILITY`, `CEMETERY` and
`UNDERGROUND`, plus full entries for `CAVERN` and `GYM` which had only
stubs. Roughly 130 maps, surveyed against the standard the finished
interiors already set: one 16px wall band carrying whatever is drawn
built into it, half-cell counters so the drawn front folds up and the
top stays on top, `bookcase` collapse for free-standing shelves,
thin-pool standees for plants, and small objects riding the furniture
they are drawn above.
What the detector was doing before, by way of what changed:
- **Rooms with no walls.** Three tile ids (`$14`, `$32`, `$48`) are
claimed by the engine's water set in EVERY tileset, and collision is
per CELL, so one of them in a cell's bottom-left corner sank the
whole cell. `$32` draws both the route gates' wall base course and
every counter front, so all 25 gate maps were a checkered floor in a
moat; the same trap put ponds through two thirds of Seafoam B4F,
under every museum vitrine, along the S.S. Anne's wall corners and
across Silph Co 1F's lobby island. The set is wider than three ids
in practice -- `LAB`, `MANSION` and `INTERIOR` each hit six to nine
-- because the test is per cell, so an innocent tile sharing a cell
with a trapped one sinks with it and has to be pinned too.
- **Towers and fused monoliths.** Counters raised to 48px dragging
their wall band with them, merchandise racks fused sideways into
32px blocks four tiles deep, cave shelf edges standing as 48px fins
beside a 16px band, the Vermilion liner folded upright into a lumpy
48px slab.
- **Furniture that was not there at all.** Anything whose cell is
walkable resolved to flat ground: 59 department-store stools, every
gate lounge table and pair of binoculars, both museum staircases,
the gym-lounge chairs, and -- via the void rule -- the black
partition walls every gym is divided by, which left their white rim
columns standing as hollow 48px fins.
- **314 gravestones** in Pokemon Tower were 8px stubs, because the
volume path measured only their bottom row and dropped the arch.
Notable readings: the S.S. Anne's hull is `roof` (a drawing seen from
above, so the art belongs on the top face); the Warden's specimens and
Celadon Gym's shrubs are `cylinder` voxel balls, the first indoor use
of that class; the Fan Club's octagonal boardroom table is `counter`
rather than `table`, because only a counter rides its upper rows onto
the top face in drawn order -- which is what draws the seated chairman
exactly once, the Pokemon Center couch case verbatim.
Fuchsia Gym's invisible maze is deliberately left flat. Raising it
would read better as a room, and would also hand the player the
solution; a shape is purely presentational, so the drawn answer wins
and the gym plays as the flat game does.
### Fixed
- **A profile pin now outranks the door fold.** `Structures.forMap`
folds a door cell into its facade so the doorway does not punch a hole
in the wall -- but it overwrote the resolved shape unconditionally,
including for AUTHORED tiles, which contradicts rule 1 of the
documented resolution order. Any pin on a tile its tileset also lists
in `doorTiles` was dead on arrival: all four Celadon Mansion
staircases and Pokemon Mansion 3F's descent are door tiles, so
`stair_*` pins there silently did nothing and the flights stayed
painted flat on the floor. The fold now skips authored tiles.
## 1.0.4
### Added
- **Viridian Forest grows real trees.** Nearly everything drawn in the
forest is ROUND, and the detector was boxing all of it: the big trees
came out as ragged mixed-height volumes (their sparse canopy-rim
tiles read 0px against 32px bodies, leaving gap-toothed hedge walls),
the stump rows merged into 16px crate walls wearing folded stump art,
and the trail signs were broken piles -- their $32 tile is the
water-fallback trap and recessed into a pond lip in the middle of the
woods. All of it is now profile-pinned to the treatments the rest of
the world already uses: every tile of the tree drawing (ball, rim
wisps, feet) and the stumps take the per-cell voxel HULL the overworld
border forest wears -- a tree spans 2x2 cells, so its four
quarter-hulls tile into one big lumpy canopy, and each stump becomes a
round bollard; the signs take the standing thin-slab `signpost`
treatment every town sign gets; and the white sparkle filler inside
the tree masses is flat ground instead of an invisible zero-height
box. The whole map now resolves to hulls, signs, ledges and ground --
a detector sweep finds no stray boxed column anywhere.
Two refinements over the first cut, both new hull-builder abilities.
A tree's drawing spans 2x2 CELLS, and per-cell hulls unfolded it onto
the ground -- the ball's top half sat one cell north of its bottom
half at the same elevation, reading as a tree cut in half. The new
`canopy` class pins the drawing's corner tile as a group anchor and
the whole 2x2-cell drawing carves as ONE 32px hull, so every tree is
a single tall round canopy (the carver is now parametric over its
canvas size, and hull stamps carry their footprint radius). And the
stumps' drawn tops are a CUT FACE -- an ellipse of growth rings seen
at an angle, not body: the new `stump` class builds the hull from the
bark rows alone and projects the ellipse across the round flat top,
near arc to the south (`stump_cap` names the ellipse's drawn height),
so the rings ride the round part in perspective.
- **Flowers stand up, and keep swaying.** The animated meadow tile
($03, the one tile the overworld animates by frame rewrite) now
renders as a billboard one voxel deep: the drawing's darkest tones
plus everything they enclose are cut out per pixel, and the ground
beneath is synthesized from the commonest flat neighbour, exactly
like the ground under a detected prop.
The interesting part is that the cutout still animates. A mesh is
static, so the geometry spans the UNION of the mask over the base art
and all three animation frames, and the animation lives entirely in
the texture: TerrainAtlas already rewrites the flower's slot in the
private animated atlas each step, and for this tile it now writes
only the current frame's mask opaque with everything else keyed to
alpha 0 -- which the voxel shader discards, and the shadow pass with
it. The standing silhouette trims itself frame by frame in texture
space, off the same engine clock as the flat path, without a vertex
moving. The class is derived, not authored: any frames-animated tile
resolves to the new `flower` class with no profile entry, the same
way tall grass derives from `grassTile` (hand-authoring still wins).
- **The cuttable bush is a standing cutout.** The four tiles Cut
deletes ($2D/$2E/$3D/$3E -- across the whole tileset they appear only
in the five cut-tree blocks) are pinned to the thin `prop` pool: a
per-pixel standee 5 voxels deep, black-outline segmented with its
enclosed pixels kept, the drawn grass dither flooding away. It
stands on plain grass ($2C) -- the very tile Cut leaves behind per
field.cutTreeSwaps -- via the profile's new `prop_ground` key, which
names the tile painted under a pinned prop instead of whatever flat
tile its neighbours vote in.
- **Gym statues: a solid plinth, a standing bird.** The statue pair
flanking every badge gym's aisle (and Bruno's room) is one cell of
figure over one cell of plinth. The plinth ($22/$23/$32/$33) is
pinned `wall`: a solid 16px block. The figure ($02/$38/$12/$13) is
pinned `prop`: a 5-voxel cutout that stands ON the plinth through the
authored-box support rule, its checkered background flooded away and
the pixels its outline encloses kept.
The whole statue keeps ONE cell of footprint. The support rule used
to extend the box under the claimed cell (the monitor-on-desk path),
which marched the plinth a second block backwards; a figure whose
support is a FULL-HEIGHT block now collapses instead -- the drawn
figure cell becomes synthesized floor, since the block below already
carries the whole base. Furniture supports keep the extension: their
drawn cell is the furniture's own upper rows, and floor there would
amputate the desk. The round boulder drawn beside some statues is
deliberately NOT pinned -- it also tiles wall-to-wall as Pewter's
rock rows, which are scenery for the detector.
- **Lt. Surge's trash cans stand up.** The can ($0B/$0C/$1B/$1C, the
lone graphic of blocks 38/39) takes the same treatment as the
cuttable bush: a 5-voxel `prop` cutout, black-outline segmented with
its enclosed pixels kept, standing on the gyms' main floor tile
($11) via `prop_ground`.
- **The Poke Marts furnished to the Center's standard.** The MART
tileset shares the Center's atlas image but is its own id, so none
of the Center's pins applied, and every mart was raw detector
output: a 32px double-height display band for a back wall, the two
shelf racks fused into one four-tile-deep monolith, and the clerk's
booth towered into a 48px slab wearing the juice poster. Pinned the
way the finished interiors are -- the back wall's SALE cases and
drink fridges one 16px face like the Center's healing consoles, the
racks collapsed to one-cell-deep shelves at drawn height like Red's
bookcases, the counter half a cell with the poster riding its top
like the nurse's tray, and the cash register standing ON the counter
through the authored-box support rule. One 4x4 layout serves every
city, so this covers all eight marts.
### Fixed
- **Cut trees now vanish in voxel mode -- and grow back.** The
engine's Cut path swapped the block with a raw `setBlock` + renderer
rebuild, never emitting `world.block_replaced` -- so this mod's
listener (which rebuilds the map's mesh exactly for this) never
heard about it, and the diorama kept showing the tree. The engine
now routes Cut through `replaceBlock`
(src/world/OverworldController.lua), whose whole purpose -- per its
own comment, "Victory Road barriers, Cut trees" -- is that same swap
plus the event. The regrowth path had the same hole one door away:
cut trees are restored block by block when the map is re-entered,
and the card-key doors are stamped closed on floor load, both
through the same silent `setBlock` -- with the mesh cache staying
warm across a round trip (that is what prevLive is for), the world
kept showing the stump you left. Both paths now announce each block.
- **A block edit no longer blinks the world down to 2D.** The
listener used to drop the edited map's mesh outright, and mesh
builds are asynchronous -- so cutting a tree (or stepping out of a
door onto a map whose trees just regrew) flashed the flat 2D world
for the frames the rebuild took. `ChunkMesher.refresh` rebuilds in
place instead: the stale mesh keeps drawing, the replacement cooks
in the background, and each slot swaps as its build lands -- the
tree pops out (or back in) with the scene never leaving 3D.
- **Flowers cull the player correctly from every angle.** The flower
billboards were baked into the terrain mesh, which draws without
the characters' camera-ward pull -- so a walker standing among
flowers won the depth test against ALL of them, including the
flower south of their feet that should overdraw them. The flower
quads now ride their own mesh, drawn after the characters with
exactly the characters' pull (the tall-grass trick): the flower in
front of a walker occludes their feet, the one behind them hides,
at every camera angle. Unlike grass the flower mesh still casts
shadows -- it is a handful of cutouts per meadow, not thousands of
tufts.
- The `voxel_anim_probe` driver crashed on engine builds without the
optional `TileRenderer.animFrame` seam, and again on tilesets whose
animation list carries a "toggle" entry (spinner rooms), which claims
a tile LIST rather than one slot. It now reads the clock through the
mod's own fallback chain and skips toggle entries in the placement
census.
- **The shoreline no longer opens into the sky beside buildings and
signs.** Water recesses 2px below the ground, and the ground tile beside
it closes the step with a small below-ground side band -- but a tile
CLAIMED by a standing object (a building footprint, a sign standee, the
bushes ringing Fuchsia's ponds) only painted its synthesized flat ground
and never emitted sides. Along every stretch where such a tile met
water, the two-pixel step was an open slit straight through to the sky
behind the mesh. The skip branch now emits the same below-ground bands
ordinary ground does, cut from the synthesized ground's own art, so the
shoreline lip is continuous whatever stands on the bank.
- **Edge-row buildings keep their facades.** The south wall of Saffron's
row houses -- profiled buildings whose front row is the map's last tile
row -- lies exactly on the boundary plane shared with Route 6, and the
prebuilt-quad keep rules dropped it: the strict body test excludes the
plane, and the closed neighbour mask (which exists to kill ring scraps
whose rects sit exactly on that line) swallowed what was left, so from
Route 6 the houses stood hollow. The two cases are geometrically
identical degenerate rects, but they FACE opposite ways: a face pointing
away from the body is this map's own facade and nothing in the
neighbour will ever draw that plane, while a face pointing into the
body is the scrap the mask is for. The mesher now reads the winding and
keeps outward faces on the body's boundary planes, on all four edges.
The roof RIM had the same problem one step further out: an edge-row
house's eave overhangs `frontEave` voxels PAST the boundary plane into
the neighbour's airspace, and those quads are neither on the plane
(the winding rescue) nor over the body -- the neighbour-body mask ate
them as ring scraps, so from across the seam the roof edge was open
sky at low camera angles. Building placements only ever scan the map
BODY, so every building quad is this map's own structure by
construction: they now carry an `own` flag the edge keep-rules never
touch.
- **Diglett's Cave mounds (and cliffs everywhere) stop sprouting
towers.** Two detector misreadings stacked up on the cave-entrance
mound. The dark east slope of the cliff drawing ($02/$24/$34) is one
texture repeated over the mound's whole height, but its corner tiles
break the repeat scan, so those columns rose to 32px -- the rock
pillar beside the entrance. And a folded doorway column reads its own
drawn extent (the door plus everything above it), which is a house's
real height when the door is a house's, but a 32px tower over a 16px
plateau when the door is a cave mouth -- the entrance jumped a block
above the mound around it. The slope chain is now profile-pinned to
one 16px course, and a doorway column answers to its REGION entirely:
height from the region's dominant column, top flat when those columns
are flat repeats (the mound) and roofed when they are drawn facades
(a house). Both cave entrances -- and every cliff built from the same
slope tiles -- now read as one level mesa with the cave mouth at
ground level. A new `voxel_mound_probe` driver prints the detector's
per-column class and height over any rectangle, which is how this was
diagnosed.
Routes 3 and 4 had a third variant of the same misreading: the repeat
scan anchors at a column's FRONT tile, and a plateau column that ends
in a one-off rounded corner tile ($13/$35) never matched -- it read
its whole capped extent and shot up as a 48px fin (several together
made a tent). When the two rows directly above the front are
identical, the column is now read as that repeat wearing a trim foot:
its unit is one course plus the trim. Doorway columns still answer to
their region first, so houses are untouched.
## 1.0.3
### Added
- **The player shows through whatever hides them.** Occlusion in this mode is
the real thing -- walk north of Red's house and the roof is genuinely in
front of you -- but a player who cannot see their own character has lost
track of where they are standing, which the flat game never allowed. The
figure now draws a second time as a translucent silhouette wherever the
world is in front of it.
No code anywhere asks whether the player is occluded: the depth buffer
already knows, and the test is the question. The silhouette is drawn with
the depth compare INVERTED -- `greater` where the scene uses `lequal` --
so it appears exactly where the ordinary draw would have lost, and nothing
at all is drawn when nothing is in the way. LOVE hands the compare straight
to `glDepthFunc`, so the two are true complements with no seam between
them.
It goes down BEFORE the characters, so the only thing it can meet in the
depth buffer is the world -- terrain, buildings, trees. Drawn after the
solid pass it would meet the player's own card instead, and every fragment
of a figure sits behind the one that just wrote it, so it would paint over
the player permanently. Characters then draw on top as usual.
It uses the FLAT card (`SpriteBillboards.shadowQuad`), not the relief slab
the solid pass draws. The slab carries front and back faces and the mode
culls neither, so with the test inverted its own back faces -- a few voxels
deeper than the front ones that just won -- read as "behind something", and
the figure repaints itself on open ground whether or not anything is in
front of it. One quad has no self-overlap, which is exactly why the shadow
pass already uses this mesh, and it cannot double-blend into a mottled
patch either. A silhouette is an outline, so the outline is the right mesh.
Depth writes are off: the pass is behind the scenery by definition, and
writing would file the hidden figure in front of the building hiding it,
which the grass pass at the end of the frame reads. The card carries the
same transform and the same camera-ward pull as the solid draw (both now
come from one shared `billboardMatrix`/`billboardPull`, so they cannot
drift), which is what keeps the leaning-over-a-near-wall case out of it:
pull already won that fight for the solid draw, so a character merely
standing close to a wall does not shimmer a silhouette over it.
It is drawn as ONE flat translucent grey, not as a dimmed copy of the
sprite. Tinting through the vertex colour could only MULTIPLY the sprite's
own pixels, which darkens each one by its own amount and keeps all the
character's internal detail -- a murky picture of Red rather than a shape.
So the fragment shader carries a `ghost` / `ghostColor` pair and replaces
the colour outright, last in the chain so neither the sun nor a voxel seam
can mottle it. Staying translucent is what keeps it reading as "behind
that wall" rather than as a hole punched through it.
`Voxel3D.GHOST_COLOR` and `GHOST_ALPHA` (0.5) are the knobs. Only the
player gets this -- NPCs and the ghosts standing on a neighbouring map are
left to honest occlusion, because it is only your own character you cannot
afford to lose behind a roof.
## 1.0.2
### Fixed
- On Android the diorama drew into the top-left corner at a fraction of the
screen -- about a third of the width and height on a 420dpi panel -- with
the field effects (dust, emotes, the cut-tree shudder) correspondingly
oversized against the world they sat on. Desktop was unaffected.
The pipeline ctx hands over `width`/`height` measured in LOVE UNITS
(`love.graphics.getDimensions`), but the engine composites a pipeline's
returned canvas with `draw(canvas, 0, 0, 0, 1/dpiX, 1/dpiY)` -- a scale
that only covers the window if the canvas is at PIXEL resolution. Sizing
the scene canvas from the ctx therefore paid the DPI scale twice: the
canvas came out that much smaller, and was then drawn that much smaller
again. On desktop the two units are the same number and nothing shows;
Android's DPI scale is the display density (2.625 at 420dpi), so that is
where it surfaced.
The scene canvas is now sized from `love.graphics.getPixelDimensions`
directly rather than from the ctx. That is the number a fixed engine would
hand over, so this does not double-correct if the ctx is ever changed to
agree with the compositor. It also squares the FX pass for free:
`ctx.scale` was ALREADY in pixels per world pixel (`Zoom.scale` over
`Renderer:fitScale`, which measures the drawable), so the closures were
being scaled for a canvas 2.6x bigger than the one they were drawing into
-- one wrong number, not two.
## 1.0.1
### Fixed
- The RED++ texture-readback fallback in `TerrainAtlas` never worked on real
drivers: LOVE refuses `Canvas:newImageData` while that canvas is currently
active, and `readback()` read the pixels back before restoring the previous
render target, so the call threw on every driver rather than only on
stubborn ones. The previous target is now put back BEFORE the read. The
headless suite could not see this -- its stub canvas does not enforce the
rule -- so it survived until a live probe ran the chain unguarded.
Harmless for vanilla tilesets, where the CPU rebuild (`gbcPixels`) answers
first and the fallback is never consulted; it was the last-resort route for
a map the palette pack does not know, which until now had no working route
at all.
## 1.0.0
First release, ported from the engine-internal voxel branch onto the
`render_pipelines` mod API.
Interior furniture gets the shapes it depicts
(mods/DRAMATIC_SHAPE/tools/voxel-survey.md is the procedure that found and
verified these).
Buildings stop being boxes wearing their own elevation. A profiled
building is voxelized from its own sprite, band by band -- the pipeline
written up in `assets/docs/buidling_to_voxel/`.
Terrain meshing goes asynchronous, instanced and bounded. The first voxel
frame used to build every neighbourhood map synchronously (a ~2.4s
freeze), retain every map's analysis forever (gigabytes over a
cross-region trek), and string stray pixels along map seams.
Real shadows. The sun moves to the southeast and drops to 45 degrees, so
shadows fall northwest -- up and to the left on screen -- and run about as
long as the thing throwing them is tall.
### Added
- `voxel` render pipeline: 3D diorama overworld with extruded terrain,
depth-buffered occlusion, leaning sprite billboards, drop shadows and
contact AO. VOXEL options row and hotkey `3` (OFF / 15 / 35 / 50 / 75).
- `tiltshift` render pipeline: a `worldPresent` post-process giving the
miniature-photo look. T-SHIFT options row and hotkey `6` (OFF / 1 / 2 / 3).
- Carved voxel models for 67 overworld sprites, plus the
`tools/build_voxels.py` that produced them.
- `data/voxel_heights.lua`, the hand-authored tile shape profile.
- Furniture shape classes in the profile: `bed` (a low slab wearing its
top-down art), `table` and `desk` (boxes at their drawn height whose
faces fold the artwork up), `relief` (a prop drawn from above -- a
game console -- lying flat and extruding a few voxels inside its
outline), the standee pools `billboard` (10px) / `prop` (5px) /
`stool` (5px, and characters standing on its walkable cell sit at
seat height) / `cutout` (one voxel: pure profile, for the vase on the
table), and the stair archetypes `stair_e`/`stair_w` (a rising flight
of real steps) and `stair_down_e`/`stair_down_w` (a sunken stairwell
descending below the floor -- stairs that lead down). Separate pools
cluster separately, so touching drawings never merge into one cutout.
- Standee clusters split into per-pixel connected components, each
standing on its own feet in the depth band of the row it is drawn in:
two stools stacked in adjacent cells become two stools, and no
fragment of a drawing ever floats at its bounding-box height. A
`cutout` keeps only its largest component -- a cast shadow's drawn
edge is background, not a floating scrap.
- `bookcase` class: free-standing shelf drawings collapse in ranks onto
one-cell-deep boxes at their full drawn height, back rows becoming
hidden floor; the trim row above a rank -- undetected structure or a
row pinned `table`, since the same trim tiles cap other furniture --
is adopted as its cap. Pinned for Oak's Lab (`DOJO`).
- Oak's Lab tables pinned `table`: the starter-ball display and the
north tables stand at real table height, the display frame's black
corner brackets no longer auto-extract into standing prisms, and the
Poke Ball / Pokedex sprites ride the authored height onto the
tabletops.
- Pinned props drawn directly above a pinned box stand ON it: the PC
monitor on its desk, the flower pot on the dining table.
- Profile pins for `REDS_HOUSE_1` / `REDS_HOUSE_2` (Red's house and the
Copycat's, both floors): bed, stools, tables, PC desk with its
standing monitor, bookcases, TV standing on the floor behind the game
console's relief, potted plant, flower pot, both staircases, and the
wall/window band.
- `mods/DRAMATIC_SHAPE/tests/voxel_survey.lua`: screenshot-survey driver
behind the repeatable inspection procedure (SURVEY_MAP / SURVEY_SPOTS /
SURVEY_LEVELS / SHOT_DIR), documented in
mods/DRAMATIC_SHAPE/tools/voxel-survey.md.
- `lib/Buildings.lua`: a building archetype. Where the volume path folds a
whole drawing upright (roof, facade and sloped ends alike) into one box,
this classifies each BAND of the drawing by the 3D surface it depicts and
applies the matching operation: top-facing rows lay flat over the
footprint, the facade extrudes straight back, an awning band juts past
the walls, and the drawn taper at the ends becomes a stepped slope in
elevation. Every visible voxel carries a real texel of the drawing, so
the model recolours with the atlas.
- A `buildings` section in `data/voxel_heights.lua`. A building is matched
by its exact tile grid (the drawings are catalogued in `assets/docs/buildings/`),
so one entry covers every map that places the same art -- Red's house,
Blue's house, Bill's, the Copycat's and the two Fuchsia houses are one
seven-placement entry, and Oak's lab is a second. Only the band table is
authored; the silhouette, the taper rate, the eave height and every
window and doorway are measured off the pixels.
- The flat-roofed civic block and its sixteen relatives -- every Pokemon
Center and every Poke Mart, Fuchsia Gym, the museum, the Game Corner,
Celadon Mansion and its department store, the Power Plant, the Route 5
and Route 22 gates, Silph Co and five anonymous scenery blocks. They are
one architecture drawn at eleven different footprints, from 4x4 cells up
to Silph Co's 8x12, and they share one band table: their lattice is drawn
from straight above, so the measured taper comes out flat and the whole
band is depth under one level roof. No new roof mode was needed -- the
drawn profile was always the shape, and a drawing with no taper simply
yields a level one.
- Pewter's museum hall (`assets/docs/buildings/B24`). It is the one
sloped-roof building in this pass, and the only one so far whose roof
texture is not a plain repeat: the drawing states its own period by
repeating the whole lattice-and-course motif, rows 8..31 again at 32..55,
so the cycle is 24 rows and the roof carries its drawn courses across the
depth instead of a bare lattice. The band below them is the roof's
fascia, wider than the wall it covers, so it belongs to the roof and
lands on the south rim. Note the museum is TWO drawings: this hall and
the east entrance beside it, which is B18 and shares its drawing with the
Route 2 gate.
- The rest of the 2:1 sloped-roof buildings: both gym drawings (the
standard one at Cinnabar, Pewter, Vermilion and Viridian plus the
Fighting Dojo, and the wider Celadon / Cerulean / Saffron one), the 4x2
cottage that houses Mr Fuji, the Cubone house, Bill's grandpa, the Name
Rater and the Viridian school, Cerulean's three wide houses, the day
care, and three scenery blocks -- among them B01, which at 19 placements
is the commonest drawing in the game. Each one's roof band is pixel for
pixel one of the three already authored, so they take that sibling's band
table unchanged: 16 rows for Red's house's, 32 for Oak's lab's, 64 for
the museum's.
- The Safari Zone rest houses and the Victory Road entrance, the first
buildings outside the `OVERWORLD` tileset -- `buildings` is keyed by
tileset and until now only had the one section. The rest house's
corrugated roof repeats every 5 rows rather than the overworld lattice's
8, which is the point of `roofCycle` being authored per building.
- Route 10's scenery block, via a new `seal` field. Its drawing has no
black base course -- it ends on a row of light brick -- so the silhouette
flood climbed in from the south border through the mortar and hollowed
the wall out, leaving 72% of the sprite in 65 pieces. `seal` names the
sides a drawing runs off rather than closing, and the flood does not seed
there: sealed, it is 95% in one piece, and the model is its twin the
museum's. No other building sets it, and none changes by a voxel.
- Together these take the mod to 31 of the catalogue's 34 drawings and 144
of its 147 placements. The last three, and why each resists, are written
up in `assets/docs/buildings/REMAINING.md`.
- A roof no longer runs past the drawing's own silhouette. These sprites
are inset from their boxes, and the columns outside the inset carry no
roof: they now get none, and the fascia belongs to the outermost columns
the drawing actually paints. Before, they raised a four-voxel slab of
black kerb the full depth of the building, flanking its walls at ground
level. Nothing shipped hit this -- Red's house and Oak's lab are drawn
edge to edge -- so no existing model changes by a single voxel.
- Windows and doorways sink a voxel behind their frames, found rather than
listed: a pane is a non-black region the drawing seals off behind its own
black outline. A nested frame (the door's own little window) layers for
free.
- `tools/building_voxels.py`: the reference implementation of the same
algorithm, with the geometric asserts and isometric previews Stage 5 of
the methodology calls for. It and the runtime agree exactly on voxel and
shell counts for all 19 templates -- 96,617 / 12,866 for Red's house up
to 3,715,963 / 146,762 for Silph Co, which ship as 3,410 and 13,994
quads. Its slope asserts read the drawn columns only and stand down for a
roof with no taper, where the check is that the roof is level instead;
its previews fit the projection to the model rather than to a fixed
camera, so a building taller or deeper than the first two still lands on
the canvas.
- A sky at the 75-degree rung. Pitched that far over, the horizon comes
into frame and a good part of the picture is void, so the void gets
filled instead of reading as the black plate it does at every rung below.
No skybox and no geometry -- it is the colour the scene canvas clears to.
**Outdoor maps only.** A house, a cave or a gym is a room with a ceiling,
and the void past its walls is the outside of a box rather than open air.
The test is `Map.isOutdoor`, the same one the engine uses for door SFX
and the town map, and the same one `Structures` already asks to decide
whether a map rings with trees.
The colour is a four-shade ramp shaped like a world palette and run
through `PaletteFX.effectiveColors`, so it answers to the display mode
exactly as the baked terrain does: blue in the colour modes, grey under
GRAY, green under CLASSIC, dark under GBC INV. A hardcoded blue would sit
wrong in every mode that is not a colour mode. It fades across the
approach to the top rung rather than switching at the keypress, so it
arrives with the camera tween.
- The mod's four controls sit on adjacent keys, and the two that never had
one now have a hotkey at all:
| key | control |
| --- | --- |
| 3 | VOXEL, the camera ladder |
| 5 | V-GRID, the wireframe |
| 6 | T-SHIFT, the blur ladder |
| 7 | V-CURVE, the horizon bend |
Only 6 arrives by the documented route. `Game:keypressed` answers the
engine's own display keys first and returns -- 2 COLORS, 3 TILT, 4 ZOOM,
5 GBC FX -- and only then offers the key to `Pipelines.hotkey`, expressly
so that "a pipeline can never shadow one". Two of the four wanted keys are
in that set, and V-GRID and V-CURVE own no render pass, so they have no
registry to claim a key from in the first place. The mod wraps
`Game:keypressed` to take the four. Polling the keyboard in `update`
would not do: it fires alongside the engine's handler rather than instead
of it, so 3 would cycle this mode AND the engine's TILT on one press.
**TILT (3) and GBC FX (5) are no longer reachable by key while this mod
is enabled.** Both are still on the OPTIONS menu. Key 9 is now free.
So the VOXEL key turns both off itself, on every press. Both fight the
diorama -- TILT is the flat fake of what this mode does for real, GBC FX
a full-screen present pass laid over the top -- and with 3 the only key
that now reaches either, it also has to be the way back from having left
one on. Every press, not just the one that switches the mode on: the
registry's own tilt exclusion covers switching ON, but the press that
cycles the ladder round to OFF would otherwise leave both running with
no key left to clear them.
The wrapper delegates rather than reimplements: `Pipelines.hotkey` still
applies its own gate and ladder, the settings borrow the same free-roam
gate the voxel pipeline uses, and a screen with its own key handler keeps
the keyboard -- so typing a nickname cannot cycle a render mode behind
the text box.
- `lib/ShadowMap.lua`: the scene rendered once from the sun into an
orthographic depth map, which the main pass then samples per fragment.
What the sun cannot see is in shadow, whatever surface it is, so a
shadow climbs a wall, drapes over a roof and slides across a passing
NPC with no case in the code -- and every caster is simply whatever the
pass draws. The terrain mesh goes in, which means buildings, trees,
ledges, signs and every prop cast, where before only characters did.
- Depth is packed into two 8-bit channels of an ordinary color canvas
(~16 bits over a ~700px frustum, a hundredth of a world pixel).
Readable depth textures are the least portable corner of the graphics
API, and the mod's contract is that an unsupported driver falls back
rather than errors: `available()` reports, and VoxelScene keeps the old
flat decals when it says no.
- The map resolution is picked per frame from a 1024/1536/2048 ladder
against a 0.45 world-pixels-per-texel target, because the light frustum
is fitted to the world view and that swings 3x between the closest zoom
and a maximised window at the widest. The frustum is snapped to whole
texels, without which every shadow edge in the world crawls as you walk.
- Ambient occlusion, the genuine article: each vertex counts the
neighbours crowding it and steps down once per neighbour, on top faces
(four corners, three neighbours each) and now on upright faces too --
the crease a wall rises out of, and the inside corners where flanking
columns box it in. It is the complement of the shadow pass rather than
a duplicate: the map draws the long directional shadow, this draws the
dark seam in every corner the sky cannot see into, at scales finer than
a shadow map texel.
- Plus a ground-contact term for the prebuilt prop quads -- per-pixel
plants, signs and lone trees, and the round-tree stamps. Those arrive
from Structures already finished, so the neighbour counting has no
columns to count; what it can still say is that the floor blocks half
the sky, so a voxel's first 6px of rise ramps back to full light. It is
what stops a prop reading as pasted over the ground rather than
standing on it, and outdoors it is most of what AO does at all, since
trees and posts are nearly all prop geometry.
- One knob for the lot: `AO_STRENGTH` in `ChunkMesher` scales every term
(they are written as darkening amounts, not multipliers), against a
floor that keeps a crank from punching holes of pure black.
- The **V-CURVE** row in OPTIONS: the curved world, the Animal Crossing
horizon. Every vertex is pushed down by the square of its horizontal
distance from the camera's focus, and that is the whole effect -- a
quadratic is nearly zero near its vertex, so the ground being played on
stays flat, and the falloff accelerates, so the far edge rolls away over
a near horizon and the town reads as sitting on a small sphere.
- It is deliberately NOT a fisheye. A fisheye is a LENS -- a screen-space
warp -- which bends straight lines everywhere including right in front of
the player, resamples every pixel to do it, and would leave this mode's
art blurred and crawling. Bending the WORLD is one line in the vertex
shader: lines near the camera stay straight and not a pixel is resampled.
- Displacing along Y only is what keeps it readable rather than
nauseating: the drop depends on where a column stands, not how tall it
is, so the world tips away and the buildings on it stay upright.
- Shadows and the wireframe ride along for free. Both are already worked
out before the bend -- the shadow map in flat world space, the grid in
model space -- so the bend carries them exactly as if they had been
painted on, and neither the light frustum nor the grid needs to know the
curve exists. `Voxel3D.project` applies the same drop on the CPU, which
is what keeps the overworld's 2D field FX on their ground points.
- The strength scales with the view height, so a rung reads the same at
every zoom, and the ladder is calibrated against the far edge of the
visible ground rather than against nothing.
- `lib/ModSetting.lua`: the ladder/store/rows a setting of this mod's own
needs, now that there are two of them. V-GRID's copy of it moved here.
- A **75 degree** rung on the VOXEL ladder, below the 15/35/50 it shared
with the engine's TILT: low enough to read as a diorama shot from table
height. Tilt could not have it -- its flat plane degenerates into a
horizon line down there -- but geometry only gets more of itself to show.
- The **V-GRID** row in OPTIONS: a one-display-pixel wireframe along every
voxel edge, 3D Dot Game Heroes style. Every mesh here is built one unit
per voxel in its OWN model space, so the seams are that space's integer
planes, and reading them in model space rather than world space is what
keeps them glued to a thing however it is posed -- a character's slab
leans back by the camera's pitch and its seams lean with it.
- The seams fade out where a voxel shrinks under about 3 display pixels.
Survey zoom draws a world pixel at roughly a display pixel, and a wall
seen nearly edge-on squashes one to nothing at any zoom; drawn anyway,
the lines land closer together than they are wide and the wireframe
stops being a wireframe and becomes a flat 45% dimming of the scene.
- `lib/VoxelGrid.lua` owns the toggle. It is NOT a pipeline: it owns no
pass of the frame, it parameterises the voxel one, so it has nothing to
put in `drawWorld` or `present` and the registry rightly rejects it. A
plain mod setting instead -- `options:define` for the store and the mod
manager's page, `ui.options.rows` for the row in OPTIONS next to VOXEL
and T-SHIFT. Both rows read and write the one stored value.
- The wireframe is a SECOND COMPILATION of the scene shader rather than a
branch inside it, because it needs shader derivatives (`fwidth`) -- the
one part of the mode a driver can refuse. A refusal costs the grid and
nothing else.
- `mods/DRAMATIC_SHAPE/tests/voxel_shadow_probe.lua`: reports the fitted
frustum and the resolution rung, dumps the map itself, and shoots a
stand point at every pitch. `SHADOW_SUN="kx,kz"` retunes the bearing for
one run, `SHADOW_GRID=1` forces the wireframe on, and `SHADOW_ZOOM` pins
the zoom, without which two runs are not comparable -- a driver inherits
whatever the player left in `options.lua`, and the world view size (which
the light frustum is fitted to) swings 3x across that range.
- A `counter` class (8px, upright): half-cell furniture. One 8px band,
so exactly the drawing's bottom row stands up as the front and every
row above it rides the top face in drawn order. `table`'s 12px could
not be retuned for it; the houses share that class.
- `tilesets.POKECENTER` in `data/voxel_heights.lua`. Before it, the
detector merged the wall-touching counters and healing machines into
the wall band and towered them 3-6 blocks, flattened the machines'
near-black screens to void, read the pillar bases, plant pots and
machine bodies as ponds (the $14/$32/$48 stale-cache water fallback),
boxed each plant pair into one hedge cube, and extruded the lounge
seat -- a PERSON is drawn into its tile art -- into a monolith wearing
his face.
- The pins, by shape: the wall band, windows, poster, pillars and the
16px machine bodies are `wall`; the counters (with the nurse's tray)
are `counter` and the PC's desk is `table`; the machine screens and
the PC are `billboard`, standing on the pinned boxes below them; the
potted plants are `prop` standees like every other interior plant.
- The lounge couch with the man sitting on it is a `counter` box: its
bottom row stands up as the couch's front and the cushion and the man
ride the top face, each drawn exactly once.
He cannot be stood upright, and the reason is structural rather than a
tuning question. His skin pixels span two tile rows and stop dead at
the row 9/10 seam; folding two rows upright requires both to share a
class, which makes the box two tiles deep, and a fully folded box
repeats its north row across its whole top face. So every upright
arrangement puts his head on screen two or three times -- as a 16px
seat-back, on the front and twice more on the top; as a 32px bookcase,
a cabinet taller than the room's own walls. Dropping the seat in front
of him to floor level only changes which copy you see. Nor can he be a
standee: the drawing has no floor margin, so all three non-black
shades touch the cluster rim and the mask drains 307 of its 420
interior pixels -- 46% of him even segmented alone, because his skin
is the same light shade as the couch behind him.
- Survey evidence: full before/after passes of VIRIDIAN_POKECENTER at
15/35/50 degrees, plus spot-checks of CELADON_POKECENTER and
CELADON_HOTEL (shared tileset, both inherit correctly) and of
REDS_HOUSE_1F, OAKS_LAB and VIRIDIAN_CITY (unchanged -- the new class
is additive and no other tileset lists it).
### Changed
- Pinned props are segmented the way the art is authored: objects wear a
black outline, so background is the shades touching the cluster's edge
(white floor around a TV, grey tabletop around a vase) flooded in from
the aprons; the outline, its interior, paint whites and anything they
enclose survive -- pixel-perfect cutouts on any surface.
- Indoor structure analysis floods background from all four aprons and
accepts ground contact on any side (outdoors keeps the south-only rule
that protects roofs), so face-on furniture drawings voxelize per pixel
instead of rising as wall-height volumes.
- Profile-pinned standees are 10px deep (detected props stay 6px), so a
deliberate object like a TV keeps a body at shallow camera angles.
- Authored upright boxes fold their artwork up every face (flanks and
back wear the front stack darkened) and top faces keep the drawn
tabletop: face-on rows wear the row above the fold instead of
repeating their front art lying flat, and a run that folded entirely
tops with the furniture row drawn above it (a bookcase's shelf trim).
- Characters no longer ride a pinned stair tile's class height: stairs
are walked through at floor level, fixing the step-up onto thin air in
front of stairwells.
- A voxelized building is as tall as its facade plus its roof slab rather
than as tall as its drawing: the roof rows are DEPTH now, not height, so
Red's house is 36px over a 4x3-cell plot instead of a 48px cube.
- Round trees (the `cylinder` pin: lone canopies and the border tree
wall) stop being lathes -- the sprite wrapped around a 12-segment
column read as exactly that, art smeared on a barrel. Each cell is now
a real voxel hull: the canopy is segmented out of its cell as the
darkest-pixel outline plus everything it encloses (which also drops
the background grass that used to inflate every row to full width, and
the cast shadow under the ball), and each mask row runs its own span's
circular chord in depth -- the front view is the sprite pixel for
pixel, the plan view is the sprite's width profile turned in depth.
Dithered art with no closed outline (the tree wall) falls back to
light-shades-only flooding, per the methodology doc's boundary rule.
Sides de-outline like building extrusions so flanks read as canopy
rather than solid black, and dome caps keep their outline on the rim
while the interior samples the canopy a couple of rows deeper.
- A `post` standee pool, pinned for the overworld's vertical fence-post
cell (tiles 14/85 -- across every map the pair appears only as this
cell). The detector already turns HORIZONTAL fence runs (tile 57) into
per-post standees, but a vertical run of repeated cells trips its
scenery-repetition guard and fell to the volume path as a
fence-textured tower (Viridian's west line, Route 25).
`post` extracts every CELL as its own cluster -- pooled clustering
would stand the whole line up as one drawing-tall slab at one depth --
and classifies pixels the way the detector does (non-white is body)
rather than by the pinned-prop outline rule, which would strip the
posts to black skeletons; at the detector's own 6px depth, pinned and
detected fences look alike.
- Town signs move from the `billboard` pool to a new `signpost` pool: the
same per-pixel standing slab, but 2 voxels thin instead of 10. A sign
is a plate on a stick, and the standee body that keeps a TV from
vanishing at shallow angles read as a solid block of furniture here.
- The ground under a round tree matches the tree's own drawn background
instead of the map's commonest ground tile. The hull's segmentation
already knows which pixels are NOT the tree; those pixels are scored
against every flat ground tile the map places and the closest art
wins, per template -- so border trees drawn over checker grass stand
on checker grass even on a map that is mostly pale path (the old
fallback painted path under every mid-forest tree, which has no flat
neighbour to vote with). The drawn cast shadow stays out of the score:
no ground tile carries a shadow, and its darks would drag every match.
- Mesh builds stream in the background. `ChunkMesher` queues per-map
build jobs and `pump()` -- driven from the pipeline's update -- runs
them inside a few-millisecond frame budget (`lib/BuildBudget.lua`
suspends the build coroutine mid-loop when the slice is spent). The
camera tween holds at flat until the current map's terrain exists, so
toggling voxel mode shows a handful of flat frames instead of a frozen
one; neighbours pop in as they finish. Warp fades prefetch the
destination (the pipeline update ticks while the Transition covers the
screen, with a wider pump slice), so a door exit lands on terrain that
is already built.
- Vertex packing goes through FFI into one native buffer
(`Mesh:setVertices(ByteData)`) instead of a Lua table per vertex --
the headless table path remains for the pure `geometry()` API and its
suite.
- Round-tree hulls are carved once per (tileset, art, ground set) and
kept as stamps -- template plus cell offset, expanded during vertex
packing -- instead of materialized per-cell quad tables. A route's
border forest was ~500 quads x hundreds of cells of retained heap.
- Mesh and analysis caches evict down to the live neighbourhood (current
map + rendered neighbours, plus one set of history so a house
round-trip keeps the town warm). Evicted meshes are released
explicitly. Memory over Pallet -> Mt Moon: was ~2.9GB and monotonic,
now oscillates between ~90 and 200MB.
- `Voxel3D.SHADOW_KX/KZ` are -0.85 / -0.55, from +0.30 / +0.45: the sun
crosses to the southeast and drops from 62 degrees to 45. The bearing
leans WEST of northwest on purpose -- a character is drawn as a slab
leaning away from the camera, which covers the ground due north of its
feet, so a shadow thrown straight up-screen lands entirely underneath
the figure casting it and is never seen.
- `Voxel3D.SHADOW_ALPHA` 0.32 -> 0.40, a quarter darker.
- `FACE_SHADE` east 0.78 -> 0.84 and west 0.78 -> 0.72. The two were equal
because the old sun sat due northwest and they were symmetric about it;
under a southeastern sun east is a lit flank and west a shaded one.
- A character's shadow lookup runs off the UPRIGHT card the sun saw, not
the leaning slab the camera sees (`Voxel3D.draw`'s `sunModel`). Casting
the leaning slab instead would shrink every shadow to nothing as the
camera flattened toward top-down; looking up with the leaned position
put each sprite's own card across its front.
- The contact-shadow term in `ChunkMesher` was a one-directional stripe
keyed to a northwestern sun -- two neighbours, one corner, top faces
only. It is now the ambient occlusion above.
- The light frustum is fitted to the ground the CAMERA CAN SEE rather than
to a view-sized box around the focus, and both of its margins are now
asymmetric -- for opposite reasons. The camera sits south of its focus
and looks north, so the ground it sees runs far north and barely south;
the sun sits southeast, so the casters for that ground stand south and
east of it. Paying for a view-sized box plus caster margin on all four
sides covered about a third of what was on screen at 75 degrees, and
overpaid at 15.
- Shadows ease off at the frustum's rim instead of ending on it. Past the
low rungs the horizon is further out than any box worth paying for, and
a covered region that simply stops draws a hard line across the middle
distance where every shadow ends at once.
The Pokemon Center interiors. One `POKECENTER` group in
`data/voxel_heights.lua` plus one new class, and because
VIRIDIAN_POKECENTER places every tile the tileset's other maps use, the
one pin set covers all eleven Centers and the Celadon Hotel.
### Fixed
- The generic town-house tileset (`HOUSE` -- Blue's house, Daisy at her
table, and eighteen more homes, the schoolhouse and the trashed house
among them) is now pinned in `data/voxel_heights.lua` the way Red's
rooms already were: the dining table stops towering as a wall-height
volume and sits at table height with its front folded upright, stools
become seat-high boxes that characters sit on, the corner potted
plants become per-pixel standees instead of texture-smeared box
stacks, the bookcases get clean capped tops, and the wall band (with
its window, picture and the schoolhouse blackboard) stays one 16px
face. The schoolhouse's open book stands on the pinned tabletop as a
cutout, and the trashed house's ransacked table corner keeps table
height.
- Interior door mats lie flat again in Red's and the generic houses.
Their collision tile is $14, which the engine's stale-cache fallback
counts as water in every tileset, so the rug recessed into a pond lip;
a `ground` pin now overrides the water read.
- Stray pixels along map seams: ring props (border-tree hulls) whose
quad CENTER sat exactly on a neighbour body's edge line escaped the
strict point-in-rect mask and survived as fragments of otherwise
dropped trees. Object quads now keep/drop by their full extent,
boundary inclusive; props straddling the body edge also stay whole
instead of shedding their outer half.
- The one-step "ledge hop" when crossing a connection into a tree-ringed
map: the seam step stands the player one cell off the new map, where
`Map:cellTile` border-extends into the borderBlock -- a raised tile on
maps ringed with trees. Off-map ground now reads as height 0 (the
departed neighbour's flat walkway, which is what is actually rendered
there).
- Cycling palette modes with voxel mode on eventually killed the pipeline
outright: `attempt to call field 'atlasImageData' (a nil value) --
disabled for this session`. Nothing brought it back short of a restart.
`TerrainAtlas` reads three engine seams to animate water and flowers in
the terrain texture, and this build ships only one of them
(`defaultAnimatedTiles`). The tile clock, `animFrame`, was already read
guarded and simply degrades. `atlasImageData` was called straight -- but
only down the branch where the mod had NOT baked the atlas itself, which
is why it looked stable until a palette changed. Every mode with no world
palette for the map (`PaletteFX.pal` answering nil), plus RED++ and any
trueColor tileset, takes that branch, so the first map with animated
tiles entered under one of them threw out of `drawWorld` and the engine
disabled the pass for the session, exactly as it should.
The seam is now read guarded like its sibling, and when it is absent the
pixels are recovered rather than given up on. An atlas neither we nor
RED++ replaced is the tileset art itself, so animation carries on from
the art on disk. RED++'s per-map bake exists only as a texture --
`getGbcAtlas` throws its `ImageData` away -- so that one comes back off
the GPU: the atlas is drawn 1:1 into a canvas and read back, once per map,
with the pass's own render target captured and restored around it (the
usual `setCanvas()` would drop the rest of the frame). A driver that
refuses the readback declines to animate and keeps the static atlas.
Worst case now costs one animation, never the pipeline.
- Water and flowers did not animate in voxel mode at all, and had not since
the mode shipped -- a silent one, since the terrain was otherwise correct.
The tile clock is the third seam, and this build does not export it
either. Being read guarded, it answered 0 forever instead of throwing,
which pinned every animated tile at step 0. `animFrame` is a plain local
in `TileRenderer`, but an upvalue of the exported `tick()`, so the mod now
reads the real counter through it. That it is the ENGINE's counter is the
point: the flat tile layer draws from the same number, so toggling voxel
mode mid-cycle continues the animation rather than restarting it. A build
that exports `animFrame()` outright is preferred; a build that hides the
local falls back to wall time in 60Hz steps, which free-runs against the
2D path but still moves the water.
- Toggling palettes in voxel mode flashed the flat 2D world for a moment on
every switch.
`PaletteFX.setMode` reloads the live map to rebuild its atlas, and this
mod dropped that map's terrain mesh on any `map.reloaded` at all. Mesh
builds are asynchronous, so the frames between the drop and the first
rebuilt mesh had no terrain to draw -- and a voxel `drawWorld` with no
terrain returns nil, which is exactly how the pipeline asks for the 2D
fallback. The flash was the mod correctly reporting that it had nothing
to show.
The geometry was never stale: the mesher reads block layout and tile ids
and never reads colour, and the palette lives entirely in the texture
`TerrainAtlas` hands back per frame, keyed by palette and so already
rebuilt by the next frame. A reload whose reason is `colors` now keeps
the mesh, and the new palette lands on the diorama already on screen in
one frame. Every other reload -- warps re-entering a map, hot reload, a
replaced block -- still drops it.
- Every non-colour palette mode rendered as SGB in voxel mode: GRAY and
both INVERTED modes came through as the map's blue.
`paletteFor` hands a pipeline the map's RAW SGB zone palette. The flat
path runs that through `PaletteFX.effectiveColors` on its way to the
shade-remap shader, and that call is where the non-colour modes actually
happen -- OG and OG INV swap in the DMG greys (reversed for the latter),
CLASSIC swaps in the green set, GBC INV permutes the zone's own shades,
and only GBC and RED++ pass through. This pass bakes colour into the
atlas and the sprite sheets ahead of the draw rather than shading at blit
time, so it never reached that call and painted the raw zone palette in
every mode.
Both bakes now run the same transform the shader would have. Terrain and
characters go through one resolve, so they cannot disagree about what
mode is on.
- VOID FILL did nothing in voxel mode, in two separate ways.
**BLACK crashed the build.** The mode is not a block at all --
`TileRenderer.borderBlockFor` answers `false` for it -- and `Structures`
added 1 to that `false`. The arithmetic threw, which failed the mesh
build for every map in the neighbourhood, which left the mode with no
terrain and dropped it to the flat 2D path entirely. It now builds no
ring: `tileLookup` answers nil past the body and those keys are never
written, which the rest of the file already copes with -- every
neighbour query in it reaches one step outside the analysed range and
reads nil for its trouble, so an absent cell is the shape "nothing" has
always had here.
**WATER changed nothing on screen.** The ring is BAKED INTO THE MESH in
this mode rather than drawn each frame, and nothing dropped the cache
when the option moved, so the old ring simply stayed until the meshes
were invalidated for some other reason. The pipeline's update hook now
polls `TileRenderer.voidFill` and invalidates on a change -- polled
rather than hooked because the engine changes it from three places (the
options row, `applyOptions` on load, `setVoidFill`) and none of them
announces it, and checked ahead of the active() gate so switching it
while voxel mode is off still drops what is cached.
`mods/DRAMATIC_SHAPE/tests/voxel_void_probe.lua` walks the three modes
and reports the border block, whether the mesh built and whether the
scene took the 3D path. It deliberately does NOT invalidate the cache
itself, since doing so would hide the second half of this.
- Water and flowers did not animate. The 2D path animates them by
OVERDRAWING the animated cells on top of the static tile layer each
frame, which a single static mesh has no equivalent of -- the geometry
samples one texture and that is that. So `TerrainAtlas` animates the
texture instead: a private copy of the atlas whose animated tile slots
are rewritten when the step advances, which moves every instance of that
tile across the whole mesh at once. Which is what the Game Boy does in
the first place (`home/vcopy.asm` rewrites the tile's VRAM bytes); the
overdraw is the port's workaround for a tile layer, not the original.
~130 pixels of work three times a second, on the same
`TileRenderer.animFrame` clock the 2D path uses, so the two can never
disagree about which frame they are on.
- The frame files (`flower1..3.png`) are raw grayscale and have to land on
the colours of the tile they replace, but the two recolour paths do not
share a rule -- SGB bakes one world palette over everything, RED++ picks
a palette group per tile graphic. So the shade mapping is LEARNED from
the atlas: read the static tile's slot in the raw art and in the finished
atlas side by side and ask what each shade became. Right under both
without this file knowing which one ran.
- Terrain art was off the pixel grid by up to half a pixel, with one art
pixel per tile sampled twice and another never at all. A tile is 8
texels across 8 world pixels -- one texel per pixel exactly -- and
`ChunkMesher`'s uv inset squeezed that art into a 7-texel sample range
while the quad still covered 8 world pixels, so it advanced 7/8 of a
texel per pixel and drifted. The inset exists to stop the rasteriser
reaching a neighbouring tile along a shared edge, but half a texel was
fifty times more than that needs: 0.02 is as safe (interpolation error
is nowhere near it) and drifts 0.25% of a pixel across a whole tile.
Nothing showed the fault until the voxel wireframe drew the grid those
pixels were supposed to be sitting on.
### Changed from the pre-mod version
- The level is no longer the mod's to keep. The engine owns the ladder, the
options rows, the hotkeys, persistence and the TILT exclusion; the mod
keeps only the camera-angle tween.
- Persistence moved from `save.options.voxel` / `save.options.tiltshift` to
`save.options.pipelines.voxel` / `.tiltshift`.
- Hotkeys moved from `4`/`9` to `3`/`6`: the fork already uses `4` for
survey zoom.
- The tilt-shift pass is a declared `worldPresent` stage rather than a call
spliced into the world draw, so it composes with any world pipeline
instead of only this one.
- The cut-tree animation now draws in voxel mode; the pre-mod version
omitted it from the 3D field-effect list.