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

voxel_anim_probe.lua

tests/voxel_anim_probe.lua5.9 KB1006443ff4475402
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
-- Driver: does the water/flower tile animation reach voxel mode?
--
-- Shoots a burst of frames spaced about one animation period apart, in
-- voxel mode and (for reference) flat.  The evidence is the difference
-- between consecutive shots: water tile $14 rolls sideways a pixel a step
-- and flower tile $03 cycles three frames, so the burst should NOT be
-- eight copies of the same picture.
--
--   POKEPORT_DRIVER=mods/DRAMATIC_SHAPE/tests/voxel_anim_probe.lua lovec .
--
-- knobs (env):
--   ANIM_MAP    map id                        (default PALLET_TOWN)
--   ANIM_SPOT   "x,y[,facing]"                (default 5,6,down)
--   ANIM_SHOTS  frames in the burst           (default 6)
--   ANIM_LEVEL  voxel rung, 0 = flat          (default 3)
--   SHOT_DIR    output directory, must exist  (default "shots")
return function(game)
  local U = dofile("tests/drivers/util.lua")
  local Pipelines = require("src.render.Pipelines")
  local TileRenderer = require("src.render.TileRenderer")

  local SPEED = math.max(1,
    math.floor(tonumber(os.getenv("POKEPORT_SPEED")) or 1))
  local function wait(n) U.wait(n * SPEED) end

  local DIR = os.getenv("SHOT_DIR") or "shots"
  local mapId = os.getenv("ANIM_MAP") or "PALLET_TOWN"
  local shots = math.floor(tonumber(os.getenv("ANIM_SHOTS")) or 6)
  local level = math.floor(tonumber(os.getenv("ANIM_LEVEL")) or 3)
  local sx, sy, facing = (os.getenv("ANIM_SPOT") or "5,6,down")
    :match("^%s*(%d+)%s*,%s*(%d+)%s*,?%s*(%a*)")
  facing = (facing ~= "" and facing) or "down"

  local tileset = game.data.maps[mapId] and game.data.maps[mapId].tileset
  local specs = tileset and game.data.tilesets
                and TileRenderer.defaultAnimatedTiles(game.data.tilesets[tileset])
  print(("[anim] %s tileset=%s animated entries=%d")
    :format(mapId, tostring(tileset), specs and #specs or 0))
  for _, s in ipairs(specs or {}) do
    print(("[anim]   tile $%02X kind=%s period=%s")
      :format(s.tile or -1, tostring(s.kind), tostring(s.period)))
  end

  local Zoom = require("src.render.Zoom")
  Zoom.reset()
  Pipelines.setLevel("tiltshift", 0)
  U.teleport(game, mapId, tonumber(sx), tonumber(sy), facing)
  wait(20)

  Pipelines.setLevel("voxel", level)
  wait(30)                          -- outlast the camera tween

  -- the atlas the terrain mesh actually samples, dumped per step: if these
  -- differ but the frames do not, the animated tiles are not reaching the
  -- geometry (baked into prop prisms, say) rather than not animating
  local V = game.mods.exports["DRAMATIC_SHAPE"]
  V = V and V.lib
  local TerrainAtlas = V and V.require("TerrainAtlas")
  local PaletteFX = require("src.render.PaletteFX")
  local function dumpAtlas(tag)
    if not TerrainAtlas then return end
    local ow = game.overworld
    local map = ow and ow.map
    if not map then return end
    local colors = nil
    if not PaletteFX.usesGbcPack() and ow.paletteFor then
      colors = ow:paletteFor(map)
    end
    local img = TerrainAtlas.forMap(map, colors)
    if not img then return end
    print(("[anim]   atlas %s: animated copy=%s")
      :format(tag, tostring(img ~= map.renderer.image)))
    local ok = pcall(function()
      local w, h = img:getDimensions()
      local c = love.graphics.newCanvas(w, h)
      love.graphics.setCanvas(c)
      love.graphics.clear(0, 0, 0, 0)
      love.graphics.setColor(1, 1, 1, 1)
      love.graphics.draw(img)
      love.graphics.setCanvas()
      c:newImageData():encode("png", "atlas_" .. tag .. ".png")
    end)
    if not ok then print("[anim]   atlas dump failed") end
  end

  -- where the animated tiles ended up in the geometry: a flat top quad
  -- samples its tile through uvRect and picks the atlas patch up, but a
  -- cell Structures turned into a prop prism has its pixels baked as
  -- point-sampled voxels and would not
  if V and specs then
    local Structures = V.require("Structures")
    local ow = game.overworld
    local S = ow and ow.map and Structures.forMap(ow.map)
    if S then
      local def = ow.map.def
      for _, spec in ipairs(specs) do
        if not spec.tile then goto continue end   -- "toggle" specs claim
                                                  -- tile LISTS, not one slot
        local flat, prop, run, first = 0, 0, 0, nil
        for ty = 0, def.height * 4 - 1 do
          for tx = 0, def.width * 4 - 1 do
            local k = (ty + 64) * 4096 + (tx + 64)
            if S.tileAt[k] == spec.tile then
              if S.skip[k] then prop = prop + 1
              elseif S.runs[k] then run = run + 1
              else
                flat = flat + 1
                first = first or { tx, ty }
              end
            end
          end
        end
        print(("[anim]   tile $%02X on this map: %d flat, %d in a volume run,"
               .. " %d inside a prop%s"):format(spec.tile, flat, run, prop,
          first and (" -- first at cell " .. math.floor(first[1] / 2)
                     .. "," .. math.floor(first[2] / 2)) or ""))
        ::continue::
      end
    end
  end

  -- one animation period is 20 logic frames at 60Hz, and TileRenderer.tick
  -- runs on wall-clock steps, so ~22 rendered frames lands the next step.
  -- animFrame is an OPTIONAL engine seam this build may not export; the
  -- mod's own clock (TerrainAtlas._animFrame) reads the same counter by
  -- whatever route exists, so ask it first.
  local function clock()
    if TerrainAtlas and TerrainAtlas._animFrame then
      local ok, f = pcall(TerrainAtlas._animFrame)
      if ok and type(f) == "number" then return f end
    end
    if TileRenderer.animFrame then
      local ok, f = pcall(TileRenderer.animFrame)
      if ok and type(f) == "number" then return f end
    end
    return -1
  end
  for i = 1, shots do
    game.capturePath = ("%s/anim_%s_L%d_%02d.png"):format(DIR, mapId, level, i)
    wait(3)
    print(("[anim] shot %d at animFrame %d step %d"):format(
      i, clock(), math.floor(clock() / 20) % 8))
    dumpAtlas(tostring(i))
    wait(22)
  end

  Pipelines.setLevel("voxel", 0)
  wait(5)
  print("[anim] done")
end