voxel_anim_probe.lua
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