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

196 lines
5.7 KiB
Lua

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 = {}
self.lastRoom = nil
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
self.lastRoom = save.lastRoom
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
return writeToSource("rooms/world.sav", TSerial.pack({ width = self.width, height = self.height, lastRoom = self.lastRoom, rooms = rooms }, nil, true))
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)
if self.lastRoom == room.name then self.lastRoom = nil end
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
if self.lastRoom == oldName then self.lastRoom = newName end
self:invalidatePreview(oldName)
self:invalidatePreview(newName)
return self:save()
end
function World:setLastRoom(name)
if name and self.lastRoom ~= name then
self.lastRoom = name
return self:save()
end
return true
end
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
love.graphics.setColor(1, 1, 1, 1)
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
local colors
if object.class == "door" then
colors = object.color and { object.color } or { "red", "green", "blue" }
else
colors = { object.color }
end
for _, color in ipairs(colors) do
local path = props.sprites[color]
if path then
love.graphics.setColor(gColor[color]:set())
love.graphics.draw(loadSprite(path), x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale, 0, scale, scale)
end
end
end
end
end
love.graphics.setScissor()
end
function World:draw()
local cell = width / self.width
love.graphics.setColor(12 / 255, 12 / 255, 20 / 255, 1)
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
love.graphics.setColor(name and 1 or 70 / 255, name and 1 or 70 / 255, name and 1 or 90 / 255, 1)
love.graphics.rectangle("line", screenX, screenY, cell, cell)
end
end
end