Files
DramaticShapeVoxelMod/CHANGELOG.md
T
DramaticShape b8b2e0cd33 shiny capture
2026-08-08 14:02:29 -04:00

220 KiB

Changelog

Unreleased

Added

  • Fireflies over tall grass, on every outdoor map. Viridian Forest has had them since 1.6.2 -- blinking green motes low in the air, dealt over the forest's whole volume as part of its atmosphere. They now come out over TALL GRASS anywhere the hour reaches: a route at midnight has lights loose over its grass and none over the road beside it.

    The same particle, moved rather than rebuilt. Same mesh format, same blinking shader, same one ramp off the day/night clock -- so they come on through dusk and reach full contrast at deepest night exactly when the forest's do, because it is the same function answering (pulled out of ForestAtmos.frame as fireflyLevel so the two cannot drift apart). What changed is where they are dealt.

    Grass is the entry. data/map_atmosphere.lua is still the opt-in for fog and god rays, and still names one map; the grass fireflies need no line in it. Each map is scanned once for the cells the engine's own isGrassCell calls tall grass -- the collision tile, the rule that decides where a wild battle starts, and the same test Structures passes before it sprouts a tuft. Going by the grass GRAPHIC instead would hang lights over town plazas, where the same art turns up as decorative filler inside ordinary ground blocks. Roughly four fireflies to five grass cells, capped at 200 a map; an atmosphere entry may tune both through a grassFlies row. A map with no grass on it deals none and costs one scan.

    They are dealt INTO a cell rather than at its middle, and the shader's existing wander carries each about a cell's width from where it was dealt, in the tufts' own height band -- so a patch reads as a patch with fireflies loose over it rather than as a grid of lights. The deal is seeded off the map id, so a route's swarm is the same arrangement on every visit and no two routes get the same one.

    The connected neighbours get theirs too. A neighbour map is drawn in full -- ground, trees, grass and the firefly cards standing in it -- so a swarm that stopped at the map seam would draw a line across the picture where the lights ran out. The particle shader takes a map origin now and each neighbour's own cached mesh is drawn where its terrain is. Nothing else in the atmosphere crosses a seam, and nothing else needed to: fog and beams belong to the map you are standing in.

    This is the FULL rung of FOREST FX, like every particle in that pass. The per-tuft firefly cards in the grass mesh are the layer underneath and are unchanged -- static geometry the scene shader already carries, so grass still has something alight after dark on LOW and on Android, where this pass is not drawn at all.

    One fix falls out of it: a machine that cannot hand back a readable depth buffer used to lose the whole atmosphere pass, particles included, rather than just the beams it actually blocks. The refusal now takes the beams alone.

  • 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.

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, 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.