chroma_solstice/_helpers.lua
2026-08-07 20:56:55 -04:00

156 lines
4.5 KiB
Lua

function constrain(val, low, high)
if val > high then
return high
elseif val < low then
return low
else
return val
end
end
function map(value, inMin, inMax, outMin, outMax)
local inPercent = value / (inMax - inMin)
return (outMax - outMin) * (inPercent) + outMin
end
function lerp(a, b, amount)
return (1 - amount) * a + (amount * b)
end
-- Write to the project tree itself (not LÖVE's save dir) so edits persist in the
-- repo during dev. Uses plain io.open against love.filesystem.getSource(), which
-- is the absolute project path when running `love .` from a folder. Falls back to
-- love.filesystem.write (save dir) if the source is a packaged .love archive.
function writeToSource(relPath, contents)
local base = love.filesystem.getSource()
local file = io.open(base .. "/" .. relPath, "w")
if file then
file:write(contents)
file:close()
return true
end
return love.filesystem.write(relPath, contents)
end
-- The editor occasionally needs to rename a source level after it has written
-- the replacement. Keep that operation alongside writeToSource so packaged
-- games still fall back to LÖVE's writable save directory.
function removeFromSource(relPath)
local base = love.filesystem.getSource()
local path = base .. "/" .. relPath
local file = io.open(path, "r")
if file then
file:close()
return os.remove(path)
end
return love.filesystem.remove(relPath)
end
-- Runtime saves live in LÖVE's write directory. This deliberately uses only
-- love.filesystem operations: they cannot delete files from the source tree.
function clearRuntimeSaveData(roomName)
local removed = 0
if roomName then
-- love.filesystem.remove only affects the writable save directory, never
-- the shipped/source room with the same path.
if love.filesystem.remove("rooms/" .. roomName .. ".sav") then removed = 1 end
return removed
end
local function removeTree(path)
local info = love.filesystem.getInfo(path)
if info and info.type == "directory" then
for _, child in ipairs(love.filesystem.getDirectoryItems(path)) do
removeTree(path .. "/" .. child)
end
end
if love.filesystem.remove(path) then removed = removed + 1 end
end
-- These are the only files this game writes through love.filesystem: room
-- autosaves and the generated asset cache. Source levels are read-only to
-- this API and cannot be removed here.
removeTree("rooms")
if love.filesystem.remove("assets.prop") then removed = removed + 1 end
return removed
end
-- Read from the project tree directly so a stale copy in LÖVE's save directory
-- can't shadow the real source file. Falls back to love.filesystem for a
-- packaged .love.
function readFromSource(relPath)
local base = love.filesystem.getSource()
local file = io.open(base .. "/" .. relPath, "r")
if file then
local contents = file:read("*a")
file:close()
return contents
end
return love.filesystem.read(relPath)
end
-- Cached image loader. The filesystem is the source of truth for art, so every
-- sprite is loaded on demand by path (deduped here) rather than from a
-- hand-maintained central table.
local spriteCache = {}
function loadSprite(path)
if not spriteCache[path] then
spriteCache[path] = love.graphics.newImage(path)
end
return spriteCache[path]
end
function cellShapeFromBox(box)
local cells = {}
for i = 1, box.w do
for j = 1, box.h do
table.insert(cells, {x = i, y = j})
end
end
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)
local w, h = img:getWidth(), img:getHeight()
local box = {w = w / 16, h = h / 16}
local boxCells = cellShapeFromBox(box)
if box.w < 1 or box.h < 1 then
return boxCells
end
local cellsToRemove = {}
for _, cell in pairs(boxCells) do
local startPixel = {x = (cell.x - 1) * 16, y = (cell.y - 1) * 16}
local endPixel = {x = (cell.x * 16) - 1, y = (cell.y * 16) - 1}
local empty = true
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
empty = false
end
end
end
if empty then
cellsToRemove[cell] = cell
end
end
local i = 1
while i <= #boxCells do
if boxCells[i] == cellsToRemove[boxCells[i]] then
table.remove(boxCells, i)
else
i = i + 1
end
end
return boxCells
end