fix vertical line in battle sprites

This commit is contained in:
DramaticShape
2026-07-29 15:35:28 -04:00
parent 0f303e7975
commit 3c1781f6b1
8 changed files with 570 additions and 16 deletions
+50
View File
@@ -0,0 +1,50 @@
-- Probe: one overworld battle, held on the menu beat, shot large.
--
-- SHOT_DIR=... POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/monline_probe.lua love .
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")
love.math.setRandomSeed(20260727)
game.save.party = {
Pokemon.new(game.data, "CHARIZARD", 45),
Pokemon.new(game.data, "PIKACHU", 40),
}
game.save.player.name = "RED"
local exports = game.mods and game.mods.exports
local lib = exports and exports.DRAMATIC_SHAPE and exports.DRAMATIC_SHAPE.lib
U.log("DRAMATIC_SHAPE lib:", tostring(lib))
local Battles = lib and lib.require("OverworldBattle")
U.log("OverworldBattle:", tostring(Battles))
U.teleport(game, "ROUTE_1", 5, 8, "down")
U.wait(120)
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("class:", classes[1])
local battle = BattleState.newTrainer(game, classes[1], 1)
battle.onFinish = function() end
game.overworld:pushBattle(battle)
U.wait(70)
for _ = 1, 14 do U.tap(game, "a"); U.wait(8) end
local arena = Battles and Battles.arena()
U.log("arena:", arena and arena.shape or "none")
U.shot(game, DIR .. "/probe_menu.png")
for _ = 1, 20 do U.tap(game, "a"); U.wait(8) end
U.wait(90)
U.shot(game, DIR .. "/probe_menu2.png")
U.log("done -- " .. DIR)
love.event.quit()
end
+192
View File
@@ -0,0 +1,192 @@
-- Figures: a person drawn INTO furniture, cut out by an authored pixel
-- mask and stood up on top of it (TileShape.figures / Structures.
-- buildFigures). The Pokemon Center's seated man is the case the feature
-- exists for, so this drives the real profile entry over a hand-built
-- copy of the couch block he is drawn in -- POKECENTER blockset entry $08,
-- as every Center places it:
--
-- y=8 36 37 57 11 36/52 west wall strip, 37/53 the MAN,
-- y=9 52 53 60 27 57/60 floor he overhangs east onto,
-- y=10 38 39 54 11 38/39 cushion, 42/43 the couch's base
-- y=11 42 43 26 27
--
-- No love, no GPU and no pixel access: a figure is authored rather than
-- detected, which is exactly what lets it build headless.
--
-- luajit mods/DramaticShapeVoxelMod/tests/voxel_figure_test.lua
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
-- ------- the mod namespace (mirrors main.lua's V, minus the mod loader)
local ROOT = os.getenv("DS_MOD_PATH") or "mods/DramaticShapeVoxelMod"
local V = { path = ROOT }
local function chunkFor(rel)
local f = assert(io.open(ROOT .. "/" .. rel, "rb"), rel .. " is missing")
local src = f:read("*a")
f:close()
return assert(load(src, "@" .. ROOT .. "/" .. rel))
end
local modules, dataFiles = {}, {}
function V.require(name)
if modules[name] == nil then
modules[name] = chunkFor("lib/" .. name .. ".lua")(V)
end
return modules[name]
end
function V.data(name)
if dataFiles[name] == nil then
dataFiles[name] = chunkFor("data/" .. name .. ".lua")(V)
end
return dataFiles[name]
end
local TileShape = V.require("TileShape")
local Structures = V.require("Structures")
-- ------- the authored mask parses
local figs = TileShape.figures("POKECENTER")
T.check(type(figs) == "table" and #figs == 1,
"POKECENTER carries exactly one figure")
local fig = figs[1]
T.eq(fig.w, 2, "the figure is two tiles across")
T.eq(fig.h, 2, "the figure is two tiles tall")
T.eq(fig.n, 124, "the mask claims 124 pixels of the 256 it spans")
T.eq(fig.tiles[1], 37, "it starts at the tile his head is drawn in")
T.eq(fig.under[1], 39, "which wears the couch's own cushion once he is off")
T.eq(fig.under[2], 1, "and the floor tiles wear the clean art (57 -> 1)")
T.eq(fig.under[4], 26, "and (60 -> 26)")
-- the mask is one connected figure: an overhang that floats free of him
-- is the failure this feature exists to avoid
local W = fig.w * 8
local seen, first = {}, nil
for i in pairs(fig.mask) do first = first or i end
local stack, n = { first }, 0
seen[first] = true
while #stack > 0 do
local i = table.remove(stack)
n = n + 1
local x, y = i % W, math.floor(i / W)
for dy = -1, 1 do
for dx = -1, 1 do
local nx, ny = x + dx, y + dy
local ni = ny * W + nx
if (dx ~= 0 or dy ~= 0) and nx >= 0 and nx < W
and fig.mask[ni] and not seen[ni] then
seen[ni] = true
stack[#stack + 1] = ni
end
end
end
end
T.eq(n, fig.n, "the mask is a single connected figure")
-- ------- the couch block, as the Centers place it
local function keyOf(tx, ty) return (ty + 64) * 4096 + (tx + 64) end
local COUNTER = { class = "counter", h = 8, art = "upright",
flat = false, authored = true }
local GROUND = { class = "ground", h = 0, art = "flat",
flat = true, authored = false }
local BLOCK = { 36, 37, 57, 11,
52, 53, 60, 27,
38, 39, 54, 11,
42, 43, 26, 27 }
local COUCH = { [36] = true, [37] = true, [38] = true, [39] = true,
[42] = true, [43] = true, [52] = true, [53] = true }
local function scene()
local S = { shapeAt = {}, tileAt = {}, objectQuads = {}, skip = {},
ground = {}, runs = {} }
for i = 1, 16 do
local tx, ty = (i - 1) % 4, 8 + math.floor((i - 1) / 4)
local tile = BLOCK[i]
S.tileAt[keyOf(tx, ty)] = tile
S.shapeAt[keyOf(tx, ty)] = COUCH[tile] and COUNTER or GROUND
end
return S
end
-- collision is per 16x16 cell: the couch's cell is blocked (he is drawn
-- sitting on it), the floor cell east of it is walkable
local map = {
tileset = { id = "POKECENTER", tilesPerRow = 16,
imageWidth = 128, imageHeight = 48 },
isWalkableCell = function(_, cx) return cx >= 1 end,
}
-- ------- he comes off the couch
local S = scene()
Structures.buildFigures(S, map, 0, 3, 8, 11)
T.check(#S.objectQuads > 0, "the figure built geometry")
T.eq(S.tileAt[keyOf(1, 8)], 39, "his head tile now wears the cushion")
T.eq(S.tileAt[keyOf(1, 9)], 39, "his body tile too")
T.eq(S.tileAt[keyOf(2, 8)], 1, "the floor he overhung is clean floor again")
T.eq(S.tileAt[keyOf(2, 9)], 26, "both rows of it")
T.eq(S.tileAt[keyOf(0, 8)], 36, "the west wall strip beside him is untouched")
T.eq(S.tileAt[keyOf(1, 10)], 39, "and the couch's own cushion row is too")
-- the couch keeps its box: a figure changes ART, never class
T.eq(S.shapeAt[keyOf(1, 8)].class, "counter",
"his tiles are still the couch's half-cell box")
T.check(S.skip[keyOf(1, 8)] ~= true,
"and are not skipped -- the couch still renders there")
-- ------- where he stands
local minX, maxX, minY, maxY, minZ, maxZ
for _, q in ipairs(S.objectQuads) do
for c = 1, 4 do
local p = q[c]
minX = math.min(minX or p[1], p[1]); maxX = math.max(maxX or p[1], p[1])
minY = math.min(minY or p[2], p[2]); maxY = math.max(maxY or p[2], p[2])
minZ = math.min(minZ or p[3], p[3]); maxZ = math.max(maxZ or p[3], p[3])
end
end
T.eq(minY, 8, "his feet are on the couch's top face, not the floor")
T.eq(maxY, 24, "and he stands his drawn 16px tall from there")
T.eq(minX, 8, "he starts at the tile column he is drawn in")
T.eq(maxX, 18, "and reaches his hair's overhang, two pixels east of it")
T.eq(maxZ - minZ, 10, "a billboard-pool standee, ten voxels of body")
-- the cushion wedge under his legs stayed with the couch: his lowest row
-- is his foot alone, not the whole tile width
local footMinX
for _, q in ipairs(S.objectQuads) do
for c = 1, 4 do
if q[c][2] == 8 then footMinX = math.min(footMinX or q[c][1], q[c][1]) end
end
end
T.eq(footMinX, 14, "the couch cushion beside his foot was left behind")
-- ------- a map that does not draw him builds nothing
local other = scene()
other.tileAt[keyOf(1, 8)] = 40
Structures.buildFigures(other, map, 0, 3, 8, 11)
T.eq(#other.objectQuads, 0, "no match, no figure")
T.eq(other.tileAt[keyOf(2, 8)], 57, "and nothing repainted")
-- ------- and it never fires twice on the same drawing
local twice = scene()
Structures.buildFigures(twice, map, 0, 3, 8, 11)
local once = #twice.objectQuads
Structures.buildFigures(twice, map, 0, 3, 8, 11)
T.eq(#twice.objectQuads, once,
"the repaint replaces the pattern, so a rescan cannot match it again")
T.finish("DRAMATIC_SHAPE figures")