diff --git a/CHANGELOG.md b/CHANGELOG.md index c468440..e124aa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,81 @@ # Changelog +## Unreleased + +### Added + +- **SHINY POKEMON.** On by default, with no row to switch it off: + shininess is a property of a Pokemon, not a display mode, and one that + differed between two players' saves would be a setting rather than a + Pokemon. + + **It was always there.** Gen 1 has no shininess of its own, but it has + the four DVs Gen 2 later reads to decide it, and the engine already + ships that reading (`src/pokemon/Stats.lua`, `isShiny` -- its own + comment calls it "the RBY virtual shiny"). So nothing new is stored on + a Pokemon and nothing migrates: every save ever made already contains + the answer, and this release starts drawing it. A mon is shiny when + Defense, Speed and Special are all exactly 10 and Attack is one of + 2/3/6/7/10/11/14/15 -- which random DVs land on 1 time in 8192, the + classic rate, and the default the odds dial ships at. Deriving rather + than storing is what makes it survive a save, a box, a trade and an + evolution without a second copy of the truth to drift out of step; a + shiny Bulbasaur is a shiny Venusaur without being told. `mon.shiny` is + maintained as a cache beside it, written from the DVs and never read + as the source. + + The roll happens in `Pokemon.new`, which is where every wild, gift, + starter and traded mon is built -- before the battle bakes its sprite, + which `battle.started` is already too late for. It draws from the mod's + OWN random stream rather than the game's, so installing this does not + shift the sequence every damage roll and encounter slot comes out of. + Trainers' Pokemon come out ordinary by themselves, because the engine + pins their DVs to a fixed set -- which is also what the real games do. + + **The models are genuinely recoloured**, not tinted. The recolour runs + as part of the Stadium extraction: each species' textures are decoded + once, packed as usual, then recoloured and packed again beside it as + `NNNs.dsm`. The colours are Stadium's own -- it slides a model in HSL + rather than shipping second textures, a hue rotation in degrees plus + saturation and lightness on a quantized -8..+8 scale at 12.5% a step -- + and all 151 sets of values are shipped in `data/shiny_colors.lua`. + Five species get an explicit colour table instead, because Stadium + gives THEM a real alternate texture and no single slide can reproduce + it: Clefairy, Clefable, Jigglypuff, Wigglytuff and Gyarados, whose + bodies must stay put while a small region rotates a long way. + Generated effect frames -- flames, beams, sparks -- are excluded, so a + shiny Charizard has a shiny hide and an ordinary fire. + + Doing this at extraction rather than at load is what makes that + exclusion exact: `StadiumFx` marks its generated frames and the packer + drops the marker, so extraction is the last moment a flame is + distinguishable from a hide. The normal packs come out byte-identical + either way -- the shiny pass runs after they are written -- so + `tests/stadium_extract_test.lua` still diffs all 151 against the Python + oracle unchanged, and the format did not move. The install marker's REV + goes to 3 so an existing cache rebuilds rather than quietly showing + every shiny in its ordinary colours. + + **Flat art is tinted** rather than recoloured, because the engine bakes + a species palette into an image cache that has no idea which individual + is being drawn. The tint is derived from that species' own shiny slide, + so a shiny Golbat leans green and a shiny Charizard goes dusky. On the + 3D path each side is tinted separately, which is the only place the two + sides can differ. A multiply can only darken, so species whose shiny is + LIGHTER than their normal read quieter on the flat art than on the + model. + + **A sparkle** on arrival: a ring of additive stars that springs from + the mon's chest and fades over three quarters of a second, armed on the + frame a side's occupant changes -- which covers a send-out, a switch + and a wild foe alike. A wild Pokemon never grows out of a ball, so the + grow was the wrong edge to hang it on. + + **A star on the status page**, beside the level on page 1, drawn in the + engine's own pixel grid so it is palette-processed like every other + pixel rather than floating over the finished frame. Page 1 only: page 2 + clears that block itself. + ## 1.8.0 ### Added diff --git a/data/shiny_colors.lua b/data/shiny_colors.lua new file mode 100644 index 0000000..e6cbc54 --- /dev/null +++ b/data/shiny_colors.lua @@ -0,0 +1,1262 @@ +-- Shiny colours for the 151 Stadium models, as the Stadium games define +-- them. GENERATED -- do not hand-edit. The colour model is documented in +-- the header of lib/ShinyPalette.lua. +-- +-- Stadium does not ship a second set of textures for a shiny Pokemon. It +-- slides the colours it already has in HSL: a hue rotation in degrees, +-- plus saturation and lightness on a quantized -8..+8 scale where one +-- step is 12.5% (so +-8 is +-100%, exactly GIMP's Hue-Saturation range -- +-- s = -8 is full greyscale, l = +8 is white). +-- +-- FIVE SPECIES ARE DIFFERENT. Clefairy, Clefable, Jigglypuff, Wigglytuff +-- and Gyarados get a genuine alternate texture in Stadium, because no +-- single slide can produce their shiny: Jigglypuff's body must stay pink +-- while its irises rotate to green, and one rotation moves both or +-- neither. Those five carry `lut` -- an explicit before/after colour +-- mapping sampled from the verified texture pairs, listing only the +-- colours that actually change. A colour absent from the table is left +-- exactly as it was. +-- +-- hueRange is the min/max hue a NON-shiny nicknamed mon can slide to in +-- Stadium (derived from the trainer ID and the nickname). Carried for +-- reference; nothing reads it today. + +return { + [1] = { + name = "bulbasaur", + hueRange = { min = -45, max = 70 }, + slide = { h = -30, l = 2, s = -1 }, + }, + [2] = { + name = "ivysaur", + hueRange = { min = -40, max = 45 }, + slide = { h = -50, l = 2, s = 1 }, + }, + [3] = { + name = "venusaur", + hueRange = { min = -30, max = 40 }, + slide = { h = -40, l = 2, s = 1 }, + }, + [4] = { + name = "charmander", + hueRange = { min = -25, max = 20 }, + slide = { h = 28, l = 0, s = -1 }, + }, + [5] = { + name = "charmeleon", + hueRange = { min = -10, max = 55 }, + slide = { h = -20, l = 1, s = -1 }, + }, + [6] = { + name = "charizard", + hueRange = { min = -20, max = 15 }, + slide = { h = -136, l = 0, s = -6 }, + }, + [7] = { + name = "squirtle", + hueRange = { min = -50, max = 30 }, + slide = { h = 30, l = -3, s = 2 }, + }, + [8] = { + name = "wartortle", + hueRange = { min = -50, max = 30 }, + slide = { h = -50, l = -3, s = -2 }, + }, + [9] = { + name = "blastoise", + hueRange = { min = -30, max = 35 }, + slide = { h = -35, l = -1, s = -3 }, + }, + [10] = { + name = "caterpie", + hueRange = { min = -30, max = 80 }, + slide = { h = -45, l = 2, s = -1 }, + }, + [11] = { + name = "metapod", + hueRange = { min = -20, max = 60 }, + slide = { h = -55, l = 0, s = 0 }, + }, + [12] = { + name = "butterfree", + hueRange = { min = -40, max = 50 }, + slide = { h = 140, l = 0, s = 0 }, + }, + [13] = { + name = "weedle", + hueRange = { min = 0, max = 50 }, + slide = { h = 46, l = 0, s = 0 }, + }, + [14] = { + name = "kakuna", + hueRange = { min = -25, max = 40 }, + slide = { h = 30, l = 0, s = 0 }, + }, + [15] = { + name = "beedrill", + hueRange = { min = -20, max = 50 }, + slide = { h = 145, l = 0, s = -3 }, + }, + [16] = { + name = "pidgey", + hueRange = { min = -30, max = 30 }, + slide = { h = 40, l = 1, s = -2 }, + }, + [17] = { + name = "pidgeotto", + hueRange = { min = -30, max = 30 }, + slide = { h = 40, l = 1, s = 0 }, + }, + [18] = { + name = "pidgeot", + hueRange = { min = -35, max = 20 }, + slide = { h = 40, l = 1, s = -3 }, + }, + [19] = { + name = "rattata", + hueRange = { min = -40, max = 60 }, + slide = { h = -105, l = 0, s = -5 }, + }, + [20] = { + name = "raticate", + hueRange = { min = -30, max = 30 }, + slide = { h = -30, l = 1, s = -4 }, + }, + [21] = { + name = "spearow", + hueRange = { min = -30, max = 20 }, + slide = { h = -25, l = 1, s = -2 }, + }, + [22] = { + name = "fearow", + hueRange = { min = -25, max = 30 }, + slide = { h = 140, l = 3, s = -5 }, + }, + [23] = { + name = "ekans", + hueRange = { min = -30, max = 40 }, + slide = { h = -155, l = -2, s = -5 }, + }, + [24] = { + name = "arbok", + hueRange = { min = -70, max = 30 }, + slide = { h = -80, l = 0, s = -4 }, + }, + [25] = { + name = "pikachu", + hueRange = { min = -15, max = 25 }, + slide = { h = -25, l = 1, s = 0 }, + }, + [26] = { + name = "raichu", + hueRange = { min = -15, max = 25 }, + slide = { h = 25, l = 0, s = -2 }, + }, + [27] = { + name = "sandshrew", + hueRange = { min = -40, max = 40 }, + slide = { h = -159, l = -1, s = -4 }, + }, + [28] = { + name = "sandslash", + hueRange = { min = -20, max = 25 }, + slide = { h = 25, l = 1, s = -6 }, + }, + [29] = { + name = "nidoran_f", + hueRange = { min = -60, max = 70 }, + slide = { h = 70, l = -2, s = 2 }, + }, + [30] = { + name = "nidorina", + hueRange = { min = -80, max = 80 }, + slide = { h = 100, l = 0, s = 0 }, + }, + [31] = { + name = "nidoqueen", + hueRange = { min = -60, max = 80 }, + slide = { h = 50, l = 0, s = -5 }, + }, + [32] = { + name = "nidoran_m", + hueRange = { min = -70, max = 20 }, + slide = { h = -96, l = 0, s = 0 }, + }, + [33] = { + name = "nidorino", + hueRange = { min = -70, max = 20 }, + slide = { h = -99, l = 0, s = 0 }, + }, + [34] = { + name = "nidoking", + hueRange = { min = -50, max = 30 }, + slide = { h = -60, l = 0, s = 0 }, + }, + [35] = { + name = "clefairy", + hueRange = { min = -40, max = 50 }, + -- Stadium ships a real alternate texture for this one; 350 + -- colours move, every other colour stays exactly as it was. + lut = { + [0x080800]=0x010800, [0x100800]=0x020F00, [0x101000]=0x020F00, [0x181000]=0x031700, + [0x181008]=0x081905, [0x201000]=0x041E00, [0x201800]=0x041E00, [0x201808]=0x082204, + [0x291010]=0x29101D, [0x291800]=0x052700, [0x291808]=0x082C02, [0x291818]=0x291820, + [0x292000]=0x052700, [0x310808]=0x31081D, [0x311010]=0x311021, [0x311818]=0x311825, + [0x312000]=0x062F00, [0x312008]=0x083501, [0x312010]=0x0F340A, [0x312020]=0x312028, + [0x312900]=0x062F00, [0x312908]=0x083501, [0x312910]=0x0F340A, [0x390000]=0x39001D, + [0x390008]=0x39001D, [0x390808]=0x390821, [0x391818]=0x391829, [0x392020]=0x39202D, + [0x392900]=0x073600, [0x392908]=0x083E00, [0x392910]=0x0F3D08, [0x392918]=0x173C11, + [0x392929]=0x392931, [0x392939]=0x392931, [0x393108]=0x083E00, [0x410000]=0x410021, + [0x410008]=0x410021, [0x410808]=0x410825, [0x410810]=0x410825, [0x412020]=0x412031, + [0x412908]=0x094500, [0x412910]=0x0F4607, [0x412929]=0x412935, [0x413108]=0x094500, + [0x413110]=0x0F4607, [0x413118]=0x174510, [0x4A0008]=0x4A0025, [0x4A0808]=0x4A0829, + [0x4A0810]=0x4A0829, [0x4A2020]=0x4A2035, [0x4A3108]=0x0A4E00, [0x4A3110]=0x0F5006, + [0x4A3118]=0x174F0E, [0x4A3131]=0x4A313E, [0x4A3910]=0x0F5006, [0x4A3918]=0x174F0E, + [0x520810]=0x52082D, [0x521010]=0x521031, [0x521018]=0x521031, [0x521820]=0x521835, + [0x522929]=0x52293E, [0x523131]=0x523142, [0x523910]=0x105904, [0x523931]=0x523142, + [0x523939]=0x523946, [0x524110]=0x105904, [0x524118]=0x17580D, [0x5A1010]=0x5A1035, + [0x5A2020]=0x5A203D, [0x5A3939]=0x5A394A, [0x5A4108]=0x0C5D00, [0x5A4110]=0x106203, + [0x5A4118]=0x17600C, [0x5A4120]=0x1F5F15, [0x5A4129]=0x275E1F, [0x5A4139]=0x5A394A, + [0x5A4141]=0x5A414E, [0x5A414A]=0x5A414E, [0x5A4A18]=0x17600C, [0x5A4A20]=0x1F5F15, + [0x621010]=0x621039, [0x622929]=0x622946, [0x623131]=0x62314A, [0x623939]=0x62394E, + [0x624110]=0x106B02, [0x624141]=0x624152, [0x624A10]=0x106B02, [0x624A18]=0x17690A, + [0x624A20]=0x1F6813, [0x624A29]=0x27671D, [0x624A41]=0x624152, [0x625220]=0x1F6813, + [0x6A1010]=0x6A103D, [0x6A2931]=0x6A294A, [0x6A3131]=0x6A314E, [0x6A3939]=0x6A3952, + [0x6A4141]=0x6A4156, [0x6A4A31]=0x2F6E25, [0x6A4A4A]=0x6A4A5A, [0x6A5220]=0x1F7112, + [0x6A5241]=0x6A4156, [0x734141]=0x73415A, [0x734A4A]=0x734A5E, [0x735231]=0x2F7824, + [0x735241]=0x73415A, [0x73524A]=0x734A5E, [0x735252]=0x735263, [0x73525A]=0x735263, + [0x735A4A]=0x734A5E, [0x7B2018]=0x7B184A, [0x7B4A4A]=0x7B4A63, [0x7B5252]=0x7B5266, + [0x7B5A41]=0x7B415E, [0x7B5A4A]=0x7B4A63, [0x7B5A52]=0x7B5266, [0x7B5A5A]=0x7B5A6B, + [0x7B625A]=0x7B5A6B, [0x832018]=0x83184E, [0x832020]=0x832052, [0x83414A]=0x834162, + [0x834A52]=0x834A67, [0x835252]=0x83526A, [0x835A5A]=0x835A6E, [0x83624A]=0x834A67, + [0x83625A]=0x835A6E, [0x8B3939]=0x8B3962, [0x8B4A4A]=0x8B4A6A, [0x8B5252]=0x8B526F, + [0x8B5A5A]=0x8B5A72, [0x8B625A]=0x8B5A72, [0x8B6262]=0x8B6277, [0x8B6A52]=0x8B526F, + [0x941818]=0x941856, [0x942018]=0x941856, [0x94525A]=0x945273, [0x945A5A]=0x945A77, + [0x946262]=0x94627B, [0x946A62]=0x94627B, [0x946A6A]=0x946A7F, [0x946A73]=0x946A7F, + [0x94735A]=0x945A77, [0x9C2020]=0x9C205E, [0x9C2920]=0x9C205E, [0x9C2929]=0x9C2963, + [0x9C5A5A]=0x9C5A7B, [0x9C5A62]=0x9C5A7B, [0x9C6262]=0x9C627F, [0x9C626A]=0x9C627F, + [0x9C6A6A]=0x9C6A83, [0x9C6A73]=0x9C6A83, [0x9C735A]=0x9C5A7B, [0x9C7362]=0x9C627F, + [0x9C7373]=0x9C7388, [0x9C737B]=0x9C7388, [0xA42929]=0xA42967, [0xA43129]=0xA42967, + [0xA43131]=0xA4316B, [0xA4626A]=0xA46283, [0xA46A6A]=0xA46A87, [0xA47373]=0xA4738C, + [0xA47B62]=0xA46283, [0xA47B6A]=0xA46A87, [0xA47B73]=0xA4738C, [0xA47B7B]=0xA47B90, + [0xA47B83]=0xA47B90, [0xA4836A]=0xA46A87, [0xA48383]=0xA48393, [0xAC3131]=0xAC316F, + [0xAC3931]=0xAC316F, [0xAC3939]=0xAC3973, [0xAC4A41]=0xAC4177, [0xAC6A6A]=0xAC6A8B, + [0xAC7373]=0xAC7390, [0xAC737B]=0xAC7390, [0xAC7B7B]=0xAC7B94, [0xAC8373]=0xAC7390, + [0xAC837B]=0xAC7B94, [0xAC8383]=0xAC8398, [0xAC838B]=0xAC8398, [0xAC8B83]=0xAC8398, + [0xAC8B8B]=0xAC8B9C, [0xAC948B]=0xAC8B9C, [0xB44139]=0xB43977, [0xB44141]=0xB4417B, + [0xB44A41]=0xB4417B, [0xB44A4A]=0xB44A7F, [0xB45252]=0xB45283, [0xB47373]=0xB47394, + [0xB47B7B]=0xB47B98, [0xB48383]=0xB4839C, [0xB48B7B]=0xB47B98, [0xB48B8B]=0xB48BA0, + [0xB48B94]=0xB48BA0, [0xB49494]=0xB494A4, [0xBD4141]=0xBD417F, [0xBD4A41]=0xBD417F, + [0xBD4A4A]=0xBD4A84, [0xBD524A]=0xBD4A84, [0xBD5252]=0xBD5288, [0xBD5A52]=0xBD5288, + [0xBD5A5A]=0xBD5A8C, [0xBD7B7B]=0xBD7B9C, [0xBD8383]=0xBD83A0, [0xBD8B83]=0xBD83A0, + [0xBD8B8B]=0xBD8BA4, [0xBD8B94]=0xBD8BA4, [0xBD947B]=0xBD7B9C, [0xBD9483]=0xBD83A0, + [0xBD948B]=0xBD8BA4, [0xBD9494]=0xBD94A9, [0xBD949C]=0xBD94A9, [0xBDA49C]=0xBD9CAC, + [0xBDA4A4]=0xBDA4B0, [0xBDA4AC]=0xBDA4B0, [0xBDACA4]=0xBDA4B0, [0xC5524A]=0xC54A88, + [0xC55252]=0xC5528C, [0xC56262]=0xC56294, [0xC56A6A]=0xC56A98, [0xC58383]=0xC583A4, + [0xC58B8B]=0xC58BA8, [0xC5948B]=0xC58BA8, [0xC59494]=0xC594AC, [0xC59C8B]=0xC58BA8, + [0xC59C94]=0xC594AC, [0xC59C9C]=0xC59CB0, [0xC59CA4]=0xC59CB0, [0xC5ACAC]=0xC5ACB9, + [0xCD5252]=0xCD5290, [0xCD5A52]=0xCD5290, [0xCD5A5A]=0xCD5A94, [0xCD625A]=0xCD5A94, + [0xCD6262]=0xCD6298, [0xCD6A62]=0xCD6298, [0xCD736A]=0xCD6A9C, [0xCD7373]=0xCD73A0, + [0xCD8B8B]=0xCD8BAC, [0xCD9494]=0xCD94B0, [0xCD9C94]=0xCD94B0, [0xCD9C9C]=0xCD9CB4, + [0xCD9CA4]=0xCD9CB4, [0xCDA4A4]=0xCDA4B9, [0xCDACAC]=0xCDACBC, [0xCDB4B4]=0xCDB4C0, + [0xD5625A]=0xD55A98, [0xD56262]=0xD5629C, [0xD56A62]=0xD5629C, [0xD57373]=0xD573A4, + [0xD58B8B]=0xD58BB0, [0xD5948B]=0xD58BB0, [0xD59494]=0xD594B5, [0xD59C94]=0xD594B5, + [0xD59C9C]=0xD59CB9, [0xD5A49C]=0xD59CB9, [0xD5A4A4]=0xD5A4BC, [0xD5A4AC]=0xD5A4BC, + [0xD5ACAC]=0xD5ACC0, [0xD5B4B4]=0xD5B4C4, [0xD5BDBD]=0xD5BDC9, [0xD5C5C5]=0xD5C5CD, + [0xDE6A62]=0xDE62A0, [0xDE6A6A]=0xDE6AA4, [0xDE736A]=0xDE6AA4, [0xDE7373]=0xDE73A9, + [0xDE7B73]=0xDE73A9, [0xDE7B7B]=0xDE7BAD, [0xDE9494]=0xDE94B9, [0xDE9C94]=0xDE94B9, + [0xDE9C9C]=0xDE9CBD, [0xDEA4A4]=0xDEA4C1, [0xDEA4AC]=0xDEA4C1, [0xDEACA4]=0xDEA4C1, + [0xDEACAC]=0xDEACC5, [0xDEACB4]=0xDEACC5, [0xDEB4B4]=0xDEB4C9, [0xDEBDBD]=0xDEBDCE, + [0xDEC5C5]=0xDEC5D2, [0xE6736A]=0xE66AA8, [0xE67373]=0xE673AD, [0xE67B73]=0xE673AD, + [0xE67B7B]=0xE67BB1, [0xE67B8B]=0xE67BB1, [0xE67B94]=0xE67BB1, [0xE6837B]=0xE67BB1, + [0xE68383]=0xE683B4, [0xE68394]=0xE683B4, [0xE68B83]=0xE683B4, [0xE69494]=0xE694BD, + [0xE69CA4]=0xE69CC1, [0xE6A4A4]=0xE6A4C5, [0xE6A4AC]=0xE6A4C5, [0xE6ACA4]=0xE6A4C5, + [0xE6ACAC]=0xE6ACC9, [0xE6ACB4]=0xE6ACC9, [0xE6B4B4]=0xE6B4CD, [0xE6B4BD]=0xE6B4CD, + [0xE6BDBD]=0xE6BDD2, [0xE6C5C5]=0xE6C5D6, [0xE6CDCD]=0xE6CDDA, [0xE6CDD5]=0xE6CDDA, + [0xEE7B7B]=0xEE7BB5, [0xEE837B]=0xEE7BB5, [0xEE8383]=0xEE83B9, [0xEE8394]=0xEE83B9, + [0xEE8B83]=0xEE83B9, [0xEE8B8B]=0xEE8BBD, [0xEE8B9C]=0xEE8BBD, [0xEE948B]=0xEE8BBD, + [0xEE9494]=0xEE94C1, [0xEE949C]=0xEE94C1, [0xEE94A4]=0xEE94C1, [0xEE9CA4]=0xEE9CC5, + [0xEEA4AC]=0xEEA4C9, [0xEEACAC]=0xEEACCD, [0xEEACB4]=0xEEACCD, [0xEEB4AC]=0xEEACCD, + [0xEEB4B4]=0xEEB4D1, [0xEEB4BD]=0xEEB4D1, [0xEEBDB4]=0xEEB4D1, [0xEEBDBD]=0xEEBDD6, + [0xEEC5BD]=0xEEBDD6, [0xEEC5C5]=0xEEC5DA, [0xEECDCD]=0xEECDDE, [0xEED5D5]=0xEED5E2, + [0xF69CA4]=0xF69CC9, [0xF69CAC]=0xF69CC9, [0xF6A4AC]=0xF6A4CD, [0xF6ACAC]=0xF6ACD1, + [0xF6ACB4]=0xF6ACD1, [0xF6B4B4]=0xF6B4D5, [0xF6B4BD]=0xF6B4D5, [0xF6BDB4]=0xF6B4D5, + [0xF6BDBD]=0xF6BDDA, [0xF6BDC5]=0xF6BDDA, [0xF6C5C5]=0xF6C5DE, [0xF6CDCD]=0xF6CDE2, + [0xF6D5D5]=0xF6D5E6, [0xFFB4B4]=0xFFB4DA, [0xFFB4BD]=0xFFB4DA, [0xFFBDBD]=0xFFBDDE, + [0xFFBDC5]=0xFFBDDE, [0xFFC5BD]=0xFFBDDE, [0xFFC5C5]=0xFFC5E2, [0xFFC5CD]=0xFFC5E2, + [0xFFCDC5]=0xFFC5E2, [0xFFCDCD]=0xFFCDE6, [0xFFD5CD]=0xFFCDE6, [0xFFD5D5]=0xFFD5EA, + [0xFFDEDE]=0xFFDEEE, [0xFFE6E6]=0xFFE6F2, + }, + }, + [36] = { + name = "clefable", + hueRange = { min = -40, max = 50 }, + -- Stadium ships a real alternate texture for this one; 223 + -- colours move, every other colour stays exactly as it was. + lut = { + [0x080000]=0x080004, [0x080800]=0x010800, [0x100808]=0x10080C, [0x180000]=0x18000C, + [0x180808]=0x180810, [0x181010]=0x181014, [0x200000]=0x200010, [0x200808]=0x200814, + [0x201010]=0x201018, [0x290000]=0x290015, [0x290808]=0x290819, [0x291010]=0x29101D, + [0x292018]=0x291820, [0x310000]=0x310019, [0x310808]=0x31081D, [0x311010]=0x311021, + [0x312020]=0x312028, [0x312029]=0x312028, [0x390008]=0x39001D, [0x391010]=0x391025, + [0x391818]=0x391829, [0x392929]=0x392931, [0x393129]=0x392931, [0x410000]=0x410021, + [0x410808]=0x410825, [0x413129]=0x412935, [0x4A0000]=0x4A0025, [0x4A3131]=0x4A313E, + [0x520000]=0x520029, [0x521000]=0x520029, [0x523939]=0x523946, [0x524139]=0x523946, + [0x5A0000]=0x5A002D, [0x5A1808]=0x5A0831, [0x5A4141]=0x5A414E, [0x5A4A41]=0x5A414E, + [0x620000]=0x620031, [0x622010]=0x621039, [0x622020]=0x622041, [0x622918]=0x62183D, + [0x6A0000]=0x6A0035, [0x6A2918]=0x6A1841, [0x6A2920]=0x6A2045, [0x6A3120]=0x6A2045, + [0x730000]=0x73003A, [0x733929]=0x73294E, [0x735252]=0x735263, [0x735A52]=0x735263, + [0x7B4131]=0x7B3156, [0x7B5252]=0x7B5266, [0x7B625A]=0x7B5A6B, [0x834141]=0x834162, + [0x834A39]=0x83395E, [0x834A41]=0x834162, [0x835241]=0x834162, [0x83625A]=0x835A6E, + [0x8B4A4A]=0x8B4A6A, [0x94524A]=0x944A6F, [0x945A5A]=0x945A77, [0x946A6A]=0x946A7F, + [0x94736A]=0x946A7F, [0x9C5252]=0x9C5277, [0x9C6A62]=0x9C627F, [0x9C7362]=0x9C627F, + [0x9C7B73]=0x9C7388, [0xA4524A]=0xA44A77, [0xA46262]=0xA46283, [0xA47362]=0xA46283, + [0xA4736A]=0xA46A87, [0xA47B6A]=0xA46A87, [0xA4837B]=0xA47B90, [0xA48383]=0xA48393, + [0xA48B83]=0xA48393, [0xAC736A]=0xAC6A8B, [0xAC8373]=0xAC7390, [0xAC8B83]=0xAC8398, + [0xAC8B8B]=0xAC8B9C, [0xAC948B]=0xAC8B9C, [0xB4948B]=0xB48BA0, [0xB49494]=0xB494A4, + [0xB49C94]=0xB494A4, [0xBD6262]=0xBD6290, [0xBD7373]=0xBD7398, [0xBD9C94]=0xBD94A9, + [0xBD9C9C]=0xBD9CAC, [0xBDA494]=0xBD94A9, [0xBDA49C]=0xBD9CAC, [0xBDA4A4]=0xBDA4B0, + [0xC5625A]=0xC55A90, [0xC5736A]=0xC56A98, [0xC57B73]=0xC5739C, [0xC59C8B]=0xC58BA8, + [0xC59C94]=0xC594AC, [0xC5A494]=0xC594AC, [0xC5A49C]=0xC59CB0, [0xC5AC9C]=0xC59CB0, + [0xC5ACA4]=0xC5A4B4, [0xCD949C]=0xCD94B0, [0xCD9C9C]=0xCD9CB4, [0xCDA49C]=0xCD9CB4, + [0xCDA4A4]=0xCDA4B9, [0xCDAC9C]=0xCD9CB4, [0xCDACA4]=0xCDA4B9, [0xCDB4A4]=0xCDA4B9, + [0xCDB4AC]=0xCDACBC, [0xCDB4B4]=0xCDB4C0, [0xCDBDB4]=0xCDB4C0, [0xD56A6A]=0xD56AA0, + [0xD594A4]=0xD594B5, [0xD59C9C]=0xD59CB9, [0xD59CA4]=0xD59CB9, [0xD59CAC]=0xD59CB9, + [0xD5A4A4]=0xD5A4BC, [0xD5A4AC]=0xD5A4BC, [0xD5ACA4]=0xD5A4BC, [0xD5ACAC]=0xD5ACC0, + [0xD5ACB4]=0xD5ACC0, [0xD5B4A4]=0xD5A4BC, [0xD5B4AC]=0xD5ACC0, [0xD5B4B4]=0xD5B4C4, + [0xD5BDAC]=0xD5ACC0, [0xD5BDB4]=0xD5B4C4, [0xD5C5BD]=0xD5BDC9, [0xDE7B73]=0xDE73A9, + [0xDE94A4]=0xDE94B9, [0xDE9CA4]=0xDE9CBD, [0xDE9CAC]=0xDE9CBD, [0xDEA4AC]=0xDEA4C1, + [0xDEA4B4]=0xDEA4C1, [0xDEACB4]=0xDEACC5, [0xDEACBD]=0xDEACC5, [0xDEB4AC]=0xDEACC5, + [0xDEB4B4]=0xDEB4C9, [0xDEBDAC]=0xDEACC5, [0xDEBDB4]=0xDEB4C9, [0xDEBDBD]=0xDEBDCE, + [0xDEC5B4]=0xDEB4C9, [0xDEC5BD]=0xDEBDCE, [0xDECDBD]=0xDEBDCE, [0xDECDC5]=0xDEC5D2, + [0xE67373]=0xE673AD, [0xE67B73]=0xE673AD, [0xE68B9C]=0xE68BB9, [0xE6949C]=0xE694BD, + [0xE694A4]=0xE694BD, [0xE694AC]=0xE694BD, [0xE69CA4]=0xE69CC1, [0xE69CAC]=0xE69CC1, + [0xE69CB4]=0xE69CC1, [0xE6A4AC]=0xE6A4C5, [0xE6A4B4]=0xE6A4C5, [0xE6ACA4]=0xE6A4C5, + [0xE6ACB4]=0xE6ACC9, [0xE6B4AC]=0xE6ACC9, [0xE6BDB4]=0xE6B4CD, [0xE6BDBD]=0xE6BDD2, + [0xE6C5B4]=0xE6B4CD, [0xE6C5BD]=0xE6BDD2, [0xE6C5C5]=0xE6C5D6, [0xE6CDBD]=0xE6BDD2, + [0xE6CDC5]=0xE6C5D6, [0xE6D5CD]=0xE6CDDA, [0xEE7373]=0xEE73B1, [0xEE7B73]=0xEE73B1, + [0xEE8B9C]=0xEE8BBD, [0xEE949C]=0xEE94C1, [0xEE94A4]=0xEE94C1, [0xEE9CA4]=0xEE9CC5, + [0xEE9CAC]=0xEE9CC5, [0xEE9CB4]=0xEE9CC5, [0xEEA4AC]=0xEEA4C9, [0xEEA4B4]=0xEEA4C9, + [0xEEA4BD]=0xEEA4C9, [0xEEACB4]=0xEEACCD, [0xEEACBD]=0xEEACCD, [0xEEB4BD]=0xEEB4D1, + [0xEEBDB4]=0xEEB4D1, [0xEEC5B4]=0xEEB4D1, [0xEEC5BD]=0xEEBDD6, [0xEEC5C5]=0xEEC5DA, + [0xEECDBD]=0xEEBDD6, [0xEECDC5]=0xEEC5DA, [0xEECDCD]=0xEECDDE, [0xEED5C5]=0xEEC5DA, + [0xEED5CD]=0xEECDDE, [0xEEDED5]=0xEED5E2, [0xF68B9C]=0xF68BC1, [0xF6949C]=0xF694C5, + [0xF694A4]=0xF694C5, [0xF69CA4]=0xF69CC9, [0xF69CAC]=0xF69CC9, [0xF6A4AC]=0xF6A4CD, + [0xF6A4B4]=0xF6A4CD, [0xF6AC9C]=0xF69CC9, [0xF6BDAC]=0xF6ACD1, [0xF6C5B4]=0xF6B4D5, + [0xF6C5BD]=0xF6BDDA, [0xF6CDBD]=0xF6BDDA, [0xF6CDC5]=0xF6C5DE, [0xF6D5C5]=0xF6C5DE, + [0xF6D5CD]=0xF6CDE2, [0xF6D5D5]=0xF6D5E6, [0xF6DECD]=0xF6CDE2, [0xF6DED5]=0xF6D5E6, + [0xF6E6DE]=0xF6DEEA, [0xFF8B9C]=0xFF8BC5, [0xFF949C]=0xFF94CA, [0xFF94A4]=0xFF94CA, + [0xFF9CA4]=0xFF9CCE, [0xFFC5B4]=0xFFB4DA, [0xFFCDBD]=0xFFBDDE, [0xFFCDC5]=0xFFC5E2, + [0xFFD5C5]=0xFFC5E2, [0xFFD5CD]=0xFFCDE6, [0xFFDECD]=0xFFCDE6, [0xFFDED5]=0xFFD5EA, + [0xFFE6D5]=0xFFD5EA, [0xFFE6DE]=0xFFDEEE, [0xFFEEDE]=0xFFDEEE, [0xFFEEE6]=0xFFE6F2, + [0xFFEEEE]=0xFFEEF6, [0xFFF6EE]=0xFFEEF6, [0xFFF6F6]=0xFFF6FB, + }, + }, + [37] = { + name = "vulpix", + hueRange = { min = -30, max = 35 }, + slide = { h = 27, l = 0, s = 3 }, + }, + [38] = { + name = "ninetales", + hueRange = { min = -30, max = 50 }, + slide = { h = -130, l = 0, s = -2 }, + }, + [39] = { + name = "jigglypuff", + hueRange = { min = -40, max = 40 }, + -- Stadium ships a real alternate texture for this one; 394 + -- colours move, every other colour stays exactly as it was. + lut = { + [0x002018]=0x122000, [0x082018]=0x152206, [0x082920]=0x1A2B06, [0x083931]=0x233D04, + [0x103931]=0x273C0D, [0x104139]=0x2B450C, [0x104A39]=0x304E0C, [0x183939]=0x2A3B16, + [0x184139]=0x2F4415, [0x184A39]=0x344E14, [0x184A41]=0x344E14, [0x18524A]=0x385614, + [0x185A4A]=0x3D5F13, [0x204141]=0x32431E, [0x204A41]=0x374D1D, [0x204A4A]=0x374D1D, + [0x20524A]=0x3C561C, [0x205A52]=0x405E1C, [0x206252]=0x45671B, [0x20625A]=0x45671B, + [0x206A5A]=0x49701A, [0x206A62]=0x49701A, [0x294141]=0x364327, [0x294A41]=0x3B4C27, + [0x294A4A]=0x3B4C27, [0x29524A]=0x405526, [0x295252]=0x405526, [0x295A52]=0x445E25, + [0x295A5A]=0x445E25, [0x296252]=0x496625, [0x29625A]=0x496625, [0x296A5A]=0x4D6F24, + [0x296A62]=0x4D6F24, [0x297362]=0x527923, [0x29736A]=0x527923, [0x31524A]=0x43542F, + [0x315252]=0x43542F, [0x315A52]=0x485D2E, [0x315A5A]=0x485D2E, [0x31625A]=0x4C662D, + [0x316A62]=0x516E2D, [0x317362]=0x56782C, [0x31736A]=0x56782C, [0x317B6A]=0x5A812B, + [0x317B73]=0x5A812B, [0x395252]=0x475437, [0x395A52]=0x4B5C37, [0x395A5A]=0x4B5C37, + [0x395A62]=0x506536, [0x39625A]=0x506536, [0x396262]=0x506536, [0x396A62]=0x546E35, + [0x39736A]=0x597735, [0x397B6A]=0x5E8034, [0x397B73]=0x5E8034, [0x398373]=0x628933, + [0x398B7B]=0x679133, [0x415A5A]=0x4F5C3F, [0x416262]=0x53643F, [0x416A6A]=0x586D3E, + [0x41736A]=0x5D773D, [0x417373]=0x5D773D, [0x417B6A]=0x617F3D, [0x417B73]=0x617F3D, + [0x418373]=0x66883C, [0x41837B]=0x66883C, [0x418B7B]=0x6A913B, [0x418B83]=0x6A913B, + [0x419483]=0x6F9A3B, [0x41948B]=0x6F9A3B, [0x41A494]=0x78AB3A, [0x4A3139]=0x483348, + [0x4A6A6A]=0x5C6C48, [0x4A6A73]=0x617647, [0x4A7373]=0x617647, [0x4A7B73]=0x657F46, + [0x4A837B]=0x6A8746, [0x4A8B83]=0x6E9045, [0x4A9483]=0x739A44, [0x4A948B]=0x739A44, + [0x4A9C8B]=0x78A244, [0x4AA494]=0x7CAB43, [0x4AAC9C]=0x81B343, [0x523139]=0x503350, + [0x523141]=0x503350, [0x523941]=0x503B50, [0x527373]=0x647550, [0x52737B]=0x697E4F, + [0x527B7B]=0x697E4F, [0x528383]=0x6D874E, [0x52948B]=0x77994D, [0x529C8B]=0x7BA24C, + [0x529C94]=0x7BA24C, [0x52A494]=0x80AA4C, [0x52A49C]=0x80AA4C, [0x52AC9C]=0x84B34B, + [0x5A3941]=0x583B58, [0x5A394A]=0x583B58, [0x5A414A]=0x584358, [0x5A4152]=0x584358, + [0x5A737B]=0x6C7D58, [0x5A7B7B]=0x6C7D58, [0x5A7B83]=0x718657, [0x5A837B]=0x718657, + [0x5A8383]=0x718657, [0x5A948B]=0x7A9856, [0x5A9C94]=0x7FA155, [0x5AA494]=0x83AA54, + [0x5AA49C]=0x83AA54, [0x5AAC9C]=0x88B254, [0x5AACA4]=0x88B254, [0x5AB4A4]=0x8CBB53, + [0x62394A]=0x5F3C5F, [0x62414A]=0x604360, [0x62A49C]=0x87A95D, [0x62AC9C]=0x8BB25C, + [0x62B4A4]=0x90BA5C, [0x62B4AC]=0x90BA5C, [0x6A414A]=0x674467, [0x6A948B]=0x819767, + [0x6AB4A4]=0x93BA64, [0x6AB4AC]=0x93BA64, [0x6ABDAC]=0x98C364, [0x6ABDB4]=0x98C364, + [0x6AC5B4]=0x9DCC63, [0x6AC5BD]=0x9DCC63, [0x73525A]=0x715471, [0x73C5B4]=0xA1CB6D, + [0x73C5BD]=0xA1CB6D, [0x7B4A5A]=0x774E77, [0x7B525A]=0x785578, [0x7B5A62]=0x795C79, + [0x7B9CA4]=0x92A778, [0x7BB4AC]=0x9BB877, [0x7BBDB4]=0xA0C276, [0x7BC5B4]=0xA4CB75, + [0x7BC5BD]=0xA4CB75, [0x7BCDBD]=0xA9D375, [0x7BCDC5]=0xA9D375, [0x7BD5C5]=0xADDC74, + [0x83525A]=0x7F567F, [0x835262]=0x7F567F, [0x835A62]=0x805D80, [0x835A6A]=0x805D80, + [0x839CA4]=0x95A681, [0x83A4A4]=0x95A681, [0x83ACA4]=0x9AAF80, [0x83C5BD]=0xA8CA7E, + [0x83CDC5]=0xACD37D, [0x83D5C5]=0xB1DB7D, [0x83D5CD]=0xB1DB7D, [0x83DECD]=0xB6E57C, + [0x8B5262]=0x875687, [0x8B5A62]=0x875E87, [0x8B5A6A]=0x875E87, [0x8B626A]=0x886588, + [0x8BA4AC]=0x9DAE89, [0x8BB4AC]=0xA2B788, [0x8BB4B4]=0xA2B788, [0x8BBDB4]=0xA7C187, + [0x8BCDC5]=0xB0D286, [0x8BD5CD]=0xB4DB85, [0x945A6A]=0x905E90, [0x94626A]=0x906690, + [0x946273]=0x906690, [0x946A73]=0x916D91, [0x946A7B]=0x916D91, [0x94ACB4]=0xA6B692, + [0x94B4AC]=0xA6B692, [0x94B4B4]=0xA6B692, [0x94BDB4]=0xABC091, [0x94D5CD]=0xB8DA8F, + [0x94DED5]=0xBDE48E, [0x94EEE6]=0xC6F58D, [0x9C6273]=0x986698, [0x9C6A73]=0x986E98, + [0x9C6A7B]=0x986E98, [0x9C737B]=0x997699, [0x9C7383]=0x997699, [0x9CB4BD]=0xAEBF9A, + [0x9CBDBD]=0xAEBF9A, [0x9CC5BD]=0xB3C899, [0x9CC5C5]=0xB3C899, [0x9CDED5]=0xC1E397, + [0x9CE6DE]=0xC5EC96, [0xA44141]=0x9D489D, [0xA46A7B]=0xA06EA0, [0xA4737B]=0xA077A0, + [0xA47383]=0xA077A0, [0xA47B83]=0xA17EA1, [0xA47B8B]=0xA17EA1, [0xA4838B]=0xA285A2, + [0xA4BDBD]=0xB2BFA2, [0xA4CDC5]=0xBBD0A1, [0xA4E6DE]=0xC9EB9F, [0xA4EEDE]=0xCDF49E, + [0xAC4A4A]=0xA551A5, [0xAC4A52]=0xA551A5, [0xAC525A]=0xA559A5, [0xAC7383]=0xA877A8, + [0xAC838B]=0xA986A9, [0xAC8394]=0xA986A9, [0xAC8B94]=0xAA8DAA, [0xACBDC5]=0xBAC7AA, + [0xACC5C5]=0xBAC7AA, [0xACCDCD]=0xBECFAA, [0xACEEE6]=0xD1F3A7, [0xB44A52]=0xAC52AC, + [0xB45252]=0xAD59AD, [0xB45A5A]=0xAD61AD, [0xB45A62]=0xAD61AD, [0xB47B8B]=0xB07FB0, + [0xB4838B]=0xB087B0, [0xB48394]=0xB087B0, [0xB48B94]=0xB18EB1, [0xB48B9C]=0xB18EB1, + [0xB4949C]=0xB296B2, [0xB4C5CD]=0xC2CFB2, [0xB4EEE6]=0xD4F2B0, [0xB4F6EE]=0xD9FBAF, + [0xBD525A]=0xB55AB5, [0xBD5A62]=0xB661B6, [0xBD6262]=0xB669B6, [0xBD626A]=0xB669B6, + [0xBD6A6A]=0xB770B7, [0xBD838B]=0xB987B9, [0xBD8394]=0xB987B9, [0xBD8B94]=0xB98FB9, + [0xBD8B9C]=0xB98FB9, [0xBD949C]=0xBA97BA, [0xBD94A4]=0xBA97BA, [0xBDD5D5]=0xCAD7BB, + [0xBDEEE6]=0xD8F2B9, [0xBDEEEE]=0xD8F2B9, [0xBDF6EE]=0xDDFAB9, [0xC55A62]=0xBD62BD, + [0xC5626A]=0xBE69BE, [0xC56A6A]=0xBE71BE, [0xC56A73]=0xBE71BE, [0xC57373]=0xBF79BF, + [0xC5737B]=0xBF79BF, [0xC58B9C]=0xC18FC1, [0xC5949C]=0xC198C1, [0xC594A4]=0xC198C1, + [0xC59CA4]=0xC29FC2, [0xC5CDD5]=0xCED6C4, [0xC5D5D5]=0xCED6C4, [0xC5D5DE]=0xD3E0C3, + [0xC5DEDE]=0xD3E0C3, [0xC5EEEE]=0xDCF1C2, [0xC5F6EE]=0xE0FAC1, [0xC5F6F6]=0xE0FAC1, + [0xC5FFF6]=0xE5FFC5, [0xCD6A73]=0xC671C6, [0xCD7373]=0xC67AC6, [0xCD737B]=0xC67AC6, + [0xCD7B7B]=0xC781C7, [0xCD7B83]=0xC781C7, [0xCD8383]=0xC789C7, [0xCD838B]=0xC789C7, + [0xCD949C]=0xC998C9, [0xCD94A4]=0xC998C9, [0xCD9CA4]=0xC9A0C9, [0xCD9CAC]=0xC9A0C9, + [0xCDA4AC]=0xCAA7CA, [0xCDD5DE]=0xD6DFCC, [0xCDDEDE]=0xD6DFCC, [0xCDF6EE]=0xE4F9CA, + [0xCDF6F6]=0xE4F9CA, [0xCDFFF6]=0xE8FFCD, [0xD5737B]=0xCE7ACE, [0xD57B83]=0xCE82CE, + [0xD58383]=0xCF89CF, [0xD5838B]=0xCF89CF, [0xD58B8B]=0xCF91CF, [0xD58B94]=0xCF91CF, + [0xD58B9C]=0xCF91CF, [0xD59494]=0xD099D0, [0xD5949C]=0xD099D0, [0xD59CA4]=0xD1A0D1, + [0xD59CAC]=0xD1A0D1, [0xD5A4AC]=0xD1A8D1, [0xD5A4B4]=0xD1A8D1, [0xD5ACB4]=0xD2AFD2, + [0xD5DEE6]=0xDEE7D4, [0xD5E6E6]=0xDEE7D4, [0xD5FFF6]=0xECFFD5, [0xD5FFFF]=0xECFFD5, + [0xDE737B]=0xD67BD6, [0xDE8383]=0xD78AD7, [0xDE838B]=0xD78AD7, [0xDE8394]=0xD78AD7, + [0xDE8B8B]=0xD891D8, [0xDE8B94]=0xD891D8, [0xDE8B9C]=0xD891D8, [0xDE9494]=0xD89AD8, + [0xDE949C]=0xD89AD8, [0xDE94A4]=0xD89AD8, [0xDE9C9C]=0xD9A1D9, [0xDE9CA4]=0xD9A1D9, + [0xDEA4AC]=0xDAA8DA, [0xDEA4B4]=0xDAA8DA, [0xDEACB4]=0xDAB0DA, [0xDEACBD]=0xDAB0DA, + [0xDEB4BD]=0xDBB7DB, [0xDEF6F6]=0xEBF8DC, [0xDEFFF6]=0xF0FFDE, [0xDEFFFF]=0xF0FFDE, + [0xE67B7B]=0xDE83DE, [0xE6838B]=0xDF8ADF, [0xE68B94]=0xDF92DF, [0xE68B9C]=0xDF92DF, + [0xE69494]=0xE09AE0, [0xE6949C]=0xE09AE0, [0xE69C9C]=0xE0A2E0, [0xE69CA4]=0xE0A2E0, + [0xE69CAC]=0xE0A2E0, [0xE6A4A4]=0xE1A9E1, [0xE6A4AC]=0xE1A9E1, [0xE6A4B4]=0xE1A9E1, + [0xE6ACAC]=0xE2B0E2, [0xE6ACB4]=0xE2B0E2, [0xE6ACBD]=0xE2B0E2, [0xE6B4B4]=0xE2B8E2, + [0xE6B4BD]=0xE2B8E2, [0xE6B4C5]=0xE2B8E2, [0xE6BDC5]=0xE3C0E3, [0xE6BDCD]=0xE3C0E3, + [0xE6E6EE]=0xEAEFE5, [0xE6EEEE]=0xEAEFE5, [0xE6F6F6]=0xEFF7E5, [0xE6FFFF]=0xF4FFE6, + [0xEE8B8B]=0xE792E7, [0xEE9494]=0xE79BE7, [0xEE949C]=0xE79BE7, [0xEE9C9C]=0xE8A2E8, + [0xEE9CA4]=0xE8A2E8, [0xEEA4A4]=0xE8AAE8, [0xEEA4AC]=0xE8AAE8, [0xEEA4B4]=0xE8AAE8, + [0xEEACAC]=0xE9B1E9, [0xEEACB4]=0xE9B1E9, [0xEEACBD]=0xE9B1E9, [0xEEB4BD]=0xEAB8EA, + [0xEEB4C5]=0xEAB8EA, [0xEEBDC5]=0xEAC1EA, [0xEEBDCD]=0xEAC1EA, [0xEEC5D5]=0xEBC8EB, + [0xEEEEF6]=0xF2F7ED, [0xEEF6F6]=0xF2F7ED, [0xEEFFFF]=0xF7FFEE, [0xF68383]=0xED8CED, + [0xF68B94]=0xEE93EE, [0xF69494]=0xEF9BEF, [0xF69C9C]=0xEFA3EF, [0xF69CA4]=0xEFA3EF, + [0xF6A4A4]=0xF0AAF0, [0xF6A4AC]=0xF0AAF0, [0xF6ACAC]=0xF0B2F0, [0xF6B4B4]=0xF1B9F1, + [0xF6B4BD]=0xF1B9F1, [0xF6B4C5]=0xF1B9F1, [0xF6BDC5]=0xF2C1F2, [0xF6BDCD]=0xF2C1F2, + [0xF6C5CD]=0xF2C9F2, [0xF6C5D5]=0xF2C9F2, [0xF6D5DE]=0xF4D7F4, [0xF6FFFF]=0xFBFFF6, + [0xFF8383]=0xF68CF6, [0xFF8B8B]=0xF694F6, [0xFF9494]=0xF79CF7, [0xFF949C]=0xF79CF7, + [0xFF9C9C]=0xF8A3F8, [0xFF9CA4]=0xF8A3F8, [0xFFA4A4]=0xF8ABF8, [0xFFA4AC]=0xF8ABF8, + [0xFFACAC]=0xF9B2F9, [0xFFACB4]=0xF9B2F9, [0xFFB4B4]=0xF9BAF9, [0xFFB4BD]=0xF9BAF9, + [0xFFBDBD]=0xFAC2FA, [0xFFBDC5]=0xFAC2FA, [0xFFBDCD]=0xFAC2FA, [0xFFC5C5]=0xFBC9FB, + [0xFFC5CD]=0xFBC9FB, [0xFFC5D5]=0xFBC9FB, [0xFFCDD5]=0xFBD1FB, [0xFFCDDE]=0xFBD1FB, + [0xFFD5DE]=0xFCD8FC, [0xFFD5E6]=0xFCD8FC, [0xFFDEDE]=0xFDE0FD, [0xFFDEE6]=0xFDE0FD, + [0xFFE6E6]=0xFDE8FD, [0xFFE6EE]=0xFDE8FD, + }, + }, + [40] = { + name = "wigglytuff", + hueRange = { min = -40, max = 40 }, + -- Stadium ships a real alternate texture for this one; 409 + -- colours move, every other colour stays exactly as it was. + lut = { + [0x080008]=0x070107, [0x082018]=0x152206, [0x082920]=0x1A2B06, [0x083931]=0x233D04, + [0x100808]=0x0F090F, [0x103931]=0x273C0D, [0x104139]=0x2B450C, [0x104A39]=0x304E0C, + [0x183939]=0x2A3B16, [0x184139]=0x2F4415, [0x184A39]=0x344E14, [0x184A41]=0x344E14, + [0x18524A]=0x385614, [0x185A4A]=0x3D5F13, [0x204141]=0x32431E, [0x204A41]=0x374D1D, + [0x204A4A]=0x374D1D, [0x20524A]=0x3C561C, [0x205A52]=0x405E1C, [0x206252]=0x45671B, + [0x20625A]=0x45671B, [0x294A4A]=0x3B4C27, [0x29524A]=0x405526, [0x295252]=0x405526, + [0x295A52]=0x445E25, [0x295A5A]=0x445E25, [0x296252]=0x496625, [0x29625A]=0x496625, + [0x296A5A]=0x4D6F24, [0x29736A]=0x527923, [0x31524A]=0x43542F, [0x315252]=0x43542F, + [0x315A52]=0x485D2E, [0x315A5A]=0x485D2E, [0x31625A]=0x4C662D, [0x316A62]=0x516E2D, + [0x317362]=0x56782C, [0x31736A]=0x56782C, [0x317B6A]=0x5A812B, [0x317B73]=0x5A812B, + [0x395252]=0x475437, [0x395A52]=0x4B5C37, [0x395A5A]=0x4B5C37, [0x395A62]=0x506536, + [0x39625A]=0x506536, [0x396262]=0x506536, [0x396A62]=0x546E35, [0x39736A]=0x597735, + [0x397B6A]=0x5E8034, [0x397B73]=0x5E8034, [0x398373]=0x628933, [0x398B7B]=0x679133, + [0x415A5A]=0x4F5C3F, [0x416262]=0x53643F, [0x416A6A]=0x586D3E, [0x41736A]=0x5D773D, + [0x417373]=0x5D773D, [0x417B6A]=0x617F3D, [0x417B73]=0x617F3D, [0x418373]=0x66883C, + [0x41837B]=0x66883C, [0x418B7B]=0x6A913B, [0x418B83]=0x6A913B, [0x419483]=0x6F9A3B, + [0x41A494]=0x78AB3A, [0x4A3139]=0x483348, [0x4A6A6A]=0x5C6C48, [0x4A6A73]=0x617647, + [0x4A7373]=0x617647, [0x4A7B73]=0x657F46, [0x4A837B]=0x6A8746, [0x4A8B83]=0x6E9045, + [0x4A9483]=0x739A44, [0x4A948B]=0x739A44, [0x4A9C8B]=0x78A244, [0x4AAC9C]=0x81B343, + [0x523139]=0x503350, [0x523141]=0x503350, [0x523941]=0x503B50, [0x527373]=0x647550, + [0x52737B]=0x697E4F, [0x527B7B]=0x697E4F, [0x528383]=0x6D874E, [0x52948B]=0x77994D, + [0x529C8B]=0x7BA24C, [0x529C94]=0x7BA24C, [0x52A494]=0x80AA4C, [0x52A49C]=0x80AA4C, + [0x52AC9C]=0x84B34B, [0x5A3941]=0x583B58, [0x5A394A]=0x583B58, [0x5A414A]=0x584358, + [0x5A4152]=0x584358, [0x5A737B]=0x6C7D58, [0x5A7B7B]=0x6C7D58, [0x5A7B83]=0x718657, + [0x5A8383]=0x718657, [0x5A948B]=0x7A9856, [0x5A9C94]=0x7FA155, [0x5AA494]=0x83AA54, + [0x5AA49C]=0x83AA54, [0x5AAC9C]=0x88B254, [0x5AACA4]=0x88B254, [0x62394A]=0x5F3C5F, + [0x62414A]=0x604360, [0x62A49C]=0x87A95D, [0x62AC9C]=0x8BB25C, [0x62B4A4]=0x90BA5C, + [0x62B4AC]=0x90BA5C, [0x6A414A]=0x674467, [0x6A948B]=0x819767, [0x6AB4A4]=0x93BA64, + [0x6AB4AC]=0x93BA64, [0x6ABDAC]=0x98C364, [0x6ABDB4]=0x98C364, [0x6AC5B4]=0x9DCC63, + [0x73525A]=0x715471, [0x73C5B4]=0xA1CB6D, [0x73C5BD]=0xA1CB6D, [0x7B4A5A]=0x774E77, + [0x7B525A]=0x785578, [0x7B5A62]=0x795C79, [0x7B9CA4]=0x92A778, [0x7BB4AC]=0x9BB877, + [0x7BBDB4]=0xA0C276, [0x7BC5B4]=0xA4CB75, [0x7BC5BD]=0xA4CB75, [0x7BCDBD]=0xA9D375, + [0x7BCDC5]=0xA9D375, [0x7BD5C5]=0xADDC74, [0x83525A]=0x7F567F, [0x835262]=0x7F567F, + [0x835A62]=0x805D80, [0x835A6A]=0x805D80, [0x839CA4]=0x95A681, [0x83ACA4]=0x9AAF80, + [0x83C5BD]=0xA8CA7E, [0x83CDC5]=0xACD37D, [0x83D5C5]=0xB1DB7D, [0x83D5CD]=0xB1DB7D, + [0x8B5262]=0x875687, [0x8B5A62]=0x875E87, [0x8B5A6A]=0x875E87, [0x8B626A]=0x886588, + [0x8BA4AC]=0x9DAE89, [0x8BB4AC]=0xA2B788, [0x8BB4B4]=0xA2B788, [0x8BCDC5]=0xB0D286, + [0x8BD5CD]=0xB4DB85, [0x945A6A]=0x905E90, [0x94626A]=0x906690, [0x946273]=0x906690, + [0x946A73]=0x916D91, [0x946A7B]=0x916D91, [0x94ACB4]=0xA6B692, [0x94B4AC]=0xA6B692, + [0x94B4B4]=0xA6B692, [0x94BDB4]=0xABC091, [0x94D5CD]=0xB8DA8F, [0x94DED5]=0xBDE48E, + [0x9C6273]=0x986698, [0x9C6A73]=0x986E98, [0x9C6A7B]=0x986E98, [0x9C737B]=0x997699, + [0x9C7383]=0x997699, [0x9CB4BD]=0xAEBF9A, [0x9CBDBD]=0xAEBF9A, [0x9CC5BD]=0xB3C899, + [0x9CC5C5]=0xB3C899, [0x9CDED5]=0xC1E397, [0x9CE6DE]=0xC5EC96, [0xA44141]=0x9D489D, + [0xA46A7B]=0xA06EA0, [0xA4737B]=0xA077A0, [0xA47383]=0xA077A0, [0xA47B83]=0xA17EA1, + [0xA47B8B]=0xA17EA1, [0xA4838B]=0xA285A2, [0xA4BDBD]=0xB2BFA2, [0xA4E6DE]=0xC9EB9F, + [0xA4EEDE]=0xCDF49E, [0xAC4A4A]=0xA551A5, [0xAC4A52]=0xA551A5, [0xAC525A]=0xA559A5, + [0xAC626A]=0xA668A6, [0xAC7383]=0xA877A8, [0xAC838B]=0xA986A9, [0xAC8394]=0xA986A9, + [0xAC8B94]=0xAA8DAA, [0xACBDC5]=0xBAC7AA, [0xACC5C5]=0xBAC7AA, [0xACCDCD]=0xBECFAA, + [0xACEEE6]=0xD1F3A7, [0xB44A52]=0xAC52AC, [0xB45252]=0xAD59AD, [0xB45A5A]=0xAD61AD, + [0xB45A62]=0xAD61AD, [0xB46A73]=0xAE70AE, [0xB46A7B]=0xAE70AE, [0xB47373]=0xAF78AF, + [0xB4737B]=0xAF78AF, [0xB47B8B]=0xB07FB0, [0xB4838B]=0xB087B0, [0xB48394]=0xB087B0, + [0xB48B94]=0xB18EB1, [0xB48B9C]=0xB18EB1, [0xB4949C]=0xB296B2, [0xB4EEE6]=0xD4F2B0, + [0xB4F6EE]=0xD9FBAF, [0xBD525A]=0xB55AB5, [0xBD5A62]=0xB661B6, [0xBD6262]=0xB669B6, + [0xBD626A]=0xB669B6, [0xBD6A6A]=0xB770B7, [0xBD737B]=0xB779B7, [0xBD7B7B]=0xB880B8, + [0xBD7B83]=0xB880B8, [0xBD838B]=0xB987B9, [0xBD8394]=0xB987B9, [0xBD8B94]=0xB98FB9, + [0xBD949C]=0xBA97BA, [0xBD94A4]=0xBA97BA, [0xBDD5D5]=0xCAD7BB, [0xBDEEEE]=0xD8F2B9, + [0xBDF6EE]=0xDDFAB9, [0xC55A62]=0xBD62BD, [0xC5626A]=0xBE69BE, [0xC56A6A]=0xBE71BE, + [0xC56A73]=0xBE71BE, [0xC57373]=0xBF79BF, [0xC5737B]=0xBF79BF, [0xC57B7B]=0xBF81BF, + [0xC57B83]=0xBF81BF, [0xC58383]=0xC088C0, [0xC5838B]=0xC088C0, [0xC58B9C]=0xC18FC1, + [0xC5949C]=0xC198C1, [0xC594A4]=0xC198C1, [0xC59CA4]=0xC29FC2, [0xC5ACB4]=0xC3AEC3, + [0xC5CDD5]=0xCED6C4, [0xC5D5D5]=0xCED6C4, [0xC5D5DE]=0xD3E0C3, [0xC5DEDE]=0xD3E0C3, + [0xC5F6EE]=0xE0FAC1, [0xC5F6F6]=0xE0FAC1, [0xC5FFF6]=0xE5FFC5, [0xCD6A73]=0xC671C6, + [0xCD7373]=0xC67AC6, [0xCD737B]=0xC67AC6, [0xCD7B7B]=0xC781C7, [0xCD7B83]=0xC781C7, + [0xCD8383]=0xC789C7, [0xCD838B]=0xC789C7, [0xCD8B8B]=0xC890C8, [0xCD8B94]=0xC890C8, + [0xCD949C]=0xC998C9, [0xCD94A4]=0xC998C9, [0xCD9CA4]=0xC9A0C9, [0xCD9CAC]=0xC9A0C9, + [0xCDA4AC]=0xCAA7CA, [0xCDB4BD]=0xCBB6CB, [0xCDD5DE]=0xD6DFCC, [0xCDDEDE]=0xD6DFCC, + [0xCDF6EE]=0xE4F9CA, [0xCDF6F6]=0xE4F9CA, [0xCDFFF6]=0xE8FFCD, [0xD5737B]=0xCE7ACE, + [0xD57B83]=0xCE82CE, [0xD58383]=0xCF89CF, [0xD5838B]=0xCF89CF, [0xD58B8B]=0xCF91CF, + [0xD58B94]=0xCF91CF, [0xD58B9C]=0xCF91CF, [0xD59494]=0xD099D0, [0xD5949C]=0xD099D0, + [0xD594A4]=0xD099D0, [0xD59C9C]=0xD1A0D1, [0xD59CA4]=0xD1A0D1, [0xD59CAC]=0xD1A0D1, + [0xD5A4AC]=0xD1A8D1, [0xD5A4B4]=0xD1A8D1, [0xD5ACB4]=0xD2AFD2, [0xD5B4BD]=0xD3B6D3, + [0xD5BDBD]=0xD3BFD3, [0xD5BDC5]=0xD3BFD3, [0xD5DEE6]=0xDEE7D4, [0xD5E6E6]=0xDEE7D4, + [0xD5FFFF]=0xECFFD5, [0xDE737B]=0xD67BD6, [0xDE8383]=0xD78AD7, [0xDE838B]=0xD78AD7, + [0xDE8394]=0xD78AD7, [0xDE8B8B]=0xD891D8, [0xDE8B94]=0xD891D8, [0xDE8B9C]=0xD891D8, + [0xDE9494]=0xD89AD8, [0xDE949C]=0xD89AD8, [0xDE94A4]=0xD89AD8, [0xDE9C9C]=0xD9A1D9, + [0xDE9CA4]=0xD9A1D9, [0xDEA4A4]=0xDAA8DA, [0xDEA4AC]=0xDAA8DA, [0xDEA4B4]=0xDAA8DA, + [0xDEACB4]=0xDAB0DA, [0xDEACBD]=0xDAB0DA, [0xDEB4BD]=0xDBB7DB, [0xDEBDC5]=0xDCBFDC, + [0xDEC5CD]=0xDCC7DC, [0xDECDCD]=0xDDCEDD, [0xDEF6F6]=0xEBF8DC, [0xDEFFF6]=0xF0FFDE, + [0xDEFFFF]=0xF0FFDE, [0xE67B7B]=0xDE83DE, [0xE6838B]=0xDF8ADF, [0xE68B94]=0xDF92DF, + [0xE68B9C]=0xDF92DF, [0xE69494]=0xE09AE0, [0xE6949C]=0xE09AE0, [0xE69C9C]=0xE0A2E0, + [0xE69CA4]=0xE0A2E0, [0xE69CAC]=0xE0A2E0, [0xE6A4A4]=0xE1A9E1, [0xE6A4AC]=0xE1A9E1, + [0xE6A4B4]=0xE1A9E1, [0xE6ACAC]=0xE2B0E2, [0xE6ACB4]=0xE2B0E2, [0xE6ACBD]=0xE2B0E2, + [0xE6B4B4]=0xE2B8E2, [0xE6B4BD]=0xE2B8E2, [0xE6B4C5]=0xE2B8E2, [0xE6BDC5]=0xE3C0E3, + [0xE6BDCD]=0xE3C0E3, [0xE6C5CD]=0xE4C7E4, [0xE6E6EE]=0xEAEFE5, [0xE6EEEE]=0xEAEFE5, + [0xE6F6F6]=0xEFF7E5, [0xE6FFFF]=0xF4FFE6, [0xEE8B8B]=0xE792E7, [0xEE9494]=0xE79BE7, + [0xEE949C]=0xE79BE7, [0xEE9C9C]=0xE8A2E8, [0xEE9CA4]=0xE8A2E8, [0xEEA4A4]=0xE8AAE8, + [0xEEA4AC]=0xE8AAE8, [0xEEA4B4]=0xE8AAE8, [0xEEACAC]=0xE9B1E9, [0xEEACB4]=0xE9B1E9, + [0xEEACBD]=0xE9B1E9, [0xEEB4BD]=0xEAB8EA, [0xEEB4C5]=0xEAB8EA, [0xEEBDC5]=0xEAC1EA, + [0xEEBDCD]=0xEAC1EA, [0xEEC5CD]=0xEBC8EB, [0xEEC5D5]=0xEBC8EB, [0xEECDD5]=0xECCFEC, + [0xEED5D5]=0xECD7EC, [0xEED5DE]=0xECD7EC, [0xEEEEF6]=0xF2F7ED, [0xEEF6F6]=0xF2F7ED, + [0xEEFFFF]=0xF7FFEE, [0xF68383]=0xED8CED, [0xF68B94]=0xEE93EE, [0xF69C9C]=0xEFA3EF, + [0xF69CA4]=0xEFA3EF, [0xF6A4A4]=0xF0AAF0, [0xF6A4AC]=0xF0AAF0, [0xF6ACAC]=0xF0B2F0, + [0xF6B4B4]=0xF1B9F1, [0xF6B4BD]=0xF1B9F1, [0xF6B4C5]=0xF1B9F1, [0xF6BDC5]=0xF2C1F2, + [0xF6BDCD]=0xF2C1F2, [0xF6C5CD]=0xF2C9F2, [0xF6C5D5]=0xF2C9F2, [0xF6CDD5]=0xF3D0F3, + [0xF6D5D5]=0xF4D7F4, [0xF6D5DE]=0xF4D7F4, [0xF6FFFF]=0xFBFFF6, [0xFF8383]=0xF68CF6, + [0xFF8B8B]=0xF694F6, [0xFF9494]=0xF79CF7, [0xFF949C]=0xF79CF7, [0xFF9C9C]=0xF8A3F8, + [0xFF9CA4]=0xF8A3F8, [0xFFA4A4]=0xF8ABF8, [0xFFA4AC]=0xF8ABF8, [0xFFACAC]=0xF9B2F9, + [0xFFACB4]=0xF9B2F9, [0xFFB4B4]=0xF9BAF9, [0xFFBDBD]=0xFAC2FA, [0xFFBDCD]=0xFAC2FA, + [0xFFC5C5]=0xFBC9FB, [0xFFC5CD]=0xFBC9FB, [0xFFC5D5]=0xFBC9FB, [0xFFCDCD]=0xFBD1FB, + [0xFFCDD5]=0xFBD1FB, [0xFFCDDE]=0xFBD1FB, [0xFFD5D5]=0xFCD8FC, [0xFFD5DE]=0xFCD8FC, + [0xFFD5E6]=0xFCD8FC, [0xFFDEDE]=0xFDE0FD, [0xFFDEE6]=0xFDE0FD, [0xFFE6E6]=0xFDE8FD, + [0xFFE6EE]=0xFDE8FD, [0xFFEEEE]=0xFEEFFE, [0xFFEEF6]=0xFEEFFE, [0xFFF6F6]=0xFEF7FE, + [0xFFF6FF]=0xFEF7FE, + }, + }, + [41] = { + name = "zubat", + hueRange = { min = -30, max = 30 }, + slide = { h = -110, l = -1, s = -2 }, + }, + [42] = { + name = "golbat", + hueRange = { min = -25, max = 30 }, + slide = { h = -63, l = -6, s = -4 }, + }, + [43] = { + name = "oddish", + hueRange = { min = -40, max = 70 }, + slide = { h = -50, l = 2, s = 3 }, + }, + [44] = { + name = "gloom", + hueRange = { min = -40, max = 65 }, + slide = { h = 30, l = 3, s = 1 }, + }, + [45] = { + name = "vileplume", + hueRange = { min = -25, max = 60 }, + slide = { h = 50, l = 3, s = 1 }, + }, + [46] = { + name = "paras", + hueRange = { min = -20, max = 30 }, + slide = { h = 25, l = -3, s = 3 }, + }, + [47] = { + name = "parasect", + hueRange = { min = -20, max = 35 }, + slide = { h = 40, l = -4, s = 6 }, + }, + [48] = { + name = "venonat", + hueRange = { min = -60, max = 60 }, + slide = { h = -90, l = 0, s = 0 }, + }, + [49] = { + name = "venomoth", + hueRange = { min = -60, max = 100 }, + slide = { h = -90, l = 0, s = 1 }, + }, + [50] = { + name = "diglett", + hueRange = { min = -20, max = 35 }, + slide = { h = -50, l = 1, s = -2 }, + }, + [51] = { + name = "dugtrio", + hueRange = { min = -20, max = 35 }, + slide = { h = -50, l = 1, s = -2 }, + }, + [52] = { + name = "meowth", + hueRange = { min = -40, max = 35 }, + slide = { h = -55, l = 2, s = 0 }, + }, + [53] = { + name = "persian", + hueRange = { min = -20, max = 50 }, + slide = { h = -30, l = 0, s = -2 }, + }, + [54] = { + name = "psyduck", + hueRange = { min = -20, max = 30 }, + slide = { h = -142, l = 2, s = -2 }, + }, + [55] = { + name = "golduck", + hueRange = { min = -60, max = 50 }, + slide = { h = 20, l = 0, s = 3 }, + }, + [56] = { + name = "mankey", + hueRange = { min = -35, max = 30 }, + slide = { h = 105, l = -1, s = -3 }, + }, + [57] = { + name = "primeape", + hueRange = { min = -35, max = 40 }, + slide = { h = -50, l = 1, s = -3 }, + }, + [58] = { + name = "growlithe", + hueRange = { min = -35, max = 10 }, + slide = { h = 20, l = 0, s = 2 }, + }, + [59] = { + name = "arcanine", + hueRange = { min = -30, max = 20 }, + slide = { h = 25, l = 1, s = -3 }, + }, + [60] = { + name = "poliwag", + hueRange = { min = -50, max = 50 }, + slide = { h = -20, l = 0, s = 3 }, + }, + [61] = { + name = "poliwhirl", + hueRange = { min = -50, max = 50 }, + slide = { h = 20, l = -2, s = 2 }, + }, + [62] = { + name = "poliwrath", + hueRange = { min = -50, max = 50 }, + slide = { h = -58, l = -2, s = 3 }, + }, + [63] = { + name = "abra", + hueRange = { min = -20, max = 30 }, + slide = { h = 20, l = 1, s = -1 }, + }, + [64] = { + name = "kadabra", + hueRange = { min = -30, max = 30 }, + slide = { h = 10, l = 1, s = 1 }, + }, + [65] = { + name = "alakazam", + hueRange = { min = -30, max = 30 }, + slide = { h = 20, l = -2, s = 1 }, + }, + [66] = { + name = "machop", + hueRange = { min = -40, max = 60 }, + slide = { h = 100, l = 0, s = 0 }, + }, + [67] = { + name = "machoke", + hueRange = { min = -70, max = 60 }, + slide = { h = -120, l = 1, s = -2 }, + }, + [68] = { + name = "machamp", + hueRange = { min = -70, max = 80 }, + slide = { h = 100, l = 0, s = -2 }, + }, + [69] = { + name = "bellsprout", + hueRange = { min = -15, max = 15 }, + slide = { h = -25, l = 0, s = -3 }, + }, + [70] = { + name = "weepinbell", + hueRange = { min = -25, max = 40 }, + slide = { h = -75, l = -1, s = -3 }, + }, + [71] = { + name = "victreebel", + hueRange = { min = -30, max = 30 }, + slide = { h = 40, l = -2, s = -3 }, + }, + [72] = { + name = "tentacool", + hueRange = { min = -40, max = 40 }, + slide = { h = 50, l = 2, s = 2 }, + }, + [73] = { + name = "tentacruel", + hueRange = { min = -40, max = 40 }, + slide = { h = 50, l = 2, s = 2 }, + }, + [74] = { + name = "geodude", + hueRange = { min = -180, max = 180 }, + slide = { h = -70, l = 0, s = 0 }, + }, + [75] = { + name = "graveler", + hueRange = { min = -180, max = 180 }, + slide = { h = -55, l = 0, s = 0 }, + }, + [76] = { + name = "golem", + hueRange = { min = -50, max = 20 }, + slide = { h = -55, l = 0, s = -2 }, + }, + [77] = { + name = "ponyta", + hueRange = { min = -25, max = 20 }, + slide = { h = 40, l = 2, s = -1 }, + }, + [78] = { + name = "rapidash", + hueRange = { min = -25, max = 20 }, + slide = { h = 40, l = 2, s = -1 }, + }, + [79] = { + name = "slowpoke", + hueRange = { min = -40, max = 40 }, + slide = { h = -90, l = -6, s = -3 }, + }, + [80] = { + name = "slowbro", + hueRange = { min = -40, max = 40 }, + slide = { h = -75, l = -4, s = -2 }, + }, + [81] = { + name = "magnemite", + hueRange = { min = -180, max = 180 }, + slide = { h = -30, l = 2, s = -4 }, + }, + [82] = { + name = "magneton", + hueRange = { min = -180, max = 180 }, + slide = { h = -30, l = 2, s = -4 }, + }, + [83] = { + name = "farfetchd", + hueRange = { min = -30, max = 30 }, + slide = { h = 15, l = 1, s = 2 }, + }, + [84] = { + name = "doduo", + hueRange = { min = -10, max = 40 }, + slide = { h = 31, l = 1, s = 2 }, + }, + [85] = { + name = "dodrio", + hueRange = { min = -10, max = 40 }, + slide = { h = 31, l = 1, s = 2 }, + }, + [86] = { + name = "seel", + hueRange = { min = -40, max = 30 }, + slide = { h = -180, l = 3, s = -4 }, + }, + [87] = { + name = "dewgong", + hueRange = { min = -10, max = 10 }, + slide = { h = -180, l = 3, s = -4 }, + }, + [88] = { + name = "grimer", + hueRange = { min = -180, max = 180 }, + slide = { h = 125, l = -1, s = 1 }, + }, + [89] = { + name = "muk", + hueRange = { min = -180, max = 180 }, + slide = { h = -115, l = 0, s = 1 }, + }, + [90] = { + name = "shellder", + hueRange = { min = -70, max = 40 }, + slide = { h = 120, l = 0, s = 0 }, + }, + [91] = { + name = "cloyster", + hueRange = { min = -70, max = 60 }, + slide = { h = 140, l = 0, s = 0 }, + }, + [92] = { + name = "gastly", + hueRange = { min = -80, max = 50 }, + slide = { h = -124, l = 0, s = 0 }, + }, + [93] = { + name = "haunter", + hueRange = { min = -70, max = 30 }, + slide = { h = -50, l = -2, s = -2 }, + }, + [94] = { + name = "gengar", + hueRange = { min = -90, max = 30 }, + slide = { h = -55, l = -2, s = -1 }, + }, + [95] = { + name = "onix", + hueRange = { min = -180, max = 180 }, + slide = { h = -171, l = -3, s = 0 }, + }, + [96] = { + name = "drowzee", + hueRange = { min = -10, max = 30 }, + slide = { h = -110, l = 4, s = -4 }, + }, + [97] = { + name = "hypno", + hueRange = { min = -15, max = 30 }, + slide = { h = -80, l = 1, s = -3 }, + }, + [98] = { + name = "krabby", + hueRange = { min = -20, max = 30 }, + slide = { h = 40, l = -1, s = 0 }, + }, + [99] = { + name = "kingler", + hueRange = { min = -20, max = 25 }, + slide = { h = 35, l = -1, s = 2 }, + }, + [100] = { + name = "voltorb", + hueRange = { min = -70, max = 10 }, + slide = { h = -95, l = 0, s = 0 }, + }, + [101] = { + name = "electrode", + hueRange = { min = -70, max = 10 }, + slide = { h = -95, l = 0, s = 0 }, + }, + [102] = { + name = "exeggcute", + hueRange = { min = -50, max = 40 }, + slide = { h = 70, l = 0, s = 0 }, + }, + [103] = { + name = "exeggutor", + hueRange = { min = -30, max = 40 }, + slide = { h = -60, l = 2, s = -3 }, + }, + [104] = { + name = "cubone", + hueRange = { min = -25, max = 5 }, + slide = { h = 160, l = 2, s = -5 }, + }, + [105] = { + name = "marowak", + hueRange = { min = -25, max = 5 }, + slide = { h = 160, l = 2, s = -5 }, + }, + [106] = { + name = "hitmonlee", + hueRange = { min = -20, max = 50 }, + slide = { h = 70, l = 0, s = -2 }, + }, + [107] = { + name = "hitmonchan", + hueRange = { min = -20, max = 50 }, + slide = { h = -145, l = 1, s = -1 }, + }, + [108] = { + name = "lickitung", + hueRange = { min = -50, max = 30 }, + slide = { h = 60, l = -1, s = 1 }, + }, + [109] = { + name = "koffing", + hueRange = { min = -60, max = 90 }, + slide = { h = -80, l = -1, s = 3 }, + }, + [110] = { + name = "weezing", + hueRange = { min = -60, max = 90 }, + slide = { h = -90, l = 0, s = 1 }, + }, + [111] = { + name = "rhyhorn", + hueRange = { min = -180, max = 180 }, + slide = { h = 150, l = -1, s = 1 }, + }, + [112] = { + name = "rhydon", + hueRange = { min = -180, max = 180 }, + slide = { h = 58, l = 1, s = 1 }, + }, + [113] = { + name = "chansey", + hueRange = { min = -50, max = 60 }, + slide = { h = 80, l = 0, s = 0 }, + }, + [114] = { + name = "tangela", + hueRange = { min = -40, max = 20 }, + slide = { h = -120, l = 0, s = 0 }, + }, + [115] = { + name = "kangaskhan", + hueRange = { min = -30, max = 50 }, + slide = { h = -176, l = 0, s = -2 }, + }, + [116] = { + name = "horsea", + hueRange = { min = -40, max = 40 }, + slide = { h = -70, l = 0, s = -1 }, + }, + [117] = { + name = "seadra", + hueRange = { min = -30, max = 35 }, + slide = { h = -65, l = 2, s = 1 }, + }, + [118] = { + name = "goldeen", + hueRange = { min = -60, max = 40 }, + slide = { h = 50, l = 0, s = 3 }, + }, + [119] = { + name = "seaking", + hueRange = { min = -50, max = 30 }, + slide = { h = 40, l = 1, s = 3 }, + }, + [120] = { + name = "staryu", + hueRange = { min = -10, max = 30 }, + slide = { h = -176, l = 0, s = 0 }, + }, + [121] = { + name = "starmie", + hueRange = { min = -40, max = 50 }, + slide = { h = -110, l = 0, s = 0 }, + }, + [122] = { + name = "mr_mime", + hueRange = { min = -30, max = 50 }, + slide = { h = 30, l = -2, s = 2 }, + }, + [123] = { + name = "scyther", + hueRange = { min = -30, max = 50 }, + slide = { h = -110, l = 0, s = 0 }, + }, + [124] = { + name = "jynx", + hueRange = { min = -10, max = 30 }, + slide = { h = 30, l = 0, s = 3 }, + }, + [125] = { + name = "electabuzz", + hueRange = { min = -20, max = 30 }, + slide = { h = -40, l = 2, s = -2 }, + }, + [126] = { + name = "magmar", + hueRange = { min = -15, max = 30 }, + slide = { h = -60, l = 1, s = -2 }, + }, + [127] = { + name = "pinsir", + hueRange = { min = -60, max = 130 }, + slide = { h = -150, l = 0, s = 0 }, + }, + [128] = { + name = "tauros", + hueRange = { min = -10, max = 30 }, + slide = { h = -20, l = 4, s = -2 }, + }, + [129] = { + name = "magikarp", + hueRange = { min = -30, max = 25 }, + slide = { h = 50, l = -1, s = 2 }, + }, + [130] = { + name = "gyarados", + hueRange = { min = -20, max = 50 }, + -- Stadium ships a real alternate texture for this one; 481 + -- colours move, every other colour stays exactly as it was. + lut = { + [0x000008]=0x070000, [0x000808]=0x070000, [0x000810]=0x0D0100, [0x001010]=0x0D0100, + [0x001018]=0x140100, [0x080008]=0x070000, [0x080010]=0x0D0100, [0x080810]=0x0D0707, + [0x080818]=0x140707, [0x080820]=0x1A0807, [0x080829]=0x220807, [0x080831]=0x280907, + [0x080839]=0x2F0907, [0x081010]=0x0D0707, [0x081018]=0x140707, [0x081020]=0x1A0807, + [0x081031]=0x280907, [0x081039]=0x2F0907, [0x081041]=0x350A07, [0x081818]=0x140707, + [0x081820]=0x1A0807, [0x081829]=0x220807, [0x081841]=0x350A07, [0x08184A]=0x3D0A07, + [0x082029]=0x220807, [0x082031]=0x280907, [0x082039]=0x2F0907, [0x082041]=0x350A07, + [0x08204A]=0x3D0A07, [0x082052]=0x430B07, [0x082931]=0x280907, [0x082941]=0x350A07, + [0x082952]=0x430B07, [0x08295A]=0x4A0B07, [0x082962]=0x500B07, [0x083131]=0x280907, + [0x083141]=0x350A07, [0x083152]=0x430B07, [0x08315A]=0x4A0B07, [0x083162]=0x500B07, + [0x08394A]=0x3D0A07, [0x08395A]=0x4A0B07, [0x083962]=0x500B07, [0x08396A]=0x570C07, + [0x083973]=0x5E0C07, [0x084162]=0x500B07, [0x08416A]=0x570C07, [0x084173]=0x5E0C07, + [0x08417B]=0x650D07, [0x084A6A]=0x570C07, [0x084A73]=0x5E0C07, [0x084A7B]=0x650D07, + [0x085273]=0x5E0C07, [0x08527B]=0x650D07, [0x085283]=0x6B0D07, [0x08528B]=0x720E07, + [0x085A8B]=0x720E07, [0x085A94]=0x790E07, [0x086294]=0x790E07, [0x08629C]=0x800F07, + [0x086A9C]=0x800F07, [0x100010]=0x0D0100, [0x100018]=0x140100, [0x100810]=0x0D0707, + [0x101820]=0x1A0E0D, [0x102029]=0x220E0D, [0x102031]=0x280F0D, [0x102931]=0x280F0D, + [0x102939]=0x2F0F0D, [0x103139]=0x2F0F0D, [0x103141]=0x35100D, [0x104A5A]=0x4A110D, + [0x105273]=0x5E130D, [0x105A8B]=0x72140D, [0x10628B]=0x72140D, [0x106294]=0x79140D, + [0x106A9C]=0x80150D, [0x106AA4]=0x86150D, [0x1073A4]=0x86150D, [0x180818]=0x140707, + [0x180820]=0x1A0807, [0x181018]=0x140E0D, [0x182939]=0x2F1514, [0x183139]=0x2F1514, + [0x183141]=0x351614, [0x18314A]=0x3D1614, [0x183941]=0x351614, [0x18394A]=0x3D1614, + [0x183952]=0x431714, [0x184152]=0x431714, [0x18415A]=0x4A1714, [0x185262]=0x501814, + [0x185A7B]=0x651914, [0x186A94]=0x791A14, [0x18739C]=0x801B14, [0x1873A4]=0x861B14, + [0x1873AC]=0x8D1C14, [0x187BAC]=0x8D1C14, [0x200820]=0x1A0807, [0x200829]=0x220807, + [0x201020]=0x1A0E0D, [0x201029]=0x220E0D, [0x201831]=0x281514, [0x203941]=0x351C1A, + [0x20394A]=0x3D1D1A, [0x203952]=0x431D1A, [0x20414A]=0x3D1D1A, [0x204152]=0x431D1A, + [0x20415A]=0x4A1D1A, [0x204A5A]=0x4A1D1A, [0x204A62]=0x501E1A, [0x205262]=0x501E1A, + [0x20526A]=0x571E1A, [0x206273]=0x5E1F1A, [0x20627B]=0x651F1A, [0x206283]=0x6B201A, + [0x20739C]=0x80211A, [0x207BA4]=0x86211A, [0x207BAC]=0x8D221A, [0x207BB4]=0x94221A, + [0x2083B4]=0x94221A, [0x290829]=0x220807, [0x291029]=0x220E0D, [0x291831]=0x281514, + [0x29314A]=0x3D2322, [0x293941]=0x352322, [0x29394A]=0x3D2322, [0x294152]=0x432422, + [0x294A5A]=0x4A2422, [0x294A62]=0x502522, [0x295262]=0x502522, [0x29526A]=0x572522, + [0x295273]=0x5E2622, [0x295A6A]=0x572522, [0x295A73]=0x5E2622, [0x295A7B]=0x652622, + [0x296273]=0x5E2622, [0x29627B]=0x652622, [0x296A8B]=0x722722, [0x297394]=0x792722, + [0x29739C]=0x802822, [0x297BA4]=0x862822, [0x297BAC]=0x8D2922, [0x2983B4]=0x942922, + [0x2983BD]=0x9B2A22, [0x298BBD]=0x9B2A22, [0x311031]=0x280F0D, [0x311039]=0x2F0F0D, + [0x311839]=0x2F1514, [0x312031]=0x281B1A, [0x312039]=0x2F1C1A, [0x312941]=0x352322, + [0x313152]=0x432A28, [0x313952]=0x432A28, [0x31415A]=0x4A2A28, [0x314A5A]=0x4A2A28, + [0x314A62]=0x502B28, [0x315262]=0x502B28, [0x31526A]=0x572B28, [0x315A6A]=0x572B28, + [0x315A73]=0x5E2C28, [0x316273]=0x5E2C28, [0x31627B]=0x652C28, [0x316283]=0x6B2D28, + [0x316A7B]=0x652C28, [0x316A83]=0x6B2D28, [0x316A8B]=0x722D28, [0x31738B]=0x722D28, + [0x317394]=0x792E28, [0x31739C]=0x802E28, [0x317B9C]=0x802E28, [0x317BA4]=0x862E28, + [0x3183A4]=0x862E28, [0x3183AC]=0x8D2F28, [0x3183B4]=0x942F28, [0x318BB4]=0x942F28, + [0x318BBD]=0x9B3028, [0x318BC5]=0xA23028, [0x3194C5]=0xA23028, [0x391841]=0x351614, + [0x392039]=0x2F1C1A, [0x39395A]=0x4A312F, [0x394152]=0x43302F, [0x39525A]=0x4A312F, + [0x395262]=0x50312F, [0x39526A]=0x57312F, [0x395A62]=0x50312F, [0x395A6A]=0x57312F, + [0x396273]=0x5E322F, [0x39627B]=0x65322F, [0x396A7B]=0x65322F, [0x396A83]=0x6B332F, + [0x396A8B]=0x72332F, [0x39738B]=0x72332F, [0x397394]=0x79342F, [0x39739C]=0x80342F, + [0x397B8B]=0x72332F, [0x397B94]=0x79342F, [0x397B9C]=0x80342F, [0x397BA4]=0x86352F, + [0x3983A4]=0x86352F, [0x3983AC]=0x8D352F, [0x3983B4]=0x94352F, [0x398BB4]=0x94352F, + [0x398BBD]=0x9B362F, [0x3994BD]=0x9B362F, [0x3994C5]=0xA2362F, [0x3994CD]=0xAC342B, + [0x399CCD]=0xAC342B, [0x411841]=0x351614, [0x41184A]=0x3D1614, [0x412041]=0x351C1A, + [0x41204A]=0x3D1D1A, [0x412952]=0x432422, [0x413962]=0x50312F, [0x415A62]=0x503735, + [0x415A6A]=0x573835, [0x41626A]=0x573835, [0x416273]=0x5E3835, [0x41627B]=0x653835, + [0x416A6A]=0x573835, [0x416A83]=0x6B3935, [0x417383]=0x6B3935, [0x41738B]=0x723935, + [0x417394]=0x793A35, [0x417B8B]=0x723935, [0x417B94]=0x793A35, [0x417B9C]=0x803A35, + [0x41838B]=0x723935, [0x41839C]=0x803A35, [0x4183A4]=0x863B35, [0x4183AC]=0x8D3B35, + [0x418BA4]=0x863B35, [0x418BAC]=0x8D3B35, [0x418BB4]=0x943C35, [0x418BBD]=0x9B3C35, + [0x4194BD]=0x9B3C35, [0x4194C5]=0xA53A32, [0x419CC5]=0xA53A32, [0x419CCD]=0xAF372E, + [0x419CD5]=0xBB3329, [0x41A4D5]=0xBB3329, [0x4A204A]=0x3D1D1A, [0x4A2052]=0x431D1A, + [0x4A2952]=0x432422, [0x4A295A]=0x4A2422, [0x4A314A]=0x3D2A28, [0x4A3152]=0x432A28, + [0x4A315A]=0x4A2A28, [0x4A3962]=0x50312F, [0x4A626A]=0x573E3D, [0x4A6273]=0x5E3F3D, + [0x4A6A73]=0x5E3F3D, [0x4A7B9C]=0x80413D, [0x4A839C]=0x80413D, [0x4A83A4]=0x86423D, + [0x4A83AC]=0x8D423D, [0x4A8BA4]=0x86423D, [0x4A8BAC]=0x8D423D, [0x4A8BB4]=0x94423D, + [0x4A94AC]=0x8D423D, [0x4A94B4]=0x94423D, [0x4A94BD]=0x9E403A, [0x4A94C5]=0xA83E36, + [0x4A9CBD]=0x9E403A, [0x4A9CC5]=0xA83E36, [0x4A9CCD]=0xB33A32, [0x4AA4CD]=0xB33A32, + [0x4AA4D5]=0xBF362C, [0x4AA4DE]=0xCD3125, [0x4AACDE]=0xCD3125, [0x52205A]=0x4A1D1A, + [0x52295A]=0x4A2422, [0x522962]=0x502522, [0x52315A]=0x4A2A28, [0x523952]=0x43302F, + [0x52395A]=0x4A312F, [0x524162]=0x503735, [0x526273]=0x5E4543, [0x526A73]=0x5E4543, + [0x527383]=0x6B4643, [0x52838B]=0x724643, [0x528394]=0x794743, [0x528B9C]=0x804743, + [0x5294B4]=0x964741, [0x5294BD]=0xA1443D, [0x529CB4]=0x964741, [0x529CBD]=0xA1443D, + [0x529CC5]=0xAB4139, [0x529CCD]=0xB73D35, [0x52A4C5]=0xAB4139, [0x52A4CD]=0xB73D35, + [0x52A4D5]=0xC3392F, [0x52A4DE]=0xD13328, [0x52ACCD]=0xB73D35, [0x52ACD5]=0xC3392F, + [0x52ACDE]=0xD13328, [0x52ACE6]=0xDF2E21, [0x52B4E6]=0xDF2E21, [0x5A295A]=0x4A2422, + [0x5A2962]=0x502522, [0x5A296A]=0x572522, [0x5A315A]=0x4A2A28, [0x5A3162]=0x502B28, + [0x5A316A]=0x572B28, [0x5A6294]=0x794D4A, [0x5A6A7B]=0x654C4A, [0x5A737B]=0x654C4A, + [0x5A7383]=0x6B4C4A, [0x5A7B83]=0x6B4C4A, [0x5A838B]=0x724C4A, [0x5A839C]=0x804D4A, + [0x5A8B94]=0x794D4A, [0x5A8B9C]=0x804D4A, [0x5AA4BD]=0xA34841, [0x5AA4C5]=0xAE453D, + [0x5AA4CD]=0xBA4138, [0x5AA4D5]=0xC63C32, [0x5AACCD]=0xBA4138, [0x5AACD5]=0xC63C32, + [0x5AACDE]=0xD5362B, [0x5AB4D5]=0xC63C32, [0x5AB4DE]=0xD5362B, [0x5AB4E6]=0xDE3428, + [0x5AB4EE]=0xE83224, [0x5ABDE6]=0xDE3428, [0x5ABDEE]=0xE83224, [0x62296A]=0x572522, + [0x62316A]=0x572B28, [0x623173]=0x5E2C28, [0x627B8B]=0x725350, [0x628B8B]=0x725350, + [0x628B94]=0x795350, [0x628B9C]=0x805450, [0x6294A4]=0x88534F, [0x62A4C5]=0xB14941, + [0x62ACC5]=0xB14941, [0x62B4DE]=0xD43D32, [0x62B4E6]=0xDE3B2F, [0x62B4EE]=0xE8382B, + [0x62BDE6]=0xDE3B2F, [0x62BDEE]=0xE8382B, [0x62BDF6]=0xF33527, [0x62C5EE]=0xE8382B, + [0x62C5F6]=0xF33527, [0x6A3173]=0x5E2C28, [0x6A396A]=0x57312F, [0x6A3973]=0x5E322F, + [0x6A397B]=0x65322F, [0x6A4173]=0x5E3835, [0x6A417B]=0x653835, [0x6A4A73]=0x5E3F3D, + [0x6A4A83]=0x6B403D, [0x6A5273]=0x5E4543, [0x6A8394]=0x795957, [0x6A9CA4]=0x895854, + [0x6AB4D5]=0xC8463D, [0x6AB4DE]=0xD3443A, [0x6ABDDE]=0xD3443A, [0x6ABDE6]=0xDD4136, + [0x6ABDEE]=0xE83F32, [0x6AC5EE]=0xE83F32, [0x6AC5F6]=0xF23B2E, [0x6AC5FF]=0xFF3729, + [0x6ACDFF]=0xFF3729, [0x73397B]=0x65322F, [0x734183]=0x6B3935, [0x735273]=0x5E4543, + [0x73527B]=0x654543, [0x73A4AC]=0x945C58, [0x73ACB4]=0x9E5954, [0x73ACBD]=0xA95650, + [0x73ACC5]=0xB5524B, [0x73B4C5]=0xB5524B, [0x73B4CD]=0xBE5049, [0x73B4D5]=0xC74E46, + [0x73BDDE]=0xD24C42, [0x73BDE6]=0xDD493E, [0x73C5E6]=0xDD493E, [0x73C5EE]=0xE7463A, + [0x73C5F6]=0xF24236, [0x73CDEE]=0xE7463A, [0x73CDF6]=0xF24236, [0x73CDFF]=0xFF3E30, + [0x73D5FF]=0xFF3E30, [0x7B4183]=0x6B3935, [0x7B418B]=0x723935, [0x7B5A83]=0x6B4C4A, + [0x7B8BAC]=0x95615D, [0x7BA4B4]=0x9E5F5A, [0x7BA4BD]=0xAA5B56, [0x7BB4C5]=0xB35953, + [0x7BB4CD]=0xBD5750, [0x7BBDCD]=0xBD5750, [0x7BBDD5]=0xC6554D, [0x7BBDDE]=0xD25249, + [0x7BC5E6]=0xDC5046, [0x7BC5EE]=0xE74C41, [0x7BC5F6]=0xF2493D, [0x7BCDEE]=0xE74C41, + [0x7BCDF6]=0xF2493D, [0x7BCDFF]=0xFF4437, [0x7BD5FF]=0xFF4437, [0x7BDEFF]=0xFF4437, + [0x834A8B]=0x72403D, [0x834A94]=0x79413D, [0x83528B]=0x724643, [0x835A8B]=0x724C4A, + [0x839CA4]=0x8C6966, [0x83ACBD]=0xA9625D, [0x83B4BD]=0xA9625D, [0x83CDE6]=0xDB564D, + [0x83CDF6]=0xF14F44, [0x83D5F6]=0xF14F44, [0x83D5FF]=0xFF4A3E, [0x83DEFF]=0xFF4A3E, + [0x8B5294]=0x794743, [0x8B529C]=0x804743, [0x8BACAC]=0x956D6A, [0x8BACBD]=0xA86A65, + [0x8BBDBD]=0xA86A65, [0x8BC5D5]=0xC4635C, [0x8BCDE6]=0xDA5D54, [0x8BD5F6]=0xF1564B, + [0x8BD5FF]=0xFF5144, [0x8BDEFF]=0xFF5144, [0x8BE6FF]=0xFF5144, [0x945A9C]=0x804D4A, + [0x945AA4]=0x864E4A, [0x94BDBD]=0xA6726E, [0x94CDDE]=0xCE6861, [0x94D5F6]=0xF05D53, + [0x94DEF6]=0xF05D53, [0x94DEFF]=0xFF574B, [0x94E6FF]=0xFF574B, [0x9C5AA4]=0x864E4A, + [0x9CB4BD]=0xA47A77, [0x9CBDD5]=0xC1736D, [0x9CC5CD]=0xB77671, [0x9CD5E6]=0xD86C65, + [0x9CE6FF]=0xFF5E52, [0xA462AC]=0x91514D, [0xA46AB4]=0x9C544F, [0xA4D5DE]=0xCC7771, + [0xA4DEEE]=0xE36F67, [0xA4E6F6]=0xEF6A61, [0xA4E6FF]=0xFF6459, [0xAC6AB4]=0x9C544F, + [0xAC8BB4]=0x9D6C68, [0xACDEE6]=0xD57B74, [0xACE6FF]=0xFF6A5F, [0xB483BD]=0xA9625D, + [0xB483C5]=0xB2615B, [0xB48BC5]=0xB16863, [0xB4C5D5]=0xBC8A87, [0xB4D5D5]=0xBC8A87, + [0xB4E6E6]=0xD4827D, [0xBD7BC5]=0xB35953, [0xBD83CD]=0xBC5F58, [0xBD9CBD]=0xA47A77, + [0xBDD5D5]=0xB99391, [0xBDDEDE]=0xC5908C, [0xC5D5E6]=0xCF948F, [0xC5DEDE]=0xC39895, + [0xCD94D5]=0xC36C65, [0xCDA4DE]=0xCC7771, [0xCDC5D5]=0xB69C9A, [0xCDDEE6]=0xCC9C99, + [0xCDE6E6]=0xCC9C99, [0xD5B4DE]=0xC88682, [0xD5E6E6]=0xC8A5A3, [0xD5EEEE]=0xD79F9B, + [0xDEE6EE]=0xD2AAA7, [0xDEEEEE]=0xD2AAA7, [0xDEF6F6]=0xE4A19C, [0xE6EEEE]=0xCCB5B4, + [0xE6F6F6]=0xDFABA7, [0xEED5F6]=0xE79791, [0xEEE6EE]=0xCCB5B4, [0xF6E6F6]=0xDFABA7, + [0xFFE6FF]=0xFF968F, + }, + }, + [131] = { + name = "lapras", + hueRange = { min = -15, max = 5 }, + slide = { h = 56, l = 2, s = 0 }, + }, + [132] = { + name = "ditto", + hueRange = { min = -60, max = 20 }, + slide = { h = -80, l = -2, s = 1 }, + }, + [133] = { + name = "eevee", + hueRange = { min = -30, max = 30 }, + slide = { h = -140, l = 1, s = -5 }, + }, + [134] = { + name = "vaporeon", + hueRange = { min = -20, max = 50 }, + slide = { h = 87, l = 0, s = 0 }, + }, + [135] = { + name = "jolteon", + hueRange = { min = -25, max = 20 }, + slide = { h = -20, l = -1, s = -5 }, + }, + [136] = { + name = "flareon", + hueRange = { min = -20, max = 30 }, + slide = { h = -60, l = 0, s = -2 }, + }, + [137] = { + name = "porygon", + hueRange = { min = -40, max = 40 }, + slide = { h = -90, l = -3, s = 2 }, + }, + [138] = { + name = "omanyte", + hueRange = { min = -15, max = 60 }, + slide = { h = -30, l = 1, s = -1 }, + }, + [139] = { + name = "omastar", + hueRange = { min = -15, max = 60 }, + slide = { h = -30, l = 1, s = -1 }, + }, + [140] = { + name = "kabuto", + hueRange = { min = -20, max = 30 }, + slide = { h = 40, l = 2, s = -7 }, + }, + [141] = { + name = "kabutops", + hueRange = { min = -40, max = 50 }, + slide = { h = 50, l = 0, s = -5 }, + }, + [142] = { + name = "aerodactyl", + hueRange = { min = -60, max = 60 }, + slide = { h = 50, l = -2, s = 2 }, + }, + [143] = { + name = "snorlax", + hueRange = { min = -10, max = 60 }, + slide = { h = 30, l = 1, s = -3 }, + }, + [144] = { + name = "articuno", + hueRange = { min = -20, max = 30 }, + slide = { h = -40, l = 0, s = 1 }, + }, + [145] = { + name = "zapdos", + hueRange = { min = -20, max = 25 }, + slide = { h = -40, l = 2, s = 0 }, + }, + [146] = { + name = "moltres", + hueRange = { min = -20, max = 20 }, + slide = { h = -80, l = 5, s = -3 }, + }, + [147] = { + name = "dratini", + hueRange = { min = -40, max = 20 }, + slide = { h = 40, l = 0, s = 0 }, + }, + [148] = { + name = "dragonair", + hueRange = { min = -30, max = 10 }, + slide = { h = 50, l = 2, s = -3 }, + }, + [149] = { + name = "dragonite", + hueRange = { min = -20, max = 25 }, + slide = { h = 130, l = 0, s = -4 }, + }, + [150] = { + name = "mewtwo", + hueRange = { min = -70, max = 90 }, + slide = { h = -40, l = -1, s = -2 }, + }, + [151] = { + name = "mew", + hueRange = { min = -40, max = 60 }, + slide = { h = -80, l = 0, s = 0 }, + }, +} diff --git a/lib/BattleScene.lua b/lib/BattleScene.lua index a6ad975..a8ffb60 100644 --- a/lib/BattleScene.lua +++ b/lib/BattleScene.lua @@ -729,6 +729,12 @@ function BattleScene.render(state, arena, textures, token) -- a card wins the depth test the way a nearer thing should local cap = BattleScene.capture if cap and cap.draw then pcall(cap.draw, BattleBillboard.PULL) end + -- and a shiny's arrival sparkle, last of the three so its stars add + -- over the mon they belong to rather than under it, and still inside + -- the flash window so a burst during a hit is lit like everything else + pcall(function() + V.require("ShinyFx").draw(arena, groundY, BattleBillboard.PULL) + end) if flashing then Voxel3D.flatten(nil) end -- grass and flowers ride the same camera-ward pull the free-roam pass -- gives them, measured against THIS camera's pitch rather than the diff --git a/lib/OverworldBattle.lua b/lib/OverworldBattle.lua index a496760..0d61c1c 100644 --- a/lib/OverworldBattle.lua +++ b/lib/OverworldBattle.lua @@ -598,6 +598,11 @@ end function OverworldBattle.update(dt) if not session then return end + -- the shiny arrival sparkle's clock. Ticked here rather than in the draw + -- because a paused or covered frame still draws, and a burst that + -- advanced on draws would stall behind a text box mid-twinkle. + pcall(function() V.require("ShinyFx").update(dt) end) + local g = game() local top = g and g.stack and g.stack:top() local ow = g and g.overworld @@ -1014,6 +1019,16 @@ OverworldBattle.TEX_AX, OverworldBattle.TEX_AY = TEX_AX, TEX_AY -- Which side is being rendered, or nil. The placement wrappers read it. local texturing = nil +-- Which side is being rendered into its own canvas right now, or nil. +-- +-- Exposed because the shiny tint has two applications -- per side here, and +-- both-sides-at-once on the flat path (ShinyUI.installBattlePics) -- and +-- exactly one of them must run per draw. Asking this is what keeps them +-- from stacking, rather than relying on which module installed first. +function OverworldBattle.texturingSide() + return texturing +end + local texCanvas = {} local innerPics = nil -- captured by install() local innerHUDs = nil -- likewise, for the snapped HUD layer @@ -1086,12 +1101,29 @@ function OverworldBattle.sideTexture(battle, side) for k, v in pairs(OFF[side]) do saved[k] = battle[k]; battle[k] = v end texturing = side + -- A SHINY on this side, tinted here rather than in ShinyUI's flat-path + -- wrap. This is the one place a pic is rendered for ONE side at a time, + -- so it is the only place the two sides can be tinted differently -- a + -- shiny facing an ordinary mon gets its own colour and leaves the other + -- alone, which the engine's both-sides-at-once pic layer cannot do. + local shinyTint = nil + do + local battler = (side == "player") and battle.player or battle.enemy + local g2 = game() + shinyTint = battler and V.require("ShinyUI") + .tintFor(battler.mon, g2 and g2.data) or nil + end + local ok, err = pcall(function() g.setCanvas(canvas) g.clear(0, 0, 0, 0) g.setBlendMode("alpha") g.setColor(1, 1, 1, 1) - innerPics(battle, 0, 0, 0) + if shinyTint then + V.require("ShinyUI").withTint(shinyTint, innerPics, battle, 0, 0, 0) + else + innerPics(battle, 0, 0, 0) + end end) texturing = nil diff --git a/lib/Shiny.lua b/lib/Shiny.lua new file mode 100644 index 0000000..3a191d9 --- /dev/null +++ b/lib/Shiny.lua @@ -0,0 +1,229 @@ +-- Shiny Pokemon: the one fact, and everywhere that asks it. +-- +-- WHAT MAKES A MON SHINY HERE IS ITS DVs, and nothing else. Gen 1 has no +-- shininess of its own, but it does have the four DVs Gen 2 later read to +-- decide it, and the engine already ships that reading: +-- src/pokemon/Stats.lua:90 isShiny(dvs) -- Defense, Speed and Special all +-- exactly 10, Attack one of 2/3/6/7/10/11/14/15. The engine's own comment +-- calls it "the RBY virtual shiny" and says it is there for indicator mods. +-- This is that mod. +-- +-- Deriving rather than storing is the whole design, and it buys a great +-- deal: +-- +-- * It persists for free. DVs are already in every save, every PC box, +-- every trade. No new save field, no migration, and a save made before +-- this mod was installed already HAS shiny Pokemon in it -- they were +-- always there, nothing was ever drawn differently. +-- * It survives evolution. Evolution.apply recalculates stats from the +-- same dvs table and never touches it (src/pokemon/Evolution.lua:99), +-- so a shiny Bulbasaur is a shiny Venusaur without being told. +-- * It cannot desync. A flag stored beside the DVs is a second copy of +-- the truth, and two copies drift -- most cruelly across a trade or a +-- box deposit, where the mon travels and the sidecar does not. +-- * PKHeX and the Gen 2 games agree with us, because it is their rule. +-- +-- The odds, though, are ours to set, and that is the one thing DVs alone +-- cannot give: random DVs land on that pattern 1/16 * 1/16 * 1/16 * 8/16 = +-- exactly 1/8192, the classic rate, and there is no dial on it. So the roll +-- happens at encounter time and its VERDICT IS WRITTEN BACK INTO THE DVs +-- (forceShiny/forceCommon below). The mon does not carry a flag saying it +-- is shiny; it is made genuinely shiny by the game's own formula, and every +-- later reader -- ours, the engine's, a future mod's, PKHeX's -- reaches the +-- same answer without knowing we were involved. +-- +-- mon.shiny is maintained too, but it is a CACHE and never the source: see +-- Shiny.mark. + +-- the mod namespace (see main.lua): V.require loads a sibling module +local V = ... + +-- allowlisted for mods by name -- src/mods/Loader.lua:71 lists +-- src.pokemon.Stats precisely so an indicator mod can call isShiny +local Stats = require("src.pokemon.Stats") + +local Shiny = {} + +-- ------- the odds +-- +-- One in ODDS_DENOM. The default is 8192 because that is what random DVs +-- already produce, so a player who never changes it gets the canonical rate +-- and the canonical feel -- this mod's default is not a buff. +-- +-- The roll is made EXACT rather than additive. A naive implementation rolls +-- 1/N and forces shiny on a hit, but leaves the natural 1/8192 in place on a +-- miss, so the true rate is N and 8192 in parallel -- indistinguishable at +-- the default and quietly wrong at every other setting (at 1/100 you would +-- ship 1/99.99, and at 1/20000 you could never go rarer than 1/8192 no +-- matter what you set). forceCommon on a miss closes that: the rate is what +-- the number says. +Shiny.ODDS_DENOM = 8192 + +-- Set the denominator. Guards the degenerate values because a 0 or a +-- negative here would divide-by-zero or make every encounter shiny by +-- accident rather than by choice; 1 (always shiny) stays reachable because +-- it is genuinely useful for walking the whole model set. +function Shiny.setOdds(denom) + denom = tonumber(denom) + if not denom or denom < 1 then return Shiny.ODDS_DENOM end + Shiny.ODDS_DENOM = math.floor(denom) + return Shiny.ODDS_DENOM +end + +-- ------- reading it + +-- The eight Attack DVs that satisfy the Gen 2 pattern, in order. Kept as a +-- list as well as the engine's set because forceShiny has to CHOOSE one and +-- wants the nearest, not just any. +local SHINY_ATK = { 2, 3, 6, 7, 10, 11, 14, 15 } + +-- The HP DV is not free: Gen 1 derives it from the low bit of each of the +-- other four (src/pokemon/Stats.lua:19). Any write to the four must +-- recompute it, or the mon ends up with an HP stat the real game could +-- never produce -- which is exactly what a save inspector flags as illegal. +local function syncHpDv(dvs) + dvs.hp = (dvs.attack % 2) * 8 + (dvs.defense % 2) * 4 + + (dvs.speed % 2) * 2 + (dvs.special % 2) + return dvs +end + +-- The single question. Everything visual in this mod routes here. +function Shiny.isShiny(mon) + if type(mon) ~= "table" then return false end + return Stats.isShiny(mon.dvs) == true +end + +-- ------- writing it + +-- Make these DVs satisfy the pattern, moving them as little as it allows. +-- +-- Defense, Speed and Special have exactly one legal value each, so they are +-- simply pinned. Attack has eight, and the nearest one to whatever was +-- rolled is chosen -- a mon rolled at Attack 15 keeps 15, one rolled at 0 +-- becomes 2. That is not cosmetic: DVs are stats, and a shiny encounter +-- should not also be a stat reroll any larger than the pattern demands. +local function forceShiny(dvs) + local want, best, bestd = dvs.attack or 0, SHINY_ATK[1], nil + for _, v in ipairs(SHINY_ATK) do + local d = math.abs(v - want) + if not bestd or d < bestd then bestd, best = d, v end + end + dvs.attack = best + dvs.defense, dvs.speed, dvs.special = 10, 10, 10 + return syncHpDv(dvs) +end + +-- Make these DVs NOT satisfy the pattern, moving them as little as +-- possible: one step on Special is enough to break it, and Special is the +-- choice because in Gen 1 it is a single stat rather than the two Gen 2 +-- split it into, so the disturbance stays inside one number. +-- +-- Only ever reached by a mon that rolled non-shiny and happened to be shiny +-- by luck, which is 1/8192 of the time -- so this touches almost nothing, +-- and what it does touch it moves by one point. +local function forceCommon(dvs) + if (dvs.special or 0) == 10 then + dvs.special = 9 + elseif (dvs.defense or 0) == 10 then + dvs.defense = 9 + end + return syncHpDv(dvs) +end + +-- mon.shiny: the cache. +-- +-- The requirement is a flag ON the Pokemon, and this is it -- but it is +-- written from the DVs every time we touch a mon, never read as the truth. +-- Keeping it one-directional is what stops it becoming the second copy the +-- header warns about: if it ever disagrees with the DVs, the DVs win and +-- this is overwritten. It exists so other code -- and a save inspector, and +-- a companion mod -- can ask the cheap question without importing Stats. +function Shiny.mark(mon) + if type(mon) ~= "table" then return false end + local is = Shiny.isShiny(mon) + mon.shiny = is or nil -- nil rather than false: absent keeps saves clean + return is +end + +-- Recalculate the stats a DV write invalidated. +-- +-- Split out because both decide() and set() move DVs, and a mon left +-- carrying stats computed from its old DVs is wrong in the only way the +-- player can actually see: its HP bar. +local function restat(mon) + if not (mon.level and mon.species) then return end + local ok, data = pcall(require, "src.core.Data") + local def = ok and data and data.pokemon and data.pokemon[mon.species] + if not def then return end + local wasFull = mon.hp and mon.stats and mon.hp >= (mon.stats.hp or 0) + mon.stats = Stats.calc(def, mon.level, mon.dvs, mon.statExp) + -- A wild mon appears at full health, and a mon that WAS full stays full: + -- recomputing max HP without following it here would put a freshly + -- encountered mon on the field at less than full from its first frame. + -- A wounded mon keeps its damage, clamped to the new maximum. + if mon.hp then + mon.hp = wasFull and mon.stats.hp or math.min(mon.hp, mon.stats.hp) + end +end + +-- ------- our own randomness +-- +-- A PRIVATE stream, not love.math.random, and that is deliberate. +-- +-- The game's RNG is a shared sequence: damage rolls, crits, encounter +-- slots and DV generation all draw from it in a fixed order. Taking a draw +-- out of it for a shiny check would shift every later draw, so installing +-- this mod would quietly change the outcome of fights that have nothing to +-- do with shininess -- and the manifest promises `affects_link = false`, +-- which a shifted stream would make untrue the moment two machines +-- disagreed about whose turn consumed what. +-- +-- Seeded off the clock rather than the save, because shininess is a fact +-- about the encounter and not about the file: re-loading a save to re-roll +-- a Pokemon is the hunt, and a stream keyed to the save would hand back the +-- same answer every time. +local stream = nil + +local function roll(n) + if not stream then + if love and love.math and love.math.newRandomGenerator then + stream = love.math.newRandomGenerator(os.time(), os.clock() * 1e6) + else + -- headless (tests): math.random is nobody's shared sequence there + stream = { random = function(_, a, b) return math.random(a, b) end } + end + end + return stream:random(1, n) +end + +-- Decide a freshly-built mon, in place. +-- +-- rng may be passed to pin the verdict -- a test hands us a stub. Left nil, +-- the private stream above is used. +function Shiny.decide(mon, rng) + if type(mon) ~= "table" or type(mon.dvs) ~= "table" then return false end + -- same shape as love.math.random(lo, hi), so a caller can pass that or a + -- stub and the call below reads identically either way + rng = rng or function(_lo, hi) return roll(hi) end + local hit = rng(1, Shiny.ODDS_DENOM) == 1 + + if hit then + forceShiny(mon.dvs) + restat(mon) + elseif Stats.isShiny(mon.dvs) then + forceCommon(mon.dvs) + restat(mon) + end + return Shiny.mark(mon) +end + +-- Force a specific verdict: for tests, and for a scripted gift mon that +-- wants to be shiny on purpose. +function Shiny.set(mon, on) + if type(mon) ~= "table" or type(mon.dvs) ~= "table" then return false end + if on then forceShiny(mon.dvs) else forceCommon(mon.dvs) end + restat(mon) + return Shiny.mark(mon) +end + +return Shiny diff --git a/lib/ShinyBattle.lua b/lib/ShinyBattle.lua new file mode 100644 index 0000000..2a34fa0 --- /dev/null +++ b/lib/ShinyBattle.lua @@ -0,0 +1,97 @@ +-- Where shininess enters the game, and where it is shown. +-- +-- ------- one seam decides it +-- +-- Every Pokemon the player can ever own is built by Pokemon.new +-- (src/pokemon/Pokemon.lua:60). There are five callers and they are the +-- whole surface: +-- +-- BattleState.lua:569 the wild encounter +-- BattleState.lua:659 a trainer's party +-- BattleState.lua:785 the level-5 stand-in the Oak battle builds +-- Commands.lua:656 a gift or a starter +-- Commands.lua:969 an in-game trade +-- +-- So the roll goes THERE rather than on the encounter hooks. Two reasons, +-- and the second is decisive: +-- +-- * encounter.roll and encounter.species fire before the mon exists -- +-- they carry {species, level} and nothing to write a verdict onto. +-- * makeBattler bakes mon.sprite INSIDE newWild +-- (src/battle/BattleState.lua:455-461), before battle.started is +-- emitted. A verdict applied at battle.started is already too late for +-- the sprite the fight will draw. +-- +-- Wrapping the constructor puts the decision before every one of those, and +-- picks up gift mons, starters and trades for free rather than needing a +-- seam each. +-- +-- TRAINER MONS COME OUT NON-SHINY BY THEMSELVES, and correctly so. The +-- engine overwrites every trainer slot's DVs with a fixed TRAINER_DVS +-- (src/battle/BattleState.lua:350, :661) right after construction, and that +-- constant fails the shiny pattern. So the roll is made and then discarded +-- for them -- which matches the real games, where a trainer's Pokemon is +-- never shiny. + +-- the mod namespace (see main.lua): V.require loads a sibling module +local V = ... + +local Shiny = V.require("Shiny") + +local ShinyBattle = {} + +-- ------- install +-- +-- Idempotent by sentinel, the pattern every wrap in this mod uses +-- (OverworldBattle.install, Stadium.install): a hot reload must not stack a +-- second copy of the wrapper on top of the first. +function ShinyBattle.install() + local Pokemon = require("src.pokemon.Pokemon") + if not Pokemon.dramaticShapeShiny then + local inner = Pokemon.new + function Pokemon.new(data, species, level, rng) + local mon = inner(data, species, level, rng) + -- pcall: a mon that fails to be decided is an ordinary mon, which is + -- a blemish. A mon that fails to be BUILT is a broken game. + pcall(Shiny.decide, mon) + return mon + end + Pokemon.dramaticShapeShiny = true + end + + -- Party mons that predate the mod, and any mon built by a path we have + -- not wrapped, still answer isShiny correctly -- their DVs were always + -- there. This only refreshes the mon.shiny cache so a save opened for the + -- first time under this mod has the field populated rather than absent + -- until the mon next changes. + ShinyBattle.markParty() +end + +-- Refresh the cached flag across the player's party. +function ShinyBattle.markParty() + local ok, Game = pcall(require, "src.core.Game") + if not ok then return end + local party = Game and Game.save and Game.save.party + if type(party) ~= "table" then return end + for _, mon in ipairs(party) do + pcall(Shiny.mark, mon) + end +end + +-- ------- asking about a battler +-- +-- The battler wrapper carries the save-shaped mon on `.mon` +-- (src/battle/BattleState.lua:432-463), so the question is always about +-- that table and never about the wrapper. +function ShinyBattle.battlerIsShiny(battler) + return battler ~= nil and Shiny.isShiny(battler.mon) +end + +-- Which side of a battle, by the engine's own side names. +function ShinyBattle.sideIsShiny(battle, side) + if not battle then return false end + return ShinyBattle.battlerIsShiny(side == "player" and battle.player + or battle.enemy) +end + +return ShinyBattle diff --git a/lib/ShinyFx.lua b/lib/ShinyFx.lua new file mode 100644 index 0000000..b3b652d --- /dev/null +++ b/lib/ShinyFx.lua @@ -0,0 +1,198 @@ +-- The shiny sparkle: the flash a Pokemon makes when it first appears. +-- +-- The games announce a shiny with a burst of stars over the sprite the +-- instant it lands, before the first text box. This is that moment, in the +-- diorama: a ring of additive stars that springs outward from the mon's +-- chest, rises, and fades over about three quarters of a second. +-- +-- ------- where the moment IS +-- +-- Harder than it sounds, because the two battle paths arrive differently: +-- +-- the model rung a Pokemon grows out of its ball -- Stadium.update +-- already finds that frame (the POOF_ANIM edge) and +-- calls StadiumMon:beginGrow. +-- the pic rung a WILD foe is simply THERE on the first frame, with +-- no poof and no grow at all. There is no animation to +-- hang off. +-- +-- So the arming edge is neither of those: it is the frame a side's OCCUPANT +-- changes (Stadium's `session.at[side] ~= battler` test, the same identity +-- the mode already uses because a trainer leading with two Rattata changes +-- occupant without changing species). That edge fires for a send-out, a +-- switch and a wild foe alike, which is exactly the set of moments a shiny +-- should announce itself. +-- +-- ------- drawn additively, and why it survives the flash +-- +-- Stars are light, so they add rather than cover: `Voxel3D.blend("add")`, +-- the same treatment the Poke Ball's glow gets. They are drawn inside the +-- battle's flash window alongside the cards and models, so a sparkle during +-- a hit flash is lit by it like everything else rather than floating over +-- it as a separate layer. + +-- the mod namespace (see main.lua): V.require loads a sibling module +local V = ... + +local Voxel3D = V.require("Voxel3D") +local Mat4 = V.require("Mat4") +local BattleBillboard = V.require("BattleBillboard") + +local ShinyFx = {} + +-- ------- shape and timing + +ShinyFx.LIFE = 0.75 -- seconds from spring to gone +ShinyFx.STARS = 10 -- around the ring +ShinyFx.RISE = 10 -- world units the ring climbs over its life +ShinyFx.SPREAD = 7 -- how far out the ring opens +ShinyFx.CHEST = 9 -- height above the tile the burst starts at +ShinyFx.SIZE = 3.2 -- a star's world size at full brightness + +-- one per side, nil when nothing is playing +local live = { player = nil, enemy = nil } +local star = nil -- the generated star image, built once + +-- ------- the star +-- +-- Generated rather than shipped: it is a four-pointed twinkle, which is a +-- cheap closed form (a radial falloff times a cross-shaped spike term) and +-- costs nothing next to an asset that would have to be authored, packed, +-- loaded and kept in step with the rest of the mod's art. +local function starImage() + if star ~= nil then return star or nil end + if not (love and love.image and love.graphics) then + star = false + return nil + end + local ok, img = pcall(function() + local N = 32 + local data = love.image.newImageData(N, N) + local c = (N - 1) / 2 + for y = 0, N - 1 do + for x = 0, N - 1 do + local dx, dy = (x - c) / c, (y - c) / c + local r = math.sqrt(dx * dx + dy * dy) + -- the body: a soft core that is gone by the edge of the square + local core = math.max(0, 1 - r) + core = core * core * core + -- the spikes: bright along the two axes, narrow, and reaching + -- further out than the core does + local ax, ay = math.abs(dx), math.abs(dy) + local spike = math.max(0, 1 - ax * 6) * math.max(0, 1 - ay) + + math.max(0, 1 - ay * 6) * math.max(0, 1 - ax) + local a = math.min(1, core + spike * 0.55) + -- white with the faintest warm cast, so a sparkle over a cool + -- model still reads as light rather than as a blue smear + data:setPixel(x, y, 1, 1, 0.97, a) + end + end + return love.graphics.newImage(data) + end) + star = (ok and img) or false + return star or nil +end + +-- ------- arming + +-- Start (or restart) the burst on one side. Restarting rather than ignoring +-- a second call is deliberate: a shiny that faints and is sent back out +-- should sparkle again. +function ShinyFx.arm(side) + if side ~= "player" and side ~= "enemy" then return end + live[side] = { t = 0 } +end + +function ShinyFx.clear(side) + if side then live[side] = nil else live.player, live.enemy = nil, nil end +end + +function ShinyFx.active(side) + if side then return live[side] ~= nil end + return live.player ~= nil or live.enemy ~= nil +end + +function ShinyFx.update(dt) + dt = dt or 0 + for _, side in ipairs({ "player", "enemy" }) do + local s = live[side] + if s then + s.t = s.t + dt + if s.t >= ShinyFx.LIFE then live[side] = nil end + end + end +end + +-- ------- drawing + +-- Eased so the ring leaves fast and settles, which is what a spark does; +-- linear looks like a diagram of a spark. +local function easeOut(u) return 1 - (1 - u) * (1 - u) end + +-- Draw whatever is playing. `arena` and `groundY` come from the scene, the +-- same two the mon cards are placed from, so a sparkle lands where its +-- Pokemon is standing rather than where the layout thinks it should be. +function ShinyFx.draw(arena, groundY, pull) + if not arena then return end + local img = starImage() + if not img then return end + local mesh = BattleBillboard.mesh() + if not mesh then return end + + local drew = false + for _, side in ipairs({ "player", "enemy" }) do + local s = live[side] + local cell = (side == "player") and arena.player or arena.enemy + if s and cell then + local u = math.min(1, s.t / ShinyFx.LIFE) + local e = easeOut(u) + -- bright immediately, then out: the announcement is the first frame + local alpha = 1 - u * u + local x, z = cell[1], cell[2] + local yaw = BattleBillboard.yawToward(x, z, Voxel3D.eye) + local baseY = groundY + ShinyFx.CHEST + ShinyFx.RISE * e + + if not drew then + Voxel3D.blend("add") + Voxel3D.seams(false) + Voxel3D.glass(false) + drew = true + end + + for i = 1, ShinyFx.STARS do + -- the ring is offset half a step per side so the two sides do not + -- twinkle in lockstep when both are shiny + local a = (i / ShinyFx.STARS) * math.pi * 2 + + (side == "player" and 0.31 or 0) + local r = ShinyFx.SPREAD * e + -- stars shrink as they fade, and alternate size so the ring reads + -- as scattered rather than as a cog + local k = ShinyFx.SIZE * (1 - u * 0.6) * ((i % 2 == 0) and 0.7 or 1) + local ox = math.cos(a) * r + local oy = math.sin(a) * r * 0.55 -- flattened: we look from low + local m = Mat4.mul( + Mat4.mul(Mat4.translate(x, baseY, z), Mat4.rotateY(yaw)), + Mat4.mul(Mat4.translate(ox, oy, 0), Mat4.scale(k, k, 1))) + love.graphics.setColor(1, 1, 1, alpha) + Voxel3D.draw(mesh, img, m, pull) + end + end + end + + if drew then + love.graphics.setColor(1, 1, 1, 1) + Voxel3D.glass(true) + Voxel3D.seams(true) + Voxel3D.blend("alpha") + end +end + +-- Drop the generated image (hot reload, or a graphics context that went +-- away) -- the same contract StadiumPack.invalidate honours. +function ShinyFx.invalidate() + if star and star.release then pcall(star.release, star) end + star = nil + ShinyFx.clear() +end + +return ShinyFx diff --git a/lib/ShinyPalette.lua b/lib/ShinyPalette.lua new file mode 100644 index 0000000..823aff9 --- /dev/null +++ b/lib/ShinyPalette.lua @@ -0,0 +1,330 @@ +-- The shiny recolour: Stadium's own HSL slide, run over decoded texels. +-- +-- THE COLOUR MODEL IS STADIUM'S, not an invention. The Stadium games do not +-- ship a second set of textures for a shiny Pokemon; they convert the +-- colours the model already has to HSL and slide them -- a hue rotation in +-- degrees, plus saturation and lightness on a quantized integer scale of +-- -8..+8 where 0 is no change. One step is 12.5%, so +-8 is +-100%: exactly +-- the range of GIMP's Hue-Saturation sliders, which is where the 12.5% +-- figure was measured. s = -8 is full greyscale, l = +8 is white. +-- +-- That equivalence is why the maths below is GIMP's Hue-Saturation and not +-- a plain additive offset: +-- +-- saturation s' = s * (1 + k) multiplicative +-- lightness l' = l * (1 + k) k < 0 scale toward black +-- l' = l + k * (1 - l) k > 0 blend toward white +-- +-- The multiplicative saturation is the reason this is safe to run over a +-- whole texture rather than a masked region: a pixel with no saturation -- +-- an eye white, a tooth, a grey shadow -- is immune to BOTH the hue +-- rotation and the saturation step, for free and by construction. Only the +-- lightness step touches achromatic pixels, which is why the species +-- carrying big l values (Golbat and Slowpoke at -6, Moltres at +5) are the +-- ones worth looking at with human eyes. +-- +-- FIVE SPECIES CANNOT BE SLID. Clefairy, Clefable, Jigglypuff, Wigglytuff +-- and Gyarados get a real alternate texture in Stadium, because their shiny +-- moves one region a long way and leaves another alone -- Jigglypuff's body +-- stays pink while its irises go green -- and a single rotation moves +-- everything or nothing. +-- +-- Those five carry `lut` instead: an explicit before/after colour mapping, +-- sampled from the verified texture pairs, listing only the colours that +-- actually move. A first attempt drove them from a handful of per-region +-- anchors and picked the nearest one per pixel, which is wrong in a way +-- worth recording: with regions as far apart as Clefairy's pink body and +-- its green ear tips, a dark red shadow pixel is "nearest" to the green and +-- gets rotated 150 degrees the wrong way. The fixture caught it at a +-- 124/255 channel error. An exact table is a few tens of kilobytes and has +-- no such failure mode, so these five are data rather than algorithm. +-- +-- WHY THIS RUNS AT EXTRACTION. The textures are already decoded to RGBA in +-- memory at that moment (StadiumFragment.decodeTexture), and -- the part +-- that matters -- generated effect frames are still distinguishable there. +-- StadiumFx marks them `generated = true`, and the packer drops that field, +-- so at runtime an additive flame can only be inferred back from the prim +-- table. Recolouring a flame is wrong: a shiny Charizard has a shiny hide +-- and an ordinary fire. Doing the work while the marker still exists means +-- the discrimination is exact rather than reconstructed. +-- +-- THE MEMO IS WHAT MAKES IT AFFORDABLE. These are N64 textures: a few +-- hundred distinct colours across tens of thousands of texels. Converting +-- per DISTINCT COLOUR instead of per pixel turns the inner loop into a +-- table lookup, which is the difference between a pass that is felt during +-- the install and one that is not. + +-- the mod namespace (see main.lua): V.require loads a sibling module +local V = ... + +local ShinyPalette = {} + +local floor, min, max, abs = math.floor, math.min, math.max, math.abs +local byte, char, concat = string.byte, string.char, table.concat + +-- ------- HSL + +local function rgbToHsl(r, g, b) + r, g, b = r / 255, g / 255, b / 255 + local mx, mn = max(r, g, b), min(r, g, b) + local l = (mx + mn) / 2 + if mx == mn then return 0, 0, l end -- achromatic: hue is undefined + local d = mx - mn + local s = l > 0.5 and d / (2 - mx - mn) or d / (mx + mn) + local h + if mx == r then + h = (g - b) / d + (g < b and 6 or 0) + elseif mx == g then + h = (b - r) / d + 2 + else + h = (r - g) / d + 4 + end + return h * 60, s, l +end + +local function hue2rgb(p, q, t) + if t < 0 then t = t + 1 end + if t > 1 then t = t - 1 end + if t < 1 / 6 then return p + (q - p) * 6 * t end + if t < 1 / 2 then return q end + if t < 2 / 3 then return p + (q - p) * (2 / 3 - t) * 6 end + return p +end + +local function hslToRgb(h, s, l) + if s <= 0 then + local v = floor(l * 255 + 0.5) + return v, v, v + end + h = (h % 360) / 360 + local q = l < 0.5 and l * (1 + s) or l + s - l * s + local p = 2 * l - q + return floor(hue2rgb(p, q, h + 1 / 3) * 255 + 0.5), + floor(hue2rgb(p, q, h) * 255 + 0.5), + floor(hue2rgb(p, q, h - 1 / 3) * 255 + 0.5) +end + +-- GIMP's two curves, shared by the slide and the anchor paths so both +-- reach the same colour from the same k. +local function shiftSat(s, k) + if k == 0 then return s end + return max(0, min(1, s * (1 + k))) +end + +local function shiftLight(l, k) + if k == 0 then return l end + if k < 0 then return max(0, l * (1 + k)) end + return min(1, l + k * (1 - l)) +end + +-- ------- the two kinds of transform + +-- A whole-model slide: the 146 species Stadium recolours this way. +local function slideFn(slide) + local dh = slide.h or 0 + local ks = (slide.s or 0) * 0.125 + local kl = (slide.l or 0) * 0.125 + return function(r, g, b) + local h, s, l = rgbToHsl(r, g, b) + -- An achromatic pixel has no hue to rotate and no saturation to scale; + -- only a lightness step can reach it. Returning early is not just + -- speed, it is exactness: round-tripping grey through HSL and back can + -- move it by a unit, and a tooth that drifts is a visible defect. + if s <= 0 then + if kl == 0 then return r, g, b end + local v = floor(shiftLight(l, kl) * 255 + 0.5) + return v, v, v + end + return hslToRgb(h + dh, shiftSat(s, ks), shiftLight(l, kl)) + end +end + +-- An exact colour mapping: the five species Stadium gives a real second +-- texture. A colour absent from the table is one the alternate texture left +-- alone, so passing it straight through is the correct answer, not a +-- fallback -- that is how Wigglytuff keeps its white belly and its black +-- inner ears while its body moves to lilac. +local function lutFn(lut) + return function(r, g, b) + local hit = lut[r * 65536 + g * 256 + b] + if not hit then return r, g, b end + return floor(hit / 65536) % 256, floor(hit / 256) % 256, hit % 256 + end +end + +-- ------- the colour table +-- +-- Loaded lazily and cached. Two paths on purpose: through the mod namespace +-- when the mod is running, and straight off disk when it is not. The +-- extraction byte-diff (tests/stadium_extract_test.lua) stubs V with only +-- `require` and `mod.log`, and the recolour has to be exercisable under +-- exactly that harness -- a colour transform that can only run inside a +-- live LOVE process is a colour transform nobody will test. +local colors = nil + +local function loadColors() + if colors ~= nil then return colors or nil end + if V and V.data then + local ok, t = pcall(V.data, "shiny_colors") + if ok and type(t) == "table" then colors = t; return colors end + end + local tries = { "data/shiny_colors.lua", + "mods/DramaticShapeVoxelMod/data/shiny_colors.lua" } + for _, p in ipairs(tries) do + local chunk = loadfile(p) + if chunk then + local ok, t = pcall(chunk) + if ok and type(t) == "table" then colors = t; return colors end + end + end + colors = false -- cache the miss; do not retry the disk per species + return nil +end + +-- The spec for one dex number, or nil if we have nothing for it. +function ShinyPalette.forDex(dex) + local all = loadColors() + return all and all[dex] or nil +end + +-- Build the pixel transform for one species' spec, or nil when there is +-- nothing to do. +function ShinyPalette.transform(spec) + if type(spec) ~= "table" then return nil end + if spec.lut then + if next(spec.lut) == nil then return nil end + return lutFn(spec.lut) + end + local s = spec.slide + if not s then return nil end + if (s.h or 0) == 0 and (s.l or 0) == 0 and (s.s or 0) == 0 then return nil end + return slideFn(s) +end + +-- ------- the pass over one texture +-- +-- Memoised per distinct colour (see the header). The key packs RGB into one +-- integer because a table with 24-bit integer keys is a flat array probe, +-- where a "r,g,b" string key would allocate on every pixel -- and allocation +-- inside a multi-million-iteration loop is the whole cost. +-- +-- Alpha is copied through untouched, never premultiplied and never +-- recomputed: the transform is defined on colour alone, and a shiny +-- Gastly's soft edge must stay exactly as soft as it was. +function ShinyPalette.recolorTexels(rgba, fn) + local n = #rgba + if n == 0 or not fn then return rgba end + local memo = {} + local out, blocks = {}, {} + local bi = 0 + for i = 1, n, 4 do + local r, g, b, a = byte(rgba, i, i + 3) + local key = r * 65536 + g * 256 + b + local hit = memo[key] + if not hit then + local nr, ng, nb = fn(r, g, b) + hit = { nr, ng, nb } + memo[key] = hit + end + bi = bi + 1 + blocks[bi] = char(hit[1], hit[2], hit[3], a) + -- flushed in blocks so the concat never walks a table with millions of + -- one-texel strings in it + if bi >= 4096 then + out[#out + 1] = concat(blocks) + blocks, bi = {}, 0 + end + end + if bi > 0 then out[#out + 1] = concat(blocks, "", 1, bi) end + return concat(out) +end + +-- ------- a tint, for the flat sprites +-- +-- The 3D models get real recoloured texels. The 2D battle pics cannot: the +-- engine bakes a species palette into a cached image keyed by path and +-- palette name, and that cache has no idea which INDIVIDUAL is being drawn. +-- What is available per-draw is the draw colour, which multiplies. +-- +-- So the pic is tinted, and the tint is derived from the species' OWN shiny +-- slide rather than being a generic gold: run a spread of reference colours +-- through the real transform, take the mean ratio out to in, and that is +-- the multiply that best stands in for it. A shiny Golbat leans green, a +-- shiny Charizard goes dusky, and neither is a costume. +-- +-- ITS ONE LIMIT, stated plainly: a multiply can only darken. Where a species' +-- shiny is LIGHTER than its normal, the honest ratio is above 1 and gets +-- clamped, so those come out under-shifted -- present, but quieter than the +-- model. The floor keeps the darkest cases readable rather than muddy. +local TINT_FLOOR = 0.45 +local tintCache = {} + +-- Mid-tone references across the wheel. Deliberately not greys: the slide +-- is multiplicative in saturation, so a grey reference would report no +-- change for every species and hand back a tint of 1,1,1. +local REFS = { + { 200, 90, 70 }, { 200, 150, 70 }, { 190, 190, 80 }, { 90, 180, 90 }, + { 80, 170, 170 }, { 80, 120, 200 }, { 140, 90, 190 }, { 190, 90, 150 }, +} + +function ShinyPalette.tintFor(dex) + local hit = tintCache[dex] + if hit ~= nil then return hit or nil end + local fn = ShinyPalette.transform(ShinyPalette.forDex(dex)) + if not fn then tintCache[dex] = false; return nil end + local sr, sg, sb, n = 0, 0, 0, 0 + for _, c in ipairs(REFS) do + local r, g, b = fn(c[1], c[2], c[3]) + sr = sr + r / c[1] + sg = sg + g / c[2] + sb = sb + b / c[3] + n = n + 1 + end + local t = { + max(TINT_FLOOR, min(1, sr / n)), + max(TINT_FLOOR, min(1, sg / n)), + max(TINT_FLOOR, min(1, sb / n)), + } + -- a tint that came out as no tint at all is worse than none: it costs a + -- colour-hook wrap per draw and changes nothing + if t[1] > 0.995 and t[2] > 0.995 and t[3] > 0.995 then + tintCache[dex] = false + return nil + end + tintCache[dex] = t + return t +end + +-- ------- the pass over one species' whole texture array + +-- Recolour `textures` in place, skipping the ones that must not move. +-- +-- Two exclusions, both load-bearing: +-- +-- generated / index == -1 StadiumFx's flipbook frames -- flames, beams, +-- sparks. A shiny Pokemon has a shiny hide and +-- an ordinary fire; tinting the attack effects +-- would read as a bug. This marker exists ONLY +-- here, which is why the recolour lives at +-- extraction (see the header). +-- w or h of zero a degenerate slot with nothing to transform. +-- +-- Returns the number of textures actually touched, so the caller can tell a +-- species that recoloured from one that silently did not. +function ShinyPalette.recolorTextures(textures, spec) + local fn = ShinyPalette.transform(spec) + if not fn then return 0 end + local touched = 0 + for i = 1, #textures do + local t = textures[i] + local skip = t.generated == true or t.index == -1 + or not t.w or not t.h or t.w == 0 or t.h == 0 + if not skip and t.rgba and #t.rgba > 0 then + t.rgba = ShinyPalette.recolorTexels(t.rgba, fn) + touched = touched + 1 + end + end + return touched +end + +return ShinyPalette diff --git a/lib/ShinyUI.lua b/lib/ShinyUI.lua new file mode 100644 index 0000000..ec0ec18 --- /dev/null +++ b/lib/ShinyUI.lua @@ -0,0 +1,176 @@ +-- Where a shiny SHOWS on the flat art: the battle pics and the status page. +-- +-- The Stadium models carry genuinely recoloured texels (see ShinyPalette and +-- the extraction). Everything drawn as a Game Boy pic cannot, because the +-- engine bakes a species palette into an image cache keyed by path and +-- palette name -- a cache with no notion of WHICH Rattata is being drawn. So +-- the flat side is answered two ways: +-- +-- the battle pic tinted at draw time, per side, with the multiply +-- ShinyPalette.tintFor derives from that species' own +-- shiny slide. Under ADVANCED (`redpp`, the pokered-gbc +-- colour pack) the pic is at its most colourful and the +-- tint reads clearly; under the DMG modes there is +-- barely any colour to shift, which is why the status +-- page also carries a plain, mode-proof MARK. +-- the status page a star beside the level, drawn in the engine's own +-- GB pixel grid so it is palette-processed like every +-- other pixel on the screen rather than floating over +-- the finished frame. +-- +-- Both are monkeypatches, idempotent by sentinel, the pattern the rest of +-- this mod uses (OverworldBattle.install, Stadium.install). The summary +-- screen has no hook at all -- there is no `ui.summary.*` anywhere in the +-- engine -- so a wrap is the only route to it, and it is deliberately a thin +-- one: draw the engine's screen, then add one glyph. + +-- the mod namespace (see main.lua): V.require loads a sibling module +local V = ... + +local Shiny = V.require("Shiny") +local ShinyPalette = V.require("ShinyPalette") + +local ShinyUI = {} + +-- ------- the tint, applied to one draw +-- +-- Lifted in shape from OverworldBattle.withTint, and for the same reason it +-- exists there: the pics layer sets its own colour many times over as it +-- draws (the faint slide's fade, the damage blink), so the way to tint the +-- result without clobbering any of that is to multiply every colour it sets +-- on its way past. Restored unconditionally, including on error, because a +-- leaked setColor would tint the entire rest of the frame. +function ShinyUI.withTint(tint, fn, ...) + if not tint then return fn(...) end + local r, g, b = tint[1] or 1, tint[2] or 1, tint[3] or 1 + if r > 0.999 and g > 0.999 and b > 0.999 then return fn(...) end + local gfx = love.graphics + local setColor = gfx.setColor + gfx.setColor = function(cr, cg, cb, ca, ...) + if type(cr) == "table" then + return setColor({ (cr[1] or 1) * r, (cr[2] or 1) * g, (cr[3] or 1) * b, + cr[4] }, cg, ...) + end + if cr == nil then return setColor(cr, cg, cb, ca, ...) end + return setColor(cr * r, (cg or 1) * g, (cb or 1) * b, ca, ...) + end + local ok, err = pcall(fn, ...) + gfx.setColor = setColor + setColor(1, 1, 1, 1) + if not ok then error(err, 0) end +end + +-- The tint for a mon, or nil when it is not shiny or we have no colours. +function ShinyUI.tintFor(mon, data) + if not (mon and Shiny.isShiny(mon)) then return nil end + local def = data and data.pokemon and data.pokemon[mon.species] + local dex = def and def.dex + if not dex then return nil end + return ShinyPalette.tintFor(dex) +end + +-- ------- the star +-- +-- Drawn as rectangles rather than as a font character because the Game Boy +-- charmap has no star, and a letter would read as a typo. Four spokes and a +-- centre, in the same near-black the screen's text uses, so the palette pass +-- treats it exactly like a glyph -- under ADVANCED and under every DMG mode +-- alike. +-- +-- Seven pixels square, which is the largest that fits the gap beside the +-- level without touching the DrawLineBox bracket at column 19. +function ShinyUI.drawStar(px, py) + local g = love.graphics + local r, gg, b, a = g.getColor() + g.setColor(0, 0, 0, 1) + -- vertical, horizontal, then the four diagonal nubs + g.rectangle("fill", px + 3, py, 1, 7) + g.rectangle("fill", px, py + 3, 7, 1) + g.rectangle("fill", px + 1, py + 1, 1, 1) + g.rectangle("fill", px + 5, py + 1, 1, 1) + g.rectangle("fill", px + 1, py + 5, 1, 1) + g.rectangle("fill", px + 5, py + 5, 1, 1) + g.setColor(r, gg, b, a) +end + +-- ------- install + +function ShinyUI.install() + ShinyUI.installSummary() + ShinyUI.installBattlePics() +end + +-- The status page. Wraps the draw and adds the star afterwards, so the +-- engine's own layout is untouched and a layout change upstream costs us +-- the glyph's position and nothing else. +function ShinyUI.installSummary() + local ok, SummaryMenu = pcall(require, "src.ui.SummaryMenu") + if not ok or type(SummaryMenu) ~= "table" then return end + if SummaryMenu.dramaticShapeShiny then return end + local inner = SummaryMenu.draw + if type(inner) ~= "function" then return end + + function SummaryMenu:draw(...) + local out = { inner(self, ...) } + -- page 1 only: page 2 wipes the block the level sits in + -- (status_screen.asm ClearScreenArea over (9,2)), so a mark left there + -- would be half-erased by the engine's own clear. + if self.page == 1 and Shiny.isShiny(self.mon) then + -- beside PrintLevel at (14,2): column 13, row 2, in the gap the + -- level's own leading space leaves + pcall(ShinyUI.drawStar, 104, 17) + end + return unpack(out) + end + + SummaryMenu.dramaticShapeShiny = true +end + +-- The battle pics. The engine's pic layer is reached through +-- BattleState:drawPicsLayer, which draws BOTH sides in one call -- so a +-- per-side tint has to bracket each side separately, which is exactly what +-- OverworldBattle.sideTexture already does when it renders one side at a +-- time into its own canvas. That is where the tint belongs on the 3D path; +-- this wrap covers the FLAT path, where the engine draws the battle itself. +function ShinyUI.installBattlePics() + local ok, BattleState = pcall(require, "src.battle.BattleState") + if not ok or type(BattleState) ~= "table" then return end + if BattleState.dramaticShapeShinyPics then return end + local inner = BattleState.drawPicsLayer + if type(inner) ~= "function" then return end + + function BattleState:drawPicsLayer(...) + -- THE 3D PATH HAS ALREADY DONE THIS, per side and better: when the mod + -- is rendering one side into its own canvas it brackets that draw with + -- that side's own tint (OverworldBattle.sideTexture). Tinting again here + -- would square it. Asked as a question rather than left to install + -- order, because both wraps are installed from main.lua and whichever + -- ran first would otherwise silently decide the outcome. + local okOw, Ow = pcall(V.require, "OverworldBattle") + if okOw and Ow and Ow.texturingSide and Ow.texturingSide() then + return inner(self, ...) + end + + -- Both sides at once, so when they disagree the tint cannot be applied + -- per-side here without splitting the engine's own draw. When only ONE + -- side is shiny we tint the whole layer by it: the other side's pic is + -- dimmed slightly, which is far less wrong than a shiny drawn in its + -- ordinary colours -- and when both are shiny each gets the mean. + local data = self.game and self.game.data + local a = self.player and ShinyUI.tintFor(self.player.mon, data) + local b = self.enemy and ShinyUI.tintFor(self.enemy.mon, data) + local tint = a or b + if a and b then + tint = { (a[1] + b[1]) / 2, (a[2] + b[2]) / 2, (a[3] + b[3]) / 2 } + end + if not tint then return inner(self, ...) end + local args = { ... } + local out + ShinyUI.withTint(tint, function() out = { inner(self, unpack(args)) } end) + return unpack(out or {}) + end + + BattleState.dramaticShapeShinyPics = true +end + +return ShinyUI diff --git a/lib/Stadium.lua b/lib/Stadium.lua index 8cccf32..367425f 100644 --- a/lib/Stadium.lua +++ b/lib/Stadium.lua @@ -51,6 +51,8 @@ local V = ... local Voxel3D = V.require("Voxel3D") local StadiumPack = V.require("StadiumPack") local StadiumMon = V.require("StadiumMon") +local ShinyBattle = V.require("ShinyBattle") +local ShinyFx = V.require("ShinyFx") local Stadium = {} @@ -370,6 +372,15 @@ function Stadium.update(dt, battle, groundY) -- a fresh arrival: this Pokemon has not grown out of its ball yet if mon then mon.grow, mon.grewOwn = nil, nil end if mon and mon.rig and mon.state == "faint" then mon:play("idle") end + -- and if it is shiny, announce it. This edge rather than the grow, + -- because a WILD foe never grows -- it is on the field from the + -- first frame -- and that is the encounter a shiny most wants to be + -- announced on. See the header of ShinyFx. + if ShinyBattle.battlerIsShiny(battler) then + ShinyFx.arm(side) + else + ShinyFx.clear(side) + end end -- the collapse this side is owed, once its bar has finished emptying if session.faintPending and session.faintPending[side] then @@ -381,14 +392,26 @@ function Stadium.update(dt, battle, groundY) end end - mon:setSpecies(dex) + -- Shininess is a property of the OCCUPANT, not of the species, so it is + -- resolved here beside the dex number and passed with it. A shiny + -- Rattata and an ordinary one are the same dex and different models. + -- + -- Read off the battler rather than remembered, because Transform makes + -- the two disagree: a Ditto that copied a shiny Rattata wears the + -- Rattata's dex (session.transform above) and keeps its OWN shininess, + -- which is exactly what the games do. + local shiny = battler ~= nil and not session.transform[side] + and ShinyBattle.battlerIsShiny(battler) + + mon:setSpecies(dex, shiny) -- and tell the pack cache this one is standing there, every frame. Its -- eviction order is keyed on LOADS, and a side only loads when its -- species changes -- so without this a Pokemon that has been out for a -- few turns is the least recently loaded thing in the cache and gets its -- textures released out from under it the moment a fifth species enters - -- the battle (see StadiumPack.keep). - if mon.species then StadiumPack.keep(mon.species) end + -- the battle (see StadiumPack.keep). The shiny flag rides along: the + -- shiny and normal models are separate cache entries. + if mon.species then StadiumPack.keep(mon.species, mon.shiny) end mon.visible = (mon.rig ~= nil) and onField(battle, side, mon) and not (battler and battler.substituteHP) -- LET'S GO capture mode: the player's model is out of the shot the diff --git a/lib/StadiumBuild.lua b/lib/StadiumBuild.lua index 0440edf..be42966 100644 --- a/lib/StadiumBuild.lua +++ b/lib/StadiumBuild.lua @@ -26,6 +26,7 @@ local V = ... local StadiumRom = V.require("StadiumRom") local StadiumFragment = V.require("StadiumFragment") local StadiumFx = V.require("StadiumFx") +local ShinyPalette = V.require("ShinyPalette") local StadiumBuild = {} @@ -661,7 +662,41 @@ function StadiumBuild.species(rom, fileno) local ctx = StadiumBuild.contextTable(rows, #data.anims) local bytes, height, floorY, radius = StadiumBuild.pack(data, species, moveRows, ctx) - return { species = species, bytes = bytes, height = height, + + -- ------- and the shiny, from the same extraction + -- + -- ORDER MATTERS AND IS THE WHOLE TRICK. The normal pack is written FIRST, + -- off untouched texels, so `bytes` is bit-for-bit what it has always been + -- and tests/stadium_extract_test.lua keeps diffing green against the + -- Python oracle. Only then are the textures recoloured and the model + -- packed a second time. The oracle knows nothing about shiny and does not + -- need to: the format did not move, so there is no second implementation + -- to keep in step and no DSM4. + -- + -- Recolouring HERE rather than at load is what makes the effect textures + -- separable. StadiumFx marks its generated frames `generated = true` and + -- the packer drops the field, so this is the last moment a flame is + -- distinguishable from a hide without inferring it back from the prim + -- table. A shiny Charizard has a shiny hide and an ordinary fire. + -- + -- Failure is not fatal: a species whose colours we lack, or a transform + -- that throws, simply ships without a shiny variant and the runtime falls + -- back to the normal model. Losing a recolour is a blemish; losing the + -- install is a broken mod. + local shinyBytes + local ok, err = pcall(function() + local spec = ShinyPalette.forDex(species) + if not spec then return end + if ShinyPalette.recolorTextures(data.textures, spec) == 0 then return end + shinyBytes = StadiumBuild.pack(data, species, moveRows, ctx) + end) + if not ok and V and V.mod and V.mod.log then + V.mod.log.warn("shiny recolour failed for species %d: %s", + species, tostring(err)) + end + + return { species = species, bytes = bytes, shinyBytes = shinyBytes, + height = height, floor = floorY, radius = radius, bones = #data.bones, prims = #data.prims, anims = #data.anims, warnings = data.warnings } @@ -684,13 +719,17 @@ function StadiumBuild.job(rom, write, count) local fileno = self.done local ok, res, err = pcall(StadiumBuild.species, rom, fileno) if ok and res then - local wrote, wErr = write(res.species, res.bytes) + local wrote, wErr = write(res.species, res.bytes, res.shinyBytes) if not wrote then self.error = wErr or ("could not write species " .. res.species) self.done = self.total return false end self.bytes = self.bytes + #res.bytes + if res.shinyBytes then + self.bytes = self.bytes + #res.shinyBytes + self.shiny = (self.shiny or 0) + 1 + end self.species = res.species else self.failed[#self.failed + 1] = fileno diff --git a/lib/StadiumInstall.lua b/lib/StadiumInstall.lua index 81264f7..d1ec135 100644 --- a/lib/StadiumInstall.lua +++ b/lib/StadiumInstall.lua @@ -61,7 +61,14 @@ StadiumInstall.FORMAT = "DSM3" -- is the hermite-animation decode fix: the five keyframe species (Pidgeot, -- Dodrio, Exeggutor, Tangela, Magmar) come out garbled or bind-posed from -- any rev-1 build. -StadiumInstall.REV = 2 +-- +-- Rev 3 adds the shiny variants (NNNs.dsm). The normal packs are unchanged +-- byte for byte, so this is exactly the case REV exists for and not a FORMAT +-- bump: nothing about DSM3 moved, there is simply a second file per species +-- that a rev-2 cache does not have. Without the bump a player who already +-- installed would keep a complete-looking cache with no shiny models in it, +-- and every shiny they met would silently show its normal colours. +StadiumInstall.REV = 3 StadiumInstall.COUNT = 151 @@ -211,12 +218,34 @@ local status = { state = "idle", done = 0, total = StadiumInstall.COUNT } StadiumInstall.status = status -local function writePack(species, bytes) +-- The shiny variant rides beside its species as NNNs.dsm. +-- +-- A separate FILE rather than a second block inside NNN.dsm, and that is a +-- deliberate trade. A second block would mean a new magic (DSM4), the same +-- change mirrored into tools/stadium_pack.py, a regenerated oracle and a +-- re-run of the 34MB byte diff -- the project's central safety net disturbed +-- for a feature that does not need the format to move at all. As its own +-- file it is the SAME DSM3 a normal pack is, written by the same writer and +-- read by the same reader, and the 151 normal packs stay byte-identical. +-- +-- A species with no shiny variant simply has no NNNs.dsm, and StadiumPack +-- falls back to the normal model. That is also what a half-finished install +-- looks like, which is the behaviour we want from one. +local function writePack(species, bytes, shinyBytes) local f = fs() if not f then return false, "no filesystem" end local ok, err = f.write(("%s/%03d.dsm"):format(StadiumInstall.DIR, species), bytes) if not ok then return false, tostring(err) end + if shinyBytes then + -- A failed shiny write is not a failed install: the species still has + -- its model. Left unwritten, the runtime shows the normal one. + local sok, serr = f.write( + ("%s/%03ds.dsm"):format(StadiumInstall.DIR, species), shinyBytes) + if not sok and V.mod and V.mod.log then + V.mod.log.warn("shiny pack %03d not written: %s", species, tostring(serr)) + end + end return true end diff --git a/lib/StadiumMon.lua b/lib/StadiumMon.lua index e8420f5..d5de5b7 100644 --- a/lib/StadiumMon.lua +++ b/lib/StadiumMon.lua @@ -179,6 +179,7 @@ function StadiumMon.new(side) return setmetatable({ side = side, -- "player" or "enemy" species = nil, -- the dex number currently modelled + shiny = false, -- and whether it is the recoloured variant model = nil, rig = nil, state = "idle", @@ -195,6 +196,9 @@ end function StadiumMon:release() if self.rig then self.rig:release() end self.rig, self.model, self.species = nil, nil, nil + -- cleared with the species: a stale true here would make the next + -- setSpecies believe a shiny model was already loaded and early-return + self.shiny = false end -- ------- which species this side is showing @@ -222,14 +226,29 @@ end -- DATA rather than a list of dex numbers, so a future extraction bug that -- corrupts a species' idle falls back to the sprite instead of coming -- apart on the field -- and nothing here has to be edited when it does. -function StadiumMon:setSpecies(dex) - if dex == self.species then return self.rig ~= nil end +-- +-- `shiny` is part of the IDENTITY, not a flag applied afterwards. The early +-- return below is keyed on it for that reason: a shiny Rattata and an +-- ordinary one share a dex number but are different models, loaded from +-- different packs, and comparing on the dex alone would keep whichever +-- loaded first and colour both sides with it. That is precisely the shape +-- of bug the two-Rattata note above describes, and it is silent -- the +-- model is valid, it is simply the wrong one. +function StadiumMon:setSpecies(dex, shiny) + shiny = shiny and true or false + if dex == self.species and shiny == (self.shiny or false) then + return self.rig ~= nil + end if self.rig then self.rig:release() end self.rig, self.model, self.species = nil, nil, dex + self.shiny = shiny self.grow, self.grewOwn = nil, nil if not dex then return false end - local model = StadiumPack.load(dex) + local model = StadiumPack.load(dex, shiny) if not model then return false end + -- the pack falls back to the normal model when a species has no shiny + -- variant, so believe the model rather than the request + self.shiny = model.shiny and true or false if model.staticPose then return false end local rig = StadiumRig.new(model) if not rig then return false end diff --git a/lib/StadiumPack.lua b/lib/StadiumPack.lua index c23fa18..d37041a 100644 --- a/lib/StadiumPack.lua +++ b/lib/StadiumPack.lua @@ -62,13 +62,22 @@ local floor = math.floor StadiumPack.CACHE_DIR = "dramatic_shape/stadium" StadiumPack.DIR = "assets/stadium" -local function readPack(species) +-- The shiny variant sits beside its species as NNNs.dsm -- the same DSM3, +-- written by the same writer, differing only in its texture bytes. See the +-- note over StadiumInstall's writePack for why it is a separate file and not +-- a second block in the pack. +local function packName(dir, species, shiny) + return shiny and ("%s/%03ds.dsm"):format(dir, species) + or ("%s/%03d.dsm"):format(dir, species) +end + +local function readPack(species, shiny) -- The cache only counts when StadiumInstall's marker says it is a -- complete, CURRENT build -- an old cache (a rev the extractor has since -- fixed, a format that moved) must not shadow a fresh shipped set, and a -- half-written folder must not be read at all. Required lazily: Install -- requires this module at load, so the reverse edge cannot be taken then. - local rel = ("%s/%03d.dsm"):format(StadiumPack.CACHE_DIR, species) + local rel = packName(StadiumPack.CACHE_DIR, species, shiny) if love and love.filesystem and love.filesystem.getInfo and V.require("StadiumInstall").ready() then local okInfo, info = pcall(love.filesystem.getInfo, rel, "file") @@ -80,7 +89,7 @@ local function readPack(species) local mod = V.mod if not (mod and mod.read) then return nil end local ok, bytes = pcall(mod.read, mod, - ("%s/%03d.dsm"):format(StadiumPack.DIR, species)) + packName(StadiumPack.DIR, species, shiny)) if ok and type(bytes) == "string" and #bytes > 4 then return bytes end return nil end @@ -474,8 +483,27 @@ end -- ------- the cache -local cache = {} -- species -> model -local order = {} -- species, least recently used first +local cache = {} -- cache key -> model +local order = {} -- cache key, least recently used first + +-- The key is the species for a normal model and species+SHINY for a shiny +-- one, so the two are separate entries that cannot overwrite each other. +-- +-- They MUST be separate. The model table carries the decoded textures and +-- the lazily-built love Images hanging off them, and it is deliberately +-- shared by both sides and both VR eyes -- so a single entry per species +-- would mean a shiny Rattata and an ordinary one in the same fight fighting +-- over one texture set, and whichever loaded last would colour both. +local SHINY = 1000 -- clear of the 1..151 dex range + +local function cacheKey(species, shiny) + if not species then return nil end + return shiny and (species + SHINY) or species +end + +-- Four, because a mirror match between a shiny and a normal of the SAME +-- species is now two distinct models rather than one shared table, and both +-- sides must survive a fifth species being called out mid-fight. See keep(). StadiumPack.KEEP = 4 local function touch(species) @@ -520,30 +548,48 @@ end -- So the mode says, every frame, which two species are actually standing -- there (see Stadium.update). With KEEP at 4 and two sides, the two in use -- are always the two most recent and cannot reach the front of the queue. -function StadiumPack.keep(species) - if species and cache[species] then touch(species) end +function StadiumPack.keep(species, shiny) + local key = cacheKey(species, shiny) + if key and cache[key] then touch(key) end end -- Whether a pack for this species is on disk at all. Cheap enough to ask -- before a battle commits to the mode, and the honest test: a mod -- installed without its assets folder must decline rather than error. -function StadiumPack.available(species) - if cache[species] then return true end - return readPack(species) ~= nil +-- +-- Asked WITHOUT the shiny flag on purpose by the callers that gate the mode: +-- whether a species can be modelled at all is a question about its normal +-- pack. A missing shiny variant does not disqualify the species, it just +-- means that one mon is drawn in its ordinary colours. +function StadiumPack.available(species, shiny) + local key = cacheKey(species, shiny) + if key and cache[key] then return true end + return readPack(species, shiny) ~= nil end -- The model for a National Dex number (1..151), or nil. -function StadiumPack.load(species) +-- +-- `shiny` selects the recoloured variant. When a species has no shiny pack +-- -- an install from before rev 3, a recolour that failed at extraction, a +-- species we have no colours for -- this FALLS BACK to the normal model +-- rather than returning nil. The alternative is a shiny Pokemon that drops +-- to a flat 2D pic while its ordinary twin stands in 3D, which reads as a +-- bug; wrong colours read as a mod that has not finished installing. +function StadiumPack.load(species, shiny) if not (species and species >= 1 and species <= 151) then return nil end - local hit = cache[species] + local key = cacheKey(species, shiny) + local hit = cache[key] if hit ~= nil then - touch(species) + touch(key) return hit or nil end - local bytes = readPack(species) + local bytes = readPack(species, shiny) + if not bytes and shiny then + return StadiumPack.load(species, false) + end if not bytes then - cache[species] = false + cache[key] = false return nil end @@ -562,14 +608,22 @@ function StadiumPack.load(species) return m end) if not ok then - V.mod.log:warn("stadium: %03d.dsm did not read: %s -- that Pokemon " - .. "falls back to its flat pic", species, tostring(model)) - cache[species] = false + V.mod.log:warn("stadium: %s did not read: %s -- that Pokemon " + .. "falls back to its flat pic", + packName("", species, shiny):sub(2), tostring(model)) + -- A corrupt SHINY pack must not cost the species its model: fall back to + -- the normal one, exactly as a missing file does above. + if shiny then + cache[key] = false + return StadiumPack.load(species, false) + end + cache[key] = false return nil end - cache[species] = model - touch(species) + model.shiny = shiny and true or nil + cache[key] = model + touch(key) return model end diff --git a/main.lua b/main.lua index 810c023..3668b19 100644 --- a/main.lua +++ b/main.lua @@ -88,6 +88,8 @@ local WorldCurve = V.require("WorldCurve") local ViewBox = V.require("ViewBox") local OverworldBattle = V.require("OverworldBattle") local BattleExit = V.require("BattleExit") +local ShinyBattle = V.require("ShinyBattle") +local ShinyUI = V.require("ShinyUI") local DayNight = V.require("DayNight") local DayTint = V.require("DayTint") local Water = V.require("Water") @@ -1107,6 +1109,39 @@ end -- so this file keeps naming every engine seam the mod touches. OverworldBattle.install() +-- ------- shiny Pokemon +-- +-- ON, always, with no row to switch it off: shininess is a property of the +-- Pokemon rather than a display mode, and a Pokemon that is shiny in one +-- player's save and not another's is not a Pokemon, it is a setting. +-- +-- It rests on a fact the engine already ships. Gen 1 has no shininess of its +-- own, but it has the four DVs Gen 2 reads to decide it, and +-- src/pokemon/Stats.lua:90 carries that reading -- the engine's own comment +-- calls it "the RBY virtual shiny" and says it is there for indicator mods. +-- So nothing new is stored on a Pokemon and nothing has to migrate: every +-- save ever made already contains the answer, and this only starts drawing +-- it. See lib/Shiny.lua for why deriving beats storing. +-- +-- Three seams, each in its own file with its own reasoning: +-- ShinyBattle wraps Pokemon.new, which is where every wild, gift, +-- starter and traded mon is built, so the roll lands before +-- the sprite is baked +-- ShinyUI the battle pics' tint and the status page's mark +-- ShinyFx the arrival sparkle (armed from Stadium.update) +-- +-- The Stadium models need no seam here at all: their recolour happens at +-- extraction (lib/StadiumBuild.lua), and the battle simply asks for the +-- shiny pack. +ShinyBattle.install() +ShinyUI.install() + +-- A save opened for the first time under this mod has shiny Pokemon in it +-- already -- they always did -- so refresh the cached flag across the party +-- rather than leaving it absent until each mon next changes. +mod.events:on("save.loaded", function() ShinyBattle.markParty() end) +mod.events:on("save.created", function() ShinyBattle.markParty() end) + -- ------- the free-roam rungs' inputs and their walk -- -- 1ST and 3RD need two things no other rung does, and each is a named seam. diff --git a/tests/shiny_palette_fixture.lua b/tests/shiny_palette_fixture.lua new file mode 100644 index 0000000..e45f6e0 --- /dev/null +++ b/tests/shiny_palette_fixture.lua @@ -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}, +} diff --git a/tests/shiny_test.lua b/tests/shiny_test.lua new file mode 100644 index 0000000..c2b97bf --- /dev/null +++ b/tests/shiny_test.lua @@ -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) diff --git a/tests/stadium_extract_test.lua b/tests/stadium_extract_test.lua index 626d517..c4415df 100644 --- a/tests/stadium_extract_test.lua +++ b/tests/stadium_extract_test.lua @@ -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 diff --git a/tools/shiny_colors.py b/tools/shiny_colors.py new file mode 100644 index 0000000..11cf6b9 --- /dev/null +++ b/tools/shiny_colors.py @@ -0,0 +1,153 @@ +"""Emit the mod's data/shiny_colors.lua from the validated Stadium tables. + +PROVENANCE. data/shiny_colors.lua is 62KB of generated data and this is +where it came from, so it is checked in even though its INPUTS are not: +they live under .claude/shiny_stadium/ (the research tree: the transcribed +Stadium colour table, the extracted texture pairs, the inventory that marks +which textures are effects), and that path is gitignored along with +everything else derived from the ROM. + +So this will not run from a fresh clone, and it is not meant to -- the Lua +it writes is the shipped artefact. It is here to record HOW the numbers were +arrived at, and to be re-runnable by anyone who still has the research tree. + +The values themselves are the Stadium games' own: a hue rotation in degrees +plus saturation and lightness on a quantized -8..+8 scale at 12.5% a step. +The column order in the source table is H, L, S -- hue, LIGHTNESS, +saturation -- which was checked against the canonical pokeemerald palettes +across all 146 sliding species before being trusted. + + +Two kinds of entry, because Stadium itself has two kinds of shiny: + + slide {h, l, s} the 146 species Stadium recolours by sliding the whole + model in HSL. h is degrees; l and s are Stadium's + -8..+8 steps at 12.5% each. Three numbers reproduce the + whole model, so this is data-cheap and exact. + + lut {from = to} the 5 species Stadium ships a genuine alternate texture + for (Clefairy, Clefable, Jigglypuff, Wigglytuff, + Gyarados). No slide can express those -- Jigglypuff's + body must stay pink while its irises rotate to green -- + so they carry an explicit colour mapping, sampled from + the verified texture pairs. Exact by construction, and + only the colours that actually change are listed. +""" +import json +import os +from PIL import Image + +SS = r"C:\Users\blcha\Desktop\DRAMATIC_SHAPE\pokemon-gen1-recomp-project\mods\DramaticShapeVoxelMod\.claude\shiny_stadium" +OUT = r"C:\Users\blcha\Desktop\DRAMATIC_SHAPE\pokemon-gen1-recomp-project\mods\DramaticShapeVoxelMod\.claude\worktrees\shiny-system\data\shiny_colors.lua" + +vals = json.load(open(os.path.join(SS, "stadium_shiny_values.json"), encoding="utf-8")) +V = vals["species"] + +inv = json.load(open(os.path.join(SS, "texture_inventory.json"), encoding="utf-8")) +models = inv["models"] if isinstance(inv, dict) and "models" in inv else inv +if isinstance(models, dict): + models = list(models.values()) +kind = {} +for m in models: + dex = str(m.get("dex") or m.get("species") or "").zfill(3) + for t in m.get("textures", []): + kind[(dex, int(t.get("index", -1)))] = t.get("kind", "body") + +TEX = os.path.join(SS, "textures") +slugs = {d.split("_")[0]: d for d in os.listdir(os.path.join(TEX, "normal"))} + + +def lut_for(dex): + """Distinct colour mapping over this species' BODY textures only. + + fx textures are excluded exactly as the build excluded them: a shiny + Gyarados has shiny scales and an ordinary Hyper Beam. + """ + d = slugs[dex] + pairs = {} + for fn in sorted(os.listdir(os.path.join(TEX, "normal", d))): + idx = int(fn.replace("tex_", "").replace(".png", "")) + if kind.get((dex, idx), "body") != "body": + continue + a = Image.open(os.path.join(TEX, "normal", d, fn)).convert("RGBA") + b = Image.open(os.path.join(TEX, "shiny", d, fn)).convert("RGBA") + if a.size != b.size: + continue + pa, pb = a.load(), b.load() + for y in range(a.size[1]): + for x in range(a.size[0]): + ca, cb = pa[x, y], pb[x, y] + if ca[3] < 8: + continue + pairs.setdefault(ca[:3], cb[:3]) + return {k: v for k, v in pairs.items() if k != v} + + +lines = [] +w = lines.append + +w("-- Shiny colours for the 151 Stadium models, as the Stadium games define") +w("-- them. GENERATED -- do not hand-edit. The colour model is documented in") +w("-- the header of lib/ShinyPalette.lua.") +w("--") +w("-- Stadium does not ship a second set of textures for a shiny Pokemon. It") +w("-- slides the colours it already has in HSL: a hue rotation in degrees,") +w("-- plus saturation and lightness on a quantized -8..+8 scale where one") +w("-- step is 12.5% (so +-8 is +-100%, exactly GIMP's Hue-Saturation range --") +w("-- s = -8 is full greyscale, l = +8 is white).") +w("--") +w("-- FIVE SPECIES ARE DIFFERENT. Clefairy, Clefable, Jigglypuff, Wigglytuff") +w("-- and Gyarados get a genuine alternate texture in Stadium, because no") +w("-- single slide can produce their shiny: Jigglypuff's body must stay pink") +w("-- while its irises rotate to green, and one rotation moves both or") +w("-- neither. Those five carry `lut` -- an explicit before/after colour") +w("-- mapping sampled from the verified texture pairs, listing only the") +w("-- colours that actually change. A colour absent from the table is left") +w("-- exactly as it was.") +w("--") +w("-- hueRange is the min/max hue a NON-shiny nicknamed mon can slide to in") +w("-- Stadium (derived from the trainer ID and the nickname). Carried for") +w("-- reference; nothing reads it today.") +w("") +w("return {") + +n_slide = n_lut = n_entries = 0 +for dex in sorted(V): + e = V[dex] + hr = e["hue_range"] + w(" [%d] = {" % int(dex)) + w(' name = "%s",' % e["name"]) + w(" hueRange = { min = %d, max = %d }," % (hr["min"], hr["max"])) + if e["special_texture"]: + n_lut += 1 + lut = lut_for(dex) + n_entries += len(lut) + w(" -- Stadium ships a real alternate texture for this one; %d" + % len(lut)) + w(" -- colours move, every other colour stays exactly as it was.") + w(" lut = {") + row = [] + for src in sorted(lut): + dst = lut[src] + row.append("[0x%02X%02X%02X]=0x%02X%02X%02X," + % (src[0], src[1], src[2], dst[0], dst[1], dst[2])) + if len(row) == 4: + w(" " + " ".join(row)) + row = [] + if row: + w(" " + " ".join(row)) + w(" },") + else: + n_slide += 1 + h = e["hsl"] + w(" slide = { h = %d, l = %d, s = %d }," % (h["h"], h["l"], h["s"])) + w(" },") + +w("}") + +os.makedirs(os.path.dirname(OUT), exist_ok=True) +open(OUT, "w", encoding="utf-8").write("\n".join(lines) + "\n") +print("wrote", OUT) +print("slide species:", n_slide, " lut species:", n_lut, + " lut entries:", n_entries) +print("bytes:", os.path.getsize(OUT))