mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 15:30:51 +02:00
158 lines
5.5 KiB
Lua
158 lines
5.5 KiB
Lua
-- Overworld battles: giving a battle pic its paper back.
|
|
--
|
|
-- Gen 1 battle pics are two-bit art whose lightest shade is WHITE, and the
|
|
-- engine's decoded PNGs key that shade to alpha 0 -- which was free, because
|
|
-- the field behind them was white too. A transparent belly on a white page
|
|
-- is a white belly.
|
|
--
|
|
-- Put a route behind it and the belly is grass. Charizard's chest, the whites
|
|
-- of every eye, the highlight down a Pikachu's cheek: all of it turns into a
|
|
-- hole with the world showing through, and the mon reads as a stencil.
|
|
--
|
|
-- So the paper is put back, and only where the paper was: the pic is read
|
|
-- back once, the transparent region OUTSIDE the figure is flood-filled from
|
|
-- the border, and every transparent pixel the flood could not reach -- every
|
|
-- hole enclosed by the artwork -- is filled opaque white. The silhouette is
|
|
-- untouched, so the mon still cuts cleanly against the world; only its
|
|
-- insides stop being see-through.
|
|
--
|
|
-- Read back off the GPU rather than off the asset, deliberately. What comes
|
|
-- back is the pic the engine actually decided to draw -- species palette,
|
|
-- forced-mono rebuild, shiny recolour, a mod's replacement art -- so this
|
|
-- needs to know nothing about how any of that was arrived at. Once per pic
|
|
-- per session, cached on the image itself.
|
|
|
|
-- the mod namespace (see main.lua): V.require loads a sibling module
|
|
local V = ...
|
|
|
|
local BattlePics = {}
|
|
|
|
-- Cached by the image the engine handed over. Weak keys, so a pic that goes
|
|
-- out of scope takes its filled twin with it rather than pinning a texture
|
|
-- for the session.
|
|
local cache = setmetatable({}, { __mode = "k" })
|
|
|
|
-- What an enclosed hole is filled with. White, because white is what the
|
|
-- battle field was: this restores the pixel the artist drew and the engine
|
|
-- then keyed away, it does not invent a new one.
|
|
BattlePics.FILL = { 1, 1, 1, 1 }
|
|
|
|
-- Anything at or under this alpha counts as keyed-out rather than drawn.
|
|
local CUT = 0.5
|
|
|
|
-- Read the pixels the engine would actually blit. A LOVE Image does not hand
|
|
-- its data back, so it is drawn into a canvas of its own size and the canvas
|
|
-- is read -- which is also what makes this work for every path that produces
|
|
-- a pic, without knowing which one produced this one.
|
|
local function readBack(img)
|
|
local w, h = img:getDimensions()
|
|
if w <= 0 or h <= 0 then return nil end
|
|
local prevCanvas = love.graphics.getCanvas()
|
|
local prevBlend, prevAlpha = love.graphics.getBlendMode()
|
|
local prevR, prevG, prevB, prevA = love.graphics.getColor()
|
|
local data = nil
|
|
local ok = pcall(function()
|
|
local canvas = love.graphics.newCanvas(w, h)
|
|
love.graphics.setCanvas(canvas)
|
|
love.graphics.clear(0, 0, 0, 0)
|
|
love.graphics.setBlendMode("replace", "premultiplied")
|
|
love.graphics.setColor(1, 1, 1, 1)
|
|
love.graphics.draw(img, 0, 0)
|
|
love.graphics.setCanvas()
|
|
data = canvas:newImageData()
|
|
if canvas.release then pcall(canvas.release, canvas) end
|
|
end)
|
|
if prevCanvas then
|
|
love.graphics.setCanvas(prevCanvas)
|
|
else
|
|
love.graphics.setCanvas()
|
|
end
|
|
love.graphics.setBlendMode(prevBlend or "alpha", prevAlpha)
|
|
love.graphics.setColor(prevR or 1, prevG or 1, prevB or 1, prevA or 1)
|
|
return ok and data or nil
|
|
end
|
|
|
|
-- Mark every transparent pixel reachable from the border. That set is the
|
|
-- OUTSIDE; everything transparent it does not reach is an enclosed hole.
|
|
--
|
|
-- An explicit stack rather than recursion: a 56x56 pic is three thousand
|
|
-- pixels and a keyed-out background is most of them, which is a deeper call
|
|
-- chain than is worth risking for no gain.
|
|
local function markOutside(data, w, h)
|
|
local outside = {}
|
|
local stack, top = {}, 0
|
|
local function push(x, y)
|
|
if x < 0 or y < 0 or x >= w or y >= h then return end
|
|
local key = y * w + x
|
|
if outside[key] then return end
|
|
local _, _, _, a = data:getPixel(x, y)
|
|
if a > CUT then return end
|
|
outside[key] = true
|
|
top = top + 1
|
|
stack[top] = key
|
|
end
|
|
for x = 0, w - 1 do
|
|
push(x, 0)
|
|
push(x, h - 1)
|
|
end
|
|
for y = 0, h - 1 do
|
|
push(0, y)
|
|
push(w - 1, y)
|
|
end
|
|
while top > 0 do
|
|
local key = stack[top]
|
|
top = top - 1
|
|
local x, y = key % w, math.floor(key / w)
|
|
push(x - 1, y)
|
|
push(x + 1, y)
|
|
push(x, y - 1)
|
|
push(x, y + 1)
|
|
end
|
|
return outside
|
|
end
|
|
|
|
-- The pic with its enclosed holes filled, or the pic itself when that could
|
|
-- not be done (no pixel access, a driver that refused the readback). Never
|
|
-- nil for a non-nil argument: a caller must always have something to draw.
|
|
function BattlePics.filled(img)
|
|
if not img then return img end
|
|
local hit = cache[img]
|
|
if hit ~= nil then return hit or img end
|
|
|
|
local made = nil
|
|
local ok = pcall(function()
|
|
local data = readBack(img)
|
|
if not data then return end
|
|
local w, h = data:getDimensions()
|
|
local outside = markOutside(data, w, h)
|
|
local fill = BattlePics.FILL
|
|
local changed = false
|
|
for y = 0, h - 1 do
|
|
local row = y * w
|
|
for x = 0, w - 1 do
|
|
if not outside[row + x] then
|
|
local _, _, _, a = data:getPixel(x, y)
|
|
if a <= CUT then
|
|
data:setPixel(x, y, fill[1], fill[2], fill[3], fill[4])
|
|
changed = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
-- nothing enclosed: hand the original back rather than a copy of it
|
|
if not changed then return end
|
|
local out = love.graphics.newImage(data)
|
|
out:setFilter("nearest", "nearest")
|
|
made = out
|
|
end)
|
|
|
|
cache[img] = (ok and made) or false
|
|
return made or img
|
|
end
|
|
|
|
function BattlePics.invalidate()
|
|
cache = setmetatable({}, { __mode = "k" })
|
|
end
|
|
|
|
return BattlePics
|