diff --git a/dialogable.lua b/dialogable.lua index 5da0ed4..0cad3ba 100644 --- a/dialogable.lua +++ b/dialogable.lua @@ -6,6 +6,10 @@ Dialogable = {} function Dialogable:onBump() - if self.book then Dialogue.open(self.book) end + if self.book then + local wasOpen = Dialogue.isOpen() + Dialogue.open(self.book) + if Sound and not wasOpen then Sound.jabber() end + end return true end diff --git a/door.lua b/door.lua index 186594a..67d03ba 100644 --- a/door.lua +++ b/door.lua @@ -41,6 +41,7 @@ end function Door:unlock() self.locked = false self.collision = "none" + if Sound then Sound.play("doorUnlock") end end function Door:lock() diff --git a/main.lua b/main.lua index 798c539..c8877ba 100644 --- a/main.lua +++ b/main.lua @@ -10,6 +10,7 @@ require "_helpers" require "input" require "profiler" require "color" +require "sound" require "room" require "world" require "world_seed" @@ -26,6 +27,13 @@ narrator = require "narrator.narrator" startSignBook = require "stories.start_sign" jorgeBook = require "stories.jorge" +-- DEV/DEBUG mode. When on, the level editor is available and rooms with no +-- player copy present still render every channel, so whole levels stay visible +-- instead of a black screen. Turn it off for a player-facing build; both the +-- editor and that all-channels fallback then disappear. Override at launch with +-- the DEBUG env var (DEBUG=0 to force off, DEBUG=1 to force on). +DEBUG = os.getenv("DEBUG") ~= "0" + function love.load() globalAssetProperties = {} -- rebuilt from the filesystem (art/ + .meta) by getAllAssets @@ -95,9 +103,9 @@ function love.draw() love.graphics.setCanvas(gameCanvas) love.graphics.clear() love.graphics.setBlendMode("screen") - if editorView == "world" then + if DEBUG and editorView == "world" then gameWorld:draw() - elseif editorView == "wire" then + elseif DEBUG and editorView == "wire" then gameWorld:drawWireEditor(wireEditorCamera()) else currentRoom:draw() @@ -127,17 +135,20 @@ function love.draw() -- 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) - -- 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() - love.graphics.pop() - love.graphics.setCanvas() + -- (crisp, nearest-neighbor throughout — nothing is averaged). The editor is a + -- DEBUG-only surface, so skip the whole panel in a player-facing build. + if DEBUG then + love.graphics.setCanvas(sideBarCanvas) + -- 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() + love.graphics.pop() + love.graphics.setCanvas() + end -- Collect every visible layer before the accessibility pass. Applying the -- shader here also covers the editor sidebar, FPS display, and dialogue, @@ -147,7 +158,7 @@ function love.draw() 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) + if DEBUG then love.graphics.draw(sideBarCanvas, sideBarBox.x, sideBarBox.y) end love.graphics.print("Current FPS: "..tostring(love.timer.getFPS( )), 10, 10) PerfProfiler:draw() Dialogue.draw() @@ -208,6 +219,9 @@ function love.keypressed(key) -- worldSha = worldSha - 1 -- end + -- Editor text-field controls (room-name entry etc.): DEBUG-only. + if not DEBUG then return end + if key == "return" then response = "" editorCompleteResponse() @@ -225,24 +239,31 @@ function love.keypressed(key) end end +-- Every mouse/text callback below only ever drives the editor, so they are +-- inert outside DEBUG: a player-facing build is keyboard-only. function love.mousepressed(x, y, button) + if not DEBUG then return end editorMouseHandler(x, y, button) end function love.mousemoved(x, y) + if not DEBUG then return end editorMouseMoved(x, y) end function love.mousereleased(x, y, button) + if not DEBUG then return end editorMouseReleased(x, y, button) end function love.textinput(t) + if not DEBUG then return end response = response .. t editorTextHandler(response) end function love.wheelmoved(dx, dy) + if not DEBUG then return end -- 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) @@ -358,6 +379,8 @@ function cycleAccessibilityMode() end function _initSounds() + Sound.init() + staticWave = sfxr.newSound() staticWave.waveform = sfxr.WAVEFORM.NOISE -- was `wavetype = 3`; field renamed in this sfxr staticWave.frequency.start = .1 diff --git a/room.lua b/room.lua index 3b984f3..4ee4901 100644 --- a/room.lua +++ b/room.lua @@ -140,6 +140,9 @@ function Room:getRenderChannels() for _, color in ipairs({"red", "green", "blue"}) do if self.player[color] then table.insert(channels, color) end end + -- No player copy lives here, so the view would be black. In debug/dev mode + -- render every channel instead, letting whole (player-less) levels be seen. + if #channels == 0 and DEBUG then return {"red", "green", "blue"} end return channels end @@ -456,6 +459,7 @@ function Room:movePlayerLocal(key) end if moved then + if Sound then Sound.play("playerMove") end if useWorld then gameWorld:playerJoinCheck(self, previousPositions, newPositions) else self:playerJoinCheck(previousPositions, newPositions) end self:tick() @@ -1119,6 +1123,7 @@ function nextRoom(pos) local currentName = currentRoom.name local nextName = gameWorld and currentName and gameWorld:getNeighbor(currentName, exit) + if Sound and nextName and nextName ~= currentName then Sound.play("levelChange") end currentRoom:save() -- Missing map neighbours intentionally wrap back into the room just exited. diff --git a/sound.lua b/sound.lua new file mode 100644 index 0000000..e16bec1 --- /dev/null +++ b/sound.lua @@ -0,0 +1,47 @@ +-- Central sound-effect registry. One preloaded Source per named effect; the game +-- calls Sound.play("name") to (re)trigger it. Ambient static + music live in +-- main.lua's _initSounds; this covers the one-shot gameplay/UI feedback. +-- +-- Top level does NO audio calls on purpose: requiring this under the test love +-- stub (which never runs Sound.init) must be side-effect free. Until Sound.init +-- runs, Sound.sfx is empty and Sound.play is a harmless no-op. +Sound = {} + +Sound.sfx = {} -- name -> Source +Sound.jabbers = {} -- pool of npc chatter Sources, picked at random + +local function load(path) + return love.audio.newSource(path, "static") +end + +function Sound.init() + Sound.sfx = { + playerMove = load("sounds/player_move.ogg"), + switchFail = load("sounds/switch_fail.wav"), + switchSuccess = load("sounds/switch_success.wav"), + doorUnlock = load("sounds/door_unlock.ogg"), + levelChange = load("sounds/level_change.ogg"), + uiButton = load("sounds/ui/button.wav"), + } + Sound.jabbers = { + load("sounds/jabber_1.ogg"), + load("sounds/jabber_2.ogg"), + } +end + +-- Restart-and-play a named effect. A static Source only plays one instance at a +-- time, so stopping first makes rapid retriggers (e.g. mashing move) audible. +function Sound.play(name) + local src = Sound.sfx[name] + if not src then return end + src:stop() + src:play() +end + +-- Pick a random npc jabber clip and play it. Used when a Dialogable starts talking. +function Sound.jabber() + if #Sound.jabbers == 0 then return end + local src = Sound.jabbers[love.math.random(#Sound.jabbers)] + src:stop() + src:play() +end diff --git a/world.lua b/world.lua index 8988be5..4e15355 100644 --- a/world.lua +++ b/world.lua @@ -707,6 +707,7 @@ function World:movePlayer(room, key) current[color] = player:getGridPos() end if moved then + if Sound then Sound.play("playerMove") end self:markDirty(room) self:playerJoinCheck(room, previous, current) self:tick(room) @@ -811,6 +812,7 @@ function World:nextRoom(room, pos) elseif pos.y > gridHeight then exit, entry.y = "bottom", 1 elseif pos.y < 1 then exit, entry.y = "top", gridHeight end local nextName = self:getNeighbor(room.name, exit) or room.name + if Sound and nextName ~= room.name then Sound.play("levelChange") end local sourceLocation = self:roomLocation(room.name) local destinationLocation = self:roomLocation(nextName) local activeColors = room:getActiveColors()