require "level_editor/object_attributes" require "level_editor/gui" editorView = "room" --can be "off", "room", "world", "asset" selectedColor = "red" local bacSprite = love.graphics.newImage("art/_ui/bac.png") 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 local selectedRoom = nil local selectedRow = 1 local gameAssets = {} local inputText = "" local assetProperties = {} local hierarchy = {} local activeField = nil --this is for whenwe are doing text input things local uiButtons = {} local folderButtons = {} 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 clearSaveConfirmationScope = nil local worldSeedConfirmationExpires = 0 local worldMode = "place" local worldRoomButtons = {} local worldControls = {} local worldPage = 1 local worldPageSize = 5 local assetSearchText = "" local assetSearchButtons = {} local worldSearchText = "" local worldSearchBox = { x = 10, y = 34, w = 0, h = 28 } local function fuzzyMatches(value, query) query = (query or ""):lower():gsub("%s+", "") if query == "" then return true end local index = 1 for character in value:lower():gmatch(".") do if character == query:sub(index, index) then index = index + 1 if index > #query then return true end end end return false end local function beginSearch(field) activeField = field inputText = field == "assetSearch" and assetSearchText or worldSearchText response = inputText end local function rebuildAssetSearchButtons() assetSearchButtons = {} if assetSearchText == "" then return end local names = {} for asset in pairs(gameAssets) do if fuzzyMatches(asset, assetSearchText) then table.insert(names, asset) end end table.sort(names) for index, asset in ipairs(names) do if index > 8 then break end local sprites = {} for color, location in pairs(gameAssets[asset].sprites) do sprites[color] = love.graphics.newImage(location) end table.insert(assetSearchButtons, createUIElement{ sprites = sprites, name = "asset", text = asset, x = 30, y = 45 + (index - 1) * 62, onClic = { left = function() selectAsset(asset) end } }) end end 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 = {} for _, name in ipairs(gameWorld:listRoomNames()) do if fuzzyMatches(name, worldSearchText) then table.insert(names, name) end end 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 = 70 + 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, 6, sideBarBox.w - 16, "center") worldSearchBox.w = sideBarBox.w - 20 love.graphics.rectangle("line", worldSearchBox.x, worldSearchBox.y, worldSearchBox.w, worldSearchBox.h) love.graphics.printf("SEARCH: " .. worldSearchText, worldSearchBox.x + 6, worldSearchBox.y + 7, worldSearchBox.w - 12, "left") for _, button in ipairs(worldRoomButtons) do love.graphics.setColor(button.name == selectedRoom and 1 or 100 / 255, button.name == selectedRoom and 1 or 100 / 255, button.name == selectedRoom and 1 or 100 / 255, 1) 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(1, 1, 1, 1) 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 local savePlayerPositions = love.keyboard.isDown("lshift", "rshift") if not currentRoom:saveToSource(name, savePlayerPositions, oldName) 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 .. "'" .. (savePlayerPositions and " with player positions." or ".") end local function clearRuntimeSaves() local now = love.timer.getTime() 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 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(clearAll and nil or roomName) clearSaveConfirmationExpires = 0 clearSaveConfirmationScope = nil 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 for asset, attributes in pairs(gameAssets) do if not hierarchy[gameAssets[asset].directory] then hierarchy[gameAssets[asset].directory] = {} end table.insert(hierarchy[gameAssets[asset].directory], asset) end local folders = {} for folder in pairs(hierarchy) do table.insert(folders, folder) end table.sort(folders) for _, folder in ipairs(folders) do local button = createUIElement{ name = "folder", text = folder, x = 30, y = (sideBarBox.h - 50) * (counter / 15), sprite = folderSprite, onClic = { left = function(self) print("my asse") selectFolder(folder) end, right = function() end }, } counter = counter + 1 table.insert(folderButtons, button) end _addUIButton(createUIElement{ name = "save", sprite = love.graphics.newImage("art/_ui/save.png"), onClic = { left = function() if editorView == "room" then saveRoomFromEditor() end end, right = function() end }, x = sideBarBox.w / 4, y = sideBarBox.h * (9 / 10) }) _addUIButton(createUIElement{ name = "world", sprite = love.graphics.newImage("art/_ui/world.png"), onClic = { left = function() if editorView == "world" then assetsView() else worldView() end end, right = function() end }, 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 _addUIButton(createUIElement{ name = color, sprite = love.graphics.newImage("art/_ui/" .. color .. ".png"), onClic = { left = function(self) if editorView == "room" then setSelectedColor(color) end end, right = function() end }, x = sideBarBox.w * (_ / 5), y = sideBarBox.h * (8 / 10) }) 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) selectedColor = color end function selectFolder(folder) selectedFolder = folder local bacButton = _addUIButton(createUIElement{ name = "bacToFolderView", sprite = bacSprite, x = sideBarBox.w / 2, y = 37.5, onClic = { left = function(self) print(self, self.x) self = nil folderView(self) end, right = function(self) return end } }, "bacToFolderView") local row = 1 local counter = 1 local limit = 10 table.sort(hierarchy[selectedFolder]) for _, asset in ipairs(hierarchy[selectedFolder]) do if counter % limit == 0 then counter = 1 local forwardButton = createUIElement{ name = "forward", sprite = forwardSprite, x = sideBarBox.w / 2, y = sideBarBox.h - 250, onClic = { left = function() selectedRow = selectedRow + 1 end, right = function() end } } table.insert(assetButtons[row], forwardButton) row = row + 1 table.insert(assetButtons, {}) end local sprites = {} for color, location in pairs(gameAssets[asset].sprites) do sprites[color] = love.graphics.newImage(location) end local assetButton = createUIElement{ sprites = sprites, name = "asset", text = asset, x = 30, y = (sideBarBox.h - 225) * (counter / limit) + 20, onClic = { left = function() print(asset) selectAsset(asset) end, right = function() editAsset(asset) end } } table.insert(assetButtons[row], assetButton) counter = counter + 1 end end function selectAsset(asset) print("selecting", asset) selectedAsset = asset print(selectedAsset) end function editAsset(asset) inputText = "" editorView = "asset" selectedAsset = asset assetProperties = gameAssets[selectedAsset] local cancelButton = createUIElement{ name = "cancel", sprite = cancelSprite, x = sideBarBox.w / 3, y = sideBarBox.h - 75, onClic = { left = function(self) assetsView() end, right = function(self) return end } } local confirmButton = createUIElement{ name = "confirm", sprite = confirmSprite, x = sideBarBox.w * 2 / 3, y = sideBarBox.h - 75, onClic = { left = function(self) saveAsset() end, right = function(self) return end } } editingAssetButtons[cancelButton] = cancelButton editingAssetButtons[confirmButton] = confirmButton local assetSprites = {} for color, path in pairs(assetProperties.sprites) do assetSprites[color] = love.graphics.newImage(path) end local assetButton = createUIElement{ name = asset, sprites = assetSprites, x = sideBarBox.w / 2 - 18, y = 80, } local classFieldLabel = { draw = function() love.graphics.printf("CLASS", 0, 125, sideBarBox.w, "center") end } labels[classFieldLabel] = classFieldLabel local classButton = createUIElement{ name = "class", text = assetProperties.class or "", x = 25, y = 150, onClic = { left = function() activeField = "class" end } } local preloadLabel = { draw = function() love.graphics.printf("preload", 0, 225, sideBarBox.w, "center") end } labels[preloadLabel] = preloadLabel local preloadButton = createUIElement{ name = "preload", text = tostring(assetProperties.preload), x = 25, y = 250, onClic = { left = function() assetProperties.preload = not assetProperties.preload end } } editingAssetButtons[assetButton] = assetButton editingAssetButtons.class = classButton editingAssetButtons.preload = preloadButton end function saveAsset() gameAssets[selectedAsset] = assetProperties -- the asset's class lives in its .meta next to the art (source of truth, -- read back by getAllAssets); write it there local metaPath = "art/" .. assetProperties.directory .. "/" .. selectedAsset .. ".meta" writeToSource(metaPath, TSerial.pack({ class = assetProperties.class }, nil, true)) assetsView() end function _addUIButton(button, name) local index = name or button uiButtons[index] = button end function folderView() selectedRow = 1 selectedFolder = nil assetButtons = {{}} uiButtons.bacToFolderView = nil labels = {} end function assetsView() editingAssetButtons = {} editorView = "room" labels = {} 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 gameWorld:setLastRoom(currentRoom.name) setEditorRoomName(currentRoom.name) assetsView() end function editorMouseHandler(x, y, button) if x > width then --we need to adjust for sidebar coords x = x - width if editorView == "room" then if buttonContains({ x = 10, y = 5, w = sideBarBox.w - 20, h = 28 }, x, y) then beginSearch("assetSearch") return elseif assetSearchText ~= "" then for _, uiElement in ipairs(assetSearchButtons) do if uiElement:clicIsInBox(x, y) then uiElement:onClic(button) return end end elseif not selectedFolder then for _, uiElement in pairs(folderButtons) do if uiElement:clicIsInBox(x, y) then uiElement:onClic(button) return end end else for _, uiElement in pairs(assetButtons[selectedRow]) do if uiElement:clicIsInBox(x, y) then uiElement:onClic(button) return end end end for _, uiElement in pairs(uiButtons) do if uiElement:clicIsInBox(x, y) then uiElement:onClic(button) end end elseif editorView == "asset" then for _, uiElement in pairs(editingAssetButtons) do if uiElement:clicIsInBox(x, y) then uiElement:onClic(button) end end elseif editorView == "world" then if buttonContains(worldSearchBox, x, y) then beginSearch("worldSearch") return end 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 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) elseif editorView == "room" and activeField == "assetSearch" then inputText = text assetSearchText = text rebuildAssetSearchButtons() elseif editorView == "world" and activeField == "worldSearch" then inputText = text worldSearchText = text worldPage = 1 rebuildWorldRoomButtons() end end function editorCompleteResponse() if editorView == "asset" and activeField then assetProperties[activeField] = inputText elseif editorView == "room" and activeField == "roomName" then editorRoomName = inputText elseif editorView == "room" and activeField == "assetSearch" then assetSearchText = inputText elseif editorView == "world" and activeField == "worldSearch" then worldSearchText = inputText end activeField = nil end function sortAllAssets() setAll() end function createUIElement(t) return GUI:new(t) end function editorDraw() if editorView == "room" then love.graphics.setColor(gColor.white:set()) love.graphics.rectangle("line", 10, 5, sideBarBox.w - 20, 28) love.graphics.printf("ASSET SEARCH: " .. assetSearchText, 16, 12, sideBarBox.w - 32, "left") 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 if assetSearchText ~= "" then for _, asset in ipairs(assetSearchButtons) do asset:draw() end elseif not selectedFolder then for _, folder in pairs(folderButtons) do folder:draw() end else for _, entity in ipairs(assetButtons[selectedRow]) do entity:draw() end end elseif editorView == "asset" then 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