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

cam_shots.lua

tests/cam_shots.lua7.5 KB1006443bcbba3920
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
-- Scratch driver: the cameras the player steers -- the third-person boom's
-- zoom, and the battle shot's orbit, climb and lens, including the far
-- stops (side-on, 45 degrees up) and what BACK SPRITES takes away.
--
--   POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/cam_shots.lua \
--   SHOT_DIR=.scratchpad/camshots lovec.exe .
return function(game)
  local U = dofile("tests/drivers/util.lua")
  local Pipelines = require("src.render.Pipelines")

  local ROOT = (os.getenv("SHOT_DIR") or "shots/cam")

  local handle = game.mods.exports["DRAMATIC_SHAPE"]
  if not (handle and handle.lib) then
    print("[cam] DRAMATIC_SHAPE mod not loaded")
    return love.event.quit()
  end
  local V = handle.lib
  local FirstPerson = V.require("FirstPerson")
  local ThirdPerson = V.require("ThirdPerson")
  local BattleCam = V.require("BattleCam")
  local OverworldBattle = V.require("OverworldBattle")
  local ChunkMesher = V.require("ChunkMesher")
  local Voxel = V.require("VoxelState")
  local DayNight = V.require("DayNight")

  pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null')
  pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul')

  require("src.world.OverworldController").rollEncounter = function() return nil end
  local TileRenderer = require("src.render.TileRenderer")
  TileRenderer.tick = function() end
  -- The flower's animation frame, pinned per shot rather than left to the
  -- clock: the mesh spans the union of every frame and each one is cut back
  -- out in texture space, so a gap that only opens on frame 2 is invisible
  -- to a driver that always photographs frame 0 -- which is exactly how the
  -- first cut of the closed sides shipped looking correct.
  local ANIM = { frame = 0 }
  TileRenderer.animFrame = function() return ANIM.frame end
  DayNight.setting:sync("day")

  local function settle()
    for _ = 1, 900 do
      if ChunkMesher.pending() == 0 then break end
      U.wait(1)
    end
    U.wait(40)
  end

  local shots = 0
  local function shot(name)
    if U.shot(game, ("%s/%s.png"):format(ROOT, name)) then
      shots = shots + 1
    end
  end

  -- ------- the boom's zoom
  U.teleport(game, "PALLET_TOWN", 13, 14, "down")
  Pipelines.setLevel("voxel", Voxel.TP_LEVEL)
  Pipelines.setLevel("tiltshift", 0)
  settle()
  for _ = 1, 200 do
    if FirstPerson.blend >= 1 then break end
    U.wait(1)
  end
  FirstPerson.yaw = math.pi
  U.wait(20)
  for _, z in ipairs({ "min", "default", "max" }) do
    ThirdPerson.zoomGoal = (z == "min" and ThirdPerson.ZOOM_MIN)
                           or (z == "max" and ThirdPerson.ZOOM_MAX) or 1
    for _ = 1, 90 do U.wait(1) end
    print(("[cam] boom %-8s zoom %.2f  len %.1f"):format(z, ThirdPerson.zoom,
                                                        ThirdPerson.len))
    shot("boom_" .. z)
  end
  ThirdPerson.zoomGoal = 1
  U.wait(60)

  -- ------- the standees' closed sides
  --
  -- Pallet's flower beds and Route 1's tall grass, from a low camera close
  -- in -- the angle that showed the slabs were open at the ends of every
  -- run and let you see straight through them.
  ThirdPerson.zoomGoal = ThirdPerson.ZOOM_MIN
  for _, s in ipairs({
    { map = "PALLET_TOWN", x = 13, y = 14, yaw = math.pi,
      pitch = math.rad(6), label = "flowers_low" },
    { map = "PALLET_TOWN", x = 13, y = 14, yaw = 3 * math.pi / 4,
      pitch = math.rad(30), label = "flowers_angled" },
    { map = "ROUTE_1", x = 10, y = 28, yaw = math.pi,
      pitch = math.rad(8), label = "grass_low" },
  }) do
    U.teleport(game, s.map, s.x, s.y, "down")
    settle()
    FirstPerson.yaw, FirstPerson.pitch = s.yaw, s.pitch
    U.wait(60)
    -- EVERY animation frame, because a gap the union closed and a frame
    -- reopens is only visible on that frame
    for f = 0, 3 do
      ANIM.frame = f
      U.wait(6)
      shot(("%s_f%d"):format(s.label, f))
    end
    ANIM.frame = 0
  end
  ThirdPerson.zoomGoal = 1
  U.wait(60)

  -- ------- the battle shot
  --
  -- Staged the way the game stages one, then steered to each stop with the
  -- module's own entry points -- the same ones the wheel, the stick and a
  -- drag reach.
  Pipelines.setLevel("voxel", 4)
  U.wait(60)
  do
    -- Somebody to fight WITH: a fresh driver save has an empty party, and
    -- a trainer battle with nobody to send out ends on the frame it starts
    -- -- which is what made every steered shot below sample a dead session.
    local Pokemon = require("src.pokemon.Pokemon")
    game.save.party = {
      Pokemon.new(game.data, "CHARIZARD", 45),
      Pokemon.new(game.data, "PIKACHU", 40),
    }
    game.save.player.name = "RED"
    local BattleState = require("src.battle.BattleState")
    local class = next(game.data.trainers)
    local battle = BattleState.newTrainer(game, class, 1)
    battle.onFinish = function() end
    game.overworld:pushBattle(battle)
  end
  -- The wipe, then just enough of the send-out chatter to get both mons
  -- standing on the arena -- and NOT one tap more. A is also FIGHT and then
  -- the first move, so tapping past the intro starts an exchange and the
  -- fight can be over before the camera has been steered anywhere.
  U.wait(70)
  for _ = 1, 40 do
    if OverworldBattle.shot() then break end
    U.tap(game, "a")
    U.wait(10)
  end
  U.wait(60)
  print(("[cam] staged: arena %s  shot %s  top %s")
        :format(tostring(OverworldBattle.arena() ~= nil),
                tostring(OverworldBattle.shot() ~= nil),
                tostring(game.stack:top() ~= game.overworld)))
  if not OverworldBattle.shot() then
    print("[cam] no staged battle -- skipping the battle shots")
    print(("[cam] %d shots into %s"):format(shots, ROOT))
    return love.event.quit()
  end

  local function settleCam(label)
    for _ = 1, 120 do U.wait(1) end
    print(("[cam] battle %-20s orbit %.2f/%.2f  pitch %.2f/%.2f  "
           .. "zoom %.2f/%.2f  steerable %s  live %s")
          :format(label, BattleCam.orbit, BattleCam.orbitGoal,
                  BattleCam.pitch, BattleCam.pitchGoal,
                  BattleCam.zoom, BattleCam.zoomGoal,
                  tostring(BattleCam.steerable),
                  tostring(OverworldBattle.shot() ~= nil)))
    shot("battle_" .. label)
  end

  BattleCam.recentre()
  settleCam("home")

  -- right to the stop: this must land SQUARE to the arena's axis
  BattleCam.dragOrbit(10)
  settleCam("side_on")

  -- and there is nothing to the left of home
  BattleCam.recentre()
  BattleCam.dragOrbit(-10)
  settleCam("left_stop")

  -- up to the stop, and refusing to go below home
  BattleCam.recentre()
  BattleCam.dragPitch(10)
  settleCam("high")
  BattleCam.recentre()
  BattleCam.dragPitch(-10)
  settleCam("low_stop")

  -- the lens at both ends
  BattleCam.recentre()
  for _ = 1, 40 do BattleCam.stepZoom(-1) end
  settleCam("zoom_in")
  BattleCam.recentre()
  for _ = 1, 40 do BattleCam.stepZoom(1) end
  settleCam("zoom_out")

  -- everything at once, which is the shot a player would actually build
  BattleCam.recentre()
  BattleCam.dragOrbit(0.55)
  BattleCam.dragPitch(0.5)
  for _ = 1, 4 do BattleCam.stepZoom(-1) end
  settleCam("steered")

  -- What BACK SPRITES takes away is NOT shot here: the flag it works
  -- through is re-derived from the row every frame
  -- (OverworldBattle.update), so a driver cannot hold it down for the
  -- hundred-odd frames a settled shot needs, and a picture that claimed to
  -- show a locked camera while the camera was in fact free would be worse
  -- than no picture. The suite asserts it instead, on both axes and the
  -- lens, and on the RIG as well as on the inputs.
  BattleCam.recentre()

  print(("[cam] %d shots into %s"):format(shots, ROOT))
  love.event.quit()
end