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

shiny_shots.lua

tests/shiny_shots.lua10.2 KB10064450c6bd65ed
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 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
-- 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