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

battle_shots.lua

tests/battle_shots.lua4.8 KB100644af8ae8b808
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
-- Driver: screenshot an overworld battle against eight random trainers.
--
-- The mode's whole claim is that a fight reads as happening on the map, so
-- what has to be looked at is a spread of maps: open route, town, forest,
-- cave, gym floor. Each one gets the same three beats -- the menu (both HUD
-- panels up over whatever ground they landed on), a move animation, and the
-- frame a few seconds later, which is where the parallax drift shows.
--
-- The eight are drawn with a fixed seed, so a re-run after a tweak produces
-- the same eight battles and the shots compare directly.
--
--   SHOT_DIR=.scratchpad POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/battle_shots.lua love .
--
-- SHOT_DIR must already exist: the capture writes with io.open, which does
-- not create directories.
return function(game)
  local U = dofile("tests/drivers/util.lua")
  local DIR = os.getenv("SHOT_DIR") or ".scratchpad"
  local Pokemon = require("src.pokemon.Pokemon")
  local BattleState = require("src.battle.BattleState")

  local SEED = tonumber(os.getenv("SHOT_SEED") or "") or 20260727
  love.math.setRandomSeed(SEED)

  -- Somebody to fight with. Two very different back pics, so a pin that is
  -- wrong for one silhouette and right for another cannot hide.
  game.save.party = {
    Pokemon.new(game.data, "CHARIZARD", 45),
    Pokemon.new(game.data, "PIKACHU", 40),
  }
  game.save.player.name = "RED"

  -- Where to stage them: outdoor, wooded, urban, underground, indoor. Any id
  -- the merged dataset does not have is dropped rather than guessed at.
  local PLACES = {
    { "ROUTE_1", 5, 8 },
    { "VIRIDIAN_FOREST", 12, 20 },
    { "PALLET_TOWN", 5, 6 },
    { "MT_MOON_1F", 12, 12 },
    { "CERULEAN_CITY", 10, 12 },
    { "ROUTE_25", 12, 5 },
    { "PEWTER_GYM", 4, 8 },
    { "ROUTE_4", 10, 5 },
    { "VIRIDIAN_CITY", 20, 20 },
    { "ROUTE_3", 10, 5 },
  }
  local places = {}
  for _, p in ipairs(PLACES) do
    if game.data.maps[p[1]] then places[#places + 1] = p end
  end

  -- every trainer class the merged data carries, in a stable order so the
  -- seed picks the same ones every run
  local classes = {}
  for id, rec in pairs(game.data.trainers) do
    if type(id) == "string" and id:sub(1, 1) ~= "_"
       and type(rec) == "table" and rec.parties and rec.parties[1] then
      classes[#classes + 1] = id
    end
  end
  table.sort(classes)
  U.log(("%d trainer classes, %d places, seed %d")
        :format(#classes, #places, SEED))

  local Battles = nil
  local exports = game.mods and game.mods.exports
  local lib = exports and exports.DRAMATIC_SHAPE and exports.DRAMATIC_SHAPE.lib
  if lib then Battles = lib.require("OverworldBattle") end
  if not Battles then
    U.log("DRAMATIC_SHAPE is not loaded -- enable it and run again")
  end

  local function label(place, class)
    local arena = Battles and Battles.arena()
    if not arena then
      return ("%s / %s -- NO ARENA (plain battle screen)")
             :format(place[1], class)
    end
    return ("%s / %s -- %s arena at cell %d,%d; enemy %d,%d player %d,%d")
           :format(place[1], class, arena.shape, arena.x, arena.y,
                   arena.enemyCell[1], arena.enemyCell[2],
                   arena.playerCell[1], arena.playerCell[2])
  end

  for i = 1, 8 do
    -- the TRAINERS are the random part; the places are walked in order, so
    -- one run covers open route, town, forest, cave and gym floor rather
    -- than landing on the same meadow eight times
    local place = places[(i - 1) % #places + 1]
    local class = classes[love.math.random(#classes)]
    local rec = game.data.trainers[class]
    local partyIndex = love.math.random(#rec.parties)
    local tag = ("%02d_%s_%s"):format(i, place[1]:lower(), class:lower())

    U.teleport(game, place[1], place[2], place[3], "down")
    -- let the neighbourhood's meshes land before the fight starts, so the
    -- first battle frame is not the flat fallback
    U.wait(90)

    local battle = BattleState.newTrainer(game, class, partyIndex)
    battle.onFinish = function() end
    game.overworld:pushBattle(battle)

    -- the wipe, then the send-out chatter
    U.wait(70)
    for _ = 1, 14 do U.tap(game, "a"); U.wait(8) end
    U.log(label(place, class))
    U.shot(game, ("%s/%s_1_menu.png"):format(DIR, tag))

    -- FIGHT -> first move -> the animation
    U.tap(game, "a")
    U.wait(12)
    U.tap(game, "a")
    U.wait(24)
    U.shot(game, ("%s/%s_2_anim.png"):format(DIR, tag))

    -- and a while later, where the drift has moved the world under them
    U.wait(240)
    U.shot(game, ("%s/%s_3_drift.png"):format(DIR, tag))

    -- out of the battle and on to the next: pop whatever is above the
    -- overworld rather than trying to play the fight to its end
    while game.stack:top() and game.stack:top() ~= game.overworld do
      game.stack:pop()
    end
    U.wait(10)
  end

  U.log("done -- " .. DIR)
end