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

shiny_one.lua

tests/shiny_one.lua7.5 KB10064495102e15d2
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
-- Driver: ONE shiny encounter, at real 1x speed, held open until you close
-- the window. Meant to be launched once per Pokemon by tests/shiny_run.sh.
--
--   DS_SPECIES=GYARADOS DS_LEVEL=45 DS_MAP=CERULEAN_CITY DS_CX=10 DS_CY=12 \
--   POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/shiny_one.lua \
--   "/c/Program Files/LOVE/lovec.exe" .
--
-- ------- why the pacing is done here
--
-- A driver run is deliberately UNPACED: main.lua's pacingEnabled() returns
-- false whenever POKEPORT_DRIVER is set, so love.run spins as fast as the
-- machine will go and the loop takes one logic step (Game:update(1/60)) per
-- turn of it. That is right for a screenshot script and wrong for a capture:
-- the game runs at whatever multiple of real time the hardware manages, so
-- "wait 180 frames" is three seconds only by coincidence.
--
-- The fix does not need an engine change. The loop is blocked while this
-- coroutine is running, so sleeping HERE before each yield paces the whole
-- thing -- one 1/60 logic step per 1/60 of real time, which is 1x by
-- construction. Nothing else in the engine has to know.
--
-- ------- and why it never finishes
--
-- When a driver coroutine goes dead, main.lua calls love.event.quit(). So
-- holding the battle open forever is exactly how the window stays up until
-- somebody closes it, which is the handshake this run wants: one window, one
-- Pokemon, closed by hand, and only then the next.
return function(game)
  local U = dofile("tests/drivers/util.lua")
  local BattleState = require("src.battle.BattleState")
  local Pokemon = require("src.pokemon.Pokemon")

  local SPECIES = os.getenv("DS_SPECIES") or "GYARADOS"
  local LEVEL   = tonumber(os.getenv("DS_LEVEL") or "") or 45
  local MAP     = os.getenv("DS_MAP") or "ROUTE_1"
  local CX      = tonumber(os.getenv("DS_CX") or "") or 5
  local CY      = tonumber(os.getenv("DS_CY") or "") or 8
  local LEAD    = tonumber(os.getenv("DS_LEAD") or "") or 2.0   -- seconds

  -- ------- real-time pacing
  --
  -- Each call is one logic step AND one sixtieth of a second of wall clock.
  -- The target is carried forward rather than measured from "now" so a slow
  -- frame is absorbed by the next one instead of compounding into drift.
  local nextAt = nil
  local function step()
    local now = love.timer.getTime()
    nextAt = (nextAt and (nextAt + 1 / 60)) or (now + 1 / 60)
    local slack = nextAt - now
    if slack > 0 then
      love.timer.sleep(slack)
    else
      nextAt = now                 -- fell behind; do not try to catch up
    end
    coroutine.yield()
  end

  local function hold(seconds)
    for _ = 1, math.floor(seconds * 60) do step() end
  end

  -- Unpaced, on purpose: a model rebuild is a loading screen, not part of
  -- the capture, and there is no reason to watch it at 1x.
  local function fast(n)
    for _ = 1, n do coroutine.yield() end
  end

  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")
    return
  end
  local Shiny = lib.require("Shiny")
  local OverworldBattle = lib.require("OverworldBattle")
  local StadiumInstall = lib.require("StadiumInstall")

  if not StadiumInstall.ready() then
    U.log("building stadium models (not part of the capture)...")
    StadiumInstall.begin()
    local guard = 0
    while not StadiumInstall.ready() and guard < 2000 do
      for _ = 1, 6 do StadiumInstall.step() end
      fast(1)
      guard = guard + 1
    end
  end

  -- STADIUM A stages the fight on the MAP, which wants clear ground. A cave
  -- floor often has none, and the mode's answer there is STADIUM B: the two
  -- carried discs, which work anywhere. DS_RUNG picks between them per
  -- encounter rather than forcing one choice on every location.
  OverworldBattle.setting:setValue(os.getenv("DS_RUNG") or "stadium", game)

  -- THE PARTY IS BUILT FIRST, at ordinary odds, and the roll is only pinned
  -- afterwards. Pokemon.new is where shininess is decided, so setting the
  -- odds before this line made the player's own Pikachu shiny too -- and a
  -- shiny on the player's side tints that side's pic, which during the intro
  -- is the TRAINER, so the player sprite came out discoloured for the whole
  -- send-out. The foe is the one this run is about.
  game.save.player.name = "RED"
  game.save.party = { Pokemon.new(game.data, "PIKACHU", 50) }

  Shiny.setOdds(1)                       -- from here on: the encounter

  if not game.data.pokemon[SPECIES] then
    U.log("no such species: " .. SPECIES)
    return
  end
  if not game.data.maps[MAP] then
    U.log("no such map: " .. MAP)
    return
  end

  U.teleport(game, MAP, CX, CY, "down")
  hold(1.6)                              -- let the neighbourhood mesh

  hold(LEAD)                             -- a beat before the fight opens

  local battle = BattleState.newWild(game, SPECIES, LEVEL)
  battle.onFinish = function() end
  game.overworld:pushBattle(battle)

  -- ------- start the shot wider
  --
  -- BattleCam.zoom is a multiple of the rig's own frame height, so ABOVE one
  -- is zoomed OUT -- "the fight in its own landscape", per the note on the
  -- constants. 1.15 is fifteen percent wider, which happens to be exactly
  -- one notch of the player's own wheel (ZOOM_STEP).
  --
  -- Set AFTER the battle is pushed, because OverworldBattle's begin path
  -- calls BattleCam.reset() and that puts zoom and zoomGoal back to 1. Both
  -- are set, not just the goal: leaving the goal alone would make the shot
  -- glide outward over the first fifth of a second, and this wants to OPEN
  -- wide rather than pull back once the recording has started.
  local DS_ZOOM = tonumber(os.getenv("DS_ZOOM") or "") or 1.15
  local okCam, BattleCam = pcall(lib.require, "BattleCam")
  if okCam and BattleCam then
    BattleCam.zoom, BattleCam.zoomGoal = DS_ZOOM, DS_ZOOM
  end

  local mon = battle.enemy and battle.enemy.mon
  U.log(("%s at %s -- shiny=%s zoom=%.2f  (1x; close the window when done)")
        :format(SPECIES, MAP, tostring(Shiny.isShiny(mon)), DS_ZOOM))

  -- The battle stays up, at 1x, until the window is closed by hand. No taps:
  -- the recording should be the Pokemon standing there, not a text box being
  -- clicked through.
  --
  -- DS_AUTOCLOSE is for checking the pacing without a person in the loop: set
  -- it to a number of seconds and the run should take about that long by the
  -- wall clock, which is the only way to prove 1x is actually 1x.
  -- DS_SHOTS: capture the ARRIVAL as a strip, for checking that the sparkle
  -- fires on its own rather than only when a test arms it by hand. This is
  -- the same code path the capture run uses, which is the point -- the last
  -- bug here hid precisely because it was verified through a driver that
  -- armed the effect itself.
  local shots = os.getenv("DS_SHOTS")
  if shots then
    for k = 1, 26 do
      U.shot(game, ("%s/arrive_%02d.png"):format(shots, k))
      hold(0.15)
    end
    local ShinyFx = lib.require("ShinyFx")
    local d = ShinyFx.debug or {}
    U.log(("fx: armed=%s cleared=%s calls=%s noArena=%s noLive=%s quads=%s")
          :format(tostring(d.armed), tostring(d.cleared), tostring(d.calls),
                  tostring(d.noArena), tostring(d.noLive), tostring(d.quads)))
    U.log("arrival strip written to " .. shots)
    return
  end

  local auto = tonumber(os.getenv("DS_AUTOCLOSE") or "") or 0
  if auto > 0 then
    local t0 = love.timer.getTime()
    hold(auto)
    U.log(("autoclose: asked for %.1fs, took %.2fs")
          :format(auto, love.timer.getTime() - t0))
    return
  end
  while true do step() end
end