mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 12:50:50 +02:00
eb69bc7db8
It did not. pending() short-circuited on available(), and available() was true for any checkout carrying assets/stadium -- so when REV went to 3 for the shiny variants, such a machine was never pending, was never asked to rebuild, and quietly went on serving the old set. ready() was false, so readPack skipped the save-dir cache and read the shipped normal-only packs: every shiny Pokemon drawn in its ordinary colours, with nothing on screen saying why. A driver run sitting at "idle 0/151" for twelve thousand frames is what surfaced it. pending() is now keyed on ready(), not available(). A checkout with a ROM spends one loading screen rebuilding a set it had files for; after that it is current and never pending again. That was the cost the old short-circuit was avoiding, and it is worth paying once to make a rev bump mean something. The other half is the players who imported a ROM through the picker rather than dropping it in baseroms/. beginFrom builds from the bytes and never keeps them, so on a rev bump there is no ROM to rebuild from -- and with available() keyed on ready(), the STADIUM rungs would have vanished off the options row entirely. usable() now separates "these packs are readable" from "these packs are current": format and count, deliberately not rev. Stale packs keep the mode working and keep the rungs offered; only the recolour waits for a rebuild. Losing the shinies until then is a blemish, losing the mode is not. readPack orders the two accordingly: a current cache always wins, a stale one wins only when there is no shipped set to prefer instead -- so a cache from an extractor rev we have since fixed cannot shadow good files, while a player whose only copy IS that cache still gets Pokemon on the field. Verified by rolling the marker back to rev 2 and launching: ready=false, usable=true, available=true, pending=true, and the build runs unprompted.
252 lines
10 KiB
Lua
252 lines
10 KiB
Lua
-- Driver: photograph the shiny system, each case beside its own control.
|
|
--
|
|
-- SHOT_DIR=mods/DramaticShapeVoxelMod/.claude/shiny_update \
|
|
-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/shiny_shots.lua \
|
|
-- "/c/Program Files/LOVE/lovec.exe" .
|
|
--
|
|
-- SHOT_DIR must already exist -- the capture writes with io.open.
|
|
--
|
|
-- EVERY CASE IS SHOT TWICE, the same species on the same tile, once
|
|
-- ordinary and once shiny. A single shiny screenshot proves nothing: these
|
|
-- are N64 models under a day/night tint on generated ground, and "that
|
|
-- looks a bit purple" is not evidence. The pair is.
|
|
--
|
|
-- The four species are chosen to exercise the four things that can break:
|
|
--
|
|
-- GYARADOS one of the five Stadium gives a REAL alternate texture, so
|
|
-- it runs the explicit colour table rather than the HSL slide.
|
|
-- Blue to red, the least deniable shiny in Gen 1.
|
|
-- CHARIZARD the biggest slide in the set (H -136, S -6). Also the one
|
|
-- whose canonical shiny is famously black, which the Stadium
|
|
-- values do NOT reproduce -- they give a dusky slate-violet.
|
|
-- Shot precisely so that difference is on the record.
|
|
-- GOLBAT L -6, the darkest lightness step there is. If the guard
|
|
-- rails are wrong this is where it goes to mud.
|
|
-- PONYTA made of fire. Its flames are GENERATED frames, excluded
|
|
-- from the recolour, so the correct result is a recoloured
|
|
-- body with an ordinary mane. That exclusion is invisible in
|
|
-- every other species.
|
|
return function(game)
|
|
local U = dofile("tests/drivers/util.lua")
|
|
local DIR = os.getenv("SHOT_DIR")
|
|
or "mods/DramaticShapeVoxelMod/.claude/shiny_update"
|
|
local BattleState = require("src.battle.BattleState")
|
|
local Pokemon = require("src.pokemon.Pokemon")
|
|
|
|
local exports = game.mods and game.mods.exports
|
|
local lib = exports and exports.DRAMATIC_SHAPE and exports.DRAMATIC_SHAPE.lib
|
|
if not lib then
|
|
U.log("DRAMATIC_SHAPE is not loaded -- nothing to shoot")
|
|
return
|
|
end
|
|
local Shiny = lib.require("Shiny")
|
|
local ShinyFx = lib.require("ShinyFx")
|
|
local OverworldBattle = lib.require("OverworldBattle")
|
|
local StadiumInstall = lib.require("StadiumInstall")
|
|
local StadiumPack = lib.require("StadiumPack")
|
|
|
|
-- ------- the models
|
|
--
|
|
-- REV went to 3 with the shiny variants, so an existing cache is stale and
|
|
-- the game rebuilds all 151 on the loading screen. That is a minute or so
|
|
-- of frames, and it has to be waited out rather than assumed: shooting
|
|
-- before it lands gets flat 2D pics and a very confusing set of images.
|
|
-- Driven from HERE rather than waited on. The build is normally pumped by
|
|
-- the loading screen StadiumScreen.maybePush puts up on the first world
|
|
-- frame, but a driver owns the frame loop and that screen never came up --
|
|
-- a first run sat at "idle 0/151" for twelve thousand frames and shot four
|
|
-- unrecoloured Pokemon. Calling begin/step directly is both faster and
|
|
-- honest about what is being tested, which is the extraction, not the
|
|
-- screen that usually triggers it.
|
|
-- the upgrade question, asked before anything is built: with a stale
|
|
-- marker on disk, does this machine know it has work to do?
|
|
U.log(("upgrade check: ready=%s usable=%s available=%s pending=%s rom=%s")
|
|
:format(tostring(StadiumInstall.ready()),
|
|
tostring(StadiumInstall.usable()),
|
|
tostring(StadiumInstall.available()),
|
|
tostring(StadiumInstall.pending()),
|
|
tostring(StadiumInstall.romPresent())))
|
|
|
|
if not StadiumInstall.ready() then
|
|
local ok, err = StadiumInstall.begin()
|
|
U.log(("stadium build: begin=%s %s"):format(tostring(ok), tostring(err or "")))
|
|
local guard = 0
|
|
while not StadiumInstall.ready() and guard < 2000 do
|
|
-- several species per frame: 151 of them at one a frame is a long
|
|
-- wait for no reason, and nothing here needs to be drawn
|
|
for _ = 1, 6 do StadiumInstall.step() end
|
|
U.wait(1)
|
|
guard = guard + 1
|
|
local st = StadiumInstall.status
|
|
if st and st.state == "failed" then
|
|
U.log("stadium build FAILED: " .. tostring(st.error))
|
|
break
|
|
end
|
|
if guard % 10 == 0 then
|
|
U.log((" building: %s %d/%d"):format(tostring(st and st.state),
|
|
(st and st.done) or 0, (st and st.total) or 0))
|
|
end
|
|
end
|
|
end
|
|
U.log("stadium ready: " .. tostring(StadiumInstall.ready()))
|
|
|
|
-- and prove the shiny packs are actually THERE before shooting anything
|
|
for _, dex in ipairs({ 130, 6, 42, 77 }) do
|
|
U.log((" pack %03d: normal=%s shiny=%s"):format(
|
|
dex, tostring(StadiumPack.available(dex, false)),
|
|
tostring(StadiumPack.available(dex, true))))
|
|
end
|
|
|
|
-- STADIUM A: models, staged on the map
|
|
OverworldBattle.setting:setValue("stadium", game)
|
|
U.log("3D-BTL = " .. tostring(OverworldBattle.setting:get()))
|
|
|
|
game.save.player.name = "RED"
|
|
game.save.party = { Pokemon.new(game.data, "PIKACHU", 50) }
|
|
|
|
local CASES = {
|
|
{ "GYARADOS", 40, "ROUTE_1", 5, 8 },
|
|
{ "CHARIZARD", 50, "ROUTE_1", 5, 8 },
|
|
{ "GOLBAT", 40, "ROUTE_1", 5, 8 },
|
|
{ "PONYTA", 40, "ROUTE_1", 5, 8 },
|
|
}
|
|
|
|
-- Odds are the real lever, so the shiny half goes through the SAME path a
|
|
-- player's encounter does -- decided inside Pokemon.new by a roll -- rather
|
|
-- than being stamped on afterwards. 1 means every mon; a huge denominator
|
|
-- means none, which is what makes the control a control.
|
|
local function setOdds(shiny)
|
|
Shiny.setOdds(shiny and 1 or 100000000)
|
|
end
|
|
|
|
local function toMenu()
|
|
for _ = 1, 16 do U.tap(game, "a") U.wait(8) end
|
|
end
|
|
|
|
local function leave()
|
|
while game.stack:top() and game.stack:top() ~= game.overworld do
|
|
game.stack:pop()
|
|
end
|
|
U.wait(10)
|
|
end
|
|
|
|
for i, c in ipairs(CASES) do
|
|
local species, level, map, cx, cy = c[1], c[2], c[3], c[4], c[5]
|
|
if not game.data.pokemon[species] then
|
|
U.log("no such species in this dataset: " .. species)
|
|
else
|
|
for _, variant in ipairs({ "normal", "shiny" }) do
|
|
local shiny = (variant == "shiny")
|
|
setOdds(shiny)
|
|
|
|
U.teleport(game, map, cx, cy, "down")
|
|
U.wait(90) -- let the neighbourhood's meshes land
|
|
|
|
local battle = BattleState.newWild(game, species, level)
|
|
battle.onFinish = function() end
|
|
game.overworld:pushBattle(battle)
|
|
|
|
local mon = battle.enemy and battle.enemy.mon
|
|
U.log(("%s %s: isShiny=%s dvs=%s/%s/%s/%s flag=%s"):format(
|
|
species, variant, tostring(Shiny.isShiny(mon)),
|
|
tostring(mon and mon.dvs and mon.dvs.attack),
|
|
tostring(mon and mon.dvs and mon.dvs.defense),
|
|
tostring(mon and mon.dvs and mon.dvs.speed),
|
|
tostring(mon and mon.dvs and mon.dvs.special),
|
|
tostring(mon and mon.shiny)))
|
|
|
|
-- THE ARRIVAL. Shot during the send-out rather than after it,
|
|
-- because the sparkle is three quarters of a second long and the
|
|
-- menu is well past it. Three frames close together, so one of them
|
|
-- lands mid-burst whatever the intro's pacing does on this map.
|
|
U.wait(70)
|
|
if shiny then
|
|
for k = 1, 3 do
|
|
U.shot(game, ("%s/%d_%s_arrival_%d.png")
|
|
:format(DIR, i, species:lower(), k))
|
|
U.wait(10)
|
|
end
|
|
end
|
|
|
|
toMenu()
|
|
U.shot(game, ("%s/%d_%s_%s.png")
|
|
:format(DIR, i, species:lower(), variant))
|
|
|
|
-- and the sparkle again, armed deliberately and shot on the next
|
|
-- frame. The arrival shots above catch it in its real moment but
|
|
-- depend on intro timing; this one is the effect itself, on the
|
|
-- record, at a known point in its life.
|
|
if shiny then
|
|
for k, v in pairs(ShinyFx.debug) do ShinyFx.debug[k] = 0 end
|
|
ShinyFx.arm("enemy")
|
|
U.wait(4)
|
|
U.shot(game, ("%s/%d_%s_sparkle.png"):format(DIR, i, species:lower()))
|
|
local d = ShinyFx.debug
|
|
U.log((" fx: calls=%d noArena=%d noImage=%d noMesh=%d noLive=%d quads=%d")
|
|
:format(d.calls, d.noArena, d.noImage, d.noMesh, d.noLive,
|
|
d.quads))
|
|
end
|
|
|
|
leave()
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ------- the FLAT art
|
|
--
|
|
-- Everything above is the STADIUM rung, which draws recoloured 3D models
|
|
-- and never touches a pic. The tint is the other half of the feature and
|
|
-- needs its own rung to be visible at all: 2D-3D stands the game's own
|
|
-- battle pics up as cards, which is the path ShinyUI tints.
|
|
OverworldBattle.setting:setValue(true, game)
|
|
U.log("3D-BTL = " .. tostring(OverworldBattle.setting:get()) .. " (cards)")
|
|
for _, variant in ipairs({ "normal", "shiny" }) do
|
|
setOdds(variant == "shiny")
|
|
U.teleport(game, "ROUTE_1", 5, 8, "down")
|
|
U.wait(90)
|
|
local battle = BattleState.newWild(game, "GYARADOS", 40)
|
|
battle.onFinish = function() end
|
|
game.overworld:pushBattle(battle)
|
|
U.log(("cards %s: isShiny=%s"):format(
|
|
variant, tostring(Shiny.isShiny(battle.enemy and battle.enemy.mon))))
|
|
U.wait(70)
|
|
toMenu()
|
|
U.shot(game, ("%s/6_cards_gyarados_%s.png"):format(DIR, variant))
|
|
leave()
|
|
end
|
|
|
|
-- ------- the status page
|
|
--
|
|
-- A shiny and an ordinary mon of the SAME species, so the star is the only
|
|
-- difference between the two images.
|
|
local SummaryMenu = require("src.ui.SummaryMenu")
|
|
for _, variant in ipairs({ "normal", "shiny" }) do
|
|
setOdds(variant == "shiny")
|
|
local mon = Pokemon.new(game.data, "GYARADOS", 40)
|
|
U.log(("summary %s: isShiny=%s flag=%s"):format(
|
|
variant, tostring(Shiny.isShiny(mon)), tostring(mon.shiny)))
|
|
game.save.party = { mon }
|
|
game.stack:push(SummaryMenu.new(game, mon))
|
|
U.wait(20)
|
|
U.shot(game, ("%s/5_status_%s.png"):format(DIR, variant))
|
|
leave()
|
|
end
|
|
|
|
-- ------- the odds actually being odds
|
|
--
|
|
-- Not a screenshot, but it belongs in the same run: the rate is the thing
|
|
-- a player experiences, and it is the one claim a picture cannot make.
|
|
Shiny.setOdds(8192)
|
|
local n, hits = 4000, 0
|
|
for _ = 1, n do
|
|
if Shiny.isShiny(Pokemon.new(game.data, "RATTATA", 5)) then
|
|
hits = hits + 1
|
|
end
|
|
end
|
|
U.log(("odds check: %d shinies in %d at 1/8192 (expect ~0-2)")
|
|
:format(hits, n))
|
|
|
|
Shiny.setOdds(8192)
|
|
U.log("done -- " .. DIR)
|
|
end
|