diff options
| author | cursed22bc <admin@pixeldawn.org> | 2026-03-08 23:15:16 +0200 |
|---|---|---|
| committer | cursed22bc <admin@pixeldawn.org> | 2026-03-08 23:15:16 +0200 |
| commit | 97d0d1ecc0cfd5516cfad2d477faa4d35992e7ba (patch) | |
| tree | 5ba61a59723d5c9ceb8059388d35bdcb069af743 | |
| parent | aeb596379bbf1bec84efb294ff5bbbee922364ba (diff) | |
liquid shader and basic text render
| -rw-r--r-- | liquidSurface.lua | 22 | ||||
| -rw-r--r-- | main.lua | 2 | ||||
| -rw-r--r-- | player.lua | 39 | ||||
| -rw-r--r-- | shaders/liquid.glsl | 47 | ||||
| -rw-r--r-- | textbox.lua | 145 | ||||
| -rw-r--r-- | world.lua | 146 |
6 files changed, 366 insertions, 35 deletions
diff --git a/liquidSurface.lua b/liquidSurface.lua index 6d9b24e..da79d44 100644 --- a/liquidSurface.lua +++ b/liquidSurface.lua @@ -2,7 +2,7 @@ local LiquidSurface = {} LiquidSurface.__index = LiquidSurface local DEFAULT_COLORS = { - water = { 0.35, 0.65, 1, 0.75 }, + water = { 0.35, 0.65, 1, 0.5}, lava = { 1, 0.35, 0.1, 0.8 } } @@ -146,9 +146,8 @@ function LiquidSurface:update(dt) end end -function LiquidSurface:draw() +function LiquidSurface:drawFill() love.graphics.setColor(self.color) - for i = 2, self.columnsLength do local p1x = self.x + (i - 2) local p1y = self.bottomY - self.columns[i - 1].height @@ -162,8 +161,25 @@ function LiquidSurface:draw() p3x, p3y, p4x, p4y, p2x, p2y ) end +end +function LiquidSurface:drawLine() + local linePoints = {} + for i = 1, self.columnsLength do + linePoints[#linePoints + 1] = self.x + (i - 1) + linePoints[#linePoints + 1] = self.bottomY - self.columns[i].height + end love.graphics.setColor(1, 1, 1, 1) + love.graphics.setLineWidth(1) + love.graphics.setLineStyle("rough") + love.graphics.line(linePoints) + love.graphics.setLineStyle("smooth") + love.graphics.setColor(1, 1, 1, 1) +end + +function LiquidSurface:draw() + self:drawFill() + self:drawLine() end return LiquidSurface @@ -1,7 +1,7 @@ local VIRTUAL_WIDTH, VIRTUAL_HEIGHT = 16*10*3, 9*10*3 local CANVAS_PADDING = 6 -DEBUG = true +DEBUG = false local CANVAS_WIDTH = VIRTUAL_WIDTH + CANVAS_PADDING local CANVAS_HEIGHT = VIRTUAL_HEIGHT + CANVAS_PADDING @@ -22,6 +22,9 @@ function Player.new(world, spawnX, spawnY) self.availableJumps = 0 self:enablePhysics(world, "dynamic") + self.fixture:setFriction(0) + self.doubleJump = true + self.jumpsUsed = 0 self.animations = { idle = Animation.new("assets/player/idle.png", 16, 16, 0.6), running = Animation.new("assets/player/running.png", 16, 16, 0.9), @@ -139,7 +142,7 @@ function Player:update(dt) self.body:applyForce(move * SWIM_SPEED, moveY * SWIM_SPEED) elseif onFloor or onWaterSurface then self.body:setLinearDamping(0) - self.availableJumps = 1 + self.jumpsUsed = 0 if move ~= 0 then self.lastFacing = move self.body:setLinearVelocity(move * MOVE_SPEED, vy) @@ -198,18 +201,32 @@ end function Player:jump() local c = self.contact or { floor = 0, wall = 0, ceiling = 0 } + local onFloor = c.floor == 1 + local onWall = c.wall == 1 local onWaterSurface = self.waterSurfaceContact - local canJump = (c.floor == 1 or c.wall == 1 or onWaterSurface) and (self.availableJumps or 0) > 0 - if not canJump then return false end - if self.isInLiquid and not onWaterSurface then return false end - if canJump then - self.body:setLinearVelocity(self.body:getLinearVelocity(), JUMP_FORCE) - self.state = "jumping" - self.animations.going_up:reset() - self.availableJumps = self.availableJumps - 1 - return true + + if onWall and not onFloor then + return false + end + + local maxJumps = self.doubleJump and 2 or 1 + + if self.jumpsUsed >= maxJumps then + return false end - return false + + if self.isInLiquid and not onWaterSurface then + return false + end + + self.body:setLinearVelocity(self.body:getLinearVelocity(), JUMP_FORCE) + + self.state = "jumping" + self.animations.going_up:reset() + + self.jumpsUsed = self.jumpsUsed + 1 + + return true end function Player:draw() diff --git a/shaders/liquid.glsl b/shaders/liquid.glsl new file mode 100644 index 0000000..9078248 --- /dev/null +++ b/shaders/liquid.glsl @@ -0,0 +1,47 @@ +extern Image scene; +extern number time; +extern vec2 resolution; +extern vec2 splashCenter; +extern number splashTime; + +float getOffsetStrength(float t, vec2 dir) { + float maxRadius = 0.3; + float d = length(dir) - maxRadius * t; + d *= 1.0 - smoothstep(0.0, 0.05, abs(d)); + d *= smoothstep(0.0, 0.05, t); + d *= 1.0 - smoothstep(0.5, 1.0, t); + return d; +} + +vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords) { + vec2 uv = screen_coords / resolution; + + // waves + float x = sin(uv.y * 2.56) * 0.05 * sin(time * 0.4); + float y = sin(uv.x * 2.56) * 0.05 * cos(time * 0.4); + vec2 offset = vec2(x, y); + + // shockwave + vec2 dir = splashCenter - uv; + float dist = length(dir); + float t = splashTime * (1.0 - smoothstep(0.0, 0.3, dist)); + t = pow(max(0.0, t), 1.0 / 1.5); + + float rD = getOffsetStrength(t + 0.02, dir); + float gD = getOffsetStrength(t, dir); + float bD = getOffsetStrength(t - 0.02, dir); + + dir = normalize(dir + vec2(0.0001)); + vec2 uvR = clamp(uv + offset + dir * rD, vec2(0.0, 0.0), vec2(1.0, 1.0)); + vec2 uvG = clamp(uv + offset + dir * gD, vec2(0.0, 0.0), vec2(1.0, 1.0)); + vec2 uvB = clamp(uv + offset + dir * bD, vec2(0.0, 0.0), vec2(1.0, 1.0)); + + float red = Texel(scene, uvR).r; + float green = Texel(scene, uvG).g; + float blue = Texel(scene, uvB).b; + + vec4 col = vec4(red, green, blue, color.a); + float shading = gD * 8.0; + col.rgb += shading; + return col; +} diff --git a/textbox.lua b/textbox.lua new file mode 100644 index 0000000..35b66a4 --- /dev/null +++ b/textbox.lua @@ -0,0 +1,145 @@ +local fonts = require("fonts") + +local Textbox = {} +Textbox.__index = Textbox + +function Textbox:new() + local self = setmetatable({}, Textbox) + self.active = false + self.text = "" + self.visibleText = "" + self.mode = "show" + self.x = 0 + self.y = 0 + self.align = "center" + self.maxLines = nil + self.maxChars = nil + self.charIndex = 0 + self.timer = 0 + self.speed = 0.03 + self.width = 0 + self.height = 0 + self.lines = {} + return self +end + +function Textbox:show(text, pos, mode, opts) + opts = opts or {} + self.text = text + self.mode = mode or "show" + self.x = pos[1] + self.y = pos[2] + self.align = pos[3] or "center" + self.maxLines = opts.maxLines + self.maxChars = opts.maxChars + self.fontSize = opts.fontSize + self.centeredText = opts.centeredText + self.wrapToFit = opts.wrapToFit + self.life = opts.life + self.lifeTimer = 0 + self.charIndex = 0 + self.visibleText = "" + self.timer = 0 + self.active = true + self:layout() +end + +function Textbox:layout() + self.maxLines = self.maxLines or 4 + self.maxChars = self.maxChars or 40 + self.font = self.fontSize and fonts.get(self.fontSize) or love.graphics.getFont() + local padding = 10 + if self.wrapToFit then + local tempWidth = self.font:getWidth(string.rep("A", self.maxChars)) + local _, lines = self.font:getWrap(self.text, tempWidth) + self.lines = lines or {} + self.lines = self.lines or {} + local maxW = 0 + for _, line in ipairs(self.lines) do + local w = self.font:getWidth(line) + if w > maxW then maxW = w end + end + local lineCount = math.max(1, #self.lines) + self.width = maxW + padding * 2 + self.height = self.font:getHeight() * lineCount + padding * 2 + else + self.width = self.font:getWidth(string.rep("A", self.maxChars)) + padding * 2 + self.height = self.font:getHeight() * self.maxLines + padding * 2 + end +end + +function Textbox:wrap(text) + local lines = {} + local line = "" + for word in text:gmatch("%S+") do + if line == "" then + line = word + elseif #line + #word + 1 <= self.maxChars then + line = line .. " " .. word + else + table.insert(lines, line) + line = word + end + end + if line ~= "" then + table.insert(lines, line) + end + return lines +end + +function Textbox:update(dt) + if not self.active then return end + if self.life then + self.lifeTimer = self.lifeTimer + dt + if self.lifeTimer >= self.life then + self:hide() + return + end + end + if self.mode == "write" then + self.timer = self.timer + dt + if self.timer > self.speed then + self.timer = 0 + self.charIndex = self.charIndex + 1 + self.visibleText = self.text:sub(1, self.charIndex) + end + else + self.visibleText = self.text + end +end + +function Textbox:draw() + if not self.active then return end + local x = self.x + local y = self.y + if self.align == "center" then + x = x - self.width / 2 + y = y - self.height / 2 + elseif self.align == "right" then + x = x - self.width + end + local padding = 10 + local textAlign = self.centeredText and "center" or "left" + local textX = x + padding + local textY = y + padding + if self.centeredText then + local _, wrappedLines = self.font:getWrap(self.visibleText, self.width - padding * 2) + local numLines = wrappedLines and #wrappedLines or 1 + local textHeight = self.font:getHeight() * numLines + local vertExtra = math.max(0, self.height - padding * 2 - textHeight) + textY = y + padding + vertExtra / 2 + end + local prevFont = love.graphics.getFont() + love.graphics.setFont(self.font) + love.graphics.setColor(0, 0, 0, 0.8) + love.graphics.rectangle("fill", x, y, self.width, self.height) + love.graphics.setColor(1, 1, 1, 1) + love.graphics.printf(self.visibleText, textX, textY, self.width - padding * 2, textAlign) + love.graphics.setFont(prevFont) +end + +function Textbox:hide() + self.active = false +end + +return Textbox @@ -2,6 +2,7 @@ local Tilemap = require("tilemap") local Entity = require("entity") local Player = require("player") local Camera = require("camera") +local Textbox = require("textbox") local World = {} World.__index = World @@ -21,12 +22,16 @@ function World:new() self.liquidPolygons = {} self.liquidSurfaces = {} self.liquidSurfaceFixtures = {} + self.refractionCanvas = nil + self.liquidShader = nil + self.activeSplashes = {} self.camera = nil self.groundContacts = {} self.waterContacts = {} self.contactCounts = {} self.contactEntity = {} self.contactKind = {} + self.playerTextbox = Textbox:new() return self end @@ -129,6 +134,12 @@ function World:load(mapPath, tilesets) table.insert(self.liquidSurfaceFixtures, { body = body, fixture = fixture }) end + local ok, shader = pcall(love.graphics.newShader, "shaders/liquid.glsl") + self.liquidShader = ok and shader or nil + if not self.liquidShader then + print("Warning: liquid.glsl not loaded, using fallback (no refraction)") + end + end function World:_addContact(entity, kind) @@ -188,15 +199,19 @@ function World:_onBeginContact(udA, udB, nx, ny, contact) local function doWaterSplash(waterData, entity) if not (waterData and waterData.water and waterData.water.splash) then return end local centerX = 0 + local centerY = waterData.water.y or 0 local ok, x1, y1, x2, y2 = pcall(function() return contact:getPositions() end) if ok and x1 then centerX = (x1 + (x2 or x1)) * 0.5 + centerY = (y1 + (y2 or y1)) * 0.5 elseif entity and entity.body then centerX = entity.body:getX() + centerY = entity.body:getY() else centerX = waterData.water.x or 0 end waterData.water:splash(centerX, 2) + table.insert(self.activeSplashes, { x = centerX, y = centerY, t = 0 }) end if type(udA) == "table" and udA.type == "water" and self:_isTrackedEntity(udB) then @@ -325,32 +340,100 @@ function World:draw() viewMaxY = cy + ch + pad end - drawTileLayer(self.tilemap:getBackgroundLayer(), mapTileW, mapTileH, tileGidInfo, viewMinX, viewMinY, viewMaxX, viewMaxY) - drawTileLayer(self.tilemap:getGroundLayer(), mapTileW, mapTileH, tileGidInfo, viewMinX, viewMinY, viewMaxX, viewMaxY) + local hasLiquid = (#self.liquidPolygons > 0 or #self.liquidSurfaces > 0) + local useRefraction = hasLiquid and self.liquidShader - love.graphics.setColor(0.25, 0.5, 0.9, 0.6) + if useRefraction then + local mainCanvas = love.graphics.getCanvas() + local cw, ch = mainCanvas:getDimensions() + if not self.refractionCanvas or self.refractionCanvas:getWidth() ~= cw or self.refractionCanvas:getHeight() ~= ch then + if self.refractionCanvas then self.refractionCanvas:release() end + self.refractionCanvas = love.graphics.newCanvas(cw, ch) + self.refractionCanvas:setFilter("linear", "linear") + end -for _, liquid in ipairs(self.liquidPolygons) do - if liquid.triangles then - for _, tri in ipairs(liquid.triangles) do - love.graphics.polygon("fill", tri) + love.graphics.push() + love.graphics.setCanvas(self.refractionCanvas) + love.graphics.clear(0, 0, 0, 1) + drawTileLayer(self.tilemap:getBackgroundLayer(), mapTileW, mapTileH, tileGidInfo, viewMinX, viewMinY, viewMaxX, viewMaxY) + drawTileLayer(self.tilemap:getGroundLayer(), mapTileW, mapTileH, tileGidInfo, viewMinX, viewMinY, viewMaxX, viewMaxY) + for _, e in ipairs(self.entities) do + if e.draw then e:draw() else World.drawEntityDefault(e) end + end + drawTileLayer(self.tilemap:getForegroundLayer(), mapTileW, mapTileH, tileGidInfo, viewMinX, viewMinY, viewMaxX, viewMaxY) + love.graphics.setCanvas(mainCanvas) + love.graphics.pop() + + love.graphics.push() + love.graphics.origin() + love.graphics.draw(self.refractionCanvas, 0, 0) + love.graphics.pop() + + local splash = self.activeSplashes[#self.activeSplashes] + local splashCenterX, splashCenterY = 0.5, 0.5 + local splashTime = 0 + if splash and self.camera then + local snapX = math.floor(self.camera.x) + local snapY = math.floor(self.camera.y) + local scale = self.camera._lastScale or 2 + splashCenterX = ((splash.x - snapX) * scale) / cw + splashCenterY = ((splash.y - snapY) * scale) / ch + splashTime = splash.t + end + + self.liquidShader:send("scene", self.refractionCanvas) + self.liquidShader:send("time", love.timer.getTime()) + self.liquidShader:send("resolution", { cw, ch }) + self.liquidShader:send("splashCenter", { splashCenterX, splashCenterY }) + self.liquidShader:send("splashTime", splashTime) + love.graphics.setShader(self.liquidShader) + + love.graphics.setColor(0.25, 0.5, 0.9, 0.5) + for _, liquid in ipairs(self.liquidPolygons) do + if liquid.triangles then + for _, tri in ipairs(liquid.triangles) do + love.graphics.polygon("fill", tri) + end + end + end + + for _, surface in ipairs(self.liquidSurfaces) do + if surface.drawFill then + surface:drawFill() + end + end + + love.graphics.setShader() + else + drawTileLayer(self.tilemap:getBackgroundLayer(), mapTileW, mapTileH, tileGidInfo, viewMinX, viewMinY, viewMaxX, viewMaxY) + drawTileLayer(self.tilemap:getGroundLayer(), mapTileW, mapTileH, tileGidInfo, viewMinX, viewMinY, viewMaxX, viewMaxY) + + love.graphics.setColor(0.25, 0.5, 0.9, 0.6) + for _, liquid in ipairs(self.liquidPolygons) do + if liquid.triangles then + for _, tri in ipairs(liquid.triangles) do + end + end end - end -end -love.graphics.setColor(1,1,1,1) + for _, surface in ipairs(self.liquidSurfaces) do + if surface.drawFill then + end + end + end -for _, surface in ipairs(self.liquidSurfaces) do - if surface.draw then - surface:draw() + for _, surface in ipairs(self.liquidSurfaces) do + if surface.drawLine then + surface:drawLine() + end end -end + love.graphics.setColor(1, 1, 1, 1) -for _, p in ipairs(self.liquidPolygons) do - love.graphics.circle("fill", p.x, p.y, 3) -end - for _, e in ipairs(self.entities) do - if e.draw then e:draw() else World.drawEntityDefault(e) end + if not useRefraction then + for _, e in ipairs(self.entities) do + if e.draw then e:draw() else World.drawEntityDefault(e) end + end + drawTileLayer(self.tilemap:getForegroundLayer(), mapTileW, mapTileH, tileGidInfo, viewMinX, viewMinY, viewMaxX, viewMaxY) end if DEBUG then @@ -358,7 +441,7 @@ end World.drawPhysicsBodyOutlines(self.groundEntities) end - drawTileLayer(self.tilemap:getForegroundLayer(), mapTileW, mapTileH, tileGidInfo, viewMinX, viewMinY, viewMaxX, viewMaxY) + self.playerTextbox:draw() end function World.drawPhysicsBodyOutlines(entityList) @@ -401,6 +484,11 @@ function World:update(dt) for _, liquid in ipairs(self.liquidPolygons) do if liquid:containsEntity(self.player) then self.player.isInLiquid = true break end end + if self.player.isInLiquid and not self.player.wasInLiquid then + local px = self.player.x + (self.player.width or 16) / 2 + local py = self.player.y - 20 + self.playerTextbox:show("Entered water!", { px, py, "center" }, "write", { life = 2, fontSize = 9, centeredText = true, wrapToFit = true }) + end end @@ -416,10 +504,28 @@ function World:update(dt) if surface.update then surface:update(dt) end end + for i = #self.activeSplashes, 1, -1 do + self.activeSplashes[i].t = self.activeSplashes[i].t + dt * 2 + if self.activeSplashes[i].t >= 1 then + table.remove(self.activeSplashes, i) + end + end + if #self.activeSplashes > 4 then + for _ = 1, #self.activeSplashes - 4 do + table.remove(self.activeSplashes, 1) + end + end + if self.camera then self.camera:setTarget(self.player) self.camera:update(dt) end + + if self.playerTextbox.active and self.player then + self.playerTextbox.x = self.player.x + (self.player.width or 16) / 2 + self.playerTextbox.y = self.player.y - 20 + end + self.playerTextbox:update(dt) end return World |
