Unify editor input with room preview transform

This commit is contained in:
Your Name 2026-08-12 02:58:01 -04:00
parent 3c5e6daf06
commit 8fae34b803
5 changed files with 82 additions and 29 deletions

View file

@ -58,21 +58,14 @@ local boxPaletteSections = {}
-- erase that tile more than once. -- erase that tile more than once.
local function roomCellAt(x, y) local function roomCellAt(x, y)
if x < 0 or x >= width or y < 0 or y >= height then return nil end if x < 0 or x >= width or y < 0 or y >= height then return nil end
local cell = currentRoom:getCellFromMousePos(x, y) return currentRoom:getCellFromMousePos(x, y)
cell.x = constrain(cell.x, 1, gridWidth)
cell.y = constrain(cell.y, 1, gridHeight)
return cell
end end
local function paintRoomCell(cell, button) 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 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 elseif button == 2 then
currentRoom:attemptDelete(x, y, selectedColor) currentRoom:attemptDeleteAtCell(cell, selectedColor)
end end
end end

View file

@ -129,7 +129,10 @@ function love.draw()
-- Blitting the canvas 1:1 afterwards means no rescaling of the final image -- Blitting the canvas 1:1 afterwards means no rescaling of the final image
-- (crisp, nearest-neighbor throughout — nothing is averaged). -- (crisp, nearest-neighbor throughout — nothing is averaged).
love.graphics.setCanvas(sideBarCanvas) 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.push()
love.graphics.scale(editorRenderScale, editorRenderScale) love.graphics.scale(editorRenderScale, editorRenderScale)
editorDraw() editorDraw()

View file

@ -841,14 +841,18 @@ function Room:registerNpc(entity)
end end
function Room:getCellFromMousePos(mouseX, mouseY) function Room:getCellFromMousePos(mouseX, mouseY)
local cell = {} if gameWorld and gameWorld.levels and gameWorld.levels[self.name] == self and
cell.x = math.ceil(mouseX * gridWidth / width) not gameWorld.drawingRoomNeighborhood then
cell.y = math.ceil(mouseY * gridHeight / height) return gameWorld:screenCellInRoom(self, mouseX, mouseY)
return cell end
return {
x = math.floor(mouseX * gridWidth / width) + 1,
y = math.floor(mouseY * gridHeight / height) + 1,
}
end end
function Room:attemptDelete(mouseX, mouseY, color) function Room:attemptDeleteAtCell(cell, color)
local cell = self:getCellFromMousePos(mouseX, mouseY) if not cell then return end
-- "white" (or nil) deletes every channel; otherwise just the picked one -- "white" (or nil) deletes every channel; otherwise just the picked one
local colors local colors
@ -915,9 +919,13 @@ function Room:attemptDelete(mouseX, mouseY, color)
self:tick() self:tick()
end 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 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 -- Jorge is one dialogable NPC with three display channels, not three
-- independently placeable color-layer entities. -- independently placeable color-layer entities.
if name == "jorge" then if name == "jorge" then
@ -944,6 +952,10 @@ function Room:attemptPlace(name, props, mouseX, mouseY, color)
self:tick() self:tick()
end 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) function Room:load(name, preferRuntime)
-- Editor/startup loads must use the project source level. Runtime saves are -- Editor/startup loads must use the project source level. Runtime saves are
-- only consulted for an in-progress room-to-room game transition, otherwise -- only consulted for an in-progress room-to-room game transition, otherwise

View file

@ -391,6 +391,21 @@ test("WASD room browsing changes view without moving players", function()
currentRoom, setEditorRoomName = oldCurrentRoom, oldSetEditorRoomName currentRoom, setEditorRoomName = oldCurrentRoom, oldSetEditorRoomName
end) 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() test("one run cannot terminate at two different switches", function()
local second = switch("red", 1, 2) local second = switch("red", 1, 2)
local world, _, roomB = makeWorld(second) local world, _, roomB = makeWorld(second)

View file

@ -908,13 +908,12 @@ function World:drawOverlaps(room, color)
end end
end end
-- Render the active room at a small zoom-out, together with the eight world -- The neighbourhood renderer and editor pointer mapping share this exact
-- neighbours around it. This is display-only: individual rooms still own -- transform. Keeping it in one place prevents a visual zoom adjustment from
-- their data, collision, and authored boundary walls. -- silently shifting placement/deletion by a tile.
function World:drawRoomNeighborhood(room) function World:roomNeighborhoodTransform(room)
if self.drawingRoomNeighborhood then return end
local location = self:roomLocation(room.name) 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 -- Leave enough margin to read several cells of each neighbour, not merely
-- its boundary wall. This keeps a nearby separated player discoverable. -- its boundary wall. This keeps a nearby separated player discoverable.
@ -932,16 +931,47 @@ function World:drawRoomNeighborhood(room)
cameraY = transition.dy * (1 - eased) cameraY = transition.dy * (1 - eased)
if progress >= 1 then self.roomTransition = nil end if progress >= 1 then self.roomTransition = nil end
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 self.drawingRoomNeighborhood = true
for roomY = -1, 1 do for roomY = -1, 1 do
for roomX = -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] local adjacent = name and self.levels[name]
if adjacent then if adjacent then
love.graphics.push() love.graphics.push()
love.graphics.translate(originX + (roomX + cameraX) * width * zoom, love.graphics.translate(transform.originX + (roomX + transform.cameraX) * width * transform.zoom,
originY + (roomY + cameraY) * height * zoom) transform.originY + (roomY + transform.cameraY) * height * transform.zoom)
love.graphics.scale(zoom, zoom) love.graphics.scale(transform.zoom, transform.zoom)
adjacent:draw() adjacent:draw()
love.graphics.pop() love.graphics.pop()
end end