Improve world editor palette workflow

This commit is contained in:
Your Name 2026-08-11 16:24:51 -04:00
parent 4cb536d6c8
commit 858775fdc8
3 changed files with 336 additions and 67 deletions

View file

@ -3,7 +3,7 @@ require "level_editor/gui"
require "level_editor/layout" require "level_editor/layout"
editorView = "room" --can be "off", "room", "world", "asset" editorView = "room" --can be "off", "room", "world", "asset"
selectedColor = "red" selectedColor = "white"
local bacSprite = love.graphics.newImage("art/_ui/bac.png") local bacSprite = love.graphics.newImage("art/_ui/bac.png")
local folderSprite = love.graphics.newImage("art/_ui/folder.png") local folderSprite = love.graphics.newImage("art/_ui/folder.png")
@ -34,8 +34,6 @@ local roomNameField = nil
local editorStatus = nil local editorStatus = nil
local clearSaveConfirmationExpires = 0 local clearSaveConfirmationExpires = 0
local clearSaveConfirmationScope = nil local clearSaveConfirmationScope = nil
local worldSeedConfirmationExpires = 0
local worldMode = "place"
local worldRoomButtons = {} local worldRoomButtons = {}
local worldControls = {} local worldControls = {}
local worldPage = 1 local worldPage = 1
@ -44,7 +42,11 @@ local assetSearchText = ""
local assetSearchButtons = {} local assetSearchButtons = {}
local worldSearchText = "" local worldSearchText = ""
local worldSearchBox = { x = 10, y = 34, w = 0, h = 28 } local worldSearchBox = { x = 10, y = 34, w = 0, h = 28 }
local cloneSourceRoom = nil
local roomBrush = 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 -- 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 -- cell coordinates means a click that jitters within a tile cannot place or
@ -107,6 +109,120 @@ local function currentRoomList()
else return assetListButtons end else return assetListButtons end
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 -- 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, -- 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 -- and a flexible list filling whatever is left. Recomputed every frame so it
@ -114,7 +230,7 @@ end
local function roomSections() local function roomSections()
local pad = 10 local pad = 10
local w = math.max(0, sb.w - pad * 2) local w = math.max(0, sb.w - pad * 2)
local controlsH = 104 local controlsH = 130
local statusH = 44 local statusH = 44
local search = { x = pad, y = pad, w = w, h = 34 } local search = { x = pad, y = pad, w = w, h = 34 }
local controls = { x = pad, y = sb.h - pad - controlsH, w = w, h = controlsH } local controls = { x = pad, y = sb.h - pad - controlsH, w = w, h = controlsH }
@ -147,6 +263,13 @@ local function layoutRoomControls(c, s)
b.scale = target / b.sprite:getHeight() b.scale = target / b.sprite:getHeight()
b.w = b.sprite:getWidth() * b.scale b.w = b.sprite:getWidth() * b.scale
b.h = b.sprite:getHeight() * 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 end
return b return b
end end
@ -170,6 +293,7 @@ local function layoutRoomControls(c, s)
end end
placeRow({ "white", "red", "green", "blue" }, 22) placeRow({ "white", "red", "green", "blue" }, 22)
placeRow({ "quick_player", "quick_static_wall", "quick_switch", "quick_door", "quick_box" }, 22)
placeRow({ "save", "clearSaves", "world" }, 28) placeRow({ "save", "clearSaves", "world" }, 28)
end end
@ -178,7 +302,7 @@ end
local function buildRoomLayout() local function buildRoomLayout()
local s = roomSections() local s = roomSections()
local showBack = assetSearchText ~= "" or selectedFolder local showBack = assetSearchText ~= "" or selectedFolder or boxPaletteOpen
local sx, sw = s.search.x, s.search.w local sx, sw = s.search.x, s.search.w
if showBack then if showBack then
s.back = { x = s.search.x, y = s.search.y, w = s.search.h, h = s.search.h } 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 end
s.searchBox = { x = sx, y = s.search.y, w = sw, h = s.search.h } s.searchBox = { x = sx, y = s.search.y, w = sw, h = s.search.h }
if boxPaletteOpen then
s.boxPicker = buildBoxPicker(s.listRect)
else
s.items = currentRoomList() s.items = currentRoomList()
s.list = Layout.list(listScroll, s.listRect, 40, #s.items, 2) s.list = Layout.list(listScroll, s.listRect, 40, #s.items, 2)
end
layoutRoomControls(s.controls, s) layoutRoomControls(s.controls, s)
return s return s
@ -196,7 +324,8 @@ end
-- Mouse wheel scrolls the room list; clamped by Layout.list next frame. -- Mouse wheel scrolls the room list; clamped by Layout.list next frame.
function editorScrollRoomList(dy) 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 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 }) table.insert(worldRoomButtons, { name = roomName, x = 10, y = 70 + row * 68, w = sb.w - 20, h = 60 })
end end
worldControls = { 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, { text = "< PAGE " .. worldPage .. "/" .. pages .. " >", x = 10, y = sb.h - 110, w = sb.w - 20, h = 34,
onClic = function(buttonX) onClic = function(buttonX)
if buttonX < sb.w / 2 then worldPage = math.max(1, worldPage - 1) else worldPage = math.min(pages, worldPage + 1) end 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() local function drawWorldSidebar()
love.graphics.setColor(gColor.white:set()) 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 worldSearchBox.w = sb.w - 20
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.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") love.graphics.printf("SEARCH: " .. worldSearchText, worldSearchBox.x + 6, worldSearchBox.y + 7, worldSearchBox.w - 12, "left")
end
for _, button in ipairs(worldRoomButtons) do 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.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) love.graphics.rectangle("line", button.x, button.y, button.w, button.h)
@ -396,6 +520,7 @@ end
function _initLevelEditor() function _initLevelEditor()
gameAssets = getAllAssets() gameAssets = getAllAssets()
rebuildBoxPaletteSections()
editorRoomName = "" editorRoomName = ""
--create folder buttons --create folder buttons
@ -494,6 +619,25 @@ function _initLevelEditor()
}, color) }, color)
end 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{ roomNameField = createUIElement{
name = "roomName", name = "roomName",
text = editorRoomName, text = editorRoomName,
@ -518,6 +662,7 @@ end
function selectFolder(folder) function selectFolder(folder)
selectedFolder = folder selectedFolder = folder
boxPaletteOpen = false
listScroll.scroll = 0 listScroll.scroll = 0
assetListButtons = {} assetListButtons = {}
@ -555,6 +700,12 @@ function selectAsset(asset)
print(selectedAsset) print(selectedAsset)
end end
function selectQuickAsset(asset)
selectAsset(asset)
boxPaletteOpen = asset == "box"
boxPaletteScroll.scroll = 0
end
function editAsset(asset) function editAsset(asset)
inputText = "" inputText = ""
editorView = "asset" editorView = "asset"
@ -681,7 +832,8 @@ function worldView()
editorView = "world" editorView = "world"
activeField = nil activeField = nil
selectedFolder = nil selectedFolder = nil
worldMode = "place" selectedRoom = nil
cloneSourceRoom = nil
worldPage = 1 worldPage = 1
rebuildWorldRoomButtons() rebuildWorldRoomButtons()
end end
@ -696,6 +848,36 @@ local function newWorldRoomName(cellX, cellY)
return name return name
end 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 function openWorldRoom(cellX, cellY)
local name = gameWorld:roomAt(cellX, cellY) local name = gameWorld:roomAt(cellX, cellY)
if not name then if not name then
@ -735,24 +917,36 @@ function editorMouseHandler(x, y, button)
rebuildAssetSearchButtons() rebuildAssetSearchButtons()
elseif selectedFolder then elseif selectedFolder then
folderView() folderView()
elseif boxPaletteOpen then
boxPaletteOpen = false
boxPaletteScroll.scroll = 0
end end
return return
end end
if Layout.hit(s.searchBox, x, y) then if Layout.hit(s.searchBox, x, y) then
boxPaletteOpen = false
beginSearch("assetSearch") beginSearch("assetSearch")
return return
end end
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 -- scrolling list item
local i = s.list.at(x, y) local i = s.list.at(x, y)
if i and s.items[i] then if i and s.items[i] then
s.items[i]:onClic(button) s.items[i]:onClic(button)
return return
end end
end
-- control cluster (only the buttons that live there) -- 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] local b = uiButtons[name]
if b and b:clicIsInBox(x, y) then if b and b:clicIsInBox(x, y) then
b:onClic(button) b:onClic(button)
@ -766,18 +960,20 @@ function editorMouseHandler(x, y, button)
end end
end end
elseif editorView == "world" then elseif editorView == "world" then
if activeField == "cloneName" then return end
if buttonContains(worldSearchBox, x, y) then if buttonContains(worldSearchBox, x, y) then
beginSearch("worldSearch") beginSearch("worldSearch")
return return
end end
for _, roomButton in ipairs(worldRoomButtons) do for _, roomButton in ipairs(worldRoomButtons) do
if buttonContains(roomButton, x, y) then if buttonContains(roomButton, x, y) then
if button ~= 1 then return end
if selectedRoom == roomButton.name then if selectedRoom == roomButton.name then
selectedRoom = nil 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 else
selectedRoom = roomButton.name selectedRoom = roomButton.name
editorStatus = "Selected '" .. selectedRoom .. "' for placement. Click it again to clear." editorStatus = "Selected '" .. selectedRoom .. "'. Click a map cell to place it."
end end
return return
end end
@ -798,13 +994,18 @@ function editorMouseHandler(x, y, button)
local cellX, cellY = gameWorld:getCellFromMousePos(x, y) local cellX, cellY = gameWorld:getCellFromMousePos(x, y)
if not gameWorld:isInBounds(cellX, cellY) then return end if not gameWorld:isInBounds(cellX, cellY) then return end
if button == 2 then if button == 2 then
local removed, name = gameWorld:removeRoom(cellX, cellY) local name = gameWorld:roomAt(cellX, cellY)
if removed then editorStatus = "Removed '" .. name .. "' from the world." end if name then beginCloneDialog(name) end
elseif worldMode == "jump" or not selectedRoom then
openWorldRoom(cellX, cellY)
elseif selectedRoom then elseif selectedRoom then
gameWorld:placeRoom(selectedRoom, cellX, cellY) local placedRoom = selectedRoom
editorStatus = "Placed '" .. selectedRoom .. "' at " .. cellX .. ", " .. cellY .. "." 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 end
end end
@ -850,6 +1051,8 @@ function editorTextHandler(text)
worldSearchText = text worldSearchText = text
worldPage = 1 worldPage = 1
rebuildWorldRoomButtons() rebuildWorldRoomButtons()
elseif editorView == "world" and activeField == "cloneName" then
inputText = text
end end
end end
@ -862,10 +1065,32 @@ function editorCompleteResponse()
assetSearchText = inputText assetSearchText = inputText
elseif editorView == "world" and activeField == "worldSearch" then elseif editorView == "world" and activeField == "worldSearch" then
worldSearchText = inputText 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 end
activeField = nil activeField = nil
end 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() function sortAllAssets()
setAll() setAll()
end end
@ -882,13 +1107,20 @@ function editorDraw()
-- header: optional back button + search box -- header: optional back button + search box
if s.back then if s.back then
love.graphics.rectangle("line", s.back.x, s.back.y, s.back.w, s.back.h) love.graphics.rectangle("line", s.back.x, s.back.y, s.back.w, s.back.h)
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() local sc = (s.back.h - 8) / bacSprite:getHeight()
love.graphics.draw(bacSprite, s.back.x + 4, s.back.y + 4, 0, sc, sc) love.graphics.draw(bacSprite, s.back.x + 4, s.back.y + 4, 0, sc, sc)
end end
end
love.graphics.rectangle("line", s.searchBox.x, s.searchBox.y, s.searchBox.w, s.searchBox.h) 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") love.graphics.printf(boxPaletteOpen and "BOX VARIANTS" or "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 if s.boxPicker then
drawBoxPicker(s.boxPicker)
else
-- Scrolling list, clipped to its section so nothing bleeds into others.
Layout.clip(s.listRect) Layout.clip(s.listRect)
for i, item in ipairs(s.items) do for i, item in ipairs(s.items) do
if s.list.visible(i) then if s.list.visible(i) then
@ -904,12 +1136,22 @@ function editorDraw()
end end
Layout.unclip() Layout.unclip()
Layout.scrollbar(s.list) Layout.scrollbar(s.list)
end
-- control cluster -- control cluster
love.graphics.setColor(gColor.white:set()) love.graphics.setColor(gColor.white:set())
love.graphics.printf("LEVEL NAME", s.controls.x, s.labelY, s.controls.w, "center") 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 for _, name in ipairs({ "roomName", "white", "red", "green", "blue", "quick_player", "quick_static_wall", "quick_switch", "quick_door", "quick_box", "save", "clearSaves", "world" }) do
if uiButtons[name] then uiButtons[name]:draw() end 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 end
-- status strip between the list and the controls -- status strip between the list and the controls

View file

@ -177,6 +177,8 @@ function love.keypressed(key)
if key == "return" then if key == "return" then
response = "" response = ""
editorCompleteResponse() editorCompleteResponse()
elseif key == "escape" and editorCancelResponse() then
return
elseif key == "backspace" then elseif key == "backspace" then
local byteoffset = utf8.offset(response, -1) local byteoffset = utf8.offset(response, -1)

View file

@ -187,6 +187,30 @@ function World:drawRoomPreview(name, x, y, size)
love.graphics.setScissor() love.graphics.setScissor()
end 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() function World:draw()
local cell = width / self.width local cell = width / self.width
love.graphics.setColor(12 / 255, 12 / 255, 20 / 255, 1) 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) love.graphics.rectangle("line", screenX, screenY, cell, cell)
end end
end end
self:drawHoveredRoomName()
end end