local VIRTUAL_WIDTH, VIRTUAL_HEIGHT = 16*10*5, 9*10*5 local CANVAS_PADDING = 6 DEBUG = true local CANVAS_WIDTH = VIRTUAL_WIDTH + CANVAS_PADDING local CANVAS_HEIGHT = VIRTUAL_HEIGHT + CANVAS_PADDING local WORLD_TO_CANVAS = 3 local scale = 1 local finalScale = 1 local offsetX, offsetY = 0, 0 local dpiScale = 1 local canvas = nil local smoothCameraShader = nil local menuBgShader = nil local liquidShader = nil local menuCanvas = nil local menuTime = 0 local shaderEnabled = true local cameraModule = require("camera") local camera = nil local fonts = require("fonts") local HUD = require("hud") local hud = nil local previousState = nil local currentState = "menu" local currentMapPath = "assets/maps/tilemap.lua" local currentTilesets = { tileset = { path = "assets/maps/tileset.png", tilewidth = 16, tileheight = 16 }, misc = { path = "assets/maps/simples_pimples.png", tilewidth = 16, tileheight = 16 }, } local world = nil local menuItems = {} local menuHover = nil local gameFinishPhase = nil local gameFinishTimer = 0 local irisRadius = 0 local IRIS_CLOSE_DURATION = 1.5 local TEXT_DISPLAY_DURATION = 5.0 local states = { game = {}, menu = {}, settings = {}, hud = {}, } local function recalcScale(w, h) dpiScale = (love.window.getDPIScale and love.window.getDPIScale()) or 1 scale = math.max(1, math.min(w / VIRTUAL_WIDTH, h / VIRTUAL_HEIGHT) / dpiScale) finalScale = scale * dpiScale offsetX = math.floor((w - VIRTUAL_WIDTH * finalScale) / 2) offsetY = math.floor((h - VIRTUAL_HEIGHT * finalScale) / 2) end local function screenToCanvas(sx, sy) local drawX = offsetX - (CANVAS_PADDING * finalScale) / 2 local drawY = offsetY - (CANVAS_PADDING * finalScale) / 2 return (sx - drawX) / finalScale, (sy - drawY) / finalScale end local function buildMenuItems() local btnW, btnH = 120, 32 local centerX = CANVAS_WIDTH / 2 local centerY = CANVAS_HEIGHT / 2 local spacing = 12 local totalH = 2 * btnH + spacing local startY = centerY - totalH / 2 + 20 menuItems = { { label = "Play", x = centerX - btnW / 2, y = startY, w = btnW, h = btnH, action = "play" }, { label = "Exit", x = centerX - btnW / 2, y = startY + btnH + spacing, w = btnW, h = btnH, action = "exit" }, } end function states.game.load() local World = require("world") world = World:new() world:load(currentMapPath, currentTilesets) local target = world:getPlayer() or { x = 0, y = 0, width = 16, height = 16 } camera = cameraModule:new(target, VIRTUAL_WIDTH / 2, VIRTUAL_HEIGHT / 2, true, WORLD_TO_CANVAS) world:setCamera(camera) hud = HUD:new() gameFinishPhase = nil gameFinishTimer = 0 irisRadius = 0 end function states.game.update(dt) if not world then return end if not gameFinishPhase then world:update(dt) if world.doorOpened then gameFinishPhase = "iris" gameFinishTimer = 0 irisRadius = math.sqrt((CANVAS_WIDTH / 2) ^ 2 + (CANVAS_HEIGHT / 2) ^ 2) end elseif gameFinishPhase == "iris" then world:update(dt) gameFinishTimer = gameFinishTimer + dt local maxR = math.sqrt((CANVAS_WIDTH / 2) ^ 2 + (CANVAS_HEIGHT / 2) ^ 2) local progress = math.min(gameFinishTimer / IRIS_CLOSE_DURATION, 1) local eased = 1 - (1 - progress) ^ 2 irisRadius = maxR * (1 - eased) if progress >= 1 then gameFinishPhase = "text" gameFinishTimer = 0 irisRadius = 0 end elseif gameFinishPhase == "text" then gameFinishTimer = gameFinishTimer + dt if gameFinishTimer >= TEXT_DISPLAY_DURATION then gameFinishPhase = nil gameFinishTimer = 0 currentState = "menu" states.menu.load() end end end function states.game.draw() if gameFinishPhase ~= "text" then if camera then camera:set() end if world then world:draw() end if camera then camera:unset() end end if gameFinishPhase == "iris" then local cx = CANVAS_WIDTH / 2 local cy = CANVAS_HEIGHT / 2 local outerR = math.sqrt(cx * cx + cy * cy) + 10 local innerR = math.max(irisRadius, 0) local segments = 64 local verts = {} for i = 0, segments do local angle = (i / segments) * math.pi * 2 local cs, sn = math.cos(angle), math.sin(angle) verts[#verts + 1] = { cx + cs * innerR, cy + sn * innerR, 0, 0, 0, 0, 0, 1 } verts[#verts + 1] = { cx + cs * outerR, cy + sn * outerR, 0, 0, 0, 0, 0, 1 } end local mesh = love.graphics.newMesh(verts, "strip", "stream") love.graphics.setColor(1, 1, 1, 1) love.graphics.draw(mesh) elseif gameFinishPhase == "text" then love.graphics.setColor(0, 0, 0, 1) love.graphics.rectangle("fill", 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT) local font = fonts.get(14) love.graphics.setFont(font) love.graphics.setColor(1, 1, 1, 1) love.graphics.printf("The Frog Escaped the Evil Aquarium", 0, CANVAS_HEIGHT / 2 - font:getHeight() / 2, CANVAS_WIDTH, "center") love.graphics.setFont(fonts.default) end end function states.menu.load() camera = nil world = nil hud = nil gameFinishPhase = nil gameFinishTimer = 0 irisRadius = 0 menuTime = 0 love.mouse.setVisible(true) menuHover = nil end function states.menu.update(dt) menuTime = menuTime + dt local mx, my = love.mouse.getPosition() local cx, cy = screenToCanvas(mx, my) menuHover = nil for _, btn in ipairs(menuItems) do if cx >= btn.x and cx <= btn.x + btn.w and cy >= btn.y and cy <= btn.y + btn.h then menuHover = btn.action break end end end function states.menu.draw() local prevCanvas = love.graphics.getCanvas() love.graphics.setCanvas(menuCanvas) love.graphics.clear() if menuBgShader then love.graphics.setShader(menuBgShader) end love.graphics.setColor(1, 1, 1, 1) love.graphics.rectangle("fill", 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT) love.graphics.setShader() love.graphics.setCanvas(prevCanvas) if liquidShader then liquidShader:send("scene", menuCanvas) liquidShader:send("time", menuTime) liquidShader:send("resolution", { CANVAS_WIDTH, CANVAS_HEIGHT }) liquidShader:send("splashCenter", { -10.0, -10.0 }) liquidShader:send("splashTime", 0.0) love.graphics.setShader(liquidShader) end love.graphics.setColor(1, 1, 1, 1) love.graphics.draw(menuCanvas, 0, 0) love.graphics.setShader() local titleFont = fonts.get(20) love.graphics.setFont(titleFont) love.graphics.setColor(1, 1, 1, 1) love.graphics.printf("The Frog Escapes Evil Aquarium", 0, CANVAS_HEIGHT / 2 - 80, CANVAS_WIDTH, "center") local btnFont = fonts.get(20) love.graphics.setFont(btnFont) for _, btn in ipairs(menuItems) do local hover = menuHover == btn.action if hover then love.graphics.setColor(1, 1, 1, 1) love.graphics.setLineWidth(1) love.graphics.rectangle("line", btn.x, btn.y, btn.w, btn.h) end love.graphics.setColor(1, 1, 1, 1) local tw = btnFont:getWidth(btn.label) local th = btnFont:getHeight() love.graphics.print(btn.label, btn.x + btn.w / 2 - tw / 2, btn.y + btn.h / 2 - th / 2) end love.graphics.setFont(fonts.default) love.graphics.setColor(1, 1, 1, 1) end function states.settings.load() camera = nil world = nil end function states.settings.update(dt) end function states.settings.draw() love.graphics.print("Settings", 10, 10) end function states.hud.update(dt) if hud then local w, h = love.graphics.getWidth(), love.graphics.getHeight() local player = world and world:getPlayer() hud:update(dt, w, h, finalScale, player) end end function states.hud.draw() if camera then camera:set() end if world then world:draw() end if camera then camera:unset() end end function love.load() love.graphics.setDefaultFilter("nearest", "nearest") love.window.setTitle("The Frog Escapes Evil Aquarium") fonts.load() love.graphics.setFont(fonts.default) canvas = love.graphics.newCanvas(CANVAS_WIDTH, CANVAS_HEIGHT) canvas:setFilter("nearest", "nearest") local ok, shader = pcall(love.graphics.newShader, "shaders/smooth_camera.glsl") smoothCameraShader = ok and shader or nil if not smoothCameraShader then print("Warning: smooth_camera.glsl not loaded, using fallback (no sub-pixel offset)") end local ok2, shader2 = pcall(love.graphics.newShader, "shaders/missing_texture.glsl") menuBgShader = ok2 and shader2 or nil local ok3, shader3 = pcall(love.graphics.newShader, "shaders/liquid.glsl") liquidShader = ok3 and shader3 or nil menuCanvas = love.graphics.newCanvas(CANVAS_WIDTH, CANVAS_HEIGHT) menuCanvas:setFilter("nearest", "nearest") local w, h = love.graphics.getWidth(), love.graphics.getHeight() recalcScale(w, h) buildMenuItems() love.mouse.setVisible(true) local state = states[currentState] if state and state.load then state.load() end end function love.resize(w, h) recalcScale(w, h) if canvas then canvas:release() end canvas = love.graphics.newCanvas(CANVAS_WIDTH, CANVAS_HEIGHT) canvas:setFilter("nearest", "nearest") if menuCanvas then menuCanvas:release() end menuCanvas = love.graphics.newCanvas(CANVAS_WIDTH, CANVAS_HEIGHT) menuCanvas:setFilter("nearest", "nearest") end function love.update(dt) local state = states[currentState] if state and state.update then state.update(dt) end end function love.keypressed(key, scancode, isrepeat) if key == "f11" then love.window.setFullscreen(not love.window.getFullscreen(), "desktop") end if key == "f1" and DEBUG then shaderEnabled = not shaderEnabled end if key == "escape" and not isrepeat then if currentState == "game" or currentState == "hud" then currentState = "menu" states.menu.load() end end if key == "tab" and not isrepeat then if currentState == "game" then previousState = currentState currentState = "hud" love.mouse.setVisible(true) elseif currentState == "hud" then currentState = previousState or "game" previousState = nil love.mouse.setVisible(false) end end if (key == "space" or key == "up" or key == "w") and not isrepeat then if currentState == "game" then local player = world and world:getPlayer() if player then player:jump() end end end end function love.mousepressed(x, y, button) if currentState == "menu" and button == 1 then local cx, cy = screenToCanvas(x, y) for _, btn in ipairs(menuItems) do if cx >= btn.x and cx <= btn.x + btn.w and cy >= btn.y and cy <= btn.y + btn.h then if btn.action == "play" then currentState = "game" states.game.load() love.mouse.setVisible(false) elseif btn.action == "exit" then love.event.quit() end break end end elseif currentState == "hud" and hud then hud:mousepressed(x, y, button) end end function love.draw() love.graphics.setCanvas(canvas) love.graphics.clear() love.graphics.push() local state = states[currentState] if state and state.draw then state.draw() end love.graphics.pop() love.graphics.setCanvas() love.graphics.clear() local drawX = offsetX - (CANVAS_PADDING * finalScale) / 2 local drawY = offsetY - (CANVAS_PADDING * finalScale) / 2 local subDx, subDy = 0, 0 if world and world.camera and world.camera.getSubPixelOffset then subDx, subDy = world.camera:getSubPixelOffset() end local camScale = (world and world.camera and world.camera._lastScale) or 2 local uvOffsetX = subDx * camScale / CANVAS_WIDTH local uvOffsetY = subDy * camScale / CANVAS_HEIGHT if shaderEnabled and smoothCameraShader and (subDx ~= 0 or subDy ~= 0) then smoothCameraShader:send("offset", { uvOffsetX, uvOffsetY }) love.graphics.setShader(smoothCameraShader) end love.graphics.draw(canvas, math.floor(drawX), math.floor(drawY), 0, finalScale, finalScale) if shaderEnabled and smoothCameraShader then love.graphics.setShader() end if currentState == "hud" and hud then hud:draw() end if DEBUG then local fps = love.timer.getFPS() local shaderLoaded = smoothCameraShader ~= nil local shaderApplied = shaderEnabled and shaderLoaded and (subDx ~= 0 or subDy ~= 0) local shaderName = shaderLoaded and "smooth_camera.glsl" or "none (fallback)" local shaderState = not shaderLoaded and "unavailable" or not shaderEnabled and "disabled [F1]" or shaderApplied and "active" or "idle" local shaderLine = "shader: " .. shaderName .. " (" .. shaderState .. ")" love.graphics.setColor(0, 0, 0, 0.5) love.graphics.rectangle("fill", 4, 4, 280, 36) love.graphics.setColor(1, 1, 0, 1) love.graphics.print("FPS: " .. fps, 8, 8) love.graphics.print(shaderLine, 8, 22) love.graphics.setColor(1, 1, 1, 1) end end return nil