From b9b07265068b5484baaeb2d613508b1c650324de Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 7 Aug 2026 03:22:28 -0400 Subject: [PATCH] misc --- _helpers.lua | 11 +++- level_editor/editor.lua | 23 ++++++-- main.lua | 3 +- room.lua | 42 ++++++++++---- rooms/world.sav | 121 ++++++++++++++++++++-------------------- world.lua | 21 ++++++- 6 files changed, 140 insertions(+), 81 deletions(-) diff --git a/_helpers.lua b/_helpers.lua index b48b25d..b07f791 100644 --- a/_helpers.lua +++ b/_helpers.lua @@ -48,8 +48,15 @@ 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() +function clearRuntimeSaveData(roomName) 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) if love.filesystem.isDirectory(path) then for _, child in ipairs(love.filesystem.getDirectoryItems(path)) do @@ -60,7 +67,7 @@ function clearRuntimeSaveData() 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 + -- 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 diff --git a/level_editor/editor.lua b/level_editor/editor.lua index d1fca84..174a9ea 100644 --- a/level_editor/editor.lua +++ b/level_editor/editor.lua @@ -33,6 +33,7 @@ local editorRoomName = nil local roomNameField = nil local editorStatus = nil local clearSaveConfirmationExpires = 0 +local clearSaveConfirmationScope = nil local worldSeedConfirmationExpires = 0 local worldMode = "place" local worldRoomButtons = {} @@ -129,7 +130,8 @@ local function saveRoomFromEditor() return end - if not currentRoom:saveToSource(name) then + local savePlayerPositions = love.keyboard.isDown("lshift", "rshift") + if not currentRoom:saveToSource(name, savePlayerPositions, oldName) then editorStatus = "Could not save '" .. name .. "'." return end @@ -146,20 +148,28 @@ local function saveRoomFromEditor() editorRoomName = name roomNameField:editText(name) activeField = nil - editorStatus = "Saved source level '" .. name .. ".'" + editorStatus = "Saved source level '" .. name .. "'" .. (savePlayerPositions and " with player positions." or ".") end local function clearRuntimeSaves() local now = love.timer.getTime() - if now > clearSaveConfirmationExpires then + local clearAll = love.keyboard.isDown("lshift", "rshift") + local roomName = currentRoom.name + local scope = clearAll and "all" or roomName + if now > clearSaveConfirmationExpires or clearSaveConfirmationScope ~= scope then clearSaveConfirmationExpires = now + 3 - editorStatus = "Click the clear-save icon again within 3 seconds to erase runtime saves." + clearSaveConfirmationScope = scope + if clearAll then + editorStatus = "Shift-click the clear-save icon again within 3 seconds to erase runtime saves for every room." + else + editorStatus = "Click the clear-save icon again within 3 seconds to erase the runtime save for '" .. (roomName or "this room") .. "'." + end return end - local removed = clearRuntimeSaveData() + local removed = clearRuntimeSaveData(clearAll and nil or roomName) clearSaveConfirmationExpires = 0 - local roomName = currentRoom.name + clearSaveConfirmationScope = nil if roomName then currentRoom = Room:new{ name = roomName } setEditorRoomName(currentRoom.name) @@ -521,6 +531,7 @@ local function openWorldRoom(cellX, cellY) currentRoom = Room:new{ name = name } editorStatus = "Editing '" .. name .. ".'" end + gameWorld:setLastRoom(currentRoom.name) setEditorRoomName(currentRoom.name) assetsView() end diff --git a/main.lua b/main.lua index 40841a4..f3252a1 100644 --- a/main.lua +++ b/main.lua @@ -33,8 +33,9 @@ function love.load() inputTimer = Timer.new() currentRoom = Room:new{ - name = "start" -- TEMP: load a migrated level to eyeball it; revert once the room picker lands + name = gameWorld.lastRoom or "start" } + gameWorld:setLastRoom(currentRoom.name) setEditorRoomName(currentRoom.name) response = "" diff --git a/room.lua b/room.lua index 8e4379e..b1b6b96 100644 --- a/room.lua +++ b/room.lua @@ -36,11 +36,6 @@ function Room:initialize(t) end self:setActiveColor(color, true) end - elseif not next(self.player) then - -- fresh load of a room with no saved player: default red spawn so it's - -- playable (the player starts with only red) - self:registerPlayer(Player:new(1, 1, "red"), "red") - self:setActiveColor("red", true) end self.name = t.name @@ -702,13 +697,37 @@ end -- This is intentionally separate from Room:save. It is the editor's explicit -- "write this level into the project" action; gameplay autosaves must never -- call it. -function Room:saveToSource(name) +function Room:saveToSource(name, savePlayerPositions, playerSourceName) name = name or self.name or "test" - local room = TSerial.pack(self:serialize(), nil, true) + local saveTable = self:serialize(savePlayerPositions ~= false) + if savePlayerPositions == false then + -- A normal editor save keeps the authored spawn positions from the + -- existing source level. During a rename that source is still oldName. + local sourceName = playerSourceName or name + local existing = readFromSource("rooms/" .. sourceName .. ".sav") + local keptPlayer = false + if existing then + local previous = TSerial.unpack(existing) + for _, object in ipairs(previous.objects or {}) do + if object.class == "player" then + table.insert(saveTable.objects, object) + keptPlayer = true + end + end + end + -- A newly named blank level has no prior source spawn to retain. + if not keptPlayer then + for color, player in pairs(self.player) do + local pos = player:getGridPos() + table.insert(saveTable.objects, { class = "player", color = color, x = pos.x, y = pos.y }) + end + end + end + local room = TSerial.pack(saveTable, nil, true) return writeToSource("rooms/" .. name .. ".sav", room) end -function Room:serialize() +function Room:serialize(includePlayers) local objects = {} local function emit(class, color, entity) @@ -716,8 +735,10 @@ function Room:serialize() table.insert(objects, { class = class, color = color, x = pos.x, y = pos.y }) end - for color, player in pairs(self.player) do - emit("player", color, player) + if includePlayers ~= false then + for color, player in pairs(self.player) do + emit("player", color, player) + end end for color, collideableArray in pairs(self.collideables) do @@ -769,6 +790,7 @@ function nextRoom(pos) x = newPos.x, y = newPos.y, activeColors = currentRoom:getActiveColors() } } + if gameWorld then gameWorld:setLastRoom(currentRoom.name) end if currentRoom.name then setEditorRoomName(currentRoom.name) end end diff --git a/rooms/world.sav b/rooms/world.sav index 006e065..05fb9a1 100644 --- a/rooms/world.sav +++ b/rooms/world.sav @@ -1,151 +1,152 @@ { +height=18, +lastRoom="2", rooms={ { - y=11, name="start", - x=9 + x=9, + y=11 }, { - y=10, name="3", - x=10 + x=10, + y=10 }, { - y=10, name="4", - x=11 + x=11, + y=10 }, { - y=10, name="5", - x=12 + x=12, + y=10 }, { - y=9, name="village_1", - x=12 + x=12, + y=9 }, { - y=8, name="village_2", - x=12 + x=12, + y=8 }, { - y=8, name="village_3", - x=13 + x=13, + y=8 }, { - y=8, name="village_4", - x=11 + x=11, + y=8 }, { - y=7, name="yellow_1", - x=11 + x=11, + y=7 }, { - y=6, name="yellow_3", - x=11 + x=11, + y=6 }, { - y=7, name="yellow_2", - x=12 + x=12, + y=7 }, { - y=5, name="yellow_4", - x=11 + x=11, + y=5 }, { - y=4, name="yellow_5", - x=11 + x=11, + y=4 }, { - y=5, name="yellow_6", - x=10 + x=10, + y=5 }, { - y=1, name="default", - x=1 + x=1, + y=1 }, { - y=1, name="old_yellow_3", - x=3 + x=3, + y=1 }, { - y=1, name="test_patt", - x=5 + x=5, + y=1 }, { - y=1, name="test_patt_update", - x=7 + x=7, + y=1 }, { - y=1, name="white_asymmetrical", - x=9 + x=9, + y=1 }, { - y=1, name="white_ok", - x=11 + x=11, + y=1 }, { - y=1, name="white_primary", - x=13 + x=13, + y=1 }, { - y=1, name="yellow_arches", - x=15 + x=15, + y=1 }, { - y=1, name="yellow_asymmetrical", - x=17 + x=17, + y=1 }, { - y=3, name="yellow_cc_moustache", - x=1 + x=1, + y=3 }, { - y=3, name="yellow_color_changing_levvel_1", - x=3 + x=3, + y=3 }, { - y=3, name="yellow_skull", - x=5 + x=5, + y=3 }, { - y=3, name="yellow_smile", - x=7 + x=7, + y=3 }, { - y=10, name="2", - x=9 + x=9, + y=10 }, { - y=9, name="room_11_09", - x=11 + x=11, + y=9 } }, -width=18, -height=18 +width=18 } \ No newline at end of file diff --git a/world.lua b/world.lua index 4499049..e31a1ac 100644 --- a/world.lua +++ b/world.lua @@ -8,6 +8,7 @@ function World:initialize() self.rooms = {} self.roomByName = {} self.previewCache = {} + self.lastRoom = nil self:load() end @@ -26,6 +27,7 @@ function World:load() 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) @@ -38,7 +40,7 @@ function World:save() 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, rooms = rooms }, nil, true)) + 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) @@ -87,6 +89,7 @@ function World:removeRoom(x, y) 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 @@ -99,11 +102,20 @@ function World:renameRoom(oldName, newName) 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 @@ -149,7 +161,12 @@ function World:drawRoomPreview(name, x, y, size) else local props = globalAssetProperties[object.class] if props and props.sprites then - local colors = object.class == "door" and { "red", "green", "blue" } or { object.color } + 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