mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 09:10:49 +02:00
fix vertical line in battle sprites
This commit is contained in:
+63
-16
@@ -70,6 +70,15 @@
|
||||
-- pinned prop, overriding the flat-neighbour vote -- the cuttable bush
|
||||
-- stands on the grass Cut leaves behind, whatever borders it.
|
||||
--
|
||||
-- And it may carry `figures` (not a class either): hand-drawn pixel masks
|
||||
-- that lift a FIGURE PAINTED INTO furniture off it and stand it up as a
|
||||
-- standee, leaving the furniture its own geometry. A pin resolves a
|
||||
-- whole 8x8 tile, so it can never separate two things that share one --
|
||||
-- and nothing automatic can either when the figure has no background
|
||||
-- margin to flood from and wears the same shades as what it sits on.
|
||||
-- The Pokemon Center's seated man is the case; see POKECENTER below for
|
||||
-- the format.
|
||||
--
|
||||
-- Whole BUILDINGS are not tile pins -- one drawing packs a roof seen from
|
||||
-- above, a facade seen face-on and sloped ends as diagonal silhouettes,
|
||||
-- and no single class covers that. They live in the `buildings` list at
|
||||
@@ -694,22 +703,17 @@ return {
|
||||
-- bodies so they stand ON them) and the PC (66/70/82/86), which
|
||||
-- stands on its pinned desk
|
||||
billboard = { 58, 59, 66, 70, 74, 75, 82, 86 },
|
||||
-- Why the man on the couch rides the top face instead of standing
|
||||
-- up. He is drawn across two tile rows (skin pixels appear in
|
||||
-- rows 8 and 9 and stop dead at the row 9/10 seam), and folding
|
||||
-- two rows upright requires both to share a class, which makes the
|
||||
-- box two tiles deep -- and a fully folded box repeats its north
|
||||
-- row across its whole top face. So every upright arrangement
|
||||
-- puts his head on screen two or three times: as a 16px seat-back
|
||||
-- his head lands on the front AND twice on the top; as a 32px
|
||||
-- bookcase he becomes a cabinet taller than the room's walls.
|
||||
-- Dropping the seat to floor level to clear his face does not help
|
||||
-- either, it only moves which copy you see. He also cannot be a
|
||||
-- standee: the drawing has no floor margin, so all three non-black
|
||||
-- shades touch the cluster rim and the mask drains 307 of its 420
|
||||
-- interior pixels -- 46% of him even segmented alone, because his
|
||||
-- skin is the same light shade as the couch behind him. Riding
|
||||
-- the top face is the one arrangement that draws him exactly once.
|
||||
-- The man on the couch, cut out by hand and stood up (see `figures`
|
||||
-- below). Nothing automatic reaches him. A class pin resolves a
|
||||
-- whole 8x8 tile and he SHARES his tiles with the couch, so pinning
|
||||
-- them stands the furniture up with him; riding the couch's top
|
||||
-- face (what this did before) draws him lying flat on the cushion;
|
||||
-- and he cannot be segmented into a standee either, because the
|
||||
-- drawing has no background margin for a flood to enter by and his
|
||||
-- skin is the same light shade as the couch -- the mask drained 307
|
||||
-- of his 420 interior pixels, 46% of him, even segmented alone.
|
||||
-- An authored mask is the only thing that can tell a man from the
|
||||
-- sofa he is painted into, so that is what `figures` carries.
|
||||
-- the PC's desk body, which the PC stands on
|
||||
table = { 9, 88 },
|
||||
-- the potted plants: bush (32/33/48/49) over pot (34/35/50/51,
|
||||
@@ -721,6 +725,49 @@ return {
|
||||
-- them; the standee reads darker than the flat art, which is the
|
||||
-- accepted trade for a real plant silhouette
|
||||
prop = { 32, 33, 34, 35, 48, 49, 50, 51 },
|
||||
-- THE MAN ON THE COUCH. He is drawn INTO the lounge furniture --
|
||||
-- tiles 37 (head) and 53 (face and body) are his, and his hair and
|
||||
-- his foot spill one tile east onto the floor (57 and 60 are the
|
||||
-- plain floor tiles 1 and 26 with those pixels painted over him).
|
||||
-- `pixels` is the hand-drawn line between the man and the couch;
|
||||
-- `under` is what each tile wears once he is lifted off it: the
|
||||
-- couch's own cushion (39) where he sat, and 1 / 26, the clean
|
||||
-- floor the artist had already drawn. So the couch keeps its
|
||||
-- polygonal box and he stands on top of it, at NPC scale.
|
||||
--
|
||||
-- The mask keeps the couch's east rule (column 7) wherever it is
|
||||
-- drawn dark, because that is where his hair crosses the tile seam
|
||||
-- -- without it the overhang would float free of his head. The
|
||||
-- cushion wedge under his legs and the two floor pixels above his
|
||||
-- head are the only interior pixels it gives back.
|
||||
figures = {
|
||||
{
|
||||
class = "billboard",
|
||||
w = 2,
|
||||
tiles = { 37, 57,
|
||||
53, 60 },
|
||||
under = { 39, 1,
|
||||
39, 26 },
|
||||
pixels = {
|
||||
"..XXXXX.........",
|
||||
"XXXXXXXX........",
|
||||
"XXXXXXXXX.......",
|
||||
"XXXXXXXXXX......",
|
||||
"XXXXXXXXXX......",
|
||||
"XXXXXXX.X.......",
|
||||
"XXXXXXX.X.......",
|
||||
"XXXXXXX.X.......",
|
||||
"XXXXXXX.X.......",
|
||||
"XXXXXXXX........",
|
||||
"XXXXXXXX........",
|
||||
"XXXXXXXX........",
|
||||
"XXXXXXXXX.......",
|
||||
".XXXXXXXXX......",
|
||||
"...XXXXXX.......",
|
||||
"......XX........",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
-- The Poke Marts (one 4x4 layout serves every city; the tileset
|
||||
|
||||
@@ -42,6 +42,20 @@ local quad = nil -- nil = untried, false = unavailable
|
||||
-- The unit card: x in -0.5..0.5, y in 0..1, z = 0, UV over the whole
|
||||
-- texture. Feet on the model origin, so the model matrix only has to say
|
||||
-- where the mon is standing and how big it is.
|
||||
--
|
||||
-- Which puts this card OFF the voxel grid, alone among the meshes in this
|
||||
-- mode: the rest are built one unit per voxel in their own model space --
|
||||
-- terrain in world pixels, a character's card in the sprite's own pixels --
|
||||
-- and the wireframe is the integer planes of that space (see VoxelGrid).
|
||||
-- One unit here is the whole card, so the only integer plane inside it is
|
||||
-- x = 0, which is the pic's centre column: a single stray line straight
|
||||
-- down the middle of every Pokemon and no seams anywhere else.
|
||||
--
|
||||
-- The card stays a unit card, because a mon's size on screen is decided by
|
||||
-- the artwork's own dimensions and the distance it is standing at, and a
|
||||
-- unit card is what lets one matrix say both. Whoever draws it turns the
|
||||
-- wireframe off instead (Voxel3D.seams) -- a mesh that is not on the voxel
|
||||
-- grid does not get a voxel grid drawn on it.
|
||||
local function unitQuad()
|
||||
if quad ~= nil then return quad or nil end
|
||||
local verts = {
|
||||
@@ -93,8 +107,11 @@ function BattleBillboard.draw(tex, x, y, z, grow)
|
||||
if grow then w, h = w * grow, h * grow end
|
||||
if w <= 0 or h <= 0 then return false end
|
||||
local yaw = BattleBillboard.yawToward(x, z, Voxel3D.eye)
|
||||
-- off the voxel grid, so no wireframe on it (see unitQuad)
|
||||
Voxel3D.seams(false)
|
||||
Voxel3D.draw(mesh, tex, BattleBillboard.matrix(x, y, z, w, h, yaw),
|
||||
BattleBillboard.PULL)
|
||||
Voxel3D.seams(true)
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
@@ -378,10 +378,16 @@ function BattleScene.render(state, arena, textures, token)
|
||||
if flashing then
|
||||
Voxel3D.flatten(BattleScene.FLASH_COLOR, BattleScene.FLASH_STRENGTH)
|
||||
end
|
||||
-- and no voxel wireframe on the pair. Everything else in this frame is
|
||||
-- built a unit per voxel and wears the seams that fall out of that; a
|
||||
-- mon's card is one quad wearing the battle screen (see
|
||||
-- BattleBillboard), so it is off the grid and has no seams to draw.
|
||||
Voxel3D.seams(false)
|
||||
for _, card in ipairs(monCards(arena, groundY, textures)) do
|
||||
Voxel3D.draw(BattleBillboard.mesh(), card.tex, card.model,
|
||||
BattleBillboard.PULL)
|
||||
end
|
||||
Voxel3D.seams(true)
|
||||
if flashing then Voxel3D.flatten(nil) end
|
||||
-- grass and flowers ride the same camera-ward pull the free-roam pass
|
||||
-- gives them, measured against THIS camera's pitch rather than the
|
||||
|
||||
@@ -268,6 +268,14 @@ function Structures.forMap(map)
|
||||
-- ---- bookcases: pinned shelves collapsed to one cell of depth ----
|
||||
Structures.buildBookcases(S, map, x0, x1, y0, y1)
|
||||
|
||||
-- ---- figures: a person drawn INTO furniture, lifted off it ----
|
||||
-- Before the region flood and the volume pass, so everything after this
|
||||
-- reads the tiles the profile says are there once the figure is gone.
|
||||
-- (Its own tiles are authored furniture or walkable floor either way, so
|
||||
-- no pass below would have claimed them -- but the repaint is what those
|
||||
-- passes should see, and this needs no pixel access to do it.)
|
||||
Structures.buildFigures(S, map, x0, x1, y0, y1)
|
||||
|
||||
-- ---- flood-fill regions of structural tiles ----
|
||||
local seen = {}
|
||||
local regions = {}
|
||||
@@ -2116,6 +2124,141 @@ function Structures.buildObject(S, map, region, cluster,
|
||||
return true
|
||||
end
|
||||
|
||||
-- ---- figures: a person drawn INTO furniture, cut out and stood up ----
|
||||
|
||||
-- One authored figure at one matched position.
|
||||
--
|
||||
-- The mask IS the classification: no flood, no shade segmentation, no
|
||||
-- validation gate. Every automatic route in this file asks the art where
|
||||
-- the object ends, and a figure painted into its own furniture has no
|
||||
-- answer to give -- so the profile answers instead, and this only has to
|
||||
-- believe it. Which also means figures build HEADLESS: unlike every
|
||||
-- other standee here, nothing below reads a pixel.
|
||||
local function buildFigure(S, map, fig, tx, ty, perRow)
|
||||
local bw, bh = fig.w * 8, fig.h * 8
|
||||
|
||||
local function at(lx, ly)
|
||||
if lx < 0 or lx >= bw or ly < 0 or ly >= bh then return false end
|
||||
return fig.mask[ly * bw + lx] or false
|
||||
end
|
||||
|
||||
-- his feet: the lowest drawn row, which is what lands on the support
|
||||
local lowY = 0
|
||||
for ly = bh - 1, 0, -1 do
|
||||
local any = false
|
||||
for lx = 0, bw - 1 do
|
||||
if at(lx, ly) then any = true break end
|
||||
end
|
||||
if any then lowY = ly break end
|
||||
end
|
||||
|
||||
-- He stands ON the furniture he was drawn into -- the same lift a pinned
|
||||
-- prop above a pinned box takes (see buildObject), and gated the same
|
||||
-- way: a thing set down on furniture occupies a BLOCKED cell, while a
|
||||
-- seat you merely walk up to is in a walkable one.
|
||||
local baseY, support = 0, nil
|
||||
local bs = S.shapeAt[keyOf(tx, ty + fig.h)]
|
||||
local blocked = not map:isWalkableCell(math.floor(tx / 2),
|
||||
math.floor((ty + fig.h - 1) / 2))
|
||||
if blocked and bs and bs.authored and bs.art == "upright"
|
||||
and (bs.h or 0) > 0 then
|
||||
baseY, support = bs.h, bs
|
||||
end
|
||||
|
||||
local depth = PINNED_DEPTH[fig.class] or PINNED_DEPTH.billboard
|
||||
local z0 = ty * 8 + math.floor(lowY / 8) * 8
|
||||
+ (support and 8 or 0) + (8 - depth) / 2
|
||||
local z1 = z0 + depth
|
||||
|
||||
-- ONE object by construction: the figure keeps its drawn proportions
|
||||
-- whatever the mask's connectivity, so an overhang (his hair crossing
|
||||
-- the tile seam) stays at its drawn height instead of being dropped to
|
||||
-- the floor as a component of its own.
|
||||
local atlasW = map.tileset.imageWidth or 128
|
||||
local atlasH = map.tileset.imageHeight or 48
|
||||
local quads = S.objectQuads
|
||||
local wx0 = tx * 8
|
||||
for ly = 0, bh - 1 do
|
||||
Budget.tick()
|
||||
for lx = 0, bw - 1 do
|
||||
if at(lx, ly) then
|
||||
local tile = fig.tiles[math.floor(ly / 8) * fig.w
|
||||
+ math.floor(lx / 8) + 1]
|
||||
local u = ((tile % perRow) * 8 + lx % 8 + 0.5) / atlasW
|
||||
local v = (math.floor(tile / perRow) * 8 + ly % 8 + 0.5) / atlasH
|
||||
local x, y = wx0 + lx, baseY + lowY - ly
|
||||
local function quad(c1, c2, c3, c4, shade)
|
||||
quads[#quads + 1] = { c1, c2, c3, c4, u = u, v = v, shade = shade }
|
||||
end
|
||||
quad({ x, y, z1 }, { x + 1, y, z1 }, { x + 1, y + 1, z1 },
|
||||
{ x, y + 1, z1 }, OBJ_SHADE.front)
|
||||
quad({ x + 1, y, z0 }, { x, y, z0 }, { x, y + 1, z0 },
|
||||
{ x + 1, y + 1, z0 }, OBJ_SHADE.back)
|
||||
if not at(lx, ly - 1) then
|
||||
quad({ x, y + 1, z0 }, { x + 1, y + 1, z0 }, { x + 1, y + 1, z1 },
|
||||
{ x, y + 1, z1 }, OBJ_SHADE.top)
|
||||
end
|
||||
if y > baseY and not at(lx, ly + 1) then
|
||||
quad({ x, y, z1 }, { x + 1, y, z1 }, { x + 1, y, z0 },
|
||||
{ x, y, z0 }, OBJ_SHADE.bottom)
|
||||
end
|
||||
if not at(lx - 1, ly) then
|
||||
quad({ x, y, z0 }, { x, y, z1 }, { x, y + 1, z1 },
|
||||
{ x, y + 1, z0 }, OBJ_SHADE.side)
|
||||
end
|
||||
if not at(lx + 1, ly) then
|
||||
quad({ x + 1, y, z1 }, { x + 1, y, z0 }, { x + 1, y + 1, z0 },
|
||||
{ x + 1, y + 1, z1 }, OBJ_SHADE.side)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- What each covered tile wears now that he is off it. Only the ART
|
||||
-- changes: the couch tiles keep their `counter` box (they ARE the
|
||||
-- couch) and the floor tiles he overhung stay flat floor -- the
|
||||
-- profile just names the version of each drawing without him in it,
|
||||
-- so nothing has to be synthesized or repainted from a neighbour vote.
|
||||
for i = 1, #fig.tiles do
|
||||
local dx, dy = (i - 1) % fig.w, math.floor((i - 1) / fig.w)
|
||||
S.tileAt[keyOf(tx + dx, ty + dy)] = fig.under[i]
|
||||
end
|
||||
end
|
||||
|
||||
-- Every authored figure, wherever the map draws it.
|
||||
--
|
||||
-- Matched by TILE PATTERN rather than by coordinates: one blockset entry
|
||||
-- places this couch once in each of the eleven Pokemon Centers (and the
|
||||
-- Celadon Hotel), so the pattern finds all of them without the profile
|
||||
-- naming a single map or cell. The repaint above replaces the pattern's
|
||||
-- own tiles, so a match can never fire twice on the same drawing.
|
||||
function Structures.buildFigures(S, map, x0, x1, y0, y1)
|
||||
local figures = TileShape.figures(map.tileset.id)
|
||||
print("[fig] map", map.id, "tileset", map.tileset.id, "figures",
|
||||
figures and #figures or "nil")
|
||||
if not figures then return end
|
||||
local perRow = map.tileset.tilesPerRow or 16
|
||||
for _, fig in ipairs(figures) do
|
||||
for ty = y0, y1 - fig.h + 1 do
|
||||
for tx = x0, x1 - fig.w + 1 do
|
||||
Budget.tick()
|
||||
local hit = true
|
||||
for i = 1, #fig.tiles do
|
||||
local dx, dy = (i - 1) % fig.w, math.floor((i - 1) / fig.w)
|
||||
if S.tileAt[keyOf(tx + dx, ty + dy)] ~= fig.tiles[i] then
|
||||
hit = false
|
||||
break
|
||||
end
|
||||
end
|
||||
if hit then
|
||||
print("[fig] MATCH at", tx, ty)
|
||||
buildFigure(S, map, fig, tx, ty, perRow)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ---- tall grass ----
|
||||
|
||||
-- A tall-grass CELL is four tufts: 2x2 tiles, and each 8x8 tile is one
|
||||
|
||||
@@ -168,6 +168,7 @@ local ART = {
|
||||
|
||||
local spec = nil -- the loaded data file, or false when absent
|
||||
local cache = {} -- tileset id -> resolved shape list
|
||||
local figCache = {} -- tileset id -> parsed figure masks, or false
|
||||
|
||||
-- The shape profile ships with the mod (data/voxel_heights.lua) and is read
|
||||
-- through the mod's own file loader rather than package.path: a mod's
|
||||
@@ -369,11 +370,84 @@ function TileShape.at(map, shapes, tile, tx, ty)
|
||||
return s
|
||||
end
|
||||
|
||||
-- Hand-authored FIGURES for one tileset: a drawing painted INTO furniture,
|
||||
-- cut out by an explicit pixel mask and stood up on top of it.
|
||||
--
|
||||
-- Every other route in this file resolves a whole 8x8 TILE, which is
|
||||
-- exactly why none of them can reach a figure that shares its tiles with
|
||||
-- the thing it sits on -- and the detector's segmentation cannot either
|
||||
-- when the drawing has no background margin to flood from and wears the
|
||||
-- same shades as its furniture. So the profile authors the silhouette
|
||||
-- pixel by pixel (see data/voxel_heights.lua):
|
||||
--
|
||||
-- figures = { { class = <standee pool, for its depth>,
|
||||
-- w = <tiles across>,
|
||||
-- tiles = { ...w*h tile ids, row-major... },
|
||||
-- under = { ...w*h ids: what each tile wears once the
|
||||
-- figure is lifted off it... },
|
||||
-- pixels = { ...h*8 strings of w*8 chars, "." = not the
|
||||
-- figure... } } }
|
||||
--
|
||||
-- Returned normalized: `mask` as a set keyed by ly * (w * 8) + lx, so
|
||||
-- Structures can read it as a bitmap without re-parsing per position.
|
||||
-- A malformed entry is dropped rather than half-applied -- a typo in a
|
||||
-- mask should leave the couch alone, not carve a hole in it.
|
||||
function TileShape.figures(tilesetId)
|
||||
local hit = figCache[tilesetId]
|
||||
if hit ~= nil then return hit or nil end
|
||||
|
||||
local s = load()
|
||||
local entry = s and s.tilesets and s.tilesets[tilesetId]
|
||||
local list = entry and entry.figures
|
||||
local out = {}
|
||||
if type(list) == "table" then
|
||||
for _, f in ipairs(list) do
|
||||
local ok = type(f) == "table" and type(f.w) == "number"
|
||||
and type(f.tiles) == "table" and type(f.under) == "table"
|
||||
and type(f.pixels) == "table"
|
||||
local w = ok and math.floor(f.w) or 0
|
||||
local h = (w >= 1) and (#f.tiles / w) or 0
|
||||
ok = ok and w >= 1 and h >= 1 and h == math.floor(h)
|
||||
and #f.under == #f.tiles and #f.pixels == h * 8
|
||||
if ok then
|
||||
for i = 1, h * 8 do
|
||||
local row = f.pixels[i]
|
||||
if type(row) ~= "string" or #row ~= w * 8 then
|
||||
ok = false
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if ok then
|
||||
local mask, n = {}, 0
|
||||
for ly = 0, h * 8 - 1 do
|
||||
local row = f.pixels[ly + 1]
|
||||
for lx = 0, w * 8 - 1 do
|
||||
if row:sub(lx + 1, lx + 1) ~= "." then
|
||||
mask[ly * (w * 8) + lx] = true
|
||||
n = n + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
if n > 0 then
|
||||
out[#out + 1] = { class = f.class or "billboard",
|
||||
w = w, h = h, n = n, mask = mask,
|
||||
tiles = f.tiles, under = f.under }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
figCache[tilesetId] = (#out > 0) and out or false
|
||||
return figCache[tilesetId] or nil
|
||||
end
|
||||
|
||||
-- Drop the cache: a mod that shadows data/voxel_heights.lua or a tileset
|
||||
-- record needs the next lookup to re-resolve (hot reload, mod toggle).
|
||||
function TileShape.invalidate()
|
||||
spec = nil
|
||||
cache = {}
|
||||
figCache = {}
|
||||
end
|
||||
|
||||
return TileShape
|
||||
|
||||
@@ -558,6 +558,31 @@ function Voxel3D.flatten(color, amount)
|
||||
end
|
||||
end
|
||||
|
||||
-- Whether what is drawn next carries the voxel wireframe. false for the
|
||||
-- length of a draw, true to put it back.
|
||||
--
|
||||
-- The wireframe reads a mesh's OWN model space and darkens its integer
|
||||
-- planes (see VoxelGrid), which is only a wireframe because every mesh in
|
||||
-- this mode is built ONE UNIT PER VOXEL: terrain in world pixels, a
|
||||
-- character card in the sprite's own pixels. A mesh whose model space does
|
||||
-- not mean that gets no wireframe out of the same shader -- it gets
|
||||
-- whichever of its integer planes happen to fall inside it, which is a
|
||||
-- stray line rather than a seam.
|
||||
--
|
||||
-- So this is not a style switch. It is how a mesh that is not on the voxel
|
||||
-- grid says so, and the alternative -- rescaling such a mesh until its
|
||||
-- units happen to be voxels -- would change what it IS to satisfy a
|
||||
-- shading pass.
|
||||
--
|
||||
-- Sent rather than branched because the plain scene shader has no such
|
||||
-- uniform, and the send simply does not take there -- which is right: with
|
||||
-- no wireframe compiled in there is nothing to suppress.
|
||||
function Voxel3D.seams(on)
|
||||
if not (active and activeShader) then return end
|
||||
pcall(activeShader.send, activeShader, "gridDark",
|
||||
on and VoxelGrid.DARK or 0)
|
||||
end
|
||||
|
||||
function Voxel3D.endGhost()
|
||||
if not active then return end
|
||||
pcall(love.graphics.setDepthMode, "lequal", true)
|
||||
|
||||
@@ -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
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user