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>
268 lines
8 KiB
Lua
268 lines
8 KiB
Lua
love.graphics.setDefaultFilter("linear", "nearest")
|
|
|
|
local utf8 = require("utf8")
|
|
class = require "libs/middleclass"
|
|
Timer = require "libs/hump/timer"
|
|
sfxr = require "libs/sfxrlua/sfxr"
|
|
require "libs/TSerial"
|
|
require "_debug_helpers"
|
|
require "_helpers"
|
|
require "input"
|
|
require "color"
|
|
require "room"
|
|
require "world"
|
|
require "world_seed"
|
|
require "shaders"
|
|
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).
|
|
-- 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"
|
|
|
|
function love.load()
|
|
globalAssetProperties = {} -- rebuilt from the filesystem (art/ + .meta) by getAllAssets
|
|
|
|
_initDefaults()
|
|
_initGridVars()
|
|
_initSounds()
|
|
_initGlobalColors()
|
|
gameWorld = World:new()
|
|
_initLevelEditor()
|
|
|
|
_initCanvases()
|
|
|
|
inputTimer = Timer.new()
|
|
|
|
-- Resume the game: prefer the runtime autosave so the last saved position is
|
|
-- restored, falling back to the authored source level. Editor room-opens go
|
|
-- through their own (source-only) load path, so this only affects resume.
|
|
currentRoom = Room:new{
|
|
name = gameWorld.lastRoom or "start",
|
|
runtime = true
|
|
}
|
|
gameWorld:setLastRoom(currentRoom.name)
|
|
setEditorRoomName(currentRoom.name)
|
|
|
|
response = ""
|
|
end
|
|
|
|
-- Autosave on close so a position reached by moving within a room (short of a
|
|
-- room-to-room transition, which already autosaves in nextRoom) survives.
|
|
function love.quit()
|
|
if currentRoom then
|
|
currentRoom:save()
|
|
if gameWorld then gameWorld:setLastRoom(currentRoom.name) end
|
|
end
|
|
end
|
|
|
|
function love.update(dt)
|
|
if editorView == "world" then
|
|
Input:stopMoveRepeat()
|
|
else
|
|
local repeatedMove = Input:handler(dt)
|
|
if repeatedMove and not Dialogue.isOpen() then currentRoom:movePlayer(repeatedMove) end
|
|
end
|
|
currentRoom:update(dt)
|
|
|
|
-- if love.keyboard.isDown("w") then worldSha = worldSha + 1 end
|
|
-- if love.keyboard.isDown("s") then worldSha = worldSha - 1 end
|
|
end
|
|
|
|
function love.draw()
|
|
love.graphics.setCanvas(gameCanvas)
|
|
love.graphics.clear()
|
|
love.graphics.setBlendMode("screen")
|
|
if editorView == "world" then
|
|
gameWorld:draw()
|
|
else
|
|
currentRoom:draw()
|
|
end
|
|
|
|
love.graphics.setCanvas(finalCanvas)
|
|
love.graphics.clear()
|
|
|
|
local scaleCoefficient = .93
|
|
local samples = 7
|
|
|
|
love.graphics.push()
|
|
|
|
for i = 1, samples do
|
|
local c = 65 / 255 / (i * 1.75)
|
|
love.graphics.setColor(c, c, c)
|
|
love.graphics.scale(scaleCoefficient, scaleCoefficient)
|
|
love.graphics.translate((width / 2 - (width * scaleCoefficient) / 2) + 2, (width / 2 - (height * scaleCoefficient) / 2) + 2)
|
|
love.graphics.draw(gameCanvas, 0, 0)
|
|
end
|
|
|
|
love.graphics.pop()
|
|
love.graphics.setColor(1, 1, 1)
|
|
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")
|
|
love.graphics.setColor(1, 1, 1, 1)
|
|
love.graphics.draw(finalCanvas, 0, 0)
|
|
love.graphics.draw(sideBarCanvas, sideBarBox.x, sideBarBox.y)
|
|
love.graphics.print("Current FPS: "..tostring(love.timer.getFPS( )), 10, 10)
|
|
|
|
Dialogue.draw()
|
|
end
|
|
|
|
function love.keypressed(key)
|
|
-- Talking to a sign captures input: navigate/choose, don't move the player.
|
|
if Dialogue.isOpen() then
|
|
Dialogue.keypressed(key)
|
|
return
|
|
end
|
|
|
|
if editorView ~= "world" and Input.isPlayerMove(key) then
|
|
Input:beginMove(key)
|
|
currentRoom:movePlayer(key)
|
|
elseif editorView ~= "world" and Input.isPlayerSwitch(key) then
|
|
local c = Input.playerColor(key)
|
|
currentRoom:switchPlayerColor(c)
|
|
end
|
|
|
|
-- if key == "w" then
|
|
-- worldSha = worldSha + 1
|
|
-- end
|
|
-- if key == "s" then
|
|
-- worldSha = worldSha - 1
|
|
-- end
|
|
|
|
if key == "return" then
|
|
response = ""
|
|
editorCompleteResponse()
|
|
elseif key == "backspace" then
|
|
local byteoffset = utf8.offset(response, -1)
|
|
|
|
if byteoffset then
|
|
-- remove the last UTF-8 character.
|
|
-- string.sub operates on bytes rather than UTF-8 characters, so we couldn't do string.sub(response, 1, -2).
|
|
response = string.sub(response, 1, byteoffset - 1)
|
|
editorTextHandler(response)
|
|
end
|
|
end
|
|
end
|
|
|
|
function love.mousepressed(x, y, button)
|
|
if editorView == "room" and button == 2 then
|
|
currentRoom:attemptDelete(x, y, selectedColor)
|
|
end
|
|
editorMouseHandler(x, y, button)
|
|
end
|
|
|
|
function love.textinput(t)
|
|
response = response .. 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
|
|
_initCanvases()
|
|
end
|
|
|
|
function _initDefaults()
|
|
--set filter
|
|
love.graphics.setDefaultFilter("linear", "nearest")
|
|
love.filesystem.createDirectory("rooms")
|
|
|
|
--set global width / height / sidebar variables
|
|
_computeDimensions()
|
|
|
|
--set randomseed
|
|
math.randomseed(os.time())
|
|
|
|
--font
|
|
font = love.graphics.newFont("font/LCD_Solid.ttf", 16)
|
|
love.graphics.setFont(font)
|
|
end
|
|
|
|
-- Derive the square play area (side = smaller window dimension) and the
|
|
-- sidebar that fills the remaining horizontal space. Safe to call any time
|
|
-- the window size changes.
|
|
function _computeDimensions()
|
|
width = love.graphics.getWidth()
|
|
height = love.graphics.getHeight()
|
|
if width > height then
|
|
width = height
|
|
else
|
|
height = width
|
|
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.
|
|
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()
|
|
gridWidth, gridHeight = 11, 11
|
|
drawScale = width / gridWidth / 16
|
|
|
|
worldSha = 1
|
|
end
|
|
|
|
function _initGlobalColors()
|
|
gColor = {}
|
|
gColor.red, gColor.green, gColor.blue, gColor.white = Color:new("red"), Color:new("green"), Color:new("blue"), Color:new("white")
|
|
end
|
|
|
|
function _initSounds()
|
|
staticWave = sfxr.newSound()
|
|
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(.04)
|
|
static:setFilter({type = "lowpass", highgain = .25}) -- roll off highs + upper-mids
|
|
static:play()
|
|
end
|