From 858775fdc8d321fa7be6b53a17e99704ef4a3095 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 11 Aug 2026 16:24:51 -0400 Subject: [PATCH] Improve world editor palette workflow --- level_editor/editor.lua | 376 +++++++++++++++++++++++++++++++++------- main.lua | 2 + world.lua | 25 +++ 3 files changed, 336 insertions(+), 67 deletions(-) diff --git a/level_editor/editor.lua b/level_editor/editor.lua index fb7ed4e..fac3a00 100644 --- a/level_editor/editor.lua +++ b/level_editor/editor.lua @@ -3,7 +3,7 @@ require "level_editor/gui" require "level_editor/layout" editorView = "room" --can be "off", "room", "world", "asset" -selectedColor = "red" +selectedColor = "white" local bacSprite = love.graphics.newImage("art/_ui/bac.png") local folderSprite = love.graphics.newImage("art/_ui/folder.png") @@ -34,8 +34,6 @@ 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 @@ -44,7 +42,11 @@ local assetSearchText = "" local assetSearchButtons = {} local worldSearchText = "" local worldSearchBox = { x = 10, y = 34, w = 0, h = 28 } +local cloneSourceRoom = nil local roomBrush = nil +local boxPaletteOpen = false +local boxPaletteScroll = { scroll = 0 } +local boxPaletteSections = {} -- Convert a pointer position into a valid room cell. Keeping brush state in -- cell coordinates means a click that jitters within a tile cannot place or @@ -107,6 +109,120 @@ local function currentRoomList() else return assetListButtons end end +local function rebuildBoxPaletteSections() + boxPaletteSections = {} + for _, prefix in ipairs({ "box", "spiky", "sticky", "skewer" }) do + local names = {} + local pattern = prefix == "box" and "^box" or "^" .. prefix .. "_box" + for name in pairs(gameAssets) do + if name:match(pattern) then table.insert(names, name) end + end + table.sort(names) + table.insert(boxPaletteSections, { prefix = prefix, names = names }) + end +end + +local function buildBoxPicker(rect) + local gap, cellSize, headerH = 4, 51, 18 + local function contentMetrics(size) + local columns = math.max(1, math.floor((rect.w + gap) / (size + gap))) + local height = 0 + for _, section in ipairs(boxPaletteSections) do + height = height + headerH + math.ceil(#section.names / columns) * (size + gap) + gap + end + return columns, height + end + + -- Keep the picker to one screen when the sidebar is tall enough, while + -- preserving a usable minimum tile size for unusually short sidebars. + local columns, contentHeight = contentMetrics(cellSize) + while cellSize > 20 and contentHeight > rect.h do + cellSize = cellSize - 1 + columns, contentHeight = contentMetrics(cellSize) + end + local contentY, sections, items = 0, {}, {} + + for _, section in ipairs(boxPaletteSections) do + table.insert(sections, { prefix = section.prefix, y = contentY }) + contentY = contentY + headerH + for index, name in ipairs(section.names) do + local column = (index - 1) % columns + local row = math.floor((index - 1) / columns) + table.insert(items, { + name = name, + x = rect.x + column * (cellSize + gap), + y = contentY + row * (cellSize + gap), + w = cellSize, + h = cellSize, + }) + end + contentY = contentY + math.ceil(#section.names / columns) * (cellSize + gap) + gap + end + + local maxScroll = math.max(0, contentY - rect.h) + boxPaletteScroll.scroll = math.max(0, math.min(boxPaletteScroll.scroll or 0, maxScroll)) + local picker = { rect = rect, sections = sections, items = items, maxScroll = maxScroll, scroll = boxPaletteScroll.scroll } + function picker.at(x, y) + if not Layout.hit(rect, x, y) then return nil end + local contentYAtMouse = y - rect.y + picker.scroll + for _, item in ipairs(items) do + if x >= item.x and x < item.x + item.w and contentYAtMouse >= item.y and contentYAtMouse < item.y + item.h then + return item.name + end + end + end + return picker +end + +local function drawPaletteAsset(name, x, y, size) + local props = gameAssets[name] + if not props or not props.sprites then return end + local maxW, maxH = 1, 1 + for _, path in pairs(props.sprites) do + local sprite = loadSprite(path) + maxW, maxH = math.max(maxW, sprite:getWidth()), math.max(maxH, sprite:getHeight()) + end + local scale = math.min((size - 6) / maxW, (size - 6) / maxH) + local drawX = x + (size - maxW * scale) / 2 + local drawY = y + (size - maxH * scale) / 2 + for color, path in pairs(props.sprites) do + local sprite = loadSprite(path) + love.graphics.setColor(gColor[color]:set()) + if props.goopy and drawGoopySprite then + drawGoopySprite(sprite, drawX, drawY, scale) + elseif props.skewer and drawSkewerSprite then + drawSkewerSprite(sprite, props.cells and props.cells[color], drawX, drawY, scale) + elseif props.spiky and drawSpikySprite then + drawSpikySprite(sprite, props.cells and props.cells[color], drawX, drawY, scale) + else + love.graphics.draw(sprite, drawX, drawY, 0, scale, scale) + end + end +end + +local function drawBoxPicker(picker) + Layout.clip(picker.rect) + for _, section in ipairs(picker.sections) do + love.graphics.setColor(gColor.white:set()) + love.graphics.print(section.prefix, picker.rect.x, picker.rect.y + section.y - picker.scroll) + end + for _, item in ipairs(picker.items) do + local y = picker.rect.y + item.y - picker.scroll + if y + item.h > picker.rect.y and y < picker.rect.y + picker.rect.h then + love.graphics.setColor(1, 1, 1, 0.45) + love.graphics.rectangle("line", item.x, y, item.w, item.h) + drawPaletteAsset(item.name, item.x, y, item.w) + if item.name == selectedAsset then + love.graphics.setColor(1, 0.85, 0.2, 1) + love.graphics.setLineWidth(2) + love.graphics.rectangle("line", item.x + 1, y + 1, item.w - 2, item.h - 2) + end + end + end + Layout.unclip() + Layout.scrollbar(picker) +end + -- Carve the sidebar into fixed, non-overlapping "divs": a search header pinned -- to the top, a control cluster pinned to the bottom, a status strip above it, -- and a flexible list filling whatever is left. Recomputed every frame so it @@ -114,7 +230,7 @@ end local function roomSections() local pad = 10 local w = math.max(0, sb.w - pad * 2) - local controlsH = 104 + local controlsH = 130 local statusH = 44 local search = { x = pad, y = pad, w = w, h = 34 } local controls = { x = pad, y = sb.h - pad - controlsH, w = w, h = controlsH } @@ -143,10 +259,17 @@ local function layoutRoomControls(c, s) -- lives here instead of being scattered across the creation calls. local function sizeBtn(name, target) local b = uiButtons[name] - if b and b.sprite then - b.scale = target / b.sprite:getHeight() - b.w = b.sprite:getWidth() * b.scale - b.h = b.sprite:getHeight() * b.scale + if b and b.sprite then + b.scale = target / b.sprite:getHeight() + b.w = b.sprite:getWidth() * b.scale + b.h = b.sprite:getHeight() * b.scale + elseif b and b.sprites then + local spriteW, spriteH = 1, 1 + for _, sprite in pairs(b.sprites) do + spriteW, spriteH = math.max(spriteW, sprite:getWidth()), math.max(spriteH, sprite:getHeight()) + end + b.scale = math.min(target / spriteW, target / spriteH) + b.w, b.h = spriteW * b.scale, spriteH * b.scale end return b end @@ -170,6 +293,7 @@ local function layoutRoomControls(c, s) end placeRow({ "white", "red", "green", "blue" }, 22) + placeRow({ "quick_player", "quick_static_wall", "quick_switch", "quick_door", "quick_box" }, 22) placeRow({ "save", "clearSaves", "world" }, 28) end @@ -178,7 +302,7 @@ end local function buildRoomLayout() local s = roomSections() - local showBack = assetSearchText ~= "" or selectedFolder + local showBack = assetSearchText ~= "" or selectedFolder or boxPaletteOpen local sx, sw = s.search.x, s.search.w if showBack then s.back = { x = s.search.x, y = s.search.y, w = s.search.h, h = s.search.h } @@ -187,8 +311,12 @@ local function buildRoomLayout() end s.searchBox = { x = sx, y = s.search.y, w = sw, h = s.search.h } - s.items = currentRoomList() - s.list = Layout.list(listScroll, s.listRect, 40, #s.items, 2) + if boxPaletteOpen then + s.boxPicker = buildBoxPicker(s.listRect) + else + s.items = currentRoomList() + s.list = Layout.list(listScroll, s.listRect, 40, #s.items, 2) + end layoutRoomControls(s.controls, s) return s @@ -196,7 +324,8 @@ end -- Mouse wheel scrolls the room list; clamped by Layout.list next frame. function editorScrollRoomList(dy) - listScroll.scroll = math.max(0, (listScroll.scroll or 0) - dy * 40) + local scroll = boxPaletteOpen and boxPaletteScroll or listScroll + scroll.scroll = math.max(0, (scroll.scroll or 0) - dy * 40) end -- ──────────────────────────────────────────────────────────────────────────── @@ -264,21 +393,6 @@ local function rebuildWorldRoomButtons() table.insert(worldRoomButtons, { name = roomName, x = 10, y = 70 + row * 68, w = sb.w - 20, h = 60 }) end worldControls = { - { text = worldMode == "place" and "MODE: PLACE ROOMS" or "MODE: OPEN LEVELS", x = 10, y = sb.h - 194, w = sb.w - 20, h = 34, - onClic = function() worldMode = worldMode == "place" and "jump" or "place"; rebuildWorldRoomButtons() end }, - { text = "RESEED OLD EXITS", x = 10, y = sb.h - 152, w = sb.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 = sb.h - 110, w = sb.w - 20, h = 34, onClic = function(buttonX) if buttonX < sb.w / 2 then worldPage = math.max(1, worldPage - 1) else worldPage = math.min(pages, worldPage + 1) end @@ -289,10 +403,20 @@ 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, sb.w - 16, "center") worldSearchBox.w = sb.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") + if activeField == "cloneName" then + love.graphics.printf("CLONE '" .. cloneSourceRoom .. "' AS:", 8, 6, sb.w - 16, "center") + love.graphics.rectangle("line", worldSearchBox.x, worldSearchBox.y, worldSearchBox.w, worldSearchBox.h) + love.graphics.printf(inputText, worldSearchBox.x + 6, worldSearchBox.y + 7, sb.w - 32, "left") + love.graphics.printf("Press Enter to create and select the clone. Esc cancels.", 8, worldSearchBox.y + 34, sb.w - 16, "center") + else + local instructions = selectedRoom + and "placing '" .. selectedRoom .. "' — click a map cell" + or "click a map cell to open or create; select a room below to place it" + love.graphics.printf("ROOMS — " .. instructions, 8, 6, sb.w - 16, "center") + 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") + end 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) @@ -396,6 +520,7 @@ end function _initLevelEditor() gameAssets = getAllAssets() + rebuildBoxPaletteSections() editorRoomName = "" --create folder buttons @@ -494,6 +619,25 @@ function _initLevelEditor() }, color) end + for _, asset in ipairs({ "player", "static_wall", "switch", "door", "box" }) do + local properties = gameAssets[asset] + if properties then + local sprites = {} + for color, path in pairs(properties.sprites) do sprites[color] = love.graphics.newImage(path) end + _addUIButton(createUIElement{ + name = "quick_" .. asset, + sprites = sprites, + goopy = properties.goopy, + skewer = properties.skewer, + spiky = properties.spiky, + cells = properties.cells, + onClic = { left = function() selectQuickAsset(asset) end }, + x = 0, + y = 0, + }, "quick_" .. asset) + end + end + roomNameField = createUIElement{ name = "roomName", text = editorRoomName, @@ -518,6 +662,7 @@ end function selectFolder(folder) selectedFolder = folder + boxPaletteOpen = false listScroll.scroll = 0 assetListButtons = {} @@ -555,6 +700,12 @@ function selectAsset(asset) print(selectedAsset) end +function selectQuickAsset(asset) + selectAsset(asset) + boxPaletteOpen = asset == "box" + boxPaletteScroll.scroll = 0 +end + function editAsset(asset) inputText = "" editorView = "asset" @@ -681,7 +832,8 @@ function worldView() editorView = "world" activeField = nil selectedFolder = nil - worldMode = "place" + selectedRoom = nil + cloneSourceRoom = nil worldPage = 1 rebuildWorldRoomButtons() end @@ -696,6 +848,36 @@ local function newWorldRoomName(cellX, cellY) return name end +local function cloneWorldRoom(sourceName, cloneName) + local source = readFromSource("rooms/" .. sourceName .. ".sav") + if not source then + editorStatus = "Could not find source level '" .. sourceName .. "'." + return false + end + if readFromSource("rooms/" .. cloneName .. ".sav") then + editorStatus = "A level named '" .. cloneName .. "' already exists." + return false + end + if not writeToSource("rooms/" .. cloneName .. ".sav", source) then + editorStatus = "Could not create clone '" .. cloneName .. "'." + return false + end + + gameWorld:invalidatePreview(cloneName) + selectedRoom = cloneName + rebuildWorldRoomButtons() + editorStatus = "Cloned '" .. sourceName .. "' as '" .. cloneName .. "'. Click a map cell to place it." + return true +end + +local function beginCloneDialog(sourceName) + cloneSourceRoom = sourceName + activeField = "cloneName" + inputText = "" + response = "" + editorStatus = "Enter a name for the clone." +end + local function openWorldRoom(cellX, cellY) local name = gameWorld:roomAt(cellX, cellY) if not name then @@ -735,24 +917,36 @@ function editorMouseHandler(x, y, button) rebuildAssetSearchButtons() elseif selectedFolder then folderView() + elseif boxPaletteOpen then + boxPaletteOpen = false + boxPaletteScroll.scroll = 0 end return end if Layout.hit(s.searchBox, x, y) then + boxPaletteOpen = false beginSearch("assetSearch") return end - -- scrolling list item - local i = s.list.at(x, y) - if i and s.items[i] then - s.items[i]:onClic(button) - return + if s.boxPicker then + local asset = s.boxPicker.at(x, y) + if asset then + if button == 1 then selectAsset(asset) end + return + end + else + -- scrolling list item + local i = s.list.at(x, y) + if i and s.items[i] then + s.items[i]:onClic(button) + return + end end -- control cluster (only the buttons that live there) - for _, name in ipairs({ "roomName", "white", "red", "green", "blue", "save", "clearSaves", "world" }) do + for _, name in ipairs({ "roomName", "white", "red", "green", "blue", "quick_player", "quick_static_wall", "quick_switch", "quick_door", "quick_box", "save", "clearSaves", "world" }) do local b = uiButtons[name] if b and b:clicIsInBox(x, y) then b:onClic(button) @@ -766,18 +960,20 @@ function editorMouseHandler(x, y, button) end end elseif editorView == "world" then + if activeField == "cloneName" then return end if buttonContains(worldSearchBox, x, y) then beginSearch("worldSearch") return end for _, roomButton in ipairs(worldRoomButtons) do if buttonContains(roomButton, x, y) then + if button ~= 1 then return end if selectedRoom == roomButton.name then selectedRoom = nil - editorStatus = "Room brush cleared. Click the map to open or create a level." + editorStatus = "Placement cancelled. Click the map to open or create a level." else selectedRoom = roomButton.name - editorStatus = "Selected '" .. selectedRoom .. "' for placement. Click it again to clear." + editorStatus = "Selected '" .. selectedRoom .. "'. Click a map cell to place it." end return end @@ -798,13 +994,18 @@ function editorMouseHandler(x, y, button) 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) + local name = gameWorld:roomAt(cellX, cellY) + if name then beginCloneDialog(name) end elseif selectedRoom then - gameWorld:placeRoom(selectedRoom, cellX, cellY) - editorStatus = "Placed '" .. selectedRoom .. "' at " .. cellX .. ", " .. cellY .. "." + local placedRoom = selectedRoom + if gameWorld:placeRoom(placedRoom, cellX, cellY) then + selectedRoom = nil + editorStatus = "Placed '" .. placedRoom .. "' at " .. cellX .. ", " .. cellY .. "." + else + editorStatus = "Could not place '" .. placedRoom .. "'." + end + else + openWorldRoom(cellX, cellY) end end end @@ -850,6 +1051,8 @@ function editorTextHandler(text) worldSearchText = text worldPage = 1 rebuildWorldRoomButtons() + elseif editorView == "world" and activeField == "cloneName" then + inputText = text end end @@ -862,10 +1065,32 @@ function editorCompleteResponse() assetSearchText = inputText elseif editorView == "world" and activeField == "worldSearch" then worldSearchText = inputText + elseif editorView == "world" and activeField == "cloneName" then + local cloneName, err = validRoomName(inputText) + if not cloneName then + editorStatus = err + return + end + local sourceName = cloneSourceRoom + if cloneWorldRoom(sourceName, cloneName) then + cloneSourceRoom = nil + activeField = nil + end + return end activeField = nil end +function editorCancelResponse() + if editorView ~= "world" or activeField ~= "cloneName" then return false end + activeField = nil + cloneSourceRoom = nil + inputText = "" + response = "" + editorStatus = "Clone cancelled." + return true +end + function sortAllAssets() setAll() end @@ -882,34 +1107,51 @@ function editorDraw() -- header: optional back button + search box if s.back then love.graphics.rectangle("line", s.back.x, s.back.y, s.back.w, s.back.h) - local sc = (s.back.h - 8) / bacSprite:getHeight() - love.graphics.draw(bacSprite, s.back.x + 4, s.back.y + 4, 0, sc, sc) - end - love.graphics.rectangle("line", s.searchBox.x, s.searchBox.y, s.searchBox.w, s.searchBox.h) - love.graphics.printf("SEARCH: " .. assetSearchText, s.searchBox.x + 6, s.searchBox.y + 9, s.searchBox.w - 12, "left") - - -- scrolling list, clipped to its section so nothing bleeds into others - Layout.clip(s.listRect) - for i, item in ipairs(s.items) do - if s.list.visible(i) then - item.x, item.y = s.list.pos(i) - item.w = s.listRect.w - item:draw() - if item.name == "asset" and item.text == selectedAsset then - love.graphics.setColor(1, 0.85, 0.2, 1) - love.graphics.setLineWidth(3) - love.graphics.rectangle("line", item.x + 1, item.y + 1, item.w - 2, item.h - 2) - end + if boxPaletteOpen then + love.graphics.printf("X", s.back.x, s.back.y + 9, s.back.w, "center") + else + local sc = (s.back.h - 8) / bacSprite:getHeight() + love.graphics.draw(bacSprite, s.back.x + 4, s.back.y + 4, 0, sc, sc) end end - Layout.unclip() - Layout.scrollbar(s.list) + love.graphics.rectangle("line", s.searchBox.x, s.searchBox.y, s.searchBox.w, s.searchBox.h) + love.graphics.printf(boxPaletteOpen and "BOX VARIANTS" or "SEARCH: " .. assetSearchText, s.searchBox.x + 6, s.searchBox.y + 9, s.searchBox.w - 12, "left") + + if s.boxPicker then + drawBoxPicker(s.boxPicker) + else + -- Scrolling list, clipped to its section so nothing bleeds into others. + Layout.clip(s.listRect) + for i, item in ipairs(s.items) do + if s.list.visible(i) then + item.x, item.y = s.list.pos(i) + item.w = s.listRect.w + item:draw() + if item.name == "asset" and item.text == selectedAsset then + love.graphics.setColor(1, 0.85, 0.2, 1) + love.graphics.setLineWidth(3) + love.graphics.rectangle("line", item.x + 1, item.y + 1, item.w - 2, item.h - 2) + end + end + end + Layout.unclip() + Layout.scrollbar(s.list) + end -- control cluster love.graphics.setColor(gColor.white:set()) love.graphics.printf("LEVEL NAME", s.controls.x, s.labelY, s.controls.w, "center") - for _, name in ipairs({ "roomName", "white", "red", "green", "blue", "save", "clearSaves", "world" }) do - if uiButtons[name] then uiButtons[name]:draw() end + for _, name in ipairs({ "roomName", "white", "red", "green", "blue", "quick_player", "quick_static_wall", "quick_switch", "quick_door", "quick_box", "save", "clearSaves", "world" }) do + local button = uiButtons[name] + if button then + button:draw() + local asset = name:match("^quick_(.+)$") + if asset and selectedAsset == asset then + love.graphics.setColor(1, 0.85, 0.2, 1) + love.graphics.setLineWidth(2) + love.graphics.rectangle("line", button.x - 1, button.y - 1, button.w + 2, button.h + 2) + end + end end -- status strip between the list and the controls diff --git a/main.lua b/main.lua index c1dc6f9..fbc5c3c 100644 --- a/main.lua +++ b/main.lua @@ -177,6 +177,8 @@ function love.keypressed(key) if key == "return" then response = "" editorCompleteResponse() + elseif key == "escape" and editorCancelResponse() then + return elseif key == "backspace" then local byteoffset = utf8.offset(response, -1) diff --git a/world.lua b/world.lua index 72cea55..061e859 100644 --- a/world.lua +++ b/world.lua @@ -187,6 +187,30 @@ function World:drawRoomPreview(name, x, y, size) love.graphics.setScissor() end +function World:drawHoveredRoomName() + local mouseX, mouseY = love.mouse.getPosition() + local cellX, cellY = self:getCellFromMousePos(mouseX, mouseY) + local name = self:isInBounds(cellX, cellY) and self:roomAt(cellX, cellY) + if not name then return end + + local font = love.graphics.getFont() + local padding = 6 + local tooltipW = math.min(font:getWidth(name) + padding * 2, width - 8) + local tooltipH = font:getHeight() + padding * 2 + local tooltipX = math.min(mouseX + 14, width - tooltipW - 4) + local tooltipY = math.min(mouseY + 14, height - tooltipH - 4) + tooltipX = math.max(4, tooltipX) + tooltipY = math.max(4, tooltipY) + + -- Keep the label anchored beside the pointer, while keeping it entirely in + -- the playable map rather than allowing it to spill into the editor panel. + love.graphics.setColor(0, 0, 0, 0.9) + love.graphics.rectangle("fill", tooltipX, tooltipY, tooltipW, tooltipH) + love.graphics.setColor(1, 1, 1, 1) + love.graphics.rectangle("line", tooltipX, tooltipY, tooltipW, tooltipH) + love.graphics.printf(name, tooltipX + padding, tooltipY + padding, tooltipW - padding * 2, "left") +end + function World:draw() local cell = width / self.width love.graphics.setColor(12 / 255, 12 / 255, 20 / 255, 1) @@ -200,4 +224,5 @@ function World:draw() love.graphics.rectangle("line", screenX, screenY, cell, cell) end end + self:drawHoveredRoomName() end