DramaticShapeVoxelMod archival mirror · read-only
tests/stadium_all_shots.lua

stadium_all_shots.lua

tests/stadium_all_shots.lua6.0 KB1006441c09cc9c7f
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
-- 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