mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 13:30:52 +02:00
a3fb18a589
Gen 1 has no shininess of its own, but it has the four DVs Gen 2 reads to decide it -- and the engine already ships that reading (Stats.isShiny, its own comment calling it "the RBY virtual shiny", allowlisted for mods precisely so an indicator mod can call it). Nothing new is stored on a Pokemon and nothing migrates: every save already contains the answer, and this starts drawing it. Random DVs land on the pattern 1 in 8192, which is the classic rate and the default the odds dial ships at. Deriving rather than storing is what makes it survive a save, a box, a trade and an evolution with no second copy of the truth to drift. mon.shiny is a cache written from the DVs, never read as the source. The roll goes in Pokemon.new -- every wild, gift, starter and traded mon is built there, and it is before the battle bakes its sprite, which battle.started is already too late for. It draws from the mod's own random stream so installing this does not shift the sequence damage rolls and encounter slots come out of. Trainers stay ordinary by themselves: the engine pins their DVs, as the real games do. The models are genuinely recoloured, as part of the extraction. Each species is decoded once, packed as usual, then recoloured and packed again as NNNs.dsm. The colours are Stadium's own HSL slide (hue in degrees, saturation and lightness on a -8..+8 scale at 12.5% a step); five species carry an explicit colour table instead, because Stadium gives them a real alternate texture that no single slide reproduces -- Jigglypuff's body must stay pink while its irises rotate to green. Extraction is the right moment because StadiumFx's generated frames are still marked there and the packer drops the marker: it is the last point a flame is distinguishable from a hide. A shiny Charizard has a shiny hide and an ordinary fire. The normal packs are written BEFORE the recolour, so they come out byte-identical and stadium_extract_test still diffs all 151 against the Python oracle unchanged -- no format change, no DSM4, no second implementation to keep in step. REV goes to 3 so an existing cache rebuilds. Flat art is tinted instead, because the engine bakes a species palette into a cache with no notion of which individual is drawn. The tint comes from that species' own slide rather than a generic gold. A multiply can only darken, so species whose shiny is lighter read quieter there than on the model; the status page's star is the mode-proof mark. Tests: 58 assertions in tests/shiny_test.lua, including the colour transform against 640 real colour pairs lifted from the verified texture set, the DV model, the read side, and the end-to-end through the engine's own constructor. stadium_extract_test gains --mod (worktrees have neither the ROM nor the packs, both gitignored) and now also checks that every shiny pack is the same length as its twin and actually differs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
98 lines
3.7 KiB
Lua
98 lines
3.7 KiB
Lua
-- 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
|