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 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") if file then file:close() return true end 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 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) 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 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) if MapManager then MapManager:onClearRuntime(roomName) end 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. This mirrors writeToSource's root order. function readFromSource(relPath) for _, base in ipairs(sourceRoots()) do local contents = readFromRoot(base, relPath) if contents then return contents end end return love.filesystem.read(relPath) end 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 -- 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