idk even anymore
This commit is contained in:
parent
7fa9171ba1
commit
febaaaacc7
13 changed files with 2239 additions and 1508 deletions
35
_helpers.lua
35
_helpers.lua
|
|
@ -32,6 +32,41 @@ function writeToSource(relPath, contents)
|
|||
return love.filesystem.write(relPath, contents)
|
||||
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)
|
||||
local base = love.filesystem.getSource()
|
||||
local path = base .. "/" .. relPath
|
||||
local file = io.open(path, "r")
|
||||
if file then
|
||||
file:close()
|
||||
return os.remove(path)
|
||||
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()
|
||||
local removed = 0
|
||||
local function removeTree(path)
|
||||
if love.filesystem.isDirectory(path) 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. Falls back to love.filesystem for a
|
||||
-- packaged .love.
|
||||
|
|
|
|||
BIN
art/_ui/clear_saves.png
Normal file
BIN
art/_ui/clear_saves.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 573 B |
5
art/_ui/clear_saves.svg
Normal file
5
art/_ui/clear_saves.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="#ff6b4a" d="M5 1h6l1 2h3v2H1V3h3zm-1 5h8l-.6 9H4.6z"/>
|
||||
<path fill="#27110f" d="M6 8h1v5H6zm3 0h1v5H9z"/>
|
||||
<path fill="#fff3df" d="M2 12.6 3.4 14 8 9.4 12.6 14l1.4-1.4L9.4 8 14 3.4 12.6 2 8 6.6 3.4 2 2 3.4 6.6 8z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 323 B |
|
|
@ -9,6 +9,7 @@ local forwardSprite = love.graphics.newImage("art/_ui/forward.png")
|
|||
local folderSprite = love.graphics.newImage("art/_ui/folder.png")
|
||||
local cancelSprite = love.graphics.newImage("art/_ui/cancel.png")
|
||||
local confirmSprite = love.graphics.newImage("art/_ui/confirm.png")
|
||||
local clearSavesSprite = love.graphics.newImage("art/_ui/clear_saves.png")
|
||||
|
||||
local selectedFolder = nil
|
||||
local selectedAsset = nil
|
||||
|
|
@ -28,10 +29,150 @@ local assetButtons = { {} } --table for rows so we can do stuff
|
|||
local editingAssetButtons = {}
|
||||
|
||||
local labels = {}
|
||||
local editorRoomName = nil
|
||||
local roomNameField = nil
|
||||
local editorStatus = nil
|
||||
local clearSaveConfirmationExpires = 0
|
||||
local worldSeedConfirmationExpires = 0
|
||||
local worldMode = "place"
|
||||
local worldRoomButtons = {}
|
||||
local worldControls = {}
|
||||
local worldPage = 1
|
||||
local worldPageSize = 5
|
||||
|
||||
local function buttonContains(button, x, y)
|
||||
return x > button.x and x < button.x + button.w and y > button.y and y < button.y + button.h
|
||||
end
|
||||
|
||||
local function rebuildWorldRoomButtons()
|
||||
worldRoomButtons = {}
|
||||
local names = gameWorld:listRoomNames()
|
||||
local pages = math.max(1, math.ceil(#names / worldPageSize))
|
||||
worldPage = constrain(worldPage, 1, pages)
|
||||
local first = (worldPage - 1) * worldPageSize + 1
|
||||
for index = first, math.min(first + worldPageSize - 1, #names) do
|
||||
local roomName = names[index]
|
||||
local row = index - first
|
||||
table.insert(worldRoomButtons, { name = roomName, x = 10, y = 36 + row * 68, w = sideBarBox.w - 20, h = 60 })
|
||||
end
|
||||
worldControls = {
|
||||
{ text = worldMode == "place" and "MODE: PLACE ROOMS" or "MODE: OPEN LEVELS", x = 10, y = sideBarBox.h - 194, w = sideBarBox.w - 20, h = 34,
|
||||
onClic = function() worldMode = worldMode == "place" and "jump" or "place"; rebuildWorldRoomButtons() end },
|
||||
{ text = "RESEED OLD EXITS", x = 10, y = sideBarBox.h - 152, w = sideBarBox.w - 20, h = 34,
|
||||
onClic = function()
|
||||
local now = love.timer.getTime()
|
||||
if now > worldSeedConfirmationExpires then
|
||||
worldSeedConfirmationExpires = now + 3
|
||||
editorStatus = "Click RESEED OLD EXITS again within 3 seconds to replace the current map."
|
||||
return
|
||||
end
|
||||
worldSeedConfirmationExpires = 0
|
||||
local result = seedWorldFromLegacyExits(gameWorld)
|
||||
editorStatus = "Seeded " .. result.placed .. " rooms; " .. result.links .. " legacy links (" .. result.conflicts .. " conflicts kept separate)."
|
||||
rebuildWorldRoomButtons()
|
||||
end },
|
||||
{ text = "< PAGE " .. worldPage .. "/" .. pages .. " >", x = 10, y = sideBarBox.h - 110, w = sideBarBox.w - 20, h = 34,
|
||||
onClic = function(buttonX)
|
||||
if buttonX < sideBarBox.w / 2 then worldPage = math.max(1, worldPage - 1) else worldPage = math.min(pages, worldPage + 1) end
|
||||
rebuildWorldRoomButtons()
|
||||
end },
|
||||
}
|
||||
end
|
||||
|
||||
local function drawWorldSidebar()
|
||||
love.graphics.setColor(gColor.white:set())
|
||||
love.graphics.printf("ROOMS — " .. (worldMode == "place" and "select to place; re-click to clear" or "click a map room to open"), 8, 10, sideBarBox.w - 16, "center")
|
||||
for _, button in ipairs(worldRoomButtons) do
|
||||
love.graphics.setColor(button.name == selectedRoom and 255 or 100, button.name == selectedRoom and 255 or 100, button.name == selectedRoom and 255 or 100, 255)
|
||||
love.graphics.rectangle("line", button.x, button.y, button.w, button.h)
|
||||
gameWorld:drawRoomPreview(button.name, button.x + 4, button.y + 4, button.h - 8)
|
||||
love.graphics.setColor(gColor.white:set())
|
||||
love.graphics.printf(button.name, button.x + button.h + 4, button.y + 21, button.w - button.h - 8, "left")
|
||||
end
|
||||
for _, control in ipairs(worldControls) do
|
||||
love.graphics.setColor(255, 255, 255, 255)
|
||||
love.graphics.rectangle("line", control.x, control.y, control.w, control.h)
|
||||
love.graphics.printf(control.text, control.x, control.y + 9, control.w, "center")
|
||||
end
|
||||
uiButtons.world:draw()
|
||||
end
|
||||
|
||||
local function validRoomName(name)
|
||||
name = (name or ""):match("^%s*(.-)%s*$")
|
||||
if name == "" then return nil, "Enter a level name." end
|
||||
if not name:match("^[%w][%w_-]*$") then
|
||||
return nil, "Use letters, numbers, _ or - for level names."
|
||||
end
|
||||
return name
|
||||
end
|
||||
|
||||
local function beginRoomNameEdit()
|
||||
activeField = "roomName"
|
||||
inputText = ""
|
||||
response = ""
|
||||
roomNameField:editText("")
|
||||
editorStatus = "Type a name, press Enter, then Save."
|
||||
end
|
||||
|
||||
local function saveRoomFromEditor()
|
||||
local requestedName = editorRoomName or currentRoom.name
|
||||
if activeField == "roomName" then requestedName = inputText end
|
||||
local name, err = validRoomName(requestedName)
|
||||
if not name then
|
||||
editorStatus = err
|
||||
return
|
||||
end
|
||||
|
||||
local oldName = currentRoom.name
|
||||
if name ~= oldName and readFromSource("rooms/" .. name .. ".sav") then
|
||||
editorStatus = "A source level named '" .. name .. "' already exists."
|
||||
return
|
||||
end
|
||||
|
||||
if not currentRoom:saveToSource(name) then
|
||||
editorStatus = "Could not save '" .. name .. "'."
|
||||
return
|
||||
end
|
||||
|
||||
-- A changed name is a rename, not an accidental duplicate. Do this only
|
||||
-- after the new source file is safely written.
|
||||
if oldName and oldName ~= name then
|
||||
removeFromSource("rooms/" .. oldName .. ".sav")
|
||||
gameWorld:renameRoom(oldName, name)
|
||||
end
|
||||
gameWorld:invalidatePreview(name)
|
||||
if editorView == "world" then rebuildWorldRoomButtons() end
|
||||
currentRoom.name = name
|
||||
editorRoomName = name
|
||||
roomNameField:editText(name)
|
||||
activeField = nil
|
||||
editorStatus = "Saved source level '" .. name .. ".'"
|
||||
end
|
||||
|
||||
local function clearRuntimeSaves()
|
||||
local now = love.timer.getTime()
|
||||
if now > clearSaveConfirmationExpires then
|
||||
clearSaveConfirmationExpires = now + 3
|
||||
editorStatus = "Click the clear-save icon again within 3 seconds to erase runtime saves."
|
||||
return
|
||||
end
|
||||
|
||||
local removed = clearRuntimeSaveData()
|
||||
clearSaveConfirmationExpires = 0
|
||||
local roomName = currentRoom.name
|
||||
if roomName then
|
||||
currentRoom = Room:new{ name = roomName }
|
||||
setEditorRoomName(currentRoom.name)
|
||||
editorStatus = "Cleared " .. removed .. " runtime save item(s) and reloaded '" .. roomName .. ".'"
|
||||
else
|
||||
editorStatus = "Cleared " .. removed .. " runtime save item(s). No named level to reload."
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function _initLevelEditor()
|
||||
gameAssets = getAllAssets()
|
||||
editorRoomName = ""
|
||||
|
||||
--create folder buttons
|
||||
local counter = 1
|
||||
|
|
@ -70,8 +211,7 @@ function _initLevelEditor()
|
|||
onClic = {
|
||||
left = function()
|
||||
if editorView == "room" then
|
||||
print("saveing!!")
|
||||
currentRoom:save()
|
||||
saveRoomFromEditor()
|
||||
end
|
||||
end,
|
||||
right = function()
|
||||
|
|
@ -87,11 +227,7 @@ function _initLevelEditor()
|
|||
sprite = love.graphics.newImage("art/_ui/world.png"),
|
||||
onClic = {
|
||||
left = function()
|
||||
print(editorView)
|
||||
if editorView == "room" then
|
||||
print("saveing!!")
|
||||
currentRoom:save()
|
||||
end
|
||||
if editorView == "world" then assetsView() else worldView() end
|
||||
end,
|
||||
right = function()
|
||||
|
||||
|
|
@ -99,6 +235,17 @@ function _initLevelEditor()
|
|||
},
|
||||
x = sideBarBox.w * (3 / 4),
|
||||
y = sideBarBox.h * (9 / 10)
|
||||
}, "world")
|
||||
|
||||
_addUIButton(createUIElement{
|
||||
name = "clearSaves",
|
||||
sprite = clearSavesSprite,
|
||||
onClic = {
|
||||
left = clearRuntimeSaves,
|
||||
},
|
||||
x = sideBarBox.w / 2,
|
||||
y = sideBarBox.h * (9 / 10),
|
||||
scale = drawScale / 3
|
||||
})
|
||||
|
||||
for _, color in ipairs({"white", "red", "green", "blue"}) do
|
||||
|
|
@ -120,6 +267,22 @@ function _initLevelEditor()
|
|||
})
|
||||
end
|
||||
|
||||
roomNameField = createUIElement{
|
||||
name = "roomName",
|
||||
text = editorRoomName,
|
||||
x = 25,
|
||||
y = sideBarBox.h * (7 / 10),
|
||||
onClic = {
|
||||
left = beginRoomNameEdit,
|
||||
}
|
||||
}
|
||||
_addUIButton(roomNameField, "roomName")
|
||||
|
||||
end
|
||||
|
||||
function setEditorRoomName(name)
|
||||
editorRoomName = name or ""
|
||||
if roomNameField then roomNameField:editText(editorRoomName) end
|
||||
end
|
||||
|
||||
function setSelectedColor(color)
|
||||
|
|
@ -325,7 +488,41 @@ function assetsView()
|
|||
end
|
||||
|
||||
function worldView()
|
||||
editorView = "world"
|
||||
activeField = nil
|
||||
selectedFolder = nil
|
||||
worldMode = "place"
|
||||
worldPage = 1
|
||||
rebuildWorldRoomButtons()
|
||||
end
|
||||
|
||||
local function newWorldRoomName(cellX, cellY)
|
||||
local base = string.format("room_%02d_%02d", cellX, cellY)
|
||||
local name, suffix = base, 2
|
||||
while gameWorld:roomLocation(name) or readFromSource("rooms/" .. name .. ".sav") do
|
||||
name = base .. "_" .. suffix
|
||||
suffix = suffix + 1
|
||||
end
|
||||
return name
|
||||
end
|
||||
|
||||
local function openWorldRoom(cellX, cellY)
|
||||
local name = gameWorld:roomAt(cellX, cellY)
|
||||
if not name then
|
||||
name = newWorldRoomName(cellX, cellY)
|
||||
currentRoom = Room:new{ name = name }
|
||||
-- Creating from an empty cell is a real level creation, not merely a
|
||||
-- transient map reference, so it immediately joins the room picker.
|
||||
currentRoom:saveToSource(name)
|
||||
gameWorld:placeRoom(name, cellX, cellY)
|
||||
gameWorld:invalidatePreview(name)
|
||||
editorStatus = "Created and opened '" .. name .. ".'"
|
||||
else
|
||||
currentRoom = Room:new{ name = name }
|
||||
editorStatus = "Editing '" .. name .. ".'"
|
||||
end
|
||||
setEditorRoomName(currentRoom.name)
|
||||
assetsView()
|
||||
end
|
||||
|
||||
function editorMouseHandler(x, y, button)
|
||||
|
|
@ -361,11 +558,43 @@ function editorMouseHandler(x, y, button)
|
|||
uiElement:onClic(button)
|
||||
end
|
||||
end
|
||||
elseif editorView == "world" then
|
||||
for _, roomButton in ipairs(worldRoomButtons) do
|
||||
if buttonContains(roomButton, x, y) then
|
||||
if selectedRoom == roomButton.name then
|
||||
selectedRoom = nil
|
||||
editorStatus = "Room brush cleared. Click the map to open or create a level."
|
||||
else
|
||||
selectedRoom = roomButton.name
|
||||
editorStatus = "Selected '" .. selectedRoom .. "' for placement. Click it again to clear."
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
for _, control in ipairs(worldControls) do
|
||||
if buttonContains(control, x, y) then
|
||||
control:onClic(x)
|
||||
return
|
||||
end
|
||||
end
|
||||
if uiButtons.world:clicIsInBox(x, y) then uiButtons.world:onClic(button) end
|
||||
end
|
||||
else
|
||||
-- click landed on the grid (game area): place the selected asset
|
||||
if editorView == "room" and button == 1 and selectedAsset then
|
||||
currentRoom:attemptPlace(selectedAsset, gameAssets[selectedAsset], x, y, selectedColor)
|
||||
elseif editorView == "world" then
|
||||
local cellX, cellY = gameWorld:getCellFromMousePos(x, y)
|
||||
if not gameWorld:isInBounds(cellX, cellY) then return end
|
||||
if button == 2 then
|
||||
local removed, name = gameWorld:removeRoom(cellX, cellY)
|
||||
if removed then editorStatus = "Removed '" .. name .. "' from the world." end
|
||||
elseif worldMode == "jump" or not selectedRoom then
|
||||
openWorldRoom(cellX, cellY)
|
||||
elseif selectedRoom then
|
||||
gameWorld:placeRoom(selectedRoom, cellX, cellY)
|
||||
editorStatus = "Placed '" .. selectedRoom .. "' at " .. cellX .. ", " .. cellY .. "."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -374,15 +603,18 @@ function editorTextHandler(text)
|
|||
if editorView == "asset" and activeField then
|
||||
inputText = text
|
||||
editingAssetButtons[activeField]:editText(text)
|
||||
elseif editorView == "room" and activeField == "roomName" then
|
||||
inputText = text
|
||||
roomNameField:editText(text)
|
||||
end
|
||||
end
|
||||
|
||||
function editorCompleteResponse()
|
||||
if editorView == "asset" then
|
||||
if editorView == "asset" and activeField then
|
||||
assetProperties[activeField] = inputText
|
||||
print(assetProperties[activeField], "BLELERLLLRLRLRR")
|
||||
elseif editorView == "room" and activeField == "roomName" then
|
||||
editorRoomName = inputText
|
||||
end
|
||||
print(activeField, assetProperties[activeField], "ass")
|
||||
activeField = nil
|
||||
end
|
||||
|
||||
|
|
@ -396,6 +628,8 @@ end
|
|||
|
||||
function editorDraw()
|
||||
if editorView == "room" then
|
||||
love.graphics.setColor(gColor.white:set())
|
||||
love.graphics.printf("LEVEL NAME", 0, sideBarBox.h * (7 / 10) - 24, sideBarBox.w, "center")
|
||||
for _, button in pairs(uiButtons) do
|
||||
button:draw()
|
||||
end
|
||||
|
|
@ -412,9 +646,15 @@ function editorDraw()
|
|||
for _, button in pairs(editingAssetButtons) do
|
||||
button:draw()
|
||||
end
|
||||
elseif editorView == "world" then
|
||||
drawWorldSidebar()
|
||||
end
|
||||
|
||||
for _, label in pairs(labels) do
|
||||
label:draw()
|
||||
end
|
||||
if editorStatus then
|
||||
love.graphics.setColor(gColor.white:set())
|
||||
love.graphics.printf(editorStatus, 10, sideBarBox.h * (6 / 10), sideBarBox.w - 20, "center")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ function GUI:initialize(t)
|
|||
|
||||
if t.sprite then
|
||||
print(self.name)
|
||||
self.scale = drawScale * 2 / 3
|
||||
self.scale = t.scale or drawScale * 2 / 3
|
||||
self.w = t.sprite:getWidth() * self.scale
|
||||
self.h = t.sprite:getHeight() * self.scale
|
||||
self.sprite = t.sprite
|
||||
|
|
@ -126,4 +126,4 @@ end
|
|||
|
||||
function GUI:sha()
|
||||
return math.random(-worldSha, worldSha) * .5
|
||||
end
|
||||
end
|
||||
|
|
|
|||
17
main.lua
17
main.lua
|
|
@ -10,6 +10,8 @@ require "_helpers"
|
|||
require "input"
|
||||
require "color"
|
||||
require "room"
|
||||
require "world"
|
||||
require "world_seed"
|
||||
require "shaders"
|
||||
require "level_editor/editor"
|
||||
|
||||
|
|
@ -20,6 +22,7 @@ function love.load()
|
|||
_initGridVars()
|
||||
_initSounds()
|
||||
_initGlobalColors()
|
||||
gameWorld = World:new()
|
||||
_initLevelEditor()
|
||||
|
||||
gameCanvas = love.graphics.newCanvas()
|
||||
|
|
@ -32,6 +35,7 @@ function love.load()
|
|||
currentRoom = Room:new{
|
||||
name = "start" -- TEMP: load a migrated level to eyeball it; revert once the room picker lands
|
||||
}
|
||||
setEditorRoomName(currentRoom.name)
|
||||
|
||||
response = ""
|
||||
end
|
||||
|
|
@ -48,7 +52,11 @@ function love.draw()
|
|||
love.graphics.setCanvas(gameCanvas)
|
||||
love.graphics.clear()
|
||||
love.graphics.setBlendMode("screen")
|
||||
currentRoom:draw()
|
||||
if editorView == "world" then
|
||||
gameWorld:draw()
|
||||
else
|
||||
currentRoom:draw()
|
||||
end
|
||||
|
||||
love.graphics.setCanvas(finalCanvas)
|
||||
love.graphics.clear()
|
||||
|
|
@ -85,9 +93,9 @@ function love.draw()
|
|||
end
|
||||
|
||||
function love.keypressed(key)
|
||||
if Input.isPlayerMove(key) then
|
||||
if editorView ~= "world" and Input.isPlayerMove(key) then
|
||||
currentRoom:movePlayer(key)
|
||||
elseif Input.isPlayerSwitch(key) then
|
||||
elseif editorView ~= "world" and Input.isPlayerSwitch(key) then
|
||||
local c = Input.playerColor(key)
|
||||
currentRoom:switchPlayerColor(c)
|
||||
end
|
||||
|
|
@ -115,7 +123,7 @@ function love.keypressed(key)
|
|||
end
|
||||
|
||||
function love.mousepressed(x, y, button)
|
||||
if button == 2 then
|
||||
if editorView == "room" and button == 2 then
|
||||
currentRoom:attemptDelete(x, y, selectedColor)
|
||||
end
|
||||
editorMouseHandler(x, y, button)
|
||||
|
|
@ -175,4 +183,3 @@ function _initSounds()
|
|||
static:setVolume(.15)
|
||||
static:play()
|
||||
end
|
||||
|
||||
|
|
|
|||
30
room.lua
30
room.lua
|
|
@ -423,6 +423,10 @@ function Room:tick()
|
|||
for _, door in pairs(self.doors) do
|
||||
door:setColors(setDoorColors)
|
||||
end
|
||||
-- Door:setColors can change collision from unmoveable to none. Rebuild once
|
||||
-- more after that state change so an open-looking door is passable on the
|
||||
-- very next input, rather than leaving the previous tick's solid cell here.
|
||||
self:createCollideablesMatrix()
|
||||
self:nextRoomCheck()
|
||||
end
|
||||
|
||||
|
|
@ -653,7 +657,10 @@ function Room:attemptPlace(name, props, mouseX, mouseY, color)
|
|||
end
|
||||
|
||||
function Room:load(name)
|
||||
local room = readFromSource("rooms/" .. name .. ".sav")
|
||||
-- Runtime rooms are game state: let LÖVE resolve them from its save
|
||||
-- directory, where Room:save writes autosaves. Source files remain the
|
||||
-- shipped/editor versions until the editor explicitly saves one.
|
||||
local room = love.filesystem.read("rooms/" .. name .. ".sav")
|
||||
if not room then
|
||||
-- no save yet: start with a blank room to build in the editor
|
||||
print("no save for '" .. name .. "', starting blank")
|
||||
|
|
@ -672,10 +679,19 @@ function Room:save()
|
|||
local name = self.name or "test"
|
||||
local room = TSerial.pack(self:serialize(), nil, true)
|
||||
|
||||
local success = writeToSource("rooms/" .. name .. ".sav", room)
|
||||
local success = love.filesystem.write("rooms/" .. name .. ".sav", room)
|
||||
if not success then error "great job u fool" end
|
||||
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)
|
||||
name = name or self.name or "test"
|
||||
local room = TSerial.pack(self:serialize(), nil, true)
|
||||
return writeToSource("rooms/" .. name .. ".sav", room)
|
||||
end
|
||||
|
||||
function Room:serialize()
|
||||
local objects = {}
|
||||
|
||||
|
|
@ -734,14 +750,18 @@ function nextRoom(pos)
|
|||
newPos.y = gridHeight
|
||||
end
|
||||
|
||||
local currentName = currentRoom.name
|
||||
local nextName = gameWorld and currentName and gameWorld:getNeighbor(currentName, exit)
|
||||
currentRoom:save()
|
||||
|
||||
currentRoom = Room:new{
|
||||
-- name = "test",
|
||||
-- Missing map neighbours intentionally wrap back into the room just exited.
|
||||
-- A placed neighbour gets the same carried player state at its opposite edge.
|
||||
currentRoom = Room:new{
|
||||
name = nextName or currentName,
|
||||
player = {
|
||||
x = newPos.x, y = newPos.y, activeColors = currentRoom:getActiveColors()
|
||||
}
|
||||
}
|
||||
if currentRoom.name then setEditorRoomName(currentRoom.name) end
|
||||
|
||||
end
|
||||
|
||||
|
|
|
|||
1446
rooms/2.sav
1446
rooms/2.sav
File diff suppressed because it is too large
Load diff
1512
rooms/start.sav
1512
rooms/start.sav
File diff suppressed because it is too large
Load diff
146
rooms/world.sav
Normal file
146
rooms/world.sav
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
{
|
||||
width=18,
|
||||
height=18,
|
||||
rooms={
|
||||
{
|
||||
y=11,
|
||||
x=9,
|
||||
name="start"
|
||||
},
|
||||
{
|
||||
y=10,
|
||||
x=10,
|
||||
name="3"
|
||||
},
|
||||
{
|
||||
y=10,
|
||||
x=11,
|
||||
name="4"
|
||||
},
|
||||
{
|
||||
y=10,
|
||||
x=12,
|
||||
name="5"
|
||||
},
|
||||
{
|
||||
y=9,
|
||||
x=12,
|
||||
name="village_1"
|
||||
},
|
||||
{
|
||||
y=8,
|
||||
x=12,
|
||||
name="village_2"
|
||||
},
|
||||
{
|
||||
y=8,
|
||||
x=13,
|
||||
name="village_3"
|
||||
},
|
||||
{
|
||||
y=8,
|
||||
x=11,
|
||||
name="village_4"
|
||||
},
|
||||
{
|
||||
y=7,
|
||||
x=11,
|
||||
name="yellow_1"
|
||||
},
|
||||
{
|
||||
y=6,
|
||||
x=11,
|
||||
name="yellow_3"
|
||||
},
|
||||
{
|
||||
y=7,
|
||||
x=12,
|
||||
name="yellow_2"
|
||||
},
|
||||
{
|
||||
y=5,
|
||||
x=11,
|
||||
name="yellow_4"
|
||||
},
|
||||
{
|
||||
y=4,
|
||||
x=11,
|
||||
name="yellow_5"
|
||||
},
|
||||
{
|
||||
y=5,
|
||||
x=10,
|
||||
name="yellow_6"
|
||||
},
|
||||
{
|
||||
y=1,
|
||||
x=1,
|
||||
name="default"
|
||||
},
|
||||
{
|
||||
y=1,
|
||||
x=3,
|
||||
name="old_yellow_3"
|
||||
},
|
||||
{
|
||||
y=1,
|
||||
x=5,
|
||||
name="test_patt"
|
||||
},
|
||||
{
|
||||
y=1,
|
||||
x=7,
|
||||
name="test_patt_update"
|
||||
},
|
||||
{
|
||||
y=1,
|
||||
x=9,
|
||||
name="white_asymmetrical"
|
||||
},
|
||||
{
|
||||
y=1,
|
||||
x=11,
|
||||
name="white_ok"
|
||||
},
|
||||
{
|
||||
y=1,
|
||||
x=13,
|
||||
name="white_primary"
|
||||
},
|
||||
{
|
||||
y=1,
|
||||
x=15,
|
||||
name="yellow_arches"
|
||||
},
|
||||
{
|
||||
y=1,
|
||||
x=17,
|
||||
name="yellow_asymmetrical"
|
||||
},
|
||||
{
|
||||
y=3,
|
||||
x=1,
|
||||
name="yellow_cc_moustache"
|
||||
},
|
||||
{
|
||||
y=3,
|
||||
x=3,
|
||||
name="yellow_color_changing_levvel_1"
|
||||
},
|
||||
{
|
||||
y=3,
|
||||
x=5,
|
||||
name="yellow_skull"
|
||||
},
|
||||
{
|
||||
y=3,
|
||||
x=7,
|
||||
name="yellow_smile"
|
||||
},
|
||||
{
|
||||
y=10,
|
||||
x=9,
|
||||
name="2"
|
||||
}
|
||||
}
|
||||
}
|
||||
43
tools/migrate_legacy_room.lua
Normal file
43
tools/migrate_legacy_room.lua
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
-- Convert a pre-flat-schema room save into the current { objects = { ... } }
|
||||
-- format. Usage:
|
||||
-- lua tools/migrate_legacy_room.lua legacy.sav migrated.sav
|
||||
|
||||
local inputPath, outputPath = arg[1], arg[2]
|
||||
assert(inputPath and outputPath, "usage: lua tools/migrate_legacy_room.lua legacy.sav migrated.sav")
|
||||
|
||||
local input = assert(io.open(inputPath, "r"))
|
||||
local chunk, err = load("return " .. input:read("*a"))
|
||||
input:close()
|
||||
assert(chunk, err)
|
||||
local legacy = chunk()
|
||||
|
||||
local objects = {}
|
||||
local function append(class, object, includeColor)
|
||||
local converted = {
|
||||
class = class,
|
||||
x = object.x,
|
||||
y = object.y,
|
||||
}
|
||||
if includeColor ~= false then converted.color = object.color end
|
||||
table.insert(objects, converted)
|
||||
end
|
||||
|
||||
for _, color in ipairs({ "red", "green", "blue" }) do
|
||||
for _, object in ipairs(legacy.players[color] or {}) do append("player", object) end
|
||||
-- Doors are shared between color layers in the current schema, so they have
|
||||
-- no color field when serialized.
|
||||
for _, object in ipairs(legacy.doors[color] or {}) do append("door", object, false) end
|
||||
for _, object in ipairs(legacy.static_walls[color] or {}) do append("static_wall", object) end
|
||||
-- The legacy wall sprite was renamed to box when assets became data-driven.
|
||||
for _, object in ipairs(legacy.walls[color] or {}) do append("box", object) end
|
||||
for _, object in ipairs(legacy.switches[color] or {}) do append("switch", object) end
|
||||
end
|
||||
|
||||
for _, sage in pairs(legacy.sages or {}) do
|
||||
table.insert(objects, { class = "npc", x = sage.x, y = sage.y })
|
||||
end
|
||||
|
||||
require "libs/TSerial"
|
||||
local output = assert(io.open(outputPath, "w"))
|
||||
output:write(TSerial.pack({ objects = objects }, nil, true))
|
||||
output:close()
|
||||
179
world.lua
Normal file
179
world.lua
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
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: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
|
||||
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, 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)
|
||||
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
|
||||
self:invalidatePreview(oldName)
|
||||
self:invalidatePreview(newName)
|
||||
return self:save()
|
||||
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(255, 255, 255, 255)
|
||||
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 = object.class == "door" and { "red", "green", "blue" } or { object.color }
|
||||
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, 12, 20, 255)
|
||||
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 255 or 70, name and 255 or 70, name and 255 or 90, 255)
|
||||
love.graphics.rectangle("line", screenX, screenY, cell, cell)
|
||||
end
|
||||
end
|
||||
end
|
||||
70
world_seed.lua
Normal file
70
world_seed.lua
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
-- Make a first 18x18 layout from the exit links preserved in the legacy saves.
|
||||
-- Some old test rooms share the same one-way exit, so a grid cannot represent
|
||||
-- every such link at once. Those rooms are still placed, but isolated, rather
|
||||
-- than silently overwriting a room that already occupies that neighbour.
|
||||
function seedWorldFromLegacyExits(world)
|
||||
local available = {}
|
||||
for _, name in ipairs(world:listRoomNames()) do available[name] = true end
|
||||
local legacy = {}
|
||||
for name in pairs(available) do
|
||||
local raw = readFromSource("rooms/_pre_migration_backup/" .. name .. ".sav")
|
||||
if raw then
|
||||
local ok, save = pcall(TSerial.unpack, raw)
|
||||
if ok and type(save) == "table" then legacy[name] = save.exits or {} end
|
||||
end
|
||||
end
|
||||
|
||||
world.rooms, world.roomByName = {}, {}
|
||||
local root = available.start and "start" or world:listRoomNames()[1]
|
||||
if not root then return { placed = 0, links = 0, conflicts = 0 } end
|
||||
world:_putRoom(root, 9, 11)
|
||||
local queue, queued = { root }, { [root] = true }
|
||||
local directions = {
|
||||
top = { x = 0, y = -1 }, right = { x = 1, y = 0 },
|
||||
bottom = { x = 0, y = 1 }, left = { x = -1, y = 0 },
|
||||
}
|
||||
local order = { "top", "right", "bottom", "left" }
|
||||
local links, conflicts = 0, 0
|
||||
local index = 1
|
||||
while index <= #queue do
|
||||
local name = queue[index]
|
||||
local source = world:roomLocation(name)
|
||||
for _, exit in ipairs(order) do
|
||||
local target = legacy[name] and legacy[name][exit]
|
||||
target = target and tostring(target)
|
||||
local direction = directions[exit]
|
||||
if target and available[target] then
|
||||
local x, y = source.x + direction.x, source.y + direction.y
|
||||
local occupied, known = world:roomAt(x, y), world:roomLocation(target)
|
||||
if world:isInBounds(x, y) and (not occupied or occupied == target) and (not known or (known.x == x and known.y == y)) then
|
||||
if not known then world:_putRoom(target, x, y) end
|
||||
links = links + 1
|
||||
if not queued[target] then table.insert(queue, target); queued[target] = true end
|
||||
else
|
||||
conflicts = conflicts + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
index = index + 1
|
||||
end
|
||||
|
||||
-- Keep every migrated room visible/selectable without creating accidental
|
||||
-- exits between the disconnected legacy variants.
|
||||
for _, name in ipairs(world:listRoomNames()) do
|
||||
if not world:roomLocation(name) then
|
||||
local placed = false
|
||||
for y = 1, world.height do
|
||||
for x = 1, world.width do
|
||||
if not world:roomAt(x, y) then
|
||||
local nearby = false
|
||||
for dy = -1, 1 do for dx = -1, 1 do if world:roomAt(x + dx, y + dy) then nearby = true end end end
|
||||
if not nearby then world:_putRoom(name, x, y); placed = true; break end
|
||||
end
|
||||
end
|
||||
if placed then break end
|
||||
end
|
||||
end
|
||||
end
|
||||
world:save()
|
||||
return { placed = #world.rooms, links = links, conflicts = conflicts }
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue