From 8fae34b803da51b9b92fa8e811f1b032640c883d Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 12 Aug 2026 02:58:01 -0400 Subject: [PATCH] Unify editor input with room preview transform --- level_editor/editor.lua | 13 +++-------- main.lua | 5 ++++- room.lua | 28 ++++++++++++++++------- tests/test_wire.lua | 15 +++++++++++++ world.lua | 50 ++++++++++++++++++++++++++++++++--------- 5 files changed, 82 insertions(+), 29 deletions(-) diff --git a/level_editor/editor.lua b/level_editor/editor.lua index 300cb8b..b55c092 100644 --- a/level_editor/editor.lua +++ b/level_editor/editor.lua @@ -58,21 +58,14 @@ local boxPaletteSections = {} -- erase that tile more than once. local function roomCellAt(x, y) if x < 0 or x >= width or y < 0 or y >= height then return nil end - local cell = currentRoom:getCellFromMousePos(x, y) - cell.x = constrain(cell.x, 1, gridWidth) - cell.y = constrain(cell.y, 1, gridHeight) - return cell + return currentRoom:getCellFromMousePos(x, y) end local function paintRoomCell(cell, button) - -- Use the centre of the cell so Room's existing pointer-based edit helpers - -- perform the same operation as a normal click. - local x = (cell.x - .5) * width / gridWidth - local y = (cell.y - .5) * height / gridHeight if button == 1 and selectedAsset then - currentRoom:attemptPlace(selectedAsset, gameAssets[selectedAsset], x, y, selectedColor) + currentRoom:attemptPlaceAtCell(selectedAsset, gameAssets[selectedAsset], cell, selectedColor) elseif button == 2 then - currentRoom:attemptDelete(x, y, selectedColor) + currentRoom:attemptDeleteAtCell(cell, selectedColor) end end diff --git a/main.lua b/main.lua index cb122c4..58b7fb1 100644 --- a/main.lua +++ b/main.lua @@ -129,7 +129,10 @@ function love.draw() -- 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() + -- The gameplay view now intentionally extends beyond its old room bounds. + -- Keep the editor panel opaque so that neighbourhood preview never shows + -- through empty areas of the UI. + love.graphics.clear(0, 0, 0, 1) love.graphics.push() love.graphics.scale(editorRenderScale, editorRenderScale) editorDraw() diff --git a/room.lua b/room.lua index 351650c..163ec00 100644 --- a/room.lua +++ b/room.lua @@ -841,14 +841,18 @@ function Room:registerNpc(entity) end function Room:getCellFromMousePos(mouseX, mouseY) - local cell = {} - cell.x = math.ceil(mouseX * gridWidth / width) - cell.y = math.ceil(mouseY * gridHeight / height) - return cell + if gameWorld and gameWorld.levels and gameWorld.levels[self.name] == self and + not gameWorld.drawingRoomNeighborhood then + return gameWorld:screenCellInRoom(self, mouseX, mouseY) + end + return { + x = math.floor(mouseX * gridWidth / width) + 1, + y = math.floor(mouseY * gridHeight / height) + 1, + } end -function Room:attemptDelete(mouseX, mouseY, color) - local cell = self:getCellFromMousePos(mouseX, mouseY) +function Room:attemptDeleteAtCell(cell, color) + if not cell then return end -- "white" (or nil) deletes every channel; otherwise just the picked one local colors @@ -915,9 +919,13 @@ function Room:attemptDelete(mouseX, mouseY, color) self:tick() end -function Room:attemptPlace(name, props, mouseX, mouseY, color) +function Room:attemptDelete(mouseX, mouseY, color) + return self:attemptDeleteAtCell(self:getCellFromMousePos(mouseX, mouseY), color) +end + +function Room:attemptPlaceAtCell(name, props, cell, color) if not props or not props.sprites then return end - local cell = self:getCellFromMousePos(mouseX, mouseY) + if not cell then return end -- Jorge is one dialogable NPC with three display channels, not three -- independently placeable color-layer entities. if name == "jorge" then @@ -944,6 +952,10 @@ function Room:attemptPlace(name, props, mouseX, mouseY, color) self:tick() end +function Room:attemptPlace(name, props, mouseX, mouseY, color) + return self:attemptPlaceAtCell(name, props, self:getCellFromMousePos(mouseX, mouseY), color) +end + function Room:load(name, preferRuntime) -- Editor/startup loads must use the project source level. Runtime saves are -- only consulted for an in-progress room-to-room game transition, otherwise diff --git a/tests/test_wire.lua b/tests/test_wire.lua index 3777ef5..0b785e4 100644 --- a/tests/test_wire.lua +++ b/tests/test_wire.lua @@ -391,6 +391,21 @@ test("WASD room browsing changes view without moving players", function() currentRoom, setEditorRoomName = oldCurrentRoom, oldSetEditorRoomName end) +test("screen-to-cell mapping uses the neighbourhood render transform", function() + local oldWidth, oldHeight, oldGridWidth, oldGridHeight = width, height, gridWidth, gridHeight + width, height, gridWidth, gridHeight = 110, 110, 11, 11 + local room = {name = "a"} + local world = setmetatable({}, {__index = World}) + world.roomNeighborhoodTransform = function() + return {zoom = 0.78, originX = 22, originY = 22, cameraX = 0, cameraY = 0} + end + local cell = world:screenCellInRoom(room, + 22 + width * 0.78 * (4.5 / 11), 22 + height * 0.78 * (7.5 / 11)) + assertTrue(cell.x == 5 and cell.y == 8, "rendered cell did not map back to itself") + assertTrue(world:screenCellInRoom(room, 1, 1) == nil, "preview margin mapped into active room") + width, height, gridWidth, gridHeight = oldWidth, oldHeight, oldGridWidth, oldGridHeight +end) + test("one run cannot terminate at two different switches", function() local second = switch("red", 1, 2) local world, _, roomB = makeWorld(second) diff --git a/world.lua b/world.lua index aaefd31..562d85a 100644 --- a/world.lua +++ b/world.lua @@ -908,13 +908,12 @@ function World:drawOverlaps(room, color) end end --- Render the active room at a small zoom-out, together with the eight world --- neighbours around it. This is display-only: individual rooms still own --- their data, collision, and authored boundary walls. -function World:drawRoomNeighborhood(room) - if self.drawingRoomNeighborhood then return end +-- The neighbourhood renderer and editor pointer mapping share this exact +-- transform. Keeping it in one place prevents a visual zoom adjustment from +-- silently shifting placement/deletion by a tile. +function World:roomNeighborhoodTransform(room) local location = self:roomLocation(room.name) - if not location then return end + if not location then return nil end -- Leave enough margin to read several cells of each neighbour, not merely -- its boundary wall. This keeps a nearby separated player discoverable. @@ -932,16 +931,47 @@ function World:drawRoomNeighborhood(room) cameraY = transition.dy * (1 - eased) if progress >= 1 then self.roomTransition = nil end end + return {location = location, zoom = zoom, originX = originX, originY = originY, + cameraX = cameraX, cameraY = cameraY} +end + +-- Convert a gameplay-screen coordinate to a cell in the centered room. The +-- surrounding rooms are preview-only while editing; browse to one with WASD +-- before changing it. Returns nil for the preview margin or any other room. +function World:screenCellInRoom(room, mouseX, mouseY) + local transform = self:roomNeighborhoodTransform(room) + if not transform then return nil end + local roomOffsetX = (mouseX - transform.originX) / (width * transform.zoom) - transform.cameraX + local roomOffsetY = (mouseY - transform.originY) / (height * transform.zoom) - transform.cameraY + local offsetX, offsetY = math.floor(roomOffsetX), math.floor(roomOffsetY) + if offsetX ~= 0 or offsetY ~= 0 then return nil end + local localX = (roomOffsetX - offsetX) * width + local localY = (roomOffsetY - offsetY) * height + local cell = { + x = math.floor(localX * gridWidth / width) + 1, + y = math.floor(localY * gridHeight / height) + 1, + } + if cell.x < 1 or cell.x > gridWidth or cell.y < 1 or cell.y > gridHeight then return nil end + return cell +end + +-- Render the active room at a small zoom-out, together with the eight world +-- neighbours around it. This is display-only: individual rooms still own +-- their data, collision, and authored boundary walls. +function World:drawRoomNeighborhood(room) + if self.drawingRoomNeighborhood then return end + local transform = self:roomNeighborhoodTransform(room) + if not transform then return end self.drawingRoomNeighborhood = true for roomY = -1, 1 do for roomX = -1, 1 do - local name = self:roomAt(location.x + roomX, location.y + roomY) + local name = self:roomAt(transform.location.x + roomX, transform.location.y + roomY) local adjacent = name and self.levels[name] if adjacent then love.graphics.push() - love.graphics.translate(originX + (roomX + cameraX) * width * zoom, - originY + (roomY + cameraY) * height * zoom) - love.graphics.scale(zoom, zoom) + love.graphics.translate(transform.originX + (roomX + transform.cameraX) * width * transform.zoom, + transform.originY + (roomY + transform.cameraY) * height * transform.zoom) + love.graphics.scale(transform.zoom, transform.zoom) adjacent:draw() love.graphics.pop() end