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

tp_shots.lua

tests/tp_shots.lua7.6 KB10064457e45c7a73
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
-- Scratch driver: shots of the 3RD (third-person) rung -- the eye boomed
-- off the back of the player's head, the player's own card drawn and turned
-- to face it, the boom shortening against walls indoors, and the body
-- turning to face where it walks.
--
--   POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/tp_shots.lua \
--   SHOT_DIR=.scratchpad/tpshots 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/tp")

  local handle = game.mods.exports["DRAMATIC_SHAPE"]
  if not (handle and handle.lib) then
    print("[tp] 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 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
  TileRenderer.animFrame = function() return 0 end
  DayNight.setting:sync("day")

  local Zoom = require("src.render.Zoom")
  pcall(function()
    game.save.options.zoom = 0
    Zoom.applyOptions(game.save.options)
  end)

  local function settle()
    for _ = 1, 900 do
      if ChunkMesher.pending() == 0 then break end
      U.wait(1)
    end
    for _ = 1, 300 do
      if FirstPerson.blend >= 1 and Voxel.ready
         and ChunkMesher.pending() == 0 then break end
      U.wait(1)
    end
    U.wait(40)
  end

  -- the nearest WALKABLE cell (fp_shots' helper): a guessed coordinate
  -- inside a building footprint buries the pivot in the geometry
  local function place(mapId, x, y)
    U.teleport(game, mapId, x, y, "down")
    local ow = game.stack:top()
    local map = ow and ow.map
    if not map or map:isWalkableCell(x, y) then return end
    for r = 1, 8 do
      for dy = -r, r do
        for dx = -r, r do
          if math.max(math.abs(dx), math.abs(dy)) == r then
            local cx, cy = x + dx, y + dy
            if map:inBounds(cx, cy) and map:isWalkableCell(cx, cy) then
              U.teleport(game, mapId, cx, cy, "down")
              print(("[tp] (%d,%d) not walkable; standing at (%d,%d)")
                    :format(x, y, cx, cy))
              return
            end
          end
        end
      end
    end
  end

  -- yaw is a world bearing: 0 south, pi/2 east, pi north, -pi/2 west
  local SCENES = {
    -- Pallet Town, mid-street: the player's own card seen from behind, in
    -- each compass direction plus a diagonal (the off-grid case, where a
    -- south-facing card would be edge-on to any orbit camera)
    { map = "PALLET_TOWN", x = 13, y = 14, yaw = math.pi, label = "pallet_north" },
    { map = "PALLET_TOWN", x = 13, y = 14, yaw = 0, label = "pallet_south" },
    { map = "PALLET_TOWN", x = 9, y = 7, yaw = math.pi / 2, label = "pallet_east" },
    { map = "PALLET_TOWN", x = 9, y = 7, yaw = 3 * math.pi / 4,
      label = "pallet_diag" },
    -- the shoreline: the boom over water, reflecting the player
    { map = "PALLET_TOWN", x = 9, y = 12, yaw = 0, label = "pallet_water" },
    -- pitched down, the classic third-person framing; and up, where the
    -- boom has to shorten rather than bury the eye in the ground
    { map = "PALLET_TOWN", x = 13, y = 14, yaw = math.pi,
      pitch = math.rad(35), label = "pallet_down" },
    { map = "PALLET_TOWN", x = 13, y = 14, yaw = math.pi,
      pitch = -math.rad(40), label = "pallet_skyward" },
    -- Route 1: grass and ledges, with the character in frame for scale
    { map = "ROUTE_1", x = 10, y = 28, yaw = math.pi, label = "route1_north" },
    -- interiors: the one place a 48px boom cannot fit, so the collision
    -- march is the whole shot
    { map = "VIRIDIAN_POKECENTER", x = 3, y = 5, yaw = math.pi,
      label = "center_north" },
    { map = "REDS_HOUSE_1F", x = 3, y = 4, yaw = math.pi,
      label = "reds_house" },
  }

  local shots = 0
  for _, s in ipairs(SCENES) do
    place(s.map, s.x, s.y)
    Pipelines.setLevel("voxel", Voxel.TP_LEVEL)
    Pipelines.setLevel("tiltshift", 0)
    settle()
    FirstPerson.yaw = s.yaw
    FirstPerson.pitch = s.pitch or FirstPerson.PITCH_DEFAULT
    U.wait(20)
    print(("[tp] %-16s boom %.1f of %.1f"):format(s.label, ThirdPerson.len,
                                                  ThirdPerson.BOOM))
    if U.shot(game, ("%s/%s.png"):format(ROOT, s.label)) then
      shots = shots + 1
    end
  end

  -- the slide from 1ST to 3RD: the eye walks out of the head rather than
  -- cutting, so the halfway frame is a real camera position
  place("PALLET_TOWN", 13, 14)
  Pipelines.setLevel("voxel", Voxel.FP_LEVEL)
  settle()
  if U.shot(game, ROOT .. "/from_1st.png") then shots = shots + 1 end
  Pipelines.setLevel("voxel", Voxel.TP_LEVEL)
  for _ = 1, 300 do
    if ThirdPerson.out >= 0.5 then break end
    U.wait(1)
  end
  if U.shot(game, ROOT .. "/boom_mid.png") then shots = shots + 1 end
  U.wait(60)
  if U.shot(game, ROOT .. "/boom_out.png") then shots = shots + 1 end

  -- ------- the free walk, with the body turning
  --
  -- Hold forward with the head yawed off-grid: the player must GLIDE (off
  -- the 16px grid, which no grid step can do) and the body must end up
  -- facing its own travel rather than the lens.
  place("PALLET_TOWN", 13, 14)
  Pipelines.setLevel("voxel", Voxel.TP_LEVEL)
  settle()
  local ow = game.stack:top()
  local p = ow.player
  FirstPerson.yaw = 3 * math.pi / 4       -- northeast, deliberately off-grid
  FirstPerson.pitch = FirstPerson.PITCH_DEFAULT
  local x0, y0 = p.px, p.py
  U.hold(game, "up", 90)
  local moved = math.abs(p.px - x0) + math.abs(p.py - y0)
  print(("[tp] walk moved %.1f px; off-grid: %s; body faces %s (camera %s)")
        :format(moved,
                tostring(p.px % 16 ~= 0 or p.py % 16 ~= 0),
                tostring(p.facing), tostring(FirstPerson.compassFacing())))
  if U.shot(game, ROOT .. "/walked.png") then shots = shots + 1 end

  -- strafing: the one case the body facing exists for. Hold RIGHT with the
  -- head due north and the character must walk east showing its flank.
  place("PALLET_TOWN", 13, 14)
  settle()
  FirstPerson.yaw = math.pi
  U.hold(game, "right", 40)
  print(("[tp] strafe: body faces %s, camera looks %s")
        :format(tostring(p.facing), tostring(FirstPerson.compassFacing())))
  if U.shot(game, ROOT .. "/strafe.png") then shots = shots + 1 end

  -- ------- the spin, sampled
  --
  -- Stand still and turn the camera fast, sampling the frame the player's
  -- own card actually draws every rendered frame. A standing body is
  -- pointed along the camera's own yaw, so it must show its back the whole
  -- way round; anything else is the sideways flick. Sampled through the
  -- live rig, so this measures the real thing rather than the arithmetic.
  place("PALLET_TOWN", 13, 14)
  Pipelines.setLevel("voxel", Voxel.TP_LEVEL)
  settle()
  ow = game.stack:top()
  p = ow.player
  local seen, frames = {}, 0
  for _ = 1, 240 do
    FirstPerson.lookBy(math.rad(7), 0)      -- ~420 deg/s, a hard flick
    U.wait(1)
    local f = FirstPerson.playerFacing(p.facing, p.px + 8, p.py + 8)
    seen[f] = (seen[f] or 0) + 1
    frames = frames + 1
  end
  local report = {}
  for f, n in pairs(seen) do
    report[#report + 1] = ("%s x%d"):format(f, n)
  end
  table.sort(report)
  print(("[tp] spin: %d frames, card frames seen: %s")
        :format(frames, table.concat(report, ", ")))

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