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() local removed = 0 local function removeTree(path) if love.filesystem.isDirectory(path) 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 a sprite are non-empty, for multi-cell collision shapes. function getCellsFromSprite(sprite) local w, h = sprite:getWidth(), sprite: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 img = sprite:getData() 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