The texel transform is wrong for palettes, and silently so. A lookup table
answers only the colours it contains -- the ones its MODEL is painted with --
and the engine's ADVANCED palettes are a different set entirely: BLUEMON's
blue is not any blue on the Gyarados model. Asked to shift a palette, the
table returned it unchanged, so the five table species produced no sprite
shift at all and the most dramatic shiny in the game came out identical.
paletteTransform picks the right tool per species: the slide where there is
one, the tint multiplier (which IS derived from the table) where there is
not. 149 of 151 palettes now move; the two that do not are Jigglypuff and
Wigglytuff, whose shiny genuinely leaves the body almost where it was.
Plus the two tools that make the comparison sheet. Worth saying why it is a
palette job at all: Gen 1 battle pics carry no colour -- they are four-shade
DMG grey, and every bit of colour is the palette laid over them. So a shiny
sprite is the same pixels under a shifted palette, and a sheet built any
other way would be showing something the game never draws.
Sheet at .claude/shiny_update/7_sprites_all151.png, every species beside its
own control.
Four things, all found by shooting the feature rather than by reading it.
The flat-pic tint was a no-op for the five colour-table species. tintFor ran
synthetic reference colours through the lookup, none of them were IN it, so
every ratio came back 1 and the tint was discarded -- a shiny Gyarados drew
an ordinary blue pic. Those species now measure the tint from the table's own
entries.
It was also a no-op for most SLIDE species, for a better-hidden reason: a hue
rotation moves red toward cyan and cyan toward red, so averaged over a
balanced set of references the ratios cancel and every species reports no
tint. Charizard and Ponyta both came back neutral. The tint is now measured
against the colour each species is actually MADE of -- a modal body colour,
generated into data/shiny_colors.lua as `dom`. 132 of 151 now carry a usable
tint; the rest genuinely shift too little for one to mean anything.
The sparkle drew sixty quads a frame that nobody could see. It was built to
numbers a tenth of the scale of a mon card -- a ring seven units across,
inside a Gyarados -- and additive drawing keeps the depth test, so all of it
was rejected. Sized against the card now, and pulled toward the camera the
way the move-animation card is.
And the summary PIC cannot be recoloured by touching pixels: the art is
four-shade DMG grey and the colour is applied afterwards by the palette pass.
That attempt is reverted, with the reason left where the next person will
look for it. The star is the designation that works there.
Also: tools/shiny_colors.py now resolves its own paths instead of hardcoding
a worktree, and the extract test gained a hard failure when NOTHING
recolours -- which is what a missing colour table looks like from the
outside, and it passed through it once already.
Evidence in .claude/shiny_update, every case beside its own control.
Shiny Pokemon: derived from DVs via the engine's own Stats.isShiny, models
recoloured as part of the Stadium extraction, flat art tinted, a sparkle on
arrival and a star on the status page.
Verified on the merged tree rather than only on the branch, since
shadow-toggle moved underneath it (the new menus work): the mod loads clean,
tests/shiny_test passes 49, and the extraction still reproduces all 151
packs byte-for-byte against the Python oracle with 151 shiny variants
alongside.
ShinyPalette loaded data/shiny_colors.lua by guessing cwd-relative paths
when V.data was absent, which is the headless case. Run from the project
root, none of the guesses hit -- so the extraction built all 151 species,
recoloured none of them, and reported PASS. The packs were correct; there
simply were no shiny variants in them, and nothing in the output said so.
Two fixes, because either alone leaves the trap open:
V.path is now consulted first, so the file is found relative to the MOD
the way every other resource is. The extract test's stub sets it to
whatever --mod it was given.
A count of zero recoloured is now a failure. That is precisely what a
missing colour table looks like from the outside, and a test that passes
on it is not testing the feature. It was caught by noticing the number
change between two runs that differed only in where they were started
from -- which is too thin a thread to hang it on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Gen 1 has no shininess of its own, but it has the four DVs Gen 2 reads to
decide it -- and the engine already ships that reading (Stats.isShiny, its
own comment calling it "the RBY virtual shiny", allowlisted for mods
precisely so an indicator mod can call it). Nothing new is stored on a
Pokemon and nothing migrates: every save already contains the answer, and
this starts drawing it. Random DVs land on the pattern 1 in 8192, which is
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 with no second copy of the truth to drift. mon.shiny
is a cache written from the DVs, never read as the source.
The roll goes in Pokemon.new -- every wild, gift, starter and traded mon is
built there, and it is before the battle bakes its sprite, which
battle.started is already too late for. It draws from the mod's own random
stream so installing this does not shift the sequence damage rolls and
encounter slots come out of. Trainers stay ordinary by themselves: the
engine pins their DVs, as the real games do.
The models are genuinely recoloured, as part of the extraction. Each
species is decoded once, packed as usual, then recoloured and packed again
as NNNs.dsm. The colours are Stadium's own HSL slide (hue in degrees,
saturation and lightness on a -8..+8 scale at 12.5% a step); five species
carry an explicit colour table instead, because Stadium gives them a real
alternate texture that no single slide reproduces -- Jigglypuff's body must
stay pink while its irises rotate to green.
Extraction is the right moment because StadiumFx's generated frames are
still marked there and the packer drops the marker: it is the last point a
flame is distinguishable from a hide. A shiny Charizard has a shiny hide
and an ordinary fire. The normal packs are written BEFORE the recolour, so
they come out byte-identical and stadium_extract_test still diffs all 151
against the Python oracle unchanged -- no format change, no DSM4, no second
implementation to keep in step. REV goes to 3 so an existing cache rebuilds.
Flat art is tinted instead, because the engine bakes a species palette into
a cache with no notion of which individual is drawn. The tint comes from
that species' own slide rather than a generic gold. A multiply can only
darken, so species whose shiny is lighter read quieter there than on the
model; the status page's star is the mode-proof mark.
Tests: 58 assertions in tests/shiny_test.lua, including the colour
transform against 640 real colour pairs lifted from the verified texture
set, the DV model, the read side, and the end-to-end through the engine's
own constructor. stadium_extract_test gains --mod (worktrees have neither
the ROM nor the packs, both gitignored) and now also checks that every
shiny pack is the same length as its twin and actually differs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The hip ends of the gabled house and Oak's lab come out as black teeth
marching down the slope instead of battens running with it.
The depth->row map is authored for columns the drawing paints edge to
edge. A tapered column starts further down the band, and the surface was
clamped into its first DRAWN row to stay inside the silhouette -- but a
column's first drawn row is its silhouette CAP, and the cap is outline
black. On a tapered column the map spends most of the roof's depth above
that cap (seven of the gabled house's fifty depth voxels land on it
outright), so the clamp painted one outline pixel the length of the
slope and roofCycle beat against it: black punctuated by the real batten
colour every 4 rows on the house, every 8 on the lab.
Lift to the column's first PAINTED row instead -- the same refusal to
let outline black stand as a face that measure() already makes for the
side faces, which the roof band was never given.
Geometry is untouched: voxel, shell and recess counts are identical for
all 50 templates. verify_roof now asserts no roof surface samples its
column's cap, and tests/roof_cap_shots.lua shoots the two Pallet Town
drawings A/B.
The version bump and CHANGELOG entry are left out deliberately, so this
does not collide with the release flow.