consolidate single color sprites into one master file
|
|
@ -51,10 +51,10 @@ The player is not one entity — it is **up to three simultaneous colored copies
|
|||
|
||||
## Art pipeline
|
||||
|
||||
Sprites are authored in full color, then split into per-channel `_r`/`_g`/`_b` PNGs so each color copy draws its own layer. The `art/*.sh` scripts drive this with ImageMagick `convert`:
|
||||
- `reduce_colors.sh <file>` — remap to `art/palette`/`palette.png`.
|
||||
- `extract.sh <file>` — split one image into R/G/B channel PNGs.
|
||||
- `reduce_and_extract.sh` — both steps over every PNG in the cwd (run from inside an `art/` subfolder).
|
||||
**One full-color master PNG per sprite.** The RGB channels are split at *load time*, not stored as separate files: `getAllAssets` (`level_editor/object_attributes.lua`) points all three of `sprites.{red,green,blue}` at the same master, and `Entity:draw(channel)` tints it through `gColor[channel]` — so `master × (1,0,0)` renders the red gun, and the additive `"screen"` blend recombines overlapping copies into the original color. Per-channel collision shapes come from `getChannelCells` (`_helpers.lua`), which inspects each channel of the master's ImageData. Adding a sprite (or a new sprite *state*) is now one file, not three.
|
||||
- `getAllAssets` treats every master PNG in a non-`_`-prefixed content folder as a placeable color asset, except the names in its `NON_ASSET_SPRITES` set (the palette swatch and sprites owned by dedicated classes like `player_*`, `door_unlocked`, `switch_pressed`).
|
||||
- The `_r`/`_g`/`_b` channel-splitting step is gone (older revisions had an `extract.sh`; the pre-split channel PNGs live in git history if ever needed).
|
||||
- `reduce_colors.sh <file>` / `reduce_and_extract.sh` — optional palette remap to `art/palette.png` (authoring step only; produces a master, no split).
|
||||
|
||||
## Bundled libraries (`libs/`, do not edit)
|
||||
|
||||
|
|
|
|||
35
_helpers.lua
|
|
@ -160,11 +160,12 @@ function cellShapeFromBox(box)
|
|||
return cells
|
||||
end
|
||||
|
||||
-- Which 16x16 cells of an image are non-empty, for multi-cell collision
|
||||
-- shapes. LÖVE 11 no longer exposes Image:getData(), so inspect ImageData
|
||||
-- directly instead of creating a GPU Image first.
|
||||
function getCellsFromSprite(spritePath)
|
||||
local img = love.image.newImageData(spritePath)
|
||||
-- Which 16x16 cells of an ImageData are non-empty, for multi-cell collision
|
||||
-- shapes. `channel` ("red"/"green"/"blue") restricts the test to one colour
|
||||
-- gun: a single full-colour master now feeds all three copies, so each copy's
|
||||
-- collision shape comes from its own channel of the master (a cell with only
|
||||
-- red pixels blocks the red copy but not blue). nil = "any channel lit".
|
||||
local function cellsFromImageData(img, channel)
|
||||
local w, h = img:getWidth(), img:getHeight()
|
||||
local box = {w = w / 16, h = h / 16}
|
||||
local boxCells = cellShapeFromBox(box)
|
||||
|
|
@ -182,7 +183,12 @@ function getCellsFromSprite(spritePath)
|
|||
for i = startPixel.x, endPixel.x do
|
||||
for j = startPixel.y, endPixel.y do
|
||||
local r, g, b, a = img:getPixel(i, j)
|
||||
if a > 0 and (r > 0 or g > 0 or b > 0) then
|
||||
local lit
|
||||
if channel == "red" then lit = r > 0
|
||||
elseif channel == "green" then lit = g > 0
|
||||
elseif channel == "blue" then lit = b > 0
|
||||
else lit = r > 0 or g > 0 or b > 0 end
|
||||
if a > 0 and lit then
|
||||
empty = false
|
||||
end
|
||||
end
|
||||
|
|
@ -204,3 +210,20 @@ function getCellsFromSprite(spritePath)
|
|||
|
||||
return boxCells
|
||||
end
|
||||
|
||||
-- LÖVE 11 no longer exposes Image:getData(), so inspect ImageData directly
|
||||
-- instead of creating a GPU Image first.
|
||||
function getCellsFromSprite(spritePath, channel)
|
||||
return cellsFromImageData(love.image.newImageData(spritePath), channel)
|
||||
end
|
||||
|
||||
-- Per-channel collision cell shapes for one full-colour master, loaded once.
|
||||
-- Returns { red = cells, green = cells, blue = cells }.
|
||||
function getChannelCells(spritePath)
|
||||
local img = love.image.newImageData(spritePath)
|
||||
return {
|
||||
red = cellsFromImageData(img, "red"),
|
||||
green = cellsFromImageData(img, "green"),
|
||||
blue = cellsFromImageData(img, "blue"),
|
||||
}
|
||||
end
|
||||
|
|
|
|||
BIN
art/animals/sad_frog.png
Normal file
|
After Width: | Height: | Size: 503 B |
|
Before Width: | Height: | Size: 308 B |
|
Before Width: | Height: | Size: 390 B |
|
Before Width: | Height: | Size: 310 B |
|
Before Width: | Height: | Size: 458 B |
BIN
art/animals/smile_dog.png
Normal file
|
After Width: | Height: | Size: 470 B |
|
Before Width: | Height: | Size: 385 B |
|
Before Width: | Height: | Size: 385 B |
|
Before Width: | Height: | Size: 382 B |
|
Before Width: | Height: | Size: 422 B |
4
door.lua
|
|
@ -9,7 +9,7 @@ function Door:initialize(x, y, color)
|
|||
y = y,
|
||||
color = color,
|
||||
collision = "unmoveable",
|
||||
sprite = loadSprite("art/game/door_" .. color:sub(1, 1) .. ".png"),
|
||||
sprite = loadSprite("art/game/door.png"),
|
||||
})
|
||||
self.isDoor = true
|
||||
self.locked = true
|
||||
|
|
@ -21,7 +21,7 @@ end
|
|||
function Door:refreshSprites()
|
||||
local sprite
|
||||
if self.locked then
|
||||
sprite = loadSprite("art/game/door_" .. self.doorColor:sub(1, 1) .. ".png")
|
||||
sprite = loadSprite("art/game/door.png")
|
||||
else
|
||||
sprite = loadSprite("art/game/door_unlocked.png")
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,6 +3,20 @@ require "libs/TSerial"
|
|||
local targetFolder = "art"
|
||||
local reanalyzeCells = true
|
||||
|
||||
-- Sprites loaded directly by dedicated entity classes or used as reference art
|
||||
-- (the palette swatch), not placeable RGB assets. Every other master PNG in a
|
||||
-- content folder is a colour asset. Keeping this list preserves the exact
|
||||
-- palette set from when assets were identified by their _r/_g/_b channel files.
|
||||
local NON_ASSET_SPRITES = {
|
||||
palette = true,
|
||||
color_changing_wall = true,
|
||||
door_locked_w = true,
|
||||
door_unlocked = true,
|
||||
player_sad = true,
|
||||
player_w = true,
|
||||
switch_pressed = true,
|
||||
}
|
||||
|
||||
function getAllAssets()
|
||||
local objectnames = {}
|
||||
local topFolder = "art"
|
||||
|
|
@ -12,24 +26,29 @@ function getAllAssets()
|
|||
-- we ignore directories which are prepended w/ an underscore
|
||||
if not string.find(folder, "%.") then
|
||||
objectnames[folder] = {}
|
||||
-- love.filesystem.createDirectory("art/" .. folder)
|
||||
-- this is a folder, probably
|
||||
local newFiles = love.filesystem.getDirectoryItems(topFolder .. "/" .. folder)
|
||||
for _, fileToAnalyze in pairs(newFiles) do
|
||||
if fileToAnalyze:find("_r.png") or fileToAnalyze:find("_g.png") or fileToAnalyze:find("_b.png")then
|
||||
local color
|
||||
if fileToAnalyze:find("_r.png") then color = "red"
|
||||
elseif fileToAnalyze:find("_g.png") then color = "green"
|
||||
elseif fileToAnalyze:find("_b.png") then color = "blue" end
|
||||
-- One full-colour master PNG per asset. The draw path tints it
|
||||
-- per gun (see Entity:draw), so red/green/blue copies are just
|
||||
-- the same master drawn through gColor[channel]; the channels are
|
||||
-- split at load time, not stored as separate files.
|
||||
if fileToAnalyze:find("%.png$") then
|
||||
local name = fileToAnalyze:sub(1, fileToAnalyze:len() - 4)
|
||||
|
||||
local name = fileToAnalyze:sub(1, fileToAnalyze:len() - 6)
|
||||
-- first folder to define a name wins; skip engine/reference art
|
||||
if not NON_ASSET_SPRITES[name]
|
||||
and not (globalAssetProperties[name] and globalAssetProperties[name].directory ~= folder) then
|
||||
|
||||
local sprite = topFolder .. "/" .. folder .. "/" .. fileToAnalyze
|
||||
|
||||
if not globalAssetProperties[name] then
|
||||
globalAssetProperties[name] = {}
|
||||
globalAssetProperties[name].directory = folder
|
||||
globalAssetProperties[name].sprites = {}
|
||||
globalAssetProperties[name].preload = false
|
||||
end
|
||||
globalAssetProperties[name].directory = folder
|
||||
-- all three channels point at the same master image
|
||||
globalAssetProperties[name].sprites = { red = sprite, green = sprite, blue = sprite }
|
||||
|
||||
-- class comes from a <name>.meta sitting next to the art
|
||||
-- (authoritative); default to immoveable if none exists
|
||||
|
|
@ -40,16 +59,13 @@ function getAllAssets()
|
|||
globalAssetProperties[name].class = "immoveable"
|
||||
end
|
||||
|
||||
local sprite = topFolder .. "/" .. folder .. "/" .. fileToAnalyze
|
||||
globalAssetProperties[name].sprites[color] = sprite
|
||||
|
||||
-- Precompute the occupied-cell shape once per asset/colour and
|
||||
-- cache it here; entities share this table by reference so
|
||||
-- collision never rescans pixels at runtime. Keyed per colour
|
||||
-- (each channel leaves its own gaps) rather than reset per file.
|
||||
globalAssetProperties[name].cells = globalAssetProperties[name].cells or {}
|
||||
if not globalAssetProperties[name].cells[color] or reanalyzeCells then
|
||||
globalAssetProperties[name].cells[color] = getCellsFromSprite(sprite)
|
||||
-- Precompute per-channel occupied-cell shapes once and cache
|
||||
-- here; entities share this table by reference so collision
|
||||
-- never rescans pixels at runtime. Keyed per colour because
|
||||
-- each channel of the master leaves its own gaps.
|
||||
if reanalyzeCells or not globalAssetProperties[name].cells then
|
||||
globalAssetProperties[name].cells = getChannelCells(sprite)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
18
npc.lua
|
|
@ -224,11 +224,10 @@ function Sign:initialize(x, y)
|
|||
x = x, y = y,
|
||||
color = "red", -- representative only; sprites are per-channel
|
||||
collision = "unmoveable",
|
||||
sprites = {
|
||||
red = loadSprite("art/npc/start_sign_r.png"),
|
||||
green = loadSprite("art/npc/start_sign_g.png"),
|
||||
blue = loadSprite("art/npc/start_sign_b.png"),
|
||||
},
|
||||
sprites = (function()
|
||||
local m = loadSprite("art/npc/start_sign.png") -- one master, tinted per gun at draw time
|
||||
return { red = m, green = m, blue = m }
|
||||
end)(),
|
||||
})
|
||||
self:initWalker()
|
||||
end
|
||||
|
|
@ -244,11 +243,10 @@ function Jorge:initialize(x, y)
|
|||
x = x, y = y,
|
||||
color = "red",
|
||||
collision = "unmoveable",
|
||||
sprites = {
|
||||
red = loadSprite("art/npc/jorge_r.png"),
|
||||
green = loadSprite("art/npc/jorge_g.png"),
|
||||
blue = loadSprite("art/npc/jorge_b.png"),
|
||||
},
|
||||
sprites = (function()
|
||||
local m = loadSprite("art/npc/jorge.png") -- one master, tinted per gun at draw time
|
||||
return { red = m, green = m, blue = m }
|
||||
end)(),
|
||||
})
|
||||
self:initWalker()
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ function Switch:initialize(x, y, color)
|
|||
color = color,
|
||||
collision = "none", --switches are floor elements that can't be pushed or bloc movement!!
|
||||
cellShape = {{x = 1, y = 1}},
|
||||
sprite = loadSprite("art/game/switch_" .. color:sub(1, 1) .. ".png")
|
||||
sprite = loadSprite("art/game/switch.png")
|
||||
})
|
||||
|
||||
self.activated = false
|
||||
|
|
|
|||