shiny Pokemon, on by default

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>
This commit is contained in:
DramaticShape
2026-08-08 12:12:25 -04:00
parent 121c87b629
commit a3fb18a589
19 changed files with 3773 additions and 34 deletions
+643
View File
@@ -0,0 +1,643 @@
-- generated fixture: {dex, srcR,srcG,srcB, wantR,wantG,wantB}
return {
{6,222,131,123,160,165,185},
{6,189,115,41,105,96,134},
{6,16,16,8,12,11,13},
{6,115,57,24,60,58,81},
{6,230,139,57,131,122,165},
{6,213,164,74,139,126,161},
{6,41,32,24,31,30,35},
{6,8,49,57,39,37,26},
{6,82,41,8,39,36,54},
{6,246,172,74,152,138,182},
{6,172,98,32,92,84,120},
{6,65,49,24,43,39,50},
{6,213,131,123,157,161,179},
{6,156,156,148,152,151,153},
{6,255,246,156,209,193,218},
{6,205,164,74,137,123,156},
{6,24,90,90,65,61,49},
{6,172,98,24,88,80,116},
{6,123,24,24,61,68,86},
{6,16,90,82,62,55,44},
{6,230,148,65,137,127,168},
{6,139,74,24,72,67,96},
{6,156,90,41,89,84,113},
{6,123,131,131,128,127,126},
{6,246,164,90,157,148,188},
{6,222,139,57,128,119,160},
{6,98,57,16,52,47,67},
{6,32,131,131,94,87,69},
{6,148,90,32,82,76,105},
{6,197,123,115,146,149,166},
{6,222,106,106,150,157,178},
{6,238,222,106,176,156,188},
{6,32,41,32,38,35,36},
{6,32,90,90,68,64,54},
{6,32,49,49,43,41,38},
{6,82,16,16,41,45,57},
{6,189,189,189,189,189,189},
{6,148,90,41,86,81,108},
{6,8,16,24,17,18,14},
{6,90,98,139,114,121,108},
{42,123,41,115,16,15,26},
{42,90,131,148,26,33,28},
{42,90,156,189,27,42,32},
{42,57,8,57,5,5,11},
{42,123,106,197,30,44,46},
{42,115,148,222,29,55,46},
{42,98,57,98,17,17,22},
{42,49,65,74,14,17,15},
{42,197,189,205,46,50,53},
{42,32,164,230,20,46,27},
{42,41,205,246,19,52,24},
{42,74,131,164,24,35,28},
{42,90,98,189,27,42,40},
{42,41,156,197,20,40,24},
{42,98,90,180,27,40,40},
{42,24,115,164,15,32,20},
{42,164,189,205,39,53,44},
{42,164,106,180,30,33,42},
{42,74,148,213,25,47,34},
{42,82,139,156,25,34,27},
{42,90,180,230,25,55,34},
{42,74,115,139,23,31,25},
{42,49,189,230,21,49,26},
{42,65,82,98,18,22,20},
{42,172,115,189,31,35,45},
{42,49,123,189,21,39,28},
{42,172,197,197,42,50,42},
{42,189,74,164,28,25,41},
{42,65,57,123,18,26,27},
{42,49,16,49,6,6,10},
{42,32,57,74,11,16,12},
{42,65,156,222,23,49,33},
{42,106,98,189,29,43,43},
{42,222,213,222,51,51,58},
{42,172,197,205,41,53,44},
{42,238,238,238,60,60,60},
{42,189,197,205,46,53,49},
{42,41,8,49,5,6,10},
{42,49,0,49,3,3,9},
{42,205,213,213,50,55,50},
{79,139,82,57,28,18,31},
{79,90,74,57,21,16,21},
{79,246,106,74,48,17,63},
{79,222,131,123,46,27,59},
{79,156,131,82,36,24,34},
{79,213,90,90,38,24,52},
{79,222,197,148,62,31,57},
{79,230,205,148,66,29,58},
{79,213,189,156,58,34,56},
{79,197,189,139,51,33,44},
{79,180,106,90,37,26,42},
{79,230,115,115,43,24,62},
{79,148,82,82,29,24,34},
{79,246,156,156,50,24,76},
{79,24,8,8,4,3,5},
{79,131,65,41,25,14,29},
{79,213,98,82,40,23,51},
{79,49,32,8,10,4,10},
{79,123,74,49,25,16,27},
{79,57,32,24,11,8,13},
{79,180,57,65,28,20,39},
{79,164,82,57,32,19,36},
{79,148,106,82,32,24,34},
{79,230,123,106,47,23,61},
{79,238,115,98,47,21,63},
{79,246,115,98,49,19,67},
{79,238,139,131,49,24,68},
{79,139,82,49,29,16,31},
{79,246,131,106,53,20,68},
{79,197,98,74,38,23,45},
{79,213,106,98,41,25,53},
{79,172,98,90,34,26,40},
{79,197,98,82,38,24,46},
{79,123,57,32,23,12,26},
{79,189,189,180,48,44,46},
{79,213,213,213,53,53,53},
{79,115,106,82,27,22,26},
{79,74,32,32,13,10,17},
{79,148,74,57,28,19,33},
{79,238,246,238,72,60,49},
{146,172,131,0,231,152,197},
{146,205,82,0,234,162,229},
{146,57,57,156,183,215,205},
{146,106,24,0,216,132,227},
{146,24,24,32,162,177,172},
{146,74,65,123,183,206,202},
{146,213,172,82,230,199,219},
{146,205,90,16,230,172,226},
{146,246,197,8,235,179,209},
{146,41,24,82,153,205,203},
{146,255,230,115,245,212,229},
{146,246,213,41,237,189,213},
{146,74,90,131,188,208,196},
{146,189,189,189,230,230,230},
{146,115,74,0,227,135,199},
{146,246,123,0,236,175,226},
{146,197,131,8,231,165,210},
{146,180,123,0,232,154,205},
{146,106,115,65,204,182,186},
{146,98,32,0,225,129,226},
{146,255,180,0,237,177,215},
{146,205,156,16,230,172,206},
{146,197,156,74,225,196,215},
{146,74,57,148,182,214,209},
{146,255,123,0,237,177,228},
{146,115,57,0,227,135,212},
{146,246,255,222,253,245,245},
{146,106,65,0,227,132,200},
{146,98,16,0,210,129,226},
{146,230,189,16,232,179,207},
{146,255,230,41,240,190,212},
{146,255,246,32,239,187,207},
{146,213,156,41,227,187,214},
{146,222,123,0,235,167,220},
{146,164,98,0,231,150,209},
{146,148,49,8,223,151,226},
{146,90,65,0,225,127,187},
{146,255,213,16,238,182,211},
{146,24,32,49,156,191,168},
{146,255,180,16,238,182,218},
{96,131,82,57,175,159,190},
{96,32,32,8,173,102,161},
{96,197,156,0,213,138,216},
{96,148,115,41,191,152,197},
{96,57,57,41,160,144,158},
{96,131,115,8,203,121,200},
{96,65,57,16,180,115,180},
{96,148,57,57,167,162,196},
{96,131,32,49,142,142,195},
{96,139,98,32,185,144,197},
{96,123,82,49,175,152,189},
{96,213,213,213,234,234,234},
{96,139,90,65,179,165,192},
{96,131,90,57,178,159,190},
{96,164,148,98,200,185,201},
{96,139,98,8,193,124,205},
{96,106,98,49,183,150,182},
{96,115,90,24,186,132,193},
{96,115,98,32,188,139,190},
{96,57,49,24,166,126,170},
{96,172,131,8,204,135,210},
{96,65,57,49,158,149,163},
{96,106,98,82,178,169,180},
{96,49,49,32,159,136,155},
{96,180,139,8,206,138,211},
{96,131,123,90,189,176,189},
{96,156,148,123,201,193,201},
{96,106,90,8,199,113,198},
{96,148,115,24,195,140,201},
{96,115,115,123,186,188,186},
{96,106,65,57,164,155,181},
{96,255,197,0,219,159,223},
{96,230,222,205,238,233,239},
{96,115,98,57,181,156,185},
{96,180,172,131,211,199,211},
{96,148,123,57,192,162,196},
{96,230,180,0,216,150,220},
{96,156,115,0,202,122,211},
{96,172,123,32,195,152,205},
{96,123,82,57,174,157,188},
{140,49,49,49,101,101,101},
{140,255,8,131,174,155,151},
{140,139,82,32,135,138,118},
{140,123,0,8,124,113,96},
{140,230,24,24,169,162,149},
{140,32,24,8,81,85,73},
{140,230,156,156,212,210,205},
{140,82,0,0,106,98,83},
{140,131,82,24,128,132,111},
{140,74,0,0,103,95,80},
{140,222,172,172,214,212,209},
{140,82,74,82,123,121,122},
{140,197,106,106,182,179,173},
{140,246,49,74,184,175,165},
{140,255,16,123,177,159,154},
{140,156,24,57,143,129,120},
{140,156,82,8,135,139,111},
{140,255,131,156,214,208,203},
{140,222,213,205,224,225,223},
{140,82,49,16,106,109,92},
{140,205,172,172,207,206,204},
{140,230,172,172,217,215,212},
{140,230,82,115,188,180,174},
{140,255,82,189,198,183,182},
{140,222,156,90,185,187,175},
{140,180,98,16,145,150,125},
{140,238,115,123,202,197,190},
{140,65,8,16,100,92,82},
{140,172,74,82,161,157,151},
{140,255,172,197,228,223,220},
{140,197,57,65,166,160,152},
{140,205,131,49,164,166,152},
{140,57,57,57,106,106,106},
{140,131,32,49,134,125,115},
{140,57,32,32,101,98,94},
{140,123,8,41,125,110,100},
{140,246,32,82,178,167,158},
{140,164,98,32,144,147,127},
{140,255,0,90,171,155,147},
{140,255,106,172,206,195,192},
{23,139,106,16,46,41,75},
{23,172,131,24,58,53,94},
{23,106,8,24,29,50,57},
{23,246,197,49,85,73,149},
{23,255,213,41,88,69,153},
{23,238,180,16,74,64,126},
{23,222,123,131,101,139,158},
{23,246,213,82,99,81,165},
{23,156,106,172,93,115,97},
{23,90,16,24,29,44,50},
{23,164,90,98,85,99,106},
{23,213,164,41,77,71,119},
{23,115,74,98,65,77,75},
{23,230,189,106,98,92,160},
{23,115,65,123,62,79,67},
{23,222,172,74,87,82,140},
{23,213,172,90,93,88,139},
{23,255,213,57,91,73,161},
{23,156,115,164,96,113,101},
{23,222,156,8,63,56,116},
{23,123,82,123,71,83,76},
{23,98,49,115,52,71,55},
{23,106,74,65,58,63,70},
{23,180,180,180,135,135,135},
{23,115,49,90,52,71,67},
{23,98,57,90,52,64,59},
{23,180,123,180,102,125,112},
{23,90,65,41,42,44,56},
{23,238,172,24,73,66,130},
{23,222,164,8,65,56,116},
{23,131,106,41,55,52,77},
{23,180,139,164,110,129,126},
{23,32,8,24,12,18,17},
{23,213,164,24,71,62,115},
{23,131,74,148,73,94,77},
{23,65,0,16,15,30,34},
{23,139,106,49,59,58,83},
{23,189,139,24,62,57,103},
{23,131,65,74,64,78,83},
{23,65,49,0,18,15,34},
{114,205,197,197,197,197,205},
{114,180,180,189,180,189,180},
{114,123,123,230,123,230,123},
{114,205,205,255,205,255,205},
{114,189,49,74,49,74,189},
{114,222,213,222,213,222,222},
{114,82,82,205,82,205,82},
{114,131,131,238,131,238,131},
{114,82,82,164,82,164,82},
{114,230,230,255,230,255,230},
{114,41,41,32,41,32,41},
{114,148,32,57,32,57,148},
{114,65,65,90,65,90,65},
{114,172,172,180,172,180,172},
{114,123,123,213,123,213,123},
{114,238,230,238,230,238,238},
{114,41,41,49,41,49,41},
{114,238,238,238,238,238,238},
{114,205,197,205,197,205,205},
{114,230,230,222,230,222,230},
{114,90,90,90,90,90,90},
{114,255,255,238,255,238,255},
{114,106,106,238,106,238,106},
{114,246,106,131,106,131,246},
{114,49,49,123,49,123,49},
{114,16,16,41,16,41,16},
{114,255,255,246,255,246,255},
{114,8,8,16,8,16,8},
{114,205,82,106,82,106,205},
{114,205,205,213,205,213,205},
{114,32,32,24,32,24,32},
{114,41,49,106,49,106,41},
{114,41,41,98,41,98,41},
{114,123,123,148,123,148,123},
{114,156,148,205,148,205,156},
{114,156,41,57,41,57,156},
{114,222,90,106,90,106,222},
{114,57,57,65,57,65,57},
{114,123,123,205,123,205,123},
{114,82,74,82,74,82,82},
{134,197,205,213,212,197,213},
{134,205,222,230,224,205,230},
{134,213,222,238,238,213,236},
{134,32,90,90,58,32,90},
{134,164,189,189,175,164,189},
{134,65,90,131,131,65,126},
{134,32,139,172,128,32,172},
{134,156,172,189,188,156,189},
{134,255,246,148,148,255,187},
{134,0,8,16,15,0,16},
{134,24,41,82,82,24,73},
{134,57,148,180,144,57,180},
{134,123,197,197,156,123,197},
{134,148,197,205,182,148,205},
{134,24,8,0,5,24,0},
{134,65,90,148,148,65,136},
{134,164,213,222,199,164,222},
{134,41,74,123,123,41,119},
{134,8,16,65,65,8,47},
{134,213,222,156,156,222,195},
{134,74,57,32,32,74,34},
{134,65,106,148,144,65,148},
{134,24,41,98,98,24,82},
{134,115,123,123,119,115,123},
{134,49,90,98,79,49,98},
{134,131,172,197,186,131,197},
{134,0,24,16,3,0,24},
{134,32,82,98,78,32,98},
{134,156,180,205,203,156,205},
{134,148,213,222,190,148,222},
{134,139,197,205,177,139,205},
{134,32,49,98,98,32,85},
{134,82,98,148,148,82,134},
{134,41,49,115,115,41,90},
{134,222,230,230,226,222,230},
{134,106,106,115,115,106,111},
{134,32,57,106,106,32,98},
{134,32,106,148,126,32,148},
{134,32,90,131,118,32,131},
{134,41,90,139,134,41,139},
{143,82,106,90,105,123,120},
{143,148,115,98,154,149,125},
{143,230,222,213,230,230,221},
{143,139,65,65,149,121,94},
{143,16,0,8,63,15,15},
{143,213,180,123,197,203,154},
{143,189,90,90,181,154,127},
{143,115,57,41,130,113,70},
{143,57,57,57,82,82,82},
{143,172,139,90,165,169,124},
{143,189,156,123,186,186,150},
{143,222,222,222,226,226,226},
{143,255,230,197,243,245,214},
{143,230,230,230,233,233,233},
{143,197,197,180,197,201,192},
{143,222,205,123,192,210,156},
{143,238,230,180,219,231,199},
{143,90,90,65,100,110,90},
{143,213,197,123,188,203,154},
{143,205,197,123,180,198,153},
{143,90,82,57,103,110,83},
{143,205,180,139,196,200,164},
{143,213,213,189,208,214,201},
{143,115,65,65,130,111,91},
{143,164,131,74,157,164,108},
{143,213,197,156,202,209,178},
{143,123,106,65,127,136,92},
{143,180,148,148,184,175,167},
{143,205,197,156,194,203,176},
{143,123,82,74,136,124,100},
{143,24,8,8,60,46,32},
{143,255,238,189,235,244,208},
{143,230,222,205,227,229,215},
{143,213,180,131,200,205,160},
{143,131,65,65,142,118,93},
{143,222,189,139,208,213,167},
{143,24,16,8,60,60,32},
{143,205,189,156,199,203,176},
{143,156,148,98,147,159,127},
{143,164,131,98,165,165,128},
{95,57,49,65,34,41,31},
{95,0,197,230,144,42,0},
{95,197,197,238,181,201,71},
{95,0,16,65,41,37,0},
{95,148,156,156,99,92,91},
{95,238,238,230,132,126,167},
{95,98,98,98,61,61,61},
{95,148,148,156,98,99,91},
{95,16,32,156,94,98,10},
{95,0,49,106,66,46,0},
{95,0,123,197,123,65,0},
{95,0,180,238,149,59,0},
{95,0,8,16,10,7,0},
{95,139,148,164,106,101,83},
{95,65,74,82,51,47,41},
{95,189,197,197,128,115,113},
{95,0,90,197,123,85,0},
{95,106,115,131,82,79,66},
{95,0,139,205,128,60,0},
{95,131,123,148,85,94,76},
{95,0,24,49,31,20,0},
{95,0,8,98,57,61,0},
{95,98,98,115,70,72,61},
{95,139,139,139,87,87,87},
{95,49,49,57,35,36,31},
{95,0,74,131,82,48,0},
{95,115,106,131,74,82,66},
{95,0,115,197,123,70,0},
{95,156,148,180,106,121,84},
{95,123,123,139,86,87,77},
{95,0,41,156,98,86,0},
{95,0,106,172,108,57,0},
{95,16,57,156,98,85,10},
{95,0,115,148,93,35,0},
{95,205,205,230,164,176,96},
{95,131,139,156,100,96,80},
{95,189,197,205,140,128,106},
{95,98,106,131,82,80,61},
{95,213,213,246,194,216,71},
{95,24,32,32,20,16,15},
{35,180,74,65,180,65,123},
{35,197,82,82,197,82,140},
{35,172,131,131,172,131,152},
{35,65,41,41,65,41,53},
{35,213,230,230,213,230,230},
{35,172,74,65,172,65,119},
{35,148,164,164,148,164,164},
{35,148,106,98,148,98,123},
{35,90,65,57,90,57,74},
{35,57,57,156,57,57,156},
{35,222,164,164,222,164,193},
{35,255,222,222,255,222,238},
{35,8,8,0,1,8,0},
{35,148,148,156,148,148,156},
{35,49,41,16,15,52,10},
{35,222,246,238,222,246,238},
{35,148,82,90,148,82,115},
{35,57,8,8,57,8,33},
{35,131,131,131,131,131,131},
{35,255,189,197,255,189,222},
{35,205,90,82,205,82,144},
{35,230,197,197,230,197,214},
{35,238,139,139,238,139,189},
{35,180,115,115,180,115,148},
{35,65,57,57,65,57,57},
{35,172,139,131,172,131,152},
{35,131,32,24,131,24,78},
{35,65,49,8,9,69,0},
{35,41,24,8,8,44,2},
{35,180,139,123,180,123,152},
{35,98,123,123,98,123,123},
{35,41,41,115,41,41,115},
{35,106,16,16,106,16,61},
{35,106,131,131,106,131,131},
{35,16,16,24,16,16,24},
{35,156,139,139,156,139,139},
{35,123,32,24,123,24,74},
{35,49,24,24,49,24,37},
{35,16,16,41,16,16,41},
{35,189,164,172,189,164,176},
{36,230,148,172,230,148,189},
{36,255,148,164,255,148,202},
{36,156,123,123,156,123,123},
{36,189,156,148,189,148,169},
{36,98,41,24,98,24,61},
{36,74,0,0,74,0,37},
{36,148,82,74,148,74,111},
{36,65,49,49,65,49,49},
{36,238,197,180,238,180,209},
{36,230,180,172,230,172,201},
{36,24,16,16,24,16,20},
{36,246,148,164,246,148,197},
{36,180,156,156,180,156,156},
{36,49,49,49,49,49,49},
{36,222,189,189,222,189,206},
{36,98,90,90,98,90,90},
{36,148,131,123,148,131,123},
{36,222,222,222,222,222,222},
{36,222,189,180,222,180,201},
{36,213,156,156,213,156,185},
{36,222,180,180,222,180,201},
{36,197,123,115,197,115,156},
{36,230,139,156,230,139,185},
{36,115,90,90,115,90,90},
{36,32,8,8,32,8,20},
{36,65,49,41,65,41,53},
{36,8,0,0,8,0,4},
{36,98,32,32,98,32,65},
{36,164,98,98,164,98,131},
{36,106,98,90,106,98,90},
{36,230,205,197,230,197,214},
{36,246,213,205,246,205,226},
{36,164,148,148,164,148,148},
{36,238,164,189,238,164,201},
{36,156,131,123,156,131,123},
{36,230,172,180,230,172,201},
{36,24,8,8,24,8,16},
{36,106,90,90,106,90,90},
{36,213,197,189,213,189,201},
{36,246,172,156,246,156,201},
{39,255,189,197,250,194,250},
{39,222,255,255,240,255,222},
{39,139,131,139,139,131,139},
{39,74,148,139,115,154,68},
{39,222,246,246,235,248,220},
{39,131,156,148,131,156,148},
{39,41,82,82,64,85,38},
{39,189,90,98,182,97,182},
{39,180,139,148,177,142,177},
{39,205,123,131,199,129,199},
{39,180,148,156,178,150,178},
{39,74,164,148,124,171,67},
{39,230,230,238,234,239,229},
{39,106,197,189,157,204,99},
{39,74,90,90,74,90,90},
{39,230,139,148,223,146,223},
{39,41,98,82,73,102,37},
{39,246,156,164,239,163,239},
{39,180,197,197,180,197,197},
{39,139,90,106,135,94,135},
{39,32,74,74,55,77,29},
{39,131,82,90,127,86,127},
{39,32,106,90,73,112,26},
{39,164,189,189,178,191,162},
{39,41,115,98,82,121,35},
{39,246,172,172,240,178,240},
{39,131,205,197,172,211,125},
{39,197,106,106,190,113,190},
{39,98,65,74,96,67,96},
{39,205,246,246,228,249,202},
{39,98,172,156,139,178,92},
{39,106,180,172,147,186,100},
{39,24,57,57,42,59,22},
{39,172,197,197,186,199,170},
{39,115,139,148,115,139,148},
{39,32,106,98,73,112,26},
{39,230,180,197,226,184,226},
{39,57,90,82,75,92,55},
{39,74,148,131,115,154,68},
{39,189,238,238,216,242,185},
{40,164,123,131,161,126,161},
{40,139,131,131,139,131,131},
{40,156,139,139,156,139,139},
{40,222,164,180,218,168,218},
{40,222,205,205,221,206,221},
{40,255,238,246,254,239,254},
{40,230,164,164,225,169,225},
{40,131,131,131,131,131,131},
{40,197,115,123,191,121,191},
{40,82,106,106,82,106,106},
{40,230,172,189,226,176,226},
{40,148,156,164,148,156,164},
{40,74,90,90,74,90,90},
{40,82,156,139,123,162,76},
{40,164,115,131,160,119,160},
{40,82,49,65,80,51,80},
{40,65,106,106,88,109,62},
{40,74,98,98,74,98,98},
{40,189,106,106,183,112,183},
{40,222,246,246,235,248,220},
{40,222,197,205,220,199,220},
{40,115,139,148,115,139,148},
{40,238,246,246,242,247,237},
{40,74,131,123,106,135,70},
{40,156,115,131,153,118,153},
{40,82,115,115,100,117,80},
{40,230,148,148,224,154,224},
{40,148,213,205,184,218,143},
{40,246,189,197,242,193,242},
{40,139,156,164,139,156,164},
{40,172,189,197,186,199,170},
{40,230,172,180,226,176,226},
{40,164,131,139,162,133,162},
{40,205,148,156,201,152,201},
{40,238,246,238,238,246,238},
{40,156,115,123,153,118,153},
{40,156,106,123,152,110,152},
{40,197,131,131,192,136,192},
{40,255,164,164,248,171,248},
{40,131,172,164,154,175,128},
{130,123,197,230,220,80,70},
{130,230,222,180,230,222,180},
{130,255,222,131,255,222,131},
{130,57,131,180,148,53,47},
{130,106,82,41,106,82,41},
{130,98,180,238,232,56,43},
{130,65,90,98,80,55,53},
{130,189,172,123,189,172,123},
{130,65,123,148,121,58,53},
{130,221,221,221,221,221,221},
{130,57,82,106,87,49,47},
{130,222,222,222,222,222,222},
{130,189,164,106,189,164,106},
{130,85,85,85,85,85,85},
{130,172,148,98,172,148,98},
{130,115,189,230,221,73,62},
{130,189,164,74,189,164,74},
{130,230,246,246,223,171,167},
{130,205,180,90,205,180,90},
{130,82,57,16,82,57,16},
{130,238,90,106,238,90,106},
{130,156,180,164,156,180,164},
{130,82,98,106,82,98,106},
{130,164,230,246,239,106,97},
{130,133,133,133,133,133,133},
{130,156,131,57,156,131,57},
{130,90,49,106,87,43,40},
{130,98,115,98,98,115,98},
{130,98,98,65,98,98,65},
{130,148,49,65,148,49,65},
{130,156,156,156,156,156,156},
{130,49,49,82,67,42,40},
{130,230,222,230,230,222,230},
{130,153,153,153,153,153,153},
{130,255,230,255,255,150,143},
{130,8,82,131,107,13,7},
{130,139,213,255,255,81,68},
{130,32,16,32,26,14,13},
{130,164,57,65,164,57,65},
{130,82,82,32,82,82,32},
}
+304
View File
@@ -0,0 +1,304 @@
-- The shiny system, headless.
--
-- luajit mods/DramaticShapeVoxelMod/tests/shiny_test.lua [--mod=DIR]
--
-- Run from the PROJECT ROOT (it requires src.pokemon.Stats, the engine's own
-- shiny predicate -- the point being that we agree with the engine rather
-- than carry a second copy of the rule).
--
-- Two halves:
--
-- the DV model that a decided mon really does satisfy the Gen 2
-- pattern, that a miss really does not, that the odds
-- are the number they claim, and that the derived HP DV
-- is kept legal.
-- the recolour against a fixture of real (normal, shiny) colour
-- pairs lifted from the verified texture set, so the
-- Lua transform is checked against the Python that
-- produced the shipped colours rather than against
-- itself.
package.path = "./?.lua;./?/init.lua;" .. package.path
local args = {}
for _, a in ipairs({ ... }) do
local k, v = a:match("^%-%-([%w_]+)=(.*)$")
if k then args[k] = v else args[a:gsub("^%-%-", "")] = true end
end
local MOD = args.mod or "mods/DramaticShapeVoxelMod"
-- ------- the mod namespace, enough of it
local loaded = {}
local V = {}
function V.require(name)
if loaded[name] == nil then
loaded[name] = assert(loadfile(MOD .. "/lib/" .. name .. ".lua"))(V)
end
return loaded[name]
end
function V.data(name)
return assert(loadfile(MOD .. "/data/" .. name .. ".lua"))(V)
end
V.mod = { log = { warn = function() end, info = function() end } }
local Shiny = V.require("Shiny")
local ShinyPalette = V.require("ShinyPalette")
local Stats = require("src.pokemon.Stats")
-- ------- a tiny harness
local pass, fail = 0, 0
local function ok(cond, what)
if cond then
pass = pass + 1
else
fail = fail + 1
io.write("FAIL: " .. what .. "\n")
end
end
local function eq(got, want, what)
ok(got == want, ("%s (got %s, want %s)")
:format(what, tostring(got), tostring(want)))
end
-- always-hit and always-miss stand-ins for the odds roll
local function hit() return 1 end
local function miss() return 2 end
local function mon(dvs, level, species)
return { species = species, level = level, dvs = dvs,
statExp = { hp = 0, attack = 0, defense = 0, speed = 0, special = 0 } }
end
-- ------- the DV model
do
local m = mon({ attack = 0, defense = 0, speed = 0, special = 0, hp = 0 })
Shiny.decide(m, hit)
ok(Shiny.isShiny(m), "a hit produces a mon the ENGINE calls shiny")
ok(Stats.isShiny(m.dvs), "and the engine's own predicate agrees")
eq(m.dvs.defense, 10, "defense pinned to 10")
eq(m.dvs.speed, 10, "speed pinned to 10")
eq(m.dvs.special, 10, "special pinned to 10")
eq(m.shiny, true, "the cached flag is set")
end
do
-- nearest legal Attack, so a shiny encounter is not also a stat reroll
local cases = { [0] = 2, [4] = 3, [5] = 6, [9] = 10, [13] = 14, [15] = 15 }
for from, want in pairs(cases) do
local m = mon({ attack = from, defense = 0, speed = 0, special = 0, hp = 0 })
Shiny.decide(m, hit)
eq(m.dvs.attack, want, ("attack %d moves to the nearest legal %d")
:format(from, want))
end
end
do
-- the HP DV is derived from the low bits of the other four; a write that
-- forgets to resync it produces a mon no real game could make
for atk = 0, 15 do
local m = mon({ attack = atk, defense = 3, speed = 7, special = 1, hp = 0 })
Shiny.decide(m, hit)
local want = (m.dvs.attack % 2) * 8 + (m.dvs.defense % 2) * 4
+ (m.dvs.speed % 2) * 2 + (m.dvs.special % 2)
eq(m.dvs.hp, want, "HP DV stays derived from the other four")
end
end
do
-- a MISS must not leave an accidentally-shiny mon shiny, or the rate is
-- the requested one and 1/8192 in parallel
local m = mon({ attack = 2, defense = 10, speed = 10, special = 10, hp = 8 })
ok(Stats.isShiny(m.dvs), "fixture starts out shiny by luck")
Shiny.decide(m, miss)
ok(not Shiny.isShiny(m), "a miss clears an accidentally-shiny mon")
eq(m.shiny, nil, "and clears the cached flag rather than storing false")
end
do
-- the odds are the number they claim. 1/1 must be every time; a large
-- denominator must essentially never fire on a fixed stub.
local saved = Shiny.ODDS_DENOM
Shiny.setOdds(1)
local m = mon({ attack = 0, defense = 0, speed = 0, special = 0, hp = 0 })
Shiny.decide(m, function(_, hi) return math.random(1, hi) end)
ok(Shiny.isShiny(m), "odds of 1 make every mon shiny")
Shiny.setOdds(0)
eq(Shiny.ODDS_DENOM, 1, "a denominator below 1 is refused")
Shiny.setOdds(saved)
eq(Shiny.ODDS_DENOM, saved, "and a sane one is accepted")
end
do
-- stats must follow the DVs, and a full-health mon must stay full: a wild
-- mon that appears at less than full HP is visible in the first frame
local Data = require("src.core.Data")
local haveData = type(Data) == "table" and type(Data.pokemon) == "table"
and Data.pokemon.PIKACHU ~= nil
if haveData then
local m = mon({ attack = 0, defense = 0, speed = 0, special = 0, hp = 0 },
10, "PIKACHU")
m.stats = Stats.calc(Data.pokemon.PIKACHU, 10, m.dvs, m.statExp)
m.hp = m.stats.hp
local before = m.stats.hp
Shiny.decide(m, hit)
ok(m.stats.hp >= before, "max HP tracks the raised DVs")
eq(m.hp, m.stats.hp, "a full-health mon stays full after a restat")
else
io.write("note: no ROM-backed data; skipped the restat check\n")
end
end
-- ------- the recolour
do
eq(type(ShinyPalette.forDex(6)), "table", "colours load for Charizard")
local ch = ShinyPalette.forDex(6)
eq(ch.slide.h, -136, "Charizard's hue slide is the Stadium value")
eq(ch.slide.s, -6, "and its saturation step")
ok(ShinyPalette.forDex(130).lut ~= nil,
"Gyarados carries an explicit table, not a slide")
ok(ShinyPalette.forDex(6).lut == nil, "and Charizard does not")
end
do
-- alpha is never touched, and a texture with nothing to do comes back
-- unchanged rather than rebuilt
local fn = ShinyPalette.transform(ShinyPalette.forDex(6))
local px = string.char(222, 131, 123, 77) .. string.char(0, 0, 0, 0)
local out = ShinyPalette.recolorTexels(px, fn)
eq(#out, #px, "the texel string keeps its length")
eq(out:byte(4), 77, "alpha survives the transform")
eq(out:byte(8), 0, "and so does a fully transparent texel's alpha")
end
do
local fixture = loadfile(MOD .. "/tests/shiny_palette_fixture.lua")
if not fixture then
io.write("note: no colour fixture; skipped the cross-check\n")
else
local rows = fixture()
local worst, bad = 0, 0
for _, r in ipairs(rows) do
local fn = ShinyPalette.transform(ShinyPalette.forDex(r[1]))
local gr, gg, gb = r[2], r[3], r[4]
if fn then gr, gg, gb = fn(r[2], r[3], r[4]) end
local d = math.max(math.abs(gr - r[5]), math.abs(gg - r[6]),
math.abs(gb - r[7]))
if d > worst then worst = d end
if d > 2 then bad = bad + 1 end
end
eq(bad, 0, ("%d colour pairs reproduce the shipped textures"):format(#rows))
-- a unit or two of drift is the two languages' float rounding; more than
-- that means the colour model itself has diverged
ok(worst <= 2, ("worst channel drift is %d, within rounding"):format(worst))
end
end
-- ------- the read side
--
-- The extraction writes NNNs.dsm beside NNN.dsm; this is the other half of
-- that contract. Driven off a directory of real packs when one is given
-- (--packs=DIR, e.g. the extract test's --out), because the interesting
-- cases are a shiny pack that EXISTS and one that does not, and both have
-- to be real files for the fallback to be worth testing.
if args.packs then
local Vp = { mod = { log = { warn = function() end, info = function() end } } }
local loadedP = {}
function Vp.require(name)
if loadedP[name] == nil then
loadedP[name] = assert(loadfile(MOD .. "/lib/" .. name .. ".lua"))(Vp)
end
return loadedP[name]
end
-- stand in for the mod's own reader, pointed at the pack directory
function Vp.mod:read(rel)
local name = rel:match("([^/]+)$")
local fp = io.open(args.packs .. "/" .. name, "rb")
if not fp then return nil end
local b = fp:read("*a")
fp:close()
return b
end
local StadiumPack = Vp.require("StadiumPack")
StadiumPack.DIR = "."
local normal = StadiumPack.load(6, false)
local shiny = StadiumPack.load(6, true)
ok(normal ~= nil, "the normal Charizard pack loads")
ok(shiny ~= nil, "and so does the shiny one")
if normal and shiny then
eq(shiny.shiny, true, "the shiny model is flagged as such")
eq(normal.shiny, nil, "and the normal one is not")
ok(normal ~= shiny, "they are SEPARATE models, not one shared table")
eq(#normal.prims, #shiny.prims, "same geometry")
eq(normal.texCount, shiny.texCount, "same texture count")
-- the point of the whole exercise: different pixels
local differs = false
for i = 1, math.min(#normal.textures, #shiny.textures) do
if normal.textures[i].rgba ~= shiny.textures[i].rgba then
differs = true
break
end
end
ok(differs, "and different texels")
end
-- a species with no shiny pack must fall back rather than vanish: that is
-- what an install from before rev 3 looks like
local missing = StadiumPack.load(999, true)
eq(missing, nil, "an out-of-range species is still nil")
end
-- ------- end to end, through the engine's own constructor
--
-- The wrap is the whole feature: if Pokemon.new does not carry the verdict,
-- nothing downstream has anything to draw. Exercised against the real
-- constructor and the fixture dataset rather than a stub, because what is
-- being tested is precisely that we hooked the thing the game calls.
do
local okKit, T = pcall(require, "tests.modkit")
local Data = okKit and T.fixtures and T.fixtures.load()
local okPk, Pokemon = pcall(require, "src.pokemon.Pokemon")
local species = Data and Data.pokemon
and (Data.pokemon.PIKACHU and "PIKACHU"
or next(Data.pokemon))
if not (okKit and okPk and species) then
io.write("note: no fixture dataset; skipped the end-to-end check\n")
else
local ShinyBattle = V.require("ShinyBattle")
ShinyBattle.install()
ShinyBattle.install() -- twice: the wrap must not stack
local saved = Shiny.ODDS_DENOM
Shiny.setOdds(1)
local m = Pokemon.new(Data, species, 7)
ok(Shiny.isShiny(m), "a mon built at odds 1 comes out shiny")
eq(m.shiny, true, "and carries the flag")
eq(m.hp, m.stats.hp, "and is at full health despite the restat")
ok(Stats.isShiny(m.dvs), "and the ENGINE agrees it is shiny")
-- and at long odds it essentially never is: 400 draws at 1/8192 would
-- fire about 5% of the time, so a single failure here is signal
Shiny.setOdds(8192)
local shinies = 0
for _ = 1, 400 do
if Shiny.isShiny(Pokemon.new(Data, species, 7)) then
shinies = shinies + 1
end
end
ok(shinies <= 2, ("400 mons at 1/8192 produced %d shinies")
:format(shinies))
Shiny.setOdds(saved)
end
end
io.write(("\n%d passed, %d failed\n"):format(pass, fail))
os.exit(fail == 0 and 0 or 1)
+37 -3
View File
@@ -1,12 +1,17 @@
-- The Lua ROM extractor, against the Python packer that is its oracle.
--
-- luajit mods/DramaticShapeVoxelMod/tests/stadium_extract_test.lua \
-- [--rom=PATH] [--oracle=DIR] [--only=25,6] [--out=DIR]
-- [--rom=PATH] [--oracle=DIR] [--only=25,6] [--out=DIR] [--mod=DIR]
--
-- Run from the PROJECT ROOT. Defaults: the ROM under
-- model_extract/baseroms/, the oracle in assets/stadium (whatever
-- tools/stadium_pack.py last wrote there).
--
-- --mod points at the mod directory whose lib/ is under test. It exists for
-- worktrees: the ROM and the packs are both gitignored, so a worktree has
-- neither, and without this the test would load the SHARED checkout's
-- modules while claiming to test the branch's.
--
-- ------- what this is for
--
-- lib/StadiumRom, StadiumFragment, StadiumFx and StadiumBuild are a port of
@@ -30,7 +35,7 @@ for _, a in ipairs({ ... }) do
if k then args[k] = v else args[a:gsub("^%-%-", "")] = true end
end
local MOD = "mods/DramaticShapeVoxelMod"
local MOD = args.mod or "mods/DramaticShapeVoxelMod"
local ROM = args.rom or (MOD .. "/model_extract/baseroms/us/baserom.z64")
local ORACLE = args.oracle or (MOD .. "/assets/stadium")
@@ -79,6 +84,7 @@ if args.only then
end
local checked, matched, missing, failed = 0, 0, 0, 0
local shinyOk, shinyMissing, shinyBad = 0, 0, 0
local firstBad = nil
local t0 = os.clock()
@@ -93,6 +99,28 @@ for fileno = 0, StadiumRom.N_POKEMON - 1 do
if args.out then
local fp = io.open(("%s/%03d.dsm"):format(args.out, res.species), "wb")
if fp then fp:write(res.bytes) fp:close() end
-- the shiny variant too, so the pair can be diffed out of process
if res.shinyBytes then
local sp = io.open(("%s/%03ds.dsm"):format(args.out, res.species), "wb")
if sp then sp:write(res.shinyBytes) sp:close() end
end
end
-- The shiny pack is the same DSM3 with recoloured texels, so it must be
-- exactly as long and must actually differ. A species that produced none
-- is counted rather than failed: it ships without a recolour and the
-- runtime falls back to its normal model.
if not res.shinyBytes then
shinyMissing = shinyMissing + 1
elseif #res.shinyBytes ~= #res.bytes then
shinyBad = shinyBad + 1
io.write(("species %d: shiny pack is %d bytes, normal is %d\n")
:format(res.species, #res.shinyBytes, #res.bytes))
elseif res.shinyBytes == res.bytes then
shinyBad = shinyBad + 1
io.write(("species %d: shiny pack is identical to normal\n")
:format(res.species))
else
shinyOk = shinyOk + 1
end
local want = readFile(("%s/%03d.dsm"):format(ORACLE, res.species))
if not want then
@@ -119,8 +147,14 @@ io.write(("\n%d checked, %d identical, %d differ, %d oracle files missing, "
.. "%d extractions failed (%.1fs)\n")
:format(checked, matched, checked - matched - missing, missing,
failed, os.clock() - t0))
io.write(("shiny: %d recoloured, %d without a variant, %d malformed\n")
:format(shinyOk, shinyMissing, shinyBad))
if matched == checked and failed == 0 and missing == 0 then
-- The oracle diff is the load-bearing assertion and is unchanged: the shiny
-- pass must not have moved a single byte of the normal packs. The shiny
-- counters are additional, and a malformed variant fails the run -- a pack
-- of the wrong length would be read as a corrupt model at runtime.
if matched == checked and failed == 0 and missing == 0 and shinyBad == 0 then
io.write("PASS -- the Lua extractor reproduces the packer exactly\n")
os.exit(0)
end