2015-03-14 05:36:02 -04:00
|
|
|
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
|
2026-08-07 00:56:33 -04:00
|
|
|
|
2026-08-12 17:20:11 -04:00
|
|
|
local function sourceRoots()
|
|
|
|
|
local roots = {}
|
|
|
|
|
local seen = {}
|
|
|
|
|
local function add(root)
|
|
|
|
|
if type(root) == "string" and root ~= "" and not seen[root] then
|
|
|
|
|
table.insert(roots, root)
|
|
|
|
|
seen[root] = true
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
add("/home/happy/code/chroma_solstice")
|
|
|
|
|
if love and love.filesystem then
|
|
|
|
|
if love.filesystem.getSource then add(love.filesystem.getSource()) end
|
|
|
|
|
end
|
|
|
|
|
return roots
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function readFromRoot(root, relPath)
|
|
|
|
|
local file = io.open(root .. "/" .. relPath, "r")
|
|
|
|
|
if not file then return nil end
|
|
|
|
|
local contents = file:read("*a")
|
|
|
|
|
file:close()
|
|
|
|
|
return contents
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function pathExists(path)
|
|
|
|
|
local file = io.open(path, "r")
|
2026-08-07 00:56:33 -04:00
|
|
|
if file then
|
|
|
|
|
file:close()
|
|
|
|
|
return true
|
|
|
|
|
end
|
2026-08-12 17:20:11 -04:00
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Write to the project tree itself (not LÖVE's save dir) so edits persist in the
|
|
|
|
|
-- repo during dev. Packaged games still fall back to love.filesystem.write.
|
|
|
|
|
function writeToSource(relPath, contents)
|
|
|
|
|
for _, base in ipairs(sourceRoots()) do
|
|
|
|
|
local path = base .. "/" .. relPath
|
|
|
|
|
local file, err = io.open(path, "w")
|
|
|
|
|
if file then
|
|
|
|
|
file:write(contents)
|
|
|
|
|
file:close()
|
|
|
|
|
print("Saved source: " .. path)
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
print("Save source failed: " .. path .. " (" .. tostring(err) .. ")")
|
|
|
|
|
end
|
|
|
|
|
print("Save failed: did not write fallback for " .. relPath)
|
|
|
|
|
return false
|
2026-08-07 00:56:33 -04:00
|
|
|
end
|
|
|
|
|
|
2026-08-07 01:39:47 -04:00
|
|
|
-- 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)
|
2026-08-12 17:20:11 -04:00
|
|
|
for _, base in ipairs(sourceRoots()) do
|
|
|
|
|
local path = base .. "/" .. relPath
|
|
|
|
|
local file = io.open(path, "r")
|
|
|
|
|
if file then
|
|
|
|
|
file:close()
|
|
|
|
|
return os.remove(path)
|
|
|
|
|
end
|
2026-08-07 01:39:47 -04:00
|
|
|
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.
|
2026-08-07 03:22:28 -04:00
|
|
|
function clearRuntimeSaveData(roomName)
|
2026-08-07 01:39:47 -04:00
|
|
|
local removed = 0
|
2026-08-07 03:22:28 -04:00
|
|
|
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
|
|
|
|
|
|
2026-08-07 01:39:47 -04:00
|
|
|
local function removeTree(path)
|
2026-08-07 20:56:42 -04:00
|
|
|
local info = love.filesystem.getInfo(path)
|
|
|
|
|
if info and info.type == "directory" then
|
2026-08-07 01:39:47 -04:00
|
|
|
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
|
2026-08-07 03:22:28 -04:00
|
|
|
-- autosaves and the generated asset cache. Source levels are read-only to
|
2026-08-07 01:39:47 -04:00
|
|
|
-- this API and cannot be removed here.
|
|
|
|
|
removeTree("rooms")
|
|
|
|
|
if love.filesystem.remove("assets.prop") then removed = removed + 1 end
|
|
|
|
|
return removed
|
|
|
|
|
end
|
|
|
|
|
|
2026-08-07 00:56:33 -04:00
|
|
|
-- Read from the project tree directly so a stale copy in LÖVE's save directory
|
2026-08-12 17:20:11 -04:00
|
|
|
-- can't shadow the real source file. This mirrors writeToSource's root order.
|
2026-08-07 00:56:33 -04:00
|
|
|
function readFromSource(relPath)
|
2026-08-12 17:20:11 -04:00
|
|
|
for _, base in ipairs(sourceRoots()) do
|
|
|
|
|
local contents = readFromRoot(base, relPath)
|
|
|
|
|
if contents then return contents end
|
2026-08-07 00:56:33 -04:00
|
|
|
end
|
|
|
|
|
return love.filesystem.read(relPath)
|
|
|
|
|
end
|
2026-08-07 01:05:08 -04:00
|
|
|
|
2026-08-12 17:20:11 -04:00
|
|
|
function getSourceDirectoryItems(relPath)
|
|
|
|
|
for _, base in ipairs(sourceRoots()) do
|
|
|
|
|
local dir = base .. "/" .. relPath
|
|
|
|
|
if pathExists(dir) then
|
|
|
|
|
local ok, proc = pcall(io.popen, "find " .. string.format("%q", dir) .. " -maxdepth 1 -mindepth 1 -printf '%f\n'")
|
|
|
|
|
local items = {}
|
|
|
|
|
if ok and proc then
|
|
|
|
|
for item in proc:lines() do table.insert(items, item) end
|
|
|
|
|
proc:close()
|
|
|
|
|
end
|
|
|
|
|
return items
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
return love.filesystem.getDirectoryItems(relPath)
|
|
|
|
|
end
|
|
|
|
|
|
2026-08-07 01:05:08 -04:00
|
|
|
-- 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
|
|
|
|
|
|
2026-08-07 20:56:42 -04:00
|
|
|
-- 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()
|
2026-08-07 01:05:08 -04:00
|
|
|
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
|