diff --git a/level_editor/editor.lua b/level_editor/editor.lua index a8fc5cf..a6603c5 100644 --- a/level_editor/editor.lua +++ b/level_editor/editor.lua @@ -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 diff --git a/level_editor/gui.lua b/level_editor/gui.lua index d5260fd..55db806 100644 --- a/level_editor/gui.lua +++ b/level_editor/gui.lua @@ -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 diff --git a/level_editor/layout.lua b/level_editor/layout.lua new file mode 100644 index 0000000..ab2b655 --- /dev/null +++ b/level_editor/layout.lua @@ -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 diff --git a/main.lua b/main.lua index 05e7211..cde834c 100644 --- a/main.lua +++ b/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