Files
2026-08-05 20:36:46 -04:00

158 lines
6.0 KiB
Lua

-- Driver: one STADIUM screenshot per species, every one of the 151 standing
-- on the ENEMY mark in its standby loop.
--
-- stadium_shots.lua photographs a handful of deliberately awkward cases; this
-- is the exhaustive sweep behind it. What it exists to catch is the class of
-- fault that is per-MODEL and invisible to the headless QA pass -- a texture
-- that comes out magenta, a mon standing in the floor or floating over it, a
-- silhouette that is subtly the wrong shape -- none of which is a number
-- stadium_anim_qa.lua can measure.
--
-- SHOT_DIR=mods/DramaticShapeVoxelMod/.claude/stadium_verify \
-- POKEPORT_SPEED=4 \
-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/stadium_all_shots.lua \
-- lovec.exe .
--
-- Files land as NNN_species.png in dex order. Run from the PROJECT ROOT.
--
-- ------- everything but the foe is nailed down
--
-- The whole value of 151 shots is that they are comparable, so the staging is
-- identical in every one: same map, same cells, same hour (the day/night
-- cycle would otherwise relight every shot), same rung, and the same small
-- Pokemon on the player's mark -- DIGLETT, chosen because it is the shortest
-- model in the set and so hides the least of the arena behind it. Whatever
-- differs between two of these shots is the foe.
--
-- ------- the model/sprite verdict is the other half
--
-- A species whose pack will not load, or whose animation data the packer
-- declined, does not error: StadiumMon.setSpecies answers false and the mode
-- quietly stands the Game Boy's flat pic on the tile instead. That is the
-- correct behaviour and it is nearly invisible in a screenshot at a glance --
-- which is exactly why it needs counting rather than looking. Stadium.showing
-- is asked for every species and the ones that decline are listed at the end.
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or ".claude/stadium_verify"
local Pokemon = require("src.pokemon.Pokemon")
local BattleState = require("src.battle.BattleState")
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 -- enable it and run again")
return
end
local Battles = lib.require("OverworldBattle")
local Stadium = lib.require("Stadium")
local Pack = lib.require("StadiumPack")
local Install = lib.require("StadiumInstall")
-- every real species the merged data carries, in dex order
local species = {}
for id, def in pairs(game.data.pokemon) do
if type(id) == "string" and type(def) == "table"
and def.dex and def.dex >= 1 and def.dex <= 151 then
species[#species + 1] = { id = id, dex = def.dex }
end
end
table.sort(species, function(a, b) return a.dex < b.dex end)
game.save.player.name = "RED"
-- ONTO THE MAP FIRST: the model build (if one is owed) is pushed from the
-- mod's update hook on the first frame the overworld is the top state, so
-- waiting for it before teleporting waits forever.
U.teleport(game, "ROUTE_1", 5, 8, "down")
local waited = 0
while (Install.pending() or Install.status.state == "building")
and waited < 5400 do
U.wait(10)
waited = waited + 10
end
if waited > 0 then
U.log(("waited %.1fs for the model build -- %s")
:format(waited / 60, tostring(Install.status.state)))
end
Battles.setting:setValue("stadium", game)
Battles.backSetting:setValue(false, game)
lib.require("DayNight").setting:setValue(os.getenv("DS_TOD") or "day", game)
local have = 0
for dex = 1, 151 do
if Pack.available(dex) then have = have + 1 end
end
U.log(("%d species, %d/151 packs on disk, rung = %s")
:format(#species, have, tostring(Battles.setting:get())))
-- let the neighbourhood's meshes land, so the first fight opens on the
-- real arena rather than the flat fallback
U.wait(90)
local asPic, stuck, missed = {}, {}, {}
for _, s in ipairs(species) do
-- the player's side is a constant (see the header), so the only thing
-- that changes between two shots is the Pokemon being verified
game.save.party = { Pokemon.new(game.data, "DIGLETT", 40) }
local battle = BattleState.newWild(game, s.id, 30)
battle.onFinish = function() end
game.overworld:pushBattle(battle)
-- the wipe and the intro chatter, then far enough past it that the
-- player's own Pokemon has been sent out -- before that the player's
-- side is legitimately the trainer's back sprite and the foe is still
-- sliding in
U.wait(70)
for _ = 1, 40 do
if battle.phase == "menu" then break end
U.tap(game, "a")
U.wait(10)
end
if battle.phase ~= "menu" then
stuck[#stuck + 1] = s.id
U.log(("STUCK before menu: %s (phase %s)")
:format(s.id, tostring(battle.phase)))
end
-- and let the send-out beat finish so the mon is standing still in its
-- standby loop rather than mid-arrival
U.wait(45)
-- model or flat pic? asked here, with the fight actually on screen
if not Stadium.showing("enemy") then
asPic[#asPic + 1] = ("%03d %s"):format(s.dex, s.id)
end
local path = ("%s/%03d_%s.png"):format(DIR, s.dex, s.id:lower())
if not U.shot(game, path) then
missed[#missed + 1] = s.id
end
while game.stack:top() and game.stack:top() ~= game.overworld do
game.stack:pop()
end
U.wait(10)
end
U.log(("---- %d shots -> %s"):format(#species - #missed, DIR))
if #asPic > 0 then
U.log(("%d species declined to a model and were shot as FLAT PICS: %s")
:format(#asPic, table.concat(asPic, ", ")))
else
U.log("all species stood as MODELS -- none fell back to a flat pic")
end
if #stuck > 0 then
U.log(("%d never reached the battle menu: %s")
:format(#stuck, table.concat(stuck, ", ")))
end
if #missed > 0 then
U.log(("%d screenshots did not reach disk: %s")
:format(#missed, table.concat(missed, ", ")))
end
U.log("done -- " .. DIR)
end