Rebuild level editor sidebar with a fixed virtual-space layout
Replace the fraction-of-window positioning (which overlapped and didn't scale) with a small layout system: the sidebar is authored in one fixed virtual coordinate space and rendered at native panel resolution, so element sizes stay constant relative to the panel at any window size and fonts scale with it. - level_editor/layout.lua: sections, scrolling list (with clip + hit-test by point), scrollbar. Clip rects are scaled to real canvas pixels. - Room view split into non-overlapping divs: search/back header, a scrollable asset list (replaces the old row-paging + forward buttons), a status strip, and a compact bottom control cluster. - Mouse wheel scrolls the list; clicks map from window coords into the virtual space via the same transform used to render. - Sidebar canvas is panel-sized and blitted 1:1 (nearest filter, never averaged) to stay crisp; editorDraw runs under a scale transform. - Center text-only fields (level name) in their rect. Also carried in main.lua (share the file, can't split cleanly here): narrator require path fixed via love.filesystem.setRequirePath; sfxr static uses waveform=NOISE again after the field rename; low-pass + lower volume on the static loop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
03532d29e4
commit
5b7ea9ec20
4 changed files with 336 additions and 144 deletions
|
|
@ -1,11 +1,11 @@
|
|||
require "level_editor/object_attributes"
|
||||
require "level_editor/gui"
|
||||
require "level_editor/layout"
|
||||
|
||||
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")
|
||||
|
|
@ -14,7 +14,6 @@ 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 = {}
|
||||
|
|
@ -25,7 +24,8 @@ 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 assetListButtons = {} -- flat list of the selected folder's assets (scrolls)
|
||||
local listScroll = { scroll = 0 } -- shared vertical scroll for the room list
|
||||
local editingAssetButtons = {}
|
||||
|
||||
local labels = {}
|
||||
|
|
@ -45,6 +45,110 @@ local assetSearchButtons = {}
|
|||
local worldSearchText = ""
|
||||
local worldSearchBox = { x = 10, y = 34, w = 0, h = 28 }
|
||||
|
||||
-- ── Room-view sidebar layout ────────────────────────────────────────────────
|
||||
-- Declared up here (not next to editorDraw) so editorMouseHandler, defined
|
||||
-- earlier in the file, can still capture these as upvalues.
|
||||
|
||||
-- Which flat list the room view is currently showing (search > folder > roots).
|
||||
local function currentRoomList()
|
||||
if assetSearchText ~= "" then return assetSearchButtons
|
||||
elseif not selectedFolder then return folderButtons
|
||||
else return assetListButtons end
|
||||
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
|
||||
-- tracks window resizes.
|
||||
local function roomSections()
|
||||
local pad = 10
|
||||
local w = math.max(0, sb.w - pad * 2)
|
||||
local controlsH = 104
|
||||
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 }
|
||||
local status = { x = pad, y = controls.y - statusH, w = w, h = statusH }
|
||||
local listY = search.y + search.h + 8
|
||||
local list = { x = pad, y = listY, w = w, h = math.max(0, status.y - 8 - listY) }
|
||||
return { search = search, listRect = list, status = status, controls = controls }
|
||||
end
|
||||
|
||||
-- Position the bottom control cluster inside its section: level-name field, a
|
||||
-- row of colour buttons, then a row of action buttons. Runs for both drawing
|
||||
-- and hit-testing so the two never disagree.
|
||||
local function layoutRoomControls(c, s)
|
||||
local vgap = 4 -- vertical space between rows
|
||||
local hgap = 8 -- horizontal space between buttons in a row
|
||||
local y = c.y
|
||||
|
||||
s.labelY = y
|
||||
y = y + 15
|
||||
|
||||
local field = uiButtons.roomName
|
||||
if field then field.x = c.x; field.y = y; field.w = c.w; field.h = 22 end
|
||||
y = y + 22 + vgap
|
||||
|
||||
-- Resize a sprite button to a target height in place, so all control sizing
|
||||
-- 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
|
||||
end
|
||||
return b
|
||||
end
|
||||
|
||||
-- Pack the row centered with a fixed gap, rather than spreading each button
|
||||
-- across a full-width slot (which left big gaps between small icons).
|
||||
local function placeRow(names, target)
|
||||
local btns, total = {}, 0
|
||||
for _, name in ipairs(names) do
|
||||
local b = sizeBtn(name, target)
|
||||
if b then table.insert(btns, b); total = total + b.w end
|
||||
end
|
||||
total = total + hgap * math.max(0, #btns - 1)
|
||||
local x = c.x + (c.w - total) / 2
|
||||
for _, b in ipairs(btns) do
|
||||
b.x = x
|
||||
b.y = y + (target - b.h) / 2
|
||||
x = x + b.w + hgap
|
||||
end
|
||||
y = y + target + vgap
|
||||
end
|
||||
|
||||
placeRow({ "white", "red", "green", "blue" }, 22)
|
||||
placeRow({ "save", "clearSaves", "world" }, 28)
|
||||
end
|
||||
|
||||
-- Build the full room-view layout (sections + a scroll list + a back/search
|
||||
-- header) without drawing. Shared by editorDraw and editorMouseHandler.
|
||||
local function buildRoomLayout()
|
||||
local s = roomSections()
|
||||
|
||||
local showBack = assetSearchText ~= "" or selectedFolder
|
||||
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 }
|
||||
sx = s.back.x + s.back.w + 6
|
||||
sw = s.search.w - s.back.w - 6
|
||||
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)
|
||||
|
||||
layoutRoomControls(s.controls, s)
|
||||
return s
|
||||
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)
|
||||
end
|
||||
-- ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
local function fuzzyMatches(value, query)
|
||||
query = (query or ""):lower():gsub("%s+", "")
|
||||
if query == "" then return true end
|
||||
|
|
@ -66,6 +170,7 @@ end
|
|||
|
||||
local function rebuildAssetSearchButtons()
|
||||
assetSearchButtons = {}
|
||||
listScroll.scroll = 0
|
||||
if assetSearchText == "" then return end
|
||||
local names = {}
|
||||
for asset in pairs(gameAssets) do
|
||||
|
|
@ -103,12 +208,12 @@ local function rebuildWorldRoomButtons()
|
|||
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 })
|
||||
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 = sideBarBox.h - 194, w = sideBarBox.w - 20, h = 34,
|
||||
{ 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 = sideBarBox.h - 152, w = sideBarBox.w - 20, h = 34,
|
||||
{ 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
|
||||
|
|
@ -121,9 +226,9 @@ local function rebuildWorldRoomButtons()
|
|||
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,
|
||||
{ text = "< PAGE " .. worldPage .. "/" .. pages .. " >", x = 10, y = sb.h - 110, w = sb.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
|
||||
if buttonX < sb.w / 2 then worldPage = math.max(1, worldPage - 1) else worldPage = math.min(pages, worldPage + 1) end
|
||||
rebuildWorldRoomButtons()
|
||||
end },
|
||||
}
|
||||
|
|
@ -131,8 +236,8 @@ 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.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")
|
||||
for _, button in ipairs(worldRoomButtons) do
|
||||
|
|
@ -147,6 +252,10 @@ local function drawWorldSidebar()
|
|||
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
|
||||
-- Pin the assets/world toggle to a fixed spot here; the room view owns its
|
||||
-- position otherwise, so give the world view a stable one of its own.
|
||||
uiButtons.world.x = sb.w / 2 - uiButtons.world.w / 2
|
||||
uiButtons.world.y = sb.h - uiButtons.world.h - 12
|
||||
uiButtons.world:draw()
|
||||
end
|
||||
|
||||
|
|
@ -254,7 +363,7 @@ function _initLevelEditor()
|
|||
name = "folder",
|
||||
text = folder,
|
||||
x = 30,
|
||||
y = (sideBarBox.h - 50) * (counter / 15),
|
||||
y = (sb.h - 50) * (counter / 15),
|
||||
sprite = folderSprite,
|
||||
onClic = {
|
||||
left = function(self)
|
||||
|
|
@ -283,9 +392,9 @@ function _initLevelEditor()
|
|||
|
||||
end
|
||||
},
|
||||
x = sideBarBox.w / 4,
|
||||
y = sideBarBox.h * (9 / 10)
|
||||
})
|
||||
x = sb.w / 4,
|
||||
y = sb.h * (9 / 10)
|
||||
}, "save")
|
||||
|
||||
_addUIButton(createUIElement{
|
||||
name = "world",
|
||||
|
|
@ -298,8 +407,8 @@ function _initLevelEditor()
|
|||
|
||||
end
|
||||
},
|
||||
x = sideBarBox.w * (3 / 4),
|
||||
y = sideBarBox.h * (9 / 10)
|
||||
x = sb.w * (3 / 4),
|
||||
y = sb.h * (9 / 10)
|
||||
}, "world")
|
||||
|
||||
_addUIButton(createUIElement{
|
||||
|
|
@ -308,10 +417,10 @@ function _initLevelEditor()
|
|||
onClic = {
|
||||
left = clearRuntimeSaves,
|
||||
},
|
||||
x = sideBarBox.w / 2,
|
||||
y = sideBarBox.h * (9 / 10),
|
||||
scale = drawScale / 3
|
||||
})
|
||||
x = sb.w / 2,
|
||||
y = sb.h * (9 / 10),
|
||||
scale = editorScale / 3
|
||||
}, "clearSaves")
|
||||
|
||||
for _, color in ipairs({"white", "red", "green", "blue"}) do
|
||||
_addUIButton(createUIElement{
|
||||
|
|
@ -327,16 +436,16 @@ function _initLevelEditor()
|
|||
|
||||
end
|
||||
},
|
||||
x = sideBarBox.w * (_ / 5),
|
||||
y = sideBarBox.h * (8 / 10)
|
||||
})
|
||||
x = sb.w * (_ / 5),
|
||||
y = sb.h * (8 / 10)
|
||||
}, color)
|
||||
end
|
||||
|
||||
roomNameField = createUIElement{
|
||||
name = "roomName",
|
||||
text = editorRoomName,
|
||||
x = 25,
|
||||
y = sideBarBox.h * (7 / 10),
|
||||
y = sb.h * (7 / 10),
|
||||
onClic = {
|
||||
left = beginRoomNameEdit,
|
||||
}
|
||||
|
|
@ -356,74 +465,29 @@ 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
|
||||
listScroll.scroll = 0
|
||||
assetListButtons = {}
|
||||
|
||||
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{
|
||||
-- x/y are placeholders; the room-view layout repositions every item
|
||||
-- each frame from the list section (see buildRoomLayout).
|
||||
table.insert(assetListButtons, createUIElement{
|
||||
sprites = sprites,
|
||||
name = "asset",
|
||||
text = asset,
|
||||
x = 30,
|
||||
y = (sideBarBox.h - 225) * (counter / limit) + 20,
|
||||
y = 0,
|
||||
onClic = {
|
||||
left = function()
|
||||
print(asset)
|
||||
selectAsset(asset)
|
||||
end,
|
||||
right = function()
|
||||
editAsset(asset)
|
||||
end
|
||||
left = function() selectAsset(asset) end,
|
||||
right = function() editAsset(asset) end,
|
||||
}
|
||||
}
|
||||
table.insert(assetButtons[row], assetButton)
|
||||
counter = counter + 1
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -444,8 +508,8 @@ function editAsset(asset)
|
|||
local cancelButton = createUIElement{
|
||||
name = "cancel",
|
||||
sprite = cancelSprite,
|
||||
x = sideBarBox.w / 3,
|
||||
y = sideBarBox.h - 75,
|
||||
x = sb.w / 3,
|
||||
y = sb.h - 75,
|
||||
onClic = {
|
||||
left = function(self)
|
||||
assetsView()
|
||||
|
|
@ -457,8 +521,8 @@ function editAsset(asset)
|
|||
local confirmButton = createUIElement{
|
||||
name = "confirm",
|
||||
sprite = confirmSprite,
|
||||
x = sideBarBox.w * 2 / 3,
|
||||
y = sideBarBox.h - 75,
|
||||
x = sb.w * 2 / 3,
|
||||
y = sb.h - 75,
|
||||
onClic = {
|
||||
left = function(self)
|
||||
saveAsset()
|
||||
|
|
@ -478,12 +542,12 @@ function editAsset(asset)
|
|||
local assetButton = createUIElement{
|
||||
name = asset,
|
||||
sprites = assetSprites,
|
||||
x = sideBarBox.w / 2 - 18,
|
||||
x = sb.w / 2 - 18,
|
||||
y = 80,
|
||||
}
|
||||
|
||||
local classFieldLabel = {
|
||||
draw = function() love.graphics.printf("CLASS", 0, 125, sideBarBox.w, "center") end
|
||||
draw = function() love.graphics.printf("CLASS", 0, 125, sb.w, "center") end
|
||||
}
|
||||
|
||||
labels[classFieldLabel] = classFieldLabel
|
||||
|
|
@ -501,7 +565,7 @@ function editAsset(asset)
|
|||
}
|
||||
|
||||
local preloadLabel = {
|
||||
draw = function() love.graphics.printf("preload", 0, 225, sideBarBox.w, "center") end
|
||||
draw = function() love.graphics.printf("preload", 0, 225, sb.w, "center") end
|
||||
}
|
||||
|
||||
labels[preloadLabel] = preloadLabel
|
||||
|
|
@ -540,10 +604,9 @@ function _addUIButton(button, name)
|
|||
end
|
||||
|
||||
function folderView()
|
||||
selectedRow = 1
|
||||
selectedFolder = nil
|
||||
assetButtons = {{}}
|
||||
uiButtons.bacToFolderView = nil
|
||||
assetListButtons = {}
|
||||
listScroll.scroll = 0
|
||||
labels = {}
|
||||
end
|
||||
|
||||
|
|
@ -594,39 +657,45 @@ end
|
|||
|
||||
function editorMouseHandler(x, y, button)
|
||||
if x > width then
|
||||
--we need to adjust for sidebar coords
|
||||
x = x - width
|
||||
-- map from real window coords into the sidebar's fixed virtual space
|
||||
-- (same transform used to scale the sidebar canvas up in love.draw)
|
||||
x = (x - width) * sb.w / sideBarBox.w
|
||||
y = y * sb.h / sideBarBox.h
|
||||
|
||||
if editorView == "room" then
|
||||
if buttonContains({ x = 10, y = 5, w = sideBarBox.w - 20, h = 28 }, x, y) then
|
||||
beginSearch("assetSearch")
|
||||
local s = buildRoomLayout()
|
||||
|
||||
-- back button: drop the search, else leave the folder
|
||||
if s.back and Layout.hit(s.back, x, y) then
|
||||
if assetSearchText ~= "" then
|
||||
assetSearchText = ""
|
||||
inputText = ""
|
||||
activeField = nil
|
||||
rebuildAssetSearchButtons()
|
||||
elseif selectedFolder then
|
||||
folderView()
|
||||
end
|
||||
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)
|
||||
if Layout.hit(s.searchBox, x, y) then
|
||||
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
|
||||
end
|
||||
|
||||
-- control cluster (only the buttons that live there)
|
||||
for _, name in ipairs({ "roomName", "white", "red", "green", "blue", "save", "clearSaves", "world" }) do
|
||||
local b = uiButtons[name]
|
||||
if b and b:clicIsInBox(x, y) then
|
||||
b:onClic(button)
|
||||
return
|
||||
end
|
||||
end
|
||||
elseif editorView == "asset" then
|
||||
|
|
@ -722,25 +791,44 @@ end
|
|||
|
||||
function editorDraw()
|
||||
if editorView == "room" then
|
||||
local s = buildRoomLayout()
|
||||
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()
|
||||
|
||||
-- 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
|
||||
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()
|
||||
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()
|
||||
end
|
||||
end
|
||||
Layout.unclip()
|
||||
Layout.scrollbar(s.list)
|
||||
|
||||
-- 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
|
||||
end
|
||||
|
||||
-- status strip between the list and the controls
|
||||
if editorStatus then
|
||||
Layout.clip(s.status)
|
||||
love.graphics.setColor(gColor.white:set())
|
||||
love.graphics.printf(editorStatus, s.status.x, s.status.y + 4, s.status.w, "center")
|
||||
Layout.unclip()
|
||||
end
|
||||
elseif editorView == "asset" then
|
||||
for _, button in pairs(editingAssetButtons) do
|
||||
button:draw()
|
||||
|
|
@ -752,8 +840,10 @@ function editorDraw()
|
|||
for _, label in pairs(labels) do
|
||||
label:draw()
|
||||
end
|
||||
if editorStatus then
|
||||
-- The room view draws its status inside its own strip (see the room branch);
|
||||
-- the other views keep the simple centered line.
|
||||
if editorStatus and editorView ~= "room" then
|
||||
love.graphics.setColor(gColor.white:set())
|
||||
love.graphics.printf(editorStatus, 10, sideBarBox.h * (6 / 10), sideBarBox.w - 20, "center")
|
||||
love.graphics.printf(editorStatus, 10, sb.h * (6 / 10), sb.w - 20, "center")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ function GUI:initialize(t)
|
|||
|
||||
if t.sprite then
|
||||
print(self.name)
|
||||
self.scale = t.scale or drawScale * 2 / 3
|
||||
self.scale = t.scale or editorScale * 2 / 3
|
||||
self.w = t.sprite:getWidth() * self.scale
|
||||
self.h = t.sprite:getHeight() * self.scale
|
||||
self.sprite = t.sprite
|
||||
|
|
@ -41,22 +41,22 @@ function GUI:initialize(t)
|
|||
h = sprite:getHeight()
|
||||
end
|
||||
if w > h then
|
||||
self.scale = (drawScale * (2 / 3)) / (w / 16)
|
||||
self.scale = (editorScale * (2 / 3)) / (w / 16)
|
||||
elseif w < h then
|
||||
self.scale = (drawScale * (2 / 3)) / (h / 16)
|
||||
self.scale = (editorScale * (2 / 3)) / (h / 16)
|
||||
else
|
||||
self.scale = (drawScale * (2 / 3)) / (w / 16)
|
||||
self.scale = (editorScale * (2 / 3)) / (w / 16)
|
||||
end
|
||||
self.w = sideBarBox.w - (self.x * 2)
|
||||
self.w = sb.w - (self.x * 2)
|
||||
self.h = h * self.scale
|
||||
else
|
||||
-- this is text
|
||||
self.x, self.y = t.x, t.y
|
||||
self.w, self.h = sideBarBox.w - self.x * 2, 50
|
||||
self.w, self.h = sb.w - self.x * 2, 50
|
||||
end
|
||||
|
||||
if self.name == "folder" then
|
||||
self.w = sideBarBox.w - self.x * 2
|
||||
self.w = sb.w - self.x * 2
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -106,9 +106,14 @@ function GUI:draw()
|
|||
if self.text then
|
||||
love.graphics.setColor(gColor.white:set())
|
||||
if not self.sprites and not self.sprite then
|
||||
-- text-only element (e.g. the level-name field): center in its rect
|
||||
love.graphics.rectangle("line", box.x, box.y, box.w, box.h)
|
||||
local th = love.graphics.getFont():getHeight()
|
||||
love.graphics.printf(self.text, box.x, box.y + (box.h - th) / 2, box.w, "center")
|
||||
else
|
||||
-- label sitting to the right of an icon
|
||||
love.graphics.print(self.text, box.x + 50, box.y + 15)
|
||||
end
|
||||
love.graphics.print(self.text, box.x + 50, box.y + 15)
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
|||
63
level_editor/layout.lua
Normal file
63
level_editor/layout.lua
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
-- A dead-simple layout helper for the editor sidebar. Everything works in the
|
||||
-- sidebar's local coordinate space (0,0 = top-left of sideBarCanvas) — the same
|
||||
-- space editorDraw / editorMouseHandler already use — so section rects double as
|
||||
-- love.graphics.setScissor rects and as hit boxes. No dependencies, no state of
|
||||
-- its own beyond the scroll table the caller hands in.
|
||||
|
||||
Layout = {}
|
||||
|
||||
-- point-in-rect
|
||||
function Layout.hit(r, x, y)
|
||||
return x >= r.x and x < r.x + r.w and y >= r.y and y < r.y + r.h
|
||||
end
|
||||
|
||||
-- Clip drawing to a section. setScissor ignores the active graphics transform,
|
||||
-- so multiply the virtual-space rect by editorRenderScale to hit real canvas
|
||||
-- pixels. Clamp to non-negative: a too-narrow/short sidebar (e.g. a square
|
||||
-- window with no room for it) would otherwise hand setScissor a negative size.
|
||||
function Layout.clip(r)
|
||||
local s = editorRenderScale or 1
|
||||
love.graphics.setScissor(r.x * s, r.y * s, math.max(0, r.w * s), math.max(0, r.h * s))
|
||||
end
|
||||
function Layout.unclip() love.graphics.setScissor() end
|
||||
|
||||
-- A vertical scrolling list confined to rect `r`. `state` is any table the
|
||||
-- caller keeps between frames to hold `state.scroll`; it is clamped here.
|
||||
-- Returns helpers to place / cull / hit-test `count` items of height `itemH`.
|
||||
function Layout.list(state, r, itemH, count, gap)
|
||||
gap = gap or 6
|
||||
local step = itemH + gap
|
||||
local content = count * step
|
||||
local maxScroll = math.max(0, content - r.h)
|
||||
state.scroll = math.max(0, math.min(state.scroll or 0, maxScroll))
|
||||
local scroll = state.scroll
|
||||
return {
|
||||
rect = r, itemH = itemH, step = step, count = count, maxScroll = maxScroll, scroll = scroll,
|
||||
-- top-left corner of item i (1-based), in sidebar space
|
||||
pos = function(i) return r.x, r.y + (i - 1) * step - scroll end,
|
||||
-- is item i at all inside the visible window? (cheap cull)
|
||||
visible = function(i)
|
||||
local y = r.y + (i - 1) * step - scroll
|
||||
return y + itemH > r.y and y < r.y + r.h
|
||||
end,
|
||||
-- index at a sidebar-space point, or nil if outside the list / no item
|
||||
at = function(x, y)
|
||||
if not Layout.hit(r, x, y) then return nil end
|
||||
local i = math.floor((y - r.y + scroll) / step) + 1
|
||||
if i >= 1 and i <= count then return i end
|
||||
return nil
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
-- Thin scrollbar thumb on the right edge of a list, drawn only when it scrolls.
|
||||
function Layout.scrollbar(list)
|
||||
if list.maxScroll <= 0 then return end
|
||||
local r = list.rect
|
||||
local thumb = math.max(16, r.h * (r.h / (r.h + list.maxScroll)))
|
||||
local ty = r.y + (r.h - thumb) * (list.scroll / list.maxScroll)
|
||||
local pr, pg, pb, pa = love.graphics.getColor()
|
||||
love.graphics.setColor(255, 255, 255, 90)
|
||||
love.graphics.rectangle("fill", r.x + r.w - 4, ty, 3, thumb)
|
||||
love.graphics.setColor(pr, pg, pb, pa)
|
||||
end
|
||||
40
main.lua
40
main.lua
|
|
@ -17,7 +17,10 @@ require "level_editor/editor"
|
|||
|
||||
-- Ink dialogue: ship the pre-parsed book (LÖVE has no lpeg to parse .ink at
|
||||
-- runtime; the narrator parser degrades to false without it, runtime is fine).
|
||||
package.path = package.path .. ";libs/narrator/?.lua"
|
||||
-- Use LOVE's require path (rooted at the game dir) so this resolves no matter
|
||||
-- what the shell's CWD is; plain package.path is CWD-relative and breaks when
|
||||
-- launched as `love /path/to/game` from elsewhere.
|
||||
love.filesystem.setRequirePath(love.filesystem.getRequirePath() .. ";libs/narrator/?.lua")
|
||||
narrator = require "narrator.narrator"
|
||||
startSignBook = require "stories.start_sign"
|
||||
|
||||
|
|
@ -101,9 +104,16 @@ function love.draw()
|
|||
love.graphics.draw(gameCanvas, 0, 0)
|
||||
love.graphics.setCanvas()
|
||||
|
||||
-- Render the sidebar at native panel resolution: the canvas is panel-sized,
|
||||
-- and editorDraw works in the fixed virtual space, scaled up by a transform.
|
||||
-- Blitting the canvas 1:1 afterwards means no rescaling of the final image
|
||||
-- (crisp, nearest-neighbor throughout — nothing is averaged).
|
||||
love.graphics.setCanvas(sideBarCanvas)
|
||||
love.graphics.clear()
|
||||
love.graphics.push()
|
||||
love.graphics.scale(editorRenderScale, editorRenderScale)
|
||||
editorDraw()
|
||||
love.graphics.pop()
|
||||
love.graphics.setCanvas()
|
||||
|
||||
love.graphics.setBlendMode("alpha")
|
||||
|
|
@ -164,6 +174,13 @@ function love.textinput(t)
|
|||
editorTextHandler(response)
|
||||
end
|
||||
|
||||
function love.wheelmoved(dx, dy)
|
||||
-- Scroll the editor's asset/folder list when the cursor is over the sidebar.
|
||||
if editorView == "room" and love.mouse.getX() > width then
|
||||
editorScrollRoomList(dy)
|
||||
end
|
||||
end
|
||||
|
||||
function love.resize(w, h)
|
||||
_computeDimensions()
|
||||
drawScale = width / gridWidth / 16
|
||||
|
|
@ -199,6 +216,21 @@ function _computeDimensions()
|
|||
end
|
||||
|
||||
sideBarBox = {x = width, y = 0, w = love.graphics.getWidth() - width, h = height}
|
||||
|
||||
-- The editor sidebar is authored in a fixed virtual space and scaled up to
|
||||
-- fill the real panel, so every element keeps the same size relative to the
|
||||
-- panel at any window size (and fonts scale with it). Width is constant;
|
||||
-- height follows the real panel's aspect ratio so the upscale is uniform
|
||||
-- (no stretching). `editorScale` is the constant sprite scale in that space.
|
||||
editorScale = 3
|
||||
local virtualW = 600
|
||||
local aspect = sideBarBox.w > 0 and (sideBarBox.h / sideBarBox.w) or 2
|
||||
sb = {w = virtualW, h = virtualW * aspect}
|
||||
|
||||
-- Factor that maps the virtual layout space onto real panel pixels. The
|
||||
-- sidebar is rasterized at this density (native res), so it stays crisp at
|
||||
-- any window size instead of scaling a fixed low-res canvas.
|
||||
editorRenderScale = sideBarBox.w > 0 and (sideBarBox.w / sb.w) or 1
|
||||
end
|
||||
|
||||
-- (Re)create the render targets at the current window size.
|
||||
|
|
@ -206,6 +238,7 @@ function _initCanvases()
|
|||
gameCanvas = love.graphics.newCanvas()
|
||||
finalCanvas = love.graphics.newCanvas()
|
||||
sideBarCanvas = love.graphics.newCanvas(math.max(1, sideBarBox.w), math.max(1, sideBarBox.h))
|
||||
sideBarCanvas:setFilter("nearest", "nearest") -- never average when sampled
|
||||
end
|
||||
|
||||
function _initGridVars()
|
||||
|
|
@ -222,13 +255,14 @@ end
|
|||
|
||||
function _initSounds()
|
||||
staticWave = sfxr.newSound()
|
||||
staticWave.wavetype = 3
|
||||
staticWave.waveform = sfxr.WAVEFORM.NOISE -- was `wavetype = 3`; field renamed in this sfxr
|
||||
staticWave.frequency.start = .1
|
||||
staticWave.envelope.release = 0
|
||||
staticWave.envelope.sustain = 1
|
||||
staticWave.envelope.decay = 0
|
||||
static = love.audio.newSource((staticWave:generateSoundData()))
|
||||
static:setLooping(true)
|
||||
static:setVolume(.15)
|
||||
static:setVolume(.04)
|
||||
static:setFilter({type = "lowpass", highgain = .25}) -- roll off highs + upper-mids
|
||||
static:play()
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue