This commit is contained in:
Your Name 2026-08-07 03:22:28 -04:00
parent f8e3d78332
commit b9b0726506
6 changed files with 140 additions and 81 deletions

View file

@ -48,8 +48,15 @@ end
-- Runtime saves live in LÖVE's write directory. This deliberately uses only -- Runtime saves live in LÖVE's write directory. This deliberately uses only
-- love.filesystem operations: they cannot delete files from the source tree. -- love.filesystem operations: they cannot delete files from the source tree.
function clearRuntimeSaveData() function clearRuntimeSaveData(roomName)
local removed = 0 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 function removeTree(path)
if love.filesystem.isDirectory(path) then if love.filesystem.isDirectory(path) then
for _, child in ipairs(love.filesystem.getDirectoryItems(path)) do for _, child in ipairs(love.filesystem.getDirectoryItems(path)) do

View file

@ -33,6 +33,7 @@ local editorRoomName = nil
local roomNameField = nil local roomNameField = nil
local editorStatus = nil local editorStatus = nil
local clearSaveConfirmationExpires = 0 local clearSaveConfirmationExpires = 0
local clearSaveConfirmationScope = nil
local worldSeedConfirmationExpires = 0 local worldSeedConfirmationExpires = 0
local worldMode = "place" local worldMode = "place"
local worldRoomButtons = {} local worldRoomButtons = {}
@ -129,7 +130,8 @@ local function saveRoomFromEditor()
return return
end 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 .. "'." editorStatus = "Could not save '" .. name .. "'."
return return
end end
@ -146,20 +148,28 @@ local function saveRoomFromEditor()
editorRoomName = name editorRoomName = name
roomNameField:editText(name) roomNameField:editText(name)
activeField = nil activeField = nil
editorStatus = "Saved source level '" .. name .. ".'" editorStatus = "Saved source level '" .. name .. "'" .. (savePlayerPositions and " with player positions." or ".")
end end
local function clearRuntimeSaves() local function clearRuntimeSaves()
local now = love.timer.getTime() 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 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 return
end end
local removed = clearRuntimeSaveData() local removed = clearRuntimeSaveData(clearAll and nil or roomName)
clearSaveConfirmationExpires = 0 clearSaveConfirmationExpires = 0
local roomName = currentRoom.name clearSaveConfirmationScope = nil
if roomName then if roomName then
currentRoom = Room:new{ name = roomName } currentRoom = Room:new{ name = roomName }
setEditorRoomName(currentRoom.name) setEditorRoomName(currentRoom.name)
@ -521,6 +531,7 @@ local function openWorldRoom(cellX, cellY)
currentRoom = Room:new{ name = name } currentRoom = Room:new{ name = name }
editorStatus = "Editing '" .. name .. ".'" editorStatus = "Editing '" .. name .. ".'"
end end
gameWorld:setLastRoom(currentRoom.name)
setEditorRoomName(currentRoom.name) setEditorRoomName(currentRoom.name)
assetsView() assetsView()
end end

View file

@ -33,8 +33,9 @@ function love.load()
inputTimer = Timer.new() inputTimer = Timer.new()
currentRoom = Room: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) setEditorRoomName(currentRoom.name)
response = "" response = ""

View file

@ -36,11 +36,6 @@ function Room:initialize(t)
end end
self:setActiveColor(color, true) self:setActiveColor(color, true)
end 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 end
self.name = t.name self.name = t.name
@ -702,13 +697,37 @@ end
-- This is intentionally separate from Room:save. It is the editor's explicit -- This is intentionally separate from Room:save. It is the editor's explicit
-- "write this level into the project" action; gameplay autosaves must never -- "write this level into the project" action; gameplay autosaves must never
-- call it. -- call it.
function Room:saveToSource(name) function Room:saveToSource(name, savePlayerPositions, playerSourceName)
name = name or self.name or "test" 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) return writeToSource("rooms/" .. name .. ".sav", room)
end end
function Room:serialize() function Room:serialize(includePlayers)
local objects = {} local objects = {}
local function emit(class, color, entity) local function emit(class, color, entity)
@ -716,9 +735,11 @@ function Room:serialize()
table.insert(objects, { class = class, color = color, x = pos.x, y = pos.y }) table.insert(objects, { class = class, color = color, x = pos.x, y = pos.y })
end end
if includePlayers ~= false then
for color, player in pairs(self.player) do for color, player in pairs(self.player) do
emit("player", color, player) emit("player", color, player)
end end
end
for color, collideableArray in pairs(self.collideables) do for color, collideableArray in pairs(self.collideables) do
for _, collideable in pairs(collideableArray) do for _, collideable in pairs(collideableArray) do
@ -769,6 +790,7 @@ function nextRoom(pos)
x = newPos.x, y = newPos.y, activeColors = currentRoom:getActiveColors() 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 if currentRoom.name then setEditorRoomName(currentRoom.name) end
end end

View file

