Improve world editor palette workflow
This commit is contained in:
parent
4cb536d6c8
commit
858775fdc8
3 changed files with 336 additions and 67 deletions
|
|
@ -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 }
|
||||||
|
|
@ -143,10 +259,17 @@ local function layoutRoomControls(c, s)
|
||||||
-- lives here instead of being scattered across the creation calls.
|
-- lives here instead of being scattered across the creation calls.
|
||||||
local function sizeBtn(name, target)
|
local function sizeBtn(name, target)
|
||||||
local b = uiButtons[name]
|
local b = uiButtons[name]
|
||||||
if b and b.sprite then
|
if b and b.sprite then
|
||||||
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 }
|
||||||
|
|
||||||
s.items = currentRoomList()
|
if boxPaletteOpen then
|
||||||
s.list = Layout.list(listScroll, s.listRect, 40, #s.items, 2)
|
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)
|
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
|
||||||
love.graphics.rectangle("line", worldSearchBox.x, worldSearchBox.y, worldSearchBox.w, worldSearchBox.h)
|
if activeField == "cloneName" then
|
||||||
love.graphics.printf("SEARCH: " .. worldSearchText, worldSearchBox.x + 6, worldSearchBox.y + 7, worldSearchBox.w - 12, "left")
|
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
|
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
|
||||||
|
|
||||||
-- scrolling list item
|
if s.boxPicker then
|
||||||
local i = s.list.at(x, y)
|
local asset = s.boxPicker.at(x, y)
|
||||||
if i and s.items[i] then
|
if asset then
|
||||||
s.items[i]:onClic(button)
|
if button == 1 then selectAsset(asset) end
|
||||||
return
|
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
|
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,34 +1107,51 @@ 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)
|
||||||
local sc = (s.back.h - 8) / bacSprite:getHeight()
|
if boxPaletteOpen then
|
||||||
love.graphics.draw(bacSprite, s.back.x + 4, s.back.y + 4, 0, sc, sc)
|
love.graphics.printf("X", s.back.x, s.back.y + 9, s.back.w, "center")
|
||||||
end
|
else
|
||||||
love.graphics.rectangle("line", s.searchBox.x, s.searchBox.y, s.searchBox.w, s.searchBox.h)
|
local sc = (s.back.h - 8) / bacSprite:getHeight()
|
||||||
love.graphics.printf("SEARCH: " .. assetSearchText, s.searchBox.x + 6, s.searchBox.y + 9, s.searchBox.w - 12, "left")
|
love.graphics.draw(bacSprite, s.back.x + 4, s.back.y + 4, 0, sc, sc)
|
||||||
|
|
||||||
-- 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
|
||||||
end
|
end
|
||||||
Layout.unclip()
|
love.graphics.rectangle("line", s.searchBox.x, s.searchBox.y, s.searchBox.w, s.searchBox.h)
|
||||||
Layout.scrollbar(s.list)
|
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
|
-- 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
|
||||||
|
|
|
||||||
2
main.lua
2
main.lua
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
25
world.lua
25
world.lua
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue