summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--liquidSurface.lua22
-rw-r--r--main.lua2
-rw-r--r--player.lua39
-rw-r--r--shaders/liquid.glsl47
-rw-r--r--textbox.lua145
-rw-r--r--world.lua146
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
diff --git a/main.lua b/main.lua
index c0e6108..d014b96 100644
--- a/main.lua
+++ b/main.lua
@@ -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
diff --git a/player.lua b/player.lua
index b6bc4b5..81e56bc 100644
--- a/player.lua
+++ b/player.lua
@@ -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
diff --git a/world.lua b/world.lua
index 5eaf1ce..86ab7ba 100644
--- a/world.lua
+++ b/world.lua
@@ -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