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

letsgo_shots.lua

tests/letsgo_shots.lua11.7 KB100644c42c22c322
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 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
-- Driver: photograph the LET'S GO capture mode end to end.
--
-- One wild encounter under FULL: the battle menu should never be seen --
-- capture mode opens over it with the ball hanging at the player's empty
-- cell and the ring pulsing on the foe -- then a synthetic mouse flick
-- throws the ball and the beats that follow are photographed: flight,
-- the open-mouth suck, the drop-and-wobble, and the outcome.
--
--   SHOT_DIR=.scratchpad/letsgo \
--   POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/letsgo_shots.lua \
--   "/c/Program Files/LOVE/lovec.exe" .
--
-- SHOT_DIR must already exist. DS_LETSGO_MODE=catching runs the same
-- beats through the bag-menu route instead of the FULL auto-entry.
return function(game)
  local U = dofile("tests/drivers/util.lua")
  local DIR = os.getenv("SHOT_DIR") or ".scratchpad"
  local MODE = os.getenv("DS_LETSGO_MODE") or "full"
  local Pokemon = require("src.pokemon.Pokemon")
  local BattleState = require("src.battle.BattleState")
  local Bag = require("src.inventory.Bag")

  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 LetsGo = lib.require("LetsGo")
  local CatchThrow = lib.require("CatchThrow")
  local BattleScene = lib.require("BattleScene")

  -- a spread of levels, because the Let's Go award pays every party
  -- member against its OWN level: a low one should pull far more from
  -- the same catch than a high one, and a flat payout would hide that
  game.save.party = {
    Pokemon.new(game.data, "CHARIZARD", 45),
    Pokemon.new(game.data, "PIKACHU", 10),
    Pokemon.new(game.data, "RATTATA", 5),
  }
  game.save.player.name = "RED"
  Bag.add(game.save, "POKE_BALL", 20, game.data)
  Bag.add(game.save, "GREAT_BALL", 5, game.data)
  -- DS_BALL forces the ball, so a MASTER_BALL run is a guaranteed catch
  -- and the experience payout can be read deterministically
  local FORCE = os.getenv("DS_BALL")
  if FORCE then
    Bag.add(game.save, FORCE, 5, game.data)
    CatchThrow.lastBall = FORCE
  end
  local expBefore = {}
  for i, m in ipairs(game.save.party) do
    expBefore[i] = { exp = m.exp, level = m.level }
  end
  LetsGo.setting:setValue(MODE, game)
  -- DS_RUNG=cards forces the 2D-3D A rung: the control run for the pic
  -- measurement path, against the STADIUM models the save may be on
  if os.getenv("DS_RUNG") == "cards" then
    lib.require("OverworldBattle").setting:setValue(true, game)
    U.log("3D-BTL forced to 2D-3D A")
  end
  U.log("LET'S GO mode: " .. tostring(LetsGo.mode()))

  U.teleport(game, "ROUTE_1", 5, 8, "down")
  U.wait(90)

  local battle = BattleState.newWild(game, "PIDGEY", 5)
  battle.onFinish = function() end
  game.overworld:pushBattle(battle)

  -- the wipe and the "Wild X appeared!" chatter: tap until the menu (or
  -- capture mode, which under FULL opens the instant the menu would)
  U.wait(70)
  for i = 1, 80 do
    if battle.phase == "menu" or CatchThrow.session() then break end
    U.tap(game, "a")
    U.wait(6)
    if i % 20 == 0 then
      U.log(("still driving intro: phase=%s queue=%d anim=%s")
            :format(tostring(battle.phase), #(battle.queue or {}),
                    tostring(battle.animPlaying)))
    end
  end

  local function phase()
    local s = CatchThrow.session()
    return s and s.phase or ("battle:" .. tostring(battle.phase))
  end

  if MODE == "catching" then
    -- the bag route: ITEM on the menu, then the ball
    U.wait(20)
    U.log("menu phase: " .. tostring(battle.phase))
    -- cursor to ITEM (menu is 2x2: fight pkmn / item run) -- down once
    U.tap(game, "down"); U.wait(4)
    U.tap(game, "a"); U.wait(20)
    -- the bag opens on the first item; balls were added first
    U.tap(game, "a"); U.wait(10)
  else
    -- FULL: capture mode should have opened on its own
    U.wait(30)
  end

  U.log("capture phase: " .. phase())
  do
    local s = CatchThrow.session()
    if s then
      local b = s.artBox
      U.log(("probe: span=%.1f enemyGB=%.0f,%.0f playerGB=%.0f,%.0f")
            :format(s.shot.enemySpan or -1, s.shot.enemy[1], s.shot.enemy[2],
                    s.shot.player[1], s.shot.player[2]))
      U.log(("probe: eye=%.0f,%.0f,%.0f  enemyW=%.0f,%.0f playerW=%.0f,%.0f")
            :format(s.shot.eye[1], s.shot.eye[2], s.shot.eye[3],
                    s.enemyPos[1], s.enemyPos[3],
                    s.playerPos[1], s.playerPos[3]))
      if b then
        U.log(("probe: artBox ax=%d ay=%d box=%d..%d,%d..%d body r=%.1f yOff=%.1f")
              :format(b.ax, b.ay, b.x0, b.x1, b.y0, b.y1,
                      s.body.r, s.body.yOff))
      else
        U.log(("probe: artBox=nil body r=%.1f yOff=%.1f")
              :format(s.body.r, s.body.yOff))
      end
    end
  end
  U.shot(game, DIR .. "/1_aim.png")

  -- ------- the reach test
  --
  -- Drag the ball to the far corners of the WINDOW -- including well
  -- below where the battle's text box used to be -- and report where it
  -- actually ends up in the GB frame. A fence shows up here as a ball
  -- that stops moving while the pointer keeps going.
  do
    local W, H = love.graphics.getDimensions()
    local s = CatchThrow.session()
    if s and love.mousepressed then
      local hx, hy = s.handGB[1], s.handGB[2]
      love.mousepressed(W * 0.5, H * 0.62, 1, false, 1)
      U.wait(2)
      local CORNERS = {
        { "bottom-centre", 0.50, 0.97 },
        { "bottom-left",   0.06, 0.97 },
        { "bottom-right",  0.94, 0.97 },
        { "top-left",      0.06, 0.10 },
      }
      for _, c in ipairs(CORNERS) do
        for i = 1, 4 do
          if love.mousemoved then
            love.mousemoved(W * c[2], H * c[3], 0, 0, false)
          end
          U.wait(1)
        end
        local q = CatchThrow.session()
        U.log(("reach %-14s -> ball GB %.0f,%.0f  (frame is 160x144)")
              :format(c[1], q and q.handGB[1] or -1, q and q.handGB[2] or -1))
        if c[1] == "bottom-centre" then
          U.shot(game, DIR .. "/1b_drag_low.png")
        end
      end
      -- a slow release is a dead flick: nothing is thrown
      if love.mousereleased then
        love.mousereleased(W * 0.06, H * 0.10, 1, false, 1)
      end
      U.wait(30)
      U.log("after reach test: " .. phase()
            .. ("  (ball home was %.0f,%.0f)"):format(hx, hy))
    end
  end

  -- A synthetic flick aimed like a hand: from the session's own geometry,
  -- pick a release point short of the ring and a sweep speed for a
  -- comfortable sigma, so the aim model (release + reach along the flick)
  -- lands the ball on the ring centre. Window units; the session maps
  -- them back to GB through the live letterbox.
  local aim = CatchThrow._aimInfo()
  if aim then
    local uw, uh = love.graphics.getDimensions()
    local function toWin(gx, gy)
      return (aim.lx + gx * aim.scale) * uw / aim.pw,
             (aim.ly + gy * aim.scale) * uh / aim.ph
    end
    -- the throw is the swipe's own velocity now: grab the ball and sweep
    -- toward the ring at a comfortable ~190 GB px/s for 8 frames.
    -- DS_SWIPE overrides it, which is how the strength band is swept:
    -- weak should fall short, comfortable should land, hard should still
    -- reach rather than sail over.
    local SPEED = tonumber(os.getenv("DS_SWIPE") or "") or 190
    local FRAMES = 8
    SPEED_USED = SPEED
    local hx, hy = aim.hand[1], aim.hand[2]
    local dx, dy = aim.ring[1] - hx, aim.ring[2] - hy
    local d = math.sqrt(dx * dx + dy * dy)
    dx, dy = dx / d, dy / d
    U.log(("aim: hand %.0f,%.0f ring %.0f,%.0f outer %.0f")
          :format(hx, hy, aim.ring[1], aim.ring[2], aim.outer))
    local step = SPEED / 60
    if love.mousepressed then
      local px, py = toWin(hx, hy)
      love.mousepressed(px, py, 1, false, 1)
    end
    for i = 1, FRAMES do
      local wx, wy = toWin(hx + dx * step * i, hy + dy * step * i)
      if love.mousemoved then love.mousemoved(wx, wy, 0, 0, false) end
      U.wait(1)
    end
    if love.mousereleased then
      local wx, wy = toWin(hx + dx * step * FRAMES, hy + dy * step * FRAMES)
      love.mousereleased(wx, wy, 1, false, 1)
    end
    -- what actually left the hand, before gravity has touched it
    local s0 = CatchThrow.session()
    if s0 and s0.vel then
      local p = s0.ballInst.pos
      U.log(("LAUNCH fwd/up/lat = %.1f %.1f %.1f  from height %.1f, %.1f px out")
            :format(math.sqrt(s0.vel[1] ^ 2 + s0.vel[3] ^ 2), s0.vel[2], 0,
                    p[2] - s0.groundY,
                    math.sqrt((s0.enemyPos[1] - p[1]) ^ 2
                              + (s0.enemyPos[3] - p[3]) ^ 2)))
    else
      U.log("LAUNCH -- nothing was thrown (flick rejected)")
    end
  else
    U.log("NO AIM INFO -- capture mode did not open")
  end

  U.wait(4)
  -- did it CONNECT? poll the flight out and say plainly which way it
  -- went, so a strength sweep reads as a table instead of a guess
  do
    local verdict, peak = "no throw", 0
    for _ = 1, 200 do
      local s = CatchThrow.session()
      if not s then verdict = "session gone" break end
      if s.phase == "flight" then
        local h = s.ballInst.pos[2] - s.groundY
        if h > peak then peak = h end
        verdict = "MISS (fell short / wide)"
      elseif s.phase == "suck" or s.phase == "drop"
             or s.phase == "wobble" then
        verdict = "HIT" .. (s.tier and (" " .. s.tier) or " (outside ring)")
        break
      elseif s.phase ~= "aim" then
        break
      end
      U.wait(1)
    end
    local body = CatchThrow.session() and CatchThrow.session().body
    U.log(("THROW swipe=%d -> %s   (apex %.1f world px, body centre %.1f)")
          :format(SPEED_USED or -1, verdict, peak, body and body.yOff or -1))
  end
  U.log("after flick: " .. phase())
  U.shot(game, DIR .. "/2_flight.png")

  -- the suck: ball open at the foe, beam on, mon shrinking
  for _ = 1, 60 do
    U.wait(1)
    local s = CatchThrow.session()
    if s and s.phase == "suck" then break end
    if not CatchThrow.session() then break end
  end
  U.log("suck check: " .. phase()
        .. "  shrink=" .. tostring(BattleScene.capture
                                   and BattleScene.capture.shrink))
  U.shot(game, DIR .. "/3_suck.png")

  -- the drop and the first wobble
  for _ = 1, 120 do
    U.wait(1)
    local s = CatchThrow.session()
    if s and s.phase == "wobble" then break end
    if not s then break end
  end
  U.wait(30)
  U.log("wobble check: " .. phase()
        .. "  shrink=" .. tostring(BattleScene.capture
                                   and BattleScene.capture.shrink))
  U.shot(game, DIR .. "/4_wobble.png")

  -- the outcome: stars or burst, then the engine's own text
  for _ = 1, 300 do
    U.wait(1)
    local s = CatchThrow.session()
    if not s or s.phase == "epilogue" or s.phase == "burst" then break end
  end
  U.wait(20)
  U.log("outcome: " .. phase())
  U.shot(game, DIR .. "/5_outcome.png")

  -- under FULL a breakout must land straight back in throw mode -- no
  -- enemy turn between; a catch plays out its epilogue instead
  -- keep tapping through the caught chatter so storeCaughtMon (and the
  -- experience payout hanging off it) actually runs
  for _ = 1, 60 do U.tap(game, "a"); U.wait(6) end
  U.log(("after outcome: %s  battle=%s  balls=%d")
        :format(phase(), tostring(battle.phase),
                game.save.inventory.POKE_BALL or 0))
  for i, m in ipairs(game.save.party) do
    local was = expBefore[i]
    local nm = game.data.pokemon[m.species].name
    U.log(("EXP %-10s Lv%-3d -> Lv%-3d  exp %d -> %d  (+%d)")
          :format(nm, was.level, m.level, was.exp, m.exp, m.exp - was.exp))
  end
  local combo = lib.require("LetsGo").combo()
  U.log("combo: " .. (combo and (combo.species .. " x" .. combo.count)
                      or "none"))
  U.shot(game, DIR .. "/6_after.png")
  U.log("done -- " .. DIR)
end