@ -1,151 +1,152 @@
{ {
height=18,
lastRoom="2",
rooms={ rooms={
{ {
y=11,
name="start", name="start",
x=9 x=9,
y=11
}, },
{ {
y=10,
name="3", name="3",
x=10 x=10,
y=10
}, },
{ {
y=10,
name="4", name="4",
x=11 x=11,
y=10
}, },
{ {
y=10,
name="5", name="5",
x=12 x=12,
y=10
}, },
{ {
y=9,
name="village_1", name="village_1",
x=12 x=12,
y=9
}, },
{ {
y=8,
name="village_2", name="village_2",
x=12 x=12,
y=8
}, },
{ {
y=8,
name="village_3", name="village_3",
x=13 x=13,
y=8
}, },
{ {
y=8,
name="village_4", name="village_4",
x=11 x=11,
y=8
}, },
{ {
y=7,
name="yellow_1", name="yellow_1",
x=11 x=11,
y=7
}, },
{ {
y=6,
name="yellow_3", name="yellow_3",
x=11 x=11,
y=6
}, },
{ {
y=7,
name="yellow_2", name="yellow_2",
x=12 x=12,
y=7
}, },
{ {
y=5,
name="yellow_4", name="yellow_4",
x=11 x=11,
y=5
}, },
{ {
y=4,
name="yellow_5", name="yellow_5",
x=11 x=11,
y=4
}, },
{ {
y=5,
name="yellow_6", name="yellow_6",
x=10 x=10,
y=5
}, },
{ {
y=1,
name="default", name="default",
x=1 x=1,
y=1
}, },
{ {
y=1,
name="old_yellow_3", name="old_yellow_3",
x=3 x=3,
y=1
}, },
{ {
y=1,
name="test_patt", name="test_patt",
x=5 x=5,
y=1
}, },
{ {
y=1,
name="test_patt_update", name="test_patt_update",
x=7 x=7,
y=1
}, },
{ {
y=1,
name="white_asymmetrical", name="white_asymmetrical",
x=9 x=9,
y=1
}, },
{ {
y=1,
name="white_ok", name="white_ok",
x=11 x=11,
y=1
}, },
{ {
y=1,
name="white_primary", name="white_primary",
x=13 x=13,
y=1
}, },
{ {
y=1,
name="yellow_arches", name="yellow_arches",
x=15 x=15,
y=1
}, },
{ {
y=1,
name="yellow_asymmetrical", name="yellow_asymmetrical",
x=17 x=17,
y=1
}, },
{ {
y=3,
name="yellow_cc_moustache", name="yellow_cc_moustache",
x=1 x=1,
y=3
}, },
{ {
y=3,
name="yellow_color_changing_levvel_1", name="yellow_color_changing_levvel_1",
x=3 x=3,
y=3
}, },
{ {
y=3,
name="yellow_skull", name="yellow_skull",
x=5 x=5,
y=3
}, },
{ {
y=3,
name="yellow_smile", name="yellow_smile",
x=7 x=7,
y=3
}, },
{ {
y=10,
name="2", name="2",
x=9 x=9,
y=10
}, },
{ {
y=9,
name="room_11_09", name="room_11_09",
x=11 x=11,
y=9
} }
}, },
width=18, width=18
height=18
} }

View file

@ -8,6 +8,7 @@ function World:initialize()
self.rooms = {} self.rooms = {}
self.roomByName = {} self.roomByName = {}
self.previewCache = {} self.previewCache = {}
self.lastRoom = nil
self:load() self:load()
end end
@ -26,6 +27,7 @@ function World:load()
if not raw then return end if not raw then return end
local ok, save = pcall(TSerial.unpack, raw) local ok, save = pcall(TSerial.unpack, raw)
if not ok or type(save) ~= "table" then return end if not ok or type(save) ~= "table" then return end
self.lastRoom = save.lastRoom
for _, room in ipairs(save.rooms or {}) do for _, room in ipairs(save.rooms or {}) do
if room.name and self:isInBounds(room.x, room.y) then if room.name and self:isInBounds(room.x, room.y) then
self:_putRoom(room.name, room.x, room.y) self:_putRoom(room.name, room.x, room.y)
@ -38,7 +40,7 @@ function World:save()
for _, room in ipairs(self.rooms) do for _, room in ipairs(self.rooms) do
table.insert(rooms, { name = room.name, x = room.x, y = room.y }) table.insert(rooms, { name = room.name, x = room.x, y = room.y })
end 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 end
function World:isInBounds(x, y) function World:isInBounds(x, y)
@ -87,6 +89,7 @@ function World:removeRoom(x, y)
if room.x == x and room.y == y then if room.x == x and room.y == y then
self.roomByName[room.name] = nil self.roomByName[room.name] = nil
table.remove(self.rooms, i) table.remove(self.rooms, i)
if self.lastRoom == room.name then self.lastRoom = nil end
return self:save(), room.name return self:save(), room.name
end end
end end
@ -99,11 +102,20 @@ function World:renameRoom(oldName, newName)
self.roomByName[oldName] = nil self.roomByName[oldName] = nil
room.name = newName room.name = newName
self.roomByName[newName] = room self.roomByName[newName] = room
if self.lastRoom == oldName then self.lastRoom = newName end
self:invalidatePreview(oldName) self:invalidatePreview(oldName)
self:invalidatePreview(newName) self:invalidatePreview(newName)
return self:save() return self:save()
end 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) function World:getNeighbor(name, exit)
local room = self:roomLocation(name) local room = self:roomLocation(name)
if not room then return nil end if not room then return nil end
@ -149,7 +161,12 @@ function World:drawRoomPreview(name, x, y, size)
else else
local props = globalAssetProperties[object.class] local props = globalAssetProperties[object.class]
if props and props.sprites then 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 for _, color in ipairs(colors) do
local path = props.sprites[color] local path = props.sprites[color]
if path then if path then