chroma_solstice/world.lua

231 lines
7.4 KiB
Lua
Raw Normal View History

2026-08-07 01:39:47 -04:00
World = class("World")
-- The world is deliberately only a layout: rooms continue to own their puzzle
-- state. Keeping the two separate lets the editor move a room without
-- rewriting its level file.
function World:initialize()
self.width, self.height = 18, 18
self.rooms = {}
self.roomByName = {}
self.previewCache = {}
2026-08-07 03:22:28 -04:00
self.lastRoom = nil
2026-08-07 01:39:47 -04:00
self:load()
end
function World:listRoomNames()
local names = {}
for _, file in ipairs(love.filesystem.getDirectoryItems("rooms")) do
local name = file:match("^(.+)%.sav$")
if name and name ~= "world" then table.insert(names, name) end
end
table.sort(names)
return names
end
function World:load()
local raw = readFromSource("rooms/world.sav")
if not raw then return end
local ok, save = pcall(TSerial.unpack, raw)
if not ok or type(save) ~= "table" then return end
2026-08-07 03:22:28 -04:00
self.lastRoom = save.lastRoom
2026-08-07 01:39:47 -04:00
for _, room in ipairs(save.rooms or {}) do
if room.name and self:isInBounds(room.x, room.y) then
self:_putRoom(room.name, room.x, room.y)
end
end
end
function World:save()
local rooms = {}
for _, room in ipairs(self.rooms) do
table.insert(rooms, { name = room.name, x = room.x, y = room.y })
end
2026-08-07 03:22:28 -04:00
return writeToSource("rooms/world.sav", TSerial.pack({ width = self.width, height = self.height, lastRoom = self.lastRoom, rooms = rooms }, nil, true))
2026-08-07 01:39:47 -04:00
end
function World:isInBounds(x, y)
return x and y and x >= 1 and x <= self.width and y >= 1 and y <= self.height
end
function World:roomAt(x, y)
for _, room in ipairs(self.rooms) do
if room.x == x and room.y == y then return room.name end
end
return nil
end
function World:roomLocation(name)
return self.roomByName[name]
end
function World:_putRoom(name, x, y)
local existing = self.roomByName[name]
if existing then
for i = #self.rooms, 1, -1 do
if self.rooms[i] == existing then table.remove(self.rooms, i) end
end
end
for i = #self.rooms, 1, -1 do
if self.rooms[i].x == x and self.rooms[i].y == y then
self.roomByName[self.rooms[i].name] = nil
table.remove(self.rooms, i)
end
end
local room = { name = name, x = x, y = y }
table.insert(self.rooms, room)
self.roomByName[name] = room
return room
end
function World:placeRoom(name, x, y)
if not name or not self:isInBounds(x, y) then return false end
self:_putRoom(name, x, y)
return self:save()
end
function World:removeRoom(x, y)
for i = #self.rooms, 1, -1 do
local room = self.rooms[i]
if room.x == x and room.y == y then
self.roomByName[room.name] = nil
table.remove(self.rooms, i)
2026-08-07 03:22:28 -04:00
if self.lastRoom == room.name then self.lastRoom = nil end
2026-08-07 01:39:47 -04:00
return self:save(), room.name
end
end
return false
end
function World:renameRoom(oldName, newName)
local room = self:roomLocation(oldName)
if not room or oldName == newName then return false end
self.roomByName[oldName] = nil
room.name = newName
self.roomByName[newName] = room
2026-08-07 03:22:28 -04:00
if self.lastRoom == oldName then self.lastRoom = newName end
2026-08-07 01:39:47 -04:00
self:invalidatePreview(oldName)
self:invalidatePreview(newName)
return self:save()
end
2026-08-07 03:22:28 -04:00
function World:setLastRoom(name)
if name and self.lastRoom ~= name then
self.lastRoom = name
return self:save()
end
return true
end
2026-08-07 01:39:47 -04:00
function World:getNeighbor(name, exit)
local room = self:roomLocation(name)
if not room then return nil end
local directions = {
left = { x = -1, y = 0 }, right = { x = 1, y = 0 },
top = { x = 0, y = -1 }, bottom = { x = 0, y = 1 },
}
local direction = directions[exit]
if not direction then return nil end
return self:roomAt(room.x + direction.x, room.y + direction.y)
end
function World:getCellFromMousePos(mouseX, mouseY)
return math.floor(mouseX / (width / self.width)) + 1, math.floor(mouseY / (height / self.height)) + 1
end
function World:getRoomData(name)
-- The current room is live: its preview reflects unsaved edits immediately.
if currentRoom and currentRoom.name == name then return currentRoom:serialize() end
if not self.previewCache[name] then
local raw = readFromSource("rooms/" .. name .. ".sav")
if not raw then return nil end
local ok, room = pcall(TSerial.unpack, raw)
if not ok then return nil end
self.previewCache[name] = room
end
return self.previewCache[name]
end
function World:invalidatePreview(name)
self.previewCache[name] = nil
end
function World:drawRoomPreview(name, x, y, size)
local room = self:getRoomData(name)
if not room then return end
local scale = size / (gridWidth * 16)
love.graphics.setScissor(x, y, size, size)
for _, object in ipairs(room.objects or {}) do
if object.class == "npc" then
2026-08-07 20:56:42 -04:00
love.graphics.setColor(1, 1, 1, 1)
2026-08-07 01:39:47 -04:00
love.graphics.print("?", x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale, 0, scale, scale)
else
local props = globalAssetProperties[object.class]
if props and props.sprites then
2026-08-07 03:22:28 -04:00
local colors
if object.class == "door" then
colors = object.color and { object.color } or { "red", "green", "blue" }
else
colors = { object.color }
end
2026-08-07 01:39:47 -04:00
for _, color in ipairs(colors) do
local path = props.sprites[color]
if path then
love.graphics.setColor(gColor[color]:set())
local sprite = loadSprite(path)
if props.goopy and drawGoopySprite then
drawGoopySprite(sprite, x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale, scale)
elseif props.skewer and drawSkewerSprite then
drawSkewerSprite(sprite, props.cells and props.cells[color], x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale, scale)
2026-08-11 20:08:58 -04:00
elseif props.spiky and drawSpikySprite then
drawSpikySprite(sprite, props.cells and props.cells[color], x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale, scale)
else
love.graphics.draw(sprite, x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale, 0, scale, scale)
end
2026-08-07 01:39:47 -04:00
end
end
end
end
end
love.graphics.setScissor()
end
2026-08-11 16:24:51 -04:00
function World:drawHoveredRoomName()
local mouseX, mouseY = love.mouse.getPosition()
local cellX, cellY = self:getCellFromMousePos(mouseX, mouseY)
local name = self:isInBounds(cellX, cellY) and self:roomAt(cellX, cellY)
if not name then return end
local font = love.graphics.getFont()
local padding = 6
local tooltipW = math.min(font:getWidth(name) + padding * 2, width - 8)
local tooltipH = font:getHeight() + padding * 2
local tooltipX = math.min(mouseX + 14, width - tooltipW - 4)
local tooltipY = math.min(mouseY + 14, height - tooltipH - 4)
tooltipX = math.max(4, tooltipX)
tooltipY = math.max(4, tooltipY)
-- Keep the label anchored beside the pointer, while keeping it entirely in
-- the playable map rather than allowing it to spill into the editor panel.
love.graphics.setColor(0, 0, 0, 0.9)
love.graphics.rectangle("fill", tooltipX, tooltipY, tooltipW, tooltipH)
love.graphics.setColor(1, 1, 1, 1)
love.graphics.rectangle("line", tooltipX, tooltipY, tooltipW, tooltipH)
love.graphics.printf(name, tooltipX + padding, tooltipY + padding, tooltipW - padding * 2, "left")
end
2026-08-07 01:39:47 -04:00
function World:draw()
local cell = width / self.width
2026-08-07 20:56:42 -04:00
love.graphics.setColor(12 / 255, 12 / 255, 20 / 255, 1)
2026-08-07 01:39:47 -04:00
love.graphics.rectangle("fill", 0, 0, width, height)
for y = 1, self.height do
for x = 1, self.width do
local screenX, screenY = (x - 1) * cell, (y - 1) * cell
local name = self:roomAt(x, y)
if name then self:drawRoomPreview(name, screenX, screenY, cell) end
2026-08-07 20:56:42 -04:00
love.graphics.setColor(name and 1 or 70 / 255, name and 1 or 70 / 255, name and 1 or 90 / 255, 1)
2026-08-07 01:39:47 -04:00
love.graphics.rectangle("line", screenX, screenY, cell, cell)
end
end
2026-08-11 16:24:51 -04:00
self:drawHoveredRoomName()
2026-08-07 01:39:47 -04:00
end