summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcursed22bc <admin@pixeldawn.org>2026-03-07 23:43:32 +0200
committercursed22bc <admin@pixeldawn.org>2026-03-07 23:43:32 +0200
commitaeb596379bbf1bec84efb294ff5bbbee922364ba (patch)
treedeaf13edba484c5d4013dfd094e515b103e2c9d9
parent8514ffe0a1aa2febe989b2ff0b3044150492a1b2 (diff)
basic water render and physics
-rw-r--r--entity.lua14
-rw-r--r--liquid.lua98
-rw-r--r--liquidSurface.lua169
-rw-r--r--player.lua53
-rw-r--r--tilemap.lua21
-rw-r--r--world.lua87
6 files changed, 435 insertions, 7 deletions
diff --git a/entity.lua b/entity.lua
index d291717..34e37f0 100644
--- a/entity.lua
+++ b/entity.lua
@@ -38,6 +38,20 @@ function Entity:syncFromPhysicsBody()
self.y = self.body:getY() - self.height + self.physicsHeight / 2
end
+function getEntityCorners(entity)
+ local x = entity.x
+ local y = entity.y
+ local w = entity.physicsWidth
+ local h = entity.physicsHeight
+
+ return {
+ {x = x, y = y},
+ {x = x + w, y = y},
+ {x = x, y = y + h},
+ {x = x + w, y = y + h}
+ }
+end
+
function Entity:getPhysicsBody()
return self.body
end
diff --git a/liquid.lua b/liquid.lua
new file mode 100644
index 0000000..cdddb02
--- /dev/null
+++ b/liquid.lua
@@ -0,0 +1,98 @@
+local Liquid = {}
+Liquid.__index = Liquid
+
+local function buildWorldPolygonPoints(entity)
+ local points = {}
+
+ if not entity.polygon then
+ return points
+ end
+
+ for i, point in ipairs(entity.polygon) do
+ points[#points+1] = {
+ x = entity.x + point.x,
+ y = entity.y + point.y
+ }
+ end
+
+ return points
+end
+
+function Liquid:new(source)
+ local self = setmetatable({}, Liquid)
+ self.entity = source
+ self.x = source.x or 0
+ self.y = source.y or 0
+ self.width = source.width or 0
+ self.height = source.height or 0
+ self.shape = source.shape
+ self.polygon = source.polygon or {}
+ self.worldPolygon = buildWorldPolygonPoints(source)
+ self.properties = source.properties or {}
+ self.type = "liquid"
+ self.triangles = {}
+
+ local flat = {}
+ for _, p in ipairs(self.worldPolygon) do
+ table.insert(flat, p.x)
+ table.insert(flat, p.y)
+ end
+
+ if #flat >= 6 then
+ self.triangles = love.math.triangulate(flat)
+ end
+ return self
+end
+
+function Liquid:getWorldPolygon()
+ return self.worldPolygon
+end
+
+function Liquid:containsEntityFully(entity)
+ local corners = getEntityCorners(entity)
+
+ for _, c in ipairs(corners) do
+ if not self:containsPoint(c.x, c.y) then
+ return false
+ end
+ end
+
+ return true
+end
+
+function Liquid:containsPoint(px, py)
+ local points = self.worldPolygon
+ local count = #points
+ if count < 3 then return false end
+
+ local inside = false
+ local j = count
+ for i = 1, count do
+ local xi, yi = points[i].x, points[i].y
+ local xj, yj = points[j].x, points[j].y
+ local intersects = ((yi > py) ~= (yj > py))
+ and (px < (xj - xi) * (py - yi) / ((yj - yi) + 1e-9) + xi)
+ if intersects then
+ inside = not inside
+ end
+ j = i
+ end
+ return inside
+end
+
+function Liquid:containsEntity(entity)
+ if not entity then return false end
+ local ex = entity.x or 0
+ local ey = entity.y or 0
+ local ew = entity.width or 0
+ local eh = entity.height or 0
+
+ local sampleX = ex + ew * 0.5
+ local sampleY = ey + eh
+ return self:containsPoint(sampleX, sampleY)
+end
+
+function Liquid:update(dt)
+end
+
+return Liquid \ No newline at end of file
diff --git a/liquidSurface.lua b/liquidSurface.lua
new file mode 100644
index 0000000..6d9b24e
--- /dev/null
+++ b/liquidSurface.lua
@@ -0,0 +1,169 @@
+local LiquidSurface = {}
+LiquidSurface.__index = LiquidSurface
+
+local DEFAULT_COLORS = {
+ water = { 0.35, 0.65, 1, 0.75 },
+ lava = { 1, 0.35, 0.1, 0.8 }
+}
+
+local function toNumber(value, fallback)
+ local number = tonumber(value)
+ if number == nil then
+ return fallback
+ end
+ return number
+end
+
+local function getSurfaceBounds(entity)
+ local polygon = entity and entity.polygon or nil
+ if not polygon or #polygon == 0 then
+ return entity.x or 0, entity.y or 0, entity.width or 0, entity.height or 0
+ end
+
+ local minX, minY = math.huge, math.huge
+ local maxX, maxY = -math.huge, -math.huge
+ for _, point in ipairs(polygon) do
+ local px = (entity.x or 0) + (point.x or 0)
+ local py = (entity.y or 0) + (point.y or 0)
+ minX = math.min(minX, px)
+ minY = math.min(minY, py)
+ maxX = math.max(maxX, px)
+ maxY = math.max(maxY, py)
+ end
+
+ return minX, minY, maxX - minX, maxY - minY
+end
+
+function LiquidSurface:new(source)
+ local self = setmetatable({}, LiquidSurface)
+ local properties = source.properties or {}
+
+ local x, y, width, height = getSurfaceBounds(source)
+ local configuredHeight = toNumber(properties.height, height)
+ self.x = x
+ self.y = y
+ self.width = math.max(2, width)
+ self.height = math.max(1, configuredHeight)
+ self.bottomY = self.y + self.height
+
+ self.type = "water"
+ self.liquidType = properties.liquid_type or "water"
+ self.color = DEFAULT_COLORS[self.liquidType] or DEFAULT_COLORS.water
+
+ self.tension = toNumber(properties.tension, 0.015)
+ self.dampening = toNumber(properties.dampening, 0.001)
+ self.spread = toNumber(properties.spread, 0.1)
+ self.propagationPasses = math.max(1, math.floor(toNumber(properties.passes, 8)))
+ self.maxSpeed = toNumber(properties.max_speed, 8)
+ self.maxWaveAmplitude = toNumber(properties.max_wave_amplitude, self.height * 0.4)
+
+ self.columnsLength = math.max(2, math.floor(self.width))
+ self.columns = {}
+ for _ = 1, self.columnsLength do
+ table.insert(self.columns, self:createColumn())
+ end
+
+ return self
+end
+
+function LiquidSurface:createColumn()
+ return {
+ height = self.height,
+ targetHeight = self.height,
+ speed = 0
+ }
+end
+
+function LiquidSurface:isFixedColumn(index)
+ return index <= 2 or index >= self.columnsLength - 1
+end
+
+function LiquidSurface:updateColumn(index, dtScale)
+ if self:isFixedColumn(index) then return end
+
+ local column = self.columns[index]
+ local heightDiff = column.targetHeight - column.height
+ local accel = self.tension * heightDiff - column.speed * self.dampening
+
+ column.speed = column.speed + accel * dtScale
+ column.speed = math.max(-self.maxSpeed, math.min(self.maxSpeed, column.speed))
+
+ column.height = column.height + column.speed * dtScale
+ local lo = column.targetHeight - self.maxWaveAmplitude
+ local hi = column.targetHeight + self.maxWaveAmplitude
+ column.height = math.max(lo, math.min(hi, column.height))
+end
+
+function LiquidSurface:splash(worldX, speed)
+ if worldX < self.x or worldX >= (self.x + self.columnsLength) then
+ return
+ end
+
+ local index = math.min(self.columnsLength, math.max(1, math.floor(worldX - self.x) + 1))
+ if self:isFixedColumn(index) then
+ return
+ end
+
+ self.columns[index].speed = self.columns[index].speed - (speed or 2)
+end
+
+function LiquidSurface:isTouched(x, y, dx, dy)
+ return math.abs(y - self.y) < math.abs(dy)
+ and x >= self.x
+ and x < (self.x + self.columnsLength)
+end
+
+function LiquidSurface:update(dt)
+ local dtScale = math.max(0, dt or 0) * 60
+
+ for i = 1, self.columnsLength do
+ self:updateColumn(i, dtScale)
+ end
+
+ local leftDeltas = {}
+ local rightDeltas = {}
+
+ for _ = 1, self.propagationPasses do
+ for i = 1, self.columnsLength do
+ if i > 1 and not self:isFixedColumn(i - 1) then
+ leftDeltas[i] = self.spread * (self.columns[i].height - self.columns[i - 1].height) * dtScale
+ self.columns[i - 1].speed = self.columns[i - 1].speed + leftDeltas[i]
+ end
+ if i < self.columnsLength and not self:isFixedColumn(i + 1) then
+ rightDeltas[i] = self.spread * (self.columns[i].height - self.columns[i + 1].height) * dtScale
+ self.columns[i + 1].speed = self.columns[i + 1].speed + rightDeltas[i]
+ end
+ end
+
+ for i = 1, self.columnsLength do
+ if i > 1 and not self:isFixedColumn(i - 1) and leftDeltas[i] then
+ self.columns[i - 1].height = self.columns[i - 1].height + leftDeltas[i]
+ end
+ if i < self.columnsLength and not self:isFixedColumn(i + 1) and rightDeltas[i] then
+ self.columns[i + 1].height = self.columns[i + 1].height + rightDeltas[i]
+ end
+ end
+ end
+end
+
+function LiquidSurface:draw()
+ 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
+ local p2x = self.x + (i - 1)
+ local p2y = self.bottomY - self.columns[i].height
+ local p3x, p3y = p2x, self.bottomY
+ local p4x, p4y = p1x, self.bottomY
+
+ love.graphics.polygon("fill",
+ p1x, p1y, p3x, p3y, p2x, p2y,
+ p3x, p3y, p4x, p4y, p2x, p2y
+ )
+ end
+
+ love.graphics.setColor(1, 1, 1, 1)
+end
+
+return LiquidSurface
diff --git a/player.lua b/player.lua
index 7995cc6..b6bc4b5 100644
--- a/player.lua
+++ b/player.lua
@@ -5,7 +5,8 @@ local Player = {}
Player.__index = Player
setmetatable(Player, { __index = Entity })
-local MOVE_SPEED = 70
+local MOVE_SPEED = 65
+local SWIM_SPEED = 15
local JUMP_FORCE = -200
local GROUND_LAYER = "ground"
@@ -57,6 +58,9 @@ function Player.new(world, spawnX, spawnY)
self.vy = 0
self.grounded = false
+ self.isInLiquid = false
+ self.wasInLiquid = false
+ self.waterSurfaceContact = false
return self
end
@@ -98,6 +102,7 @@ function Player:update(dt)
local c = self.contact or { floor = 0, wall = 0, ceiling = 0 }
local onFloor = c.floor == 1
+ local onWaterSurface = self.waterSurfaceContact
local prevOnFloor = self.wasOnFloor
local prevState = self.state
@@ -105,7 +110,35 @@ function Player:update(dt)
if love.keyboard.isDown("left", "a") then move = move - 1 end
if love.keyboard.isDown("right", "d") then move = move + 1 end
- if onFloor then
+ if self.isInLiquid then
+ local vx, vy = self.body:getLinearVelocity()
+ local mass = self.body:getMass()
+ local gx, gy = self.body:getWorld():getGravity()
+
+ if not self.wasInLiquid then
+ vx = vx * 0.4
+ vy = math.min(vy, 60)
+ self.body:setLinearVelocity(vx, vy)
+ end
+
+ self.body:setLinearDamping(6)
+
+ local buoyancy = 0.9
+ self.body:applyForce(0, -gy * mass * buoyancy)
+
+ local moveY = 0
+ if love.keyboard.isDown("up", "w") then moveY = moveY - 1 end
+ if love.keyboard.isDown("down", "s") then moveY = moveY + 1 end
+
+ local inputLen = math.sqrt(move * move + moveY * moveY)
+ if inputLen > 1 then
+ move = move / inputLen
+ moveY = moveY / inputLen
+ end
+
+ self.body:applyForce(move * SWIM_SPEED, moveY * SWIM_SPEED)
+ elseif onFloor or onWaterSurface then
+ self.body:setLinearDamping(0)
self.availableJumps = 1
if move ~= 0 then
self.lastFacing = move
@@ -114,18 +147,23 @@ function Player:update(dt)
self.body:setLinearVelocity(0, vy)
end
else
+ self.body:setLinearDamping(0)
if move ~= 0 then self.lastFacing = move end
self.body:setLinearVelocity(move * MOVE_SPEED * 0.7, vy)
end
local desiredState
- if onFloor then
+ if self.isInLiquid then
+ desiredState = "swimming"
+ elseif onFloor then
desiredState = (move ~= 0) and "running" or "idle"
else
desiredState = (vy < 0) and "jumping" or "falling"
end
- if self.state == "ground_hit" or self.state == "stop_running" then
+ if self.isInLiquid then
+ self.state = desiredState
+ elseif self.state == "ground_hit" or self.state == "stop_running" then
if not onFloor or self.currentAnim:isFinished() then
self.state = desiredState
end
@@ -143,12 +181,14 @@ function Player:update(dt)
end
self.wasOnFloor = onFloor
+ self.wasInLiquid = self.isInLiquid
local animMap = {
idle = self.animations.idle,
running = self.animations.running,
jumping = self.animations.going_up,
falling = self.animations.going_down,
+ swimming = self.animations.going_down,
stop_running = self.animations.stop_running,
ground_hit = self.animations.ground_hit,
}
@@ -158,7 +198,10 @@ end
function Player:jump()
local c = self.contact or { floor = 0, wall = 0, ceiling = 0 }
- local canJump = (c.floor == 1 or c.wall == 1) and (self.availableJumps or 0) > 0
+ 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"
diff --git a/tilemap.lua b/tilemap.lua
index ab2a52c..8ad1c9c 100644
--- a/tilemap.lua
+++ b/tilemap.lua
@@ -1,4 +1,6 @@
local Entity = require("entity")
+local Liquid = require("liquid")
+local LiquidSurface = require("liquidSurface")
local Tilemap = {}
Tilemap.__index = Tilemap
@@ -34,8 +36,11 @@ local function objectToEntity(obj)
type = obj.type,
id = obj.id,
rotation = obj.rotation,
- visible = obj.visible
+ visible = obj.visible,
+ shape = obj.shape
})
+ entity.polygon = obj.polygon
+ entity.polyline = obj.polyline
return entity
end
@@ -45,6 +50,8 @@ function Tilemap:new(mapPath, tilesets)
self.entitiesTiles = {}
self.entitiesSpawns = {}
self.entitiesCameraBorders = {}
+ self.entitiesLiquidPolygons = {}
+ self.entitiesLiquidSurfaces = {}
self.layerBackground = nil
self.layerGround = nil
self.layerForeground = nil
@@ -117,6 +124,10 @@ function Tilemap:new(mapPath, tilesets)
table.insert(self.entitiesSpawns, entity)
elseif name == "camera_border" then
table.insert(self.entitiesCameraBorders, entity)
+ elseif name == "liquid" then
+ table.insert(self.entitiesLiquidPolygons, Liquid:new(entity))
+ elseif name == "liquid_surface" then
+ table.insert(self.entitiesLiquidSurfaces, LiquidSurface:new(entity))
end
end
end
@@ -126,6 +137,14 @@ function Tilemap:new(mapPath, tilesets)
return self
end
+function Tilemap:getEntitiesLiquidPolygons()
+ return self.entitiesLiquidPolygons
+end
+
+function Tilemap:getEntitiesLiquidSurfaces()
+ return self.entitiesLiquidSurfaces
+end
+
function Tilemap:getEntitiesTiles()
return self.entitiesTiles
end
diff --git a/world.lua b/world.lua
index 16324bb..5eaf1ce 100644
--- a/world.lua
+++ b/world.lua
@@ -18,8 +18,12 @@ function World:new()
self.player = nil
self.enemies = {}
self.entities = {}
+ self.liquidPolygons = {}
+ self.liquidSurfaces = {}
+ self.liquidSurfaceFixtures = {}
self.camera = nil
self.groundContacts = {}
+ self.waterContacts = {}
self.contactCounts = {}
self.contactEntity = {}
self.contactKind = {}
@@ -94,6 +98,9 @@ function World:load(mapPath, tilesets)
self.player = nil
self.enemies = {}
self.entities = {}
+ self.liquidPolygons = self.tilemap:getEntitiesLiquidPolygons() or {}
+ self.liquidSurfaces = self.tilemap:getEntitiesLiquidSurfaces() or {}
+ self.liquidSurfaceFixtures = {}
local spawns = self.tilemap:getEntitiesSpawns()
for _, spawn in ipairs(spawns) do
local entityType = spawn:get("entity", "")
@@ -111,6 +118,17 @@ function World:load(mapPath, tilesets)
end
end
+ for _, surface in ipairs(self.liquidSurfaces) do
+ local cx = (surface.x or 0) + (surface.width or 0) * 0.5
+ local cy = (surface.y or 0) + (surface.height or 0) * 0.5
+ local body = love.physics.newBody(self.physicsWorld, cx, cy, "static")
+ local shape = love.physics.newRectangleShape(surface.width or 0, surface.height or 0)
+ local fixture = love.physics.newFixture(body, shape, 1)
+ fixture:setSensor(true)
+ fixture:setUserData({ type = "water", water = surface })
+ table.insert(self.liquidSurfaceFixtures, { body = body, fixture = fixture })
+ end
+
end
function World:_addContact(entity, kind)
@@ -166,6 +184,28 @@ function World:_onBeginContact(udA, udB, nx, ny, contact)
self.groundContacts[udA] = (self.groundContacts[udA] or 0) + 1
if self:_isPlayerLike(udA) then udA.grounded = true end
end
+
+ local function doWaterSplash(waterData, entity)
+ if not (waterData and waterData.water and waterData.water.splash) then return end
+ local centerX = 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
+ elseif entity and entity.body then
+ centerX = entity.body:getX()
+ else
+ centerX = waterData.water.x or 0
+ end
+ waterData.water:splash(centerX, 2)
+ end
+
+ if type(udA) == "table" and udA.type == "water" and self:_isTrackedEntity(udB) then
+ self.waterContacts[udB] = (self.waterContacts[udB] or 0) + 1
+ doWaterSplash(udA, udB)
+ elseif type(udB) == "table" and udB.type == "water" and self:_isTrackedEntity(udA) then
+ self.waterContacts[udA] = (self.waterContacts[udA] or 0) + 1
+ doWaterSplash(udB, udA)
+ end
end
function World:_onEndContact(udA, udB, contact)
@@ -176,6 +216,11 @@ function World:_onEndContact(udA, udB, contact)
self.contactEntity[contact] = nil
self.contactKind[contact] = nil
end
+ if type(udA) == "table" and udA.type == "water" and self:_isTrackedEntity(udB) then
+ self.waterContacts[udB] = math.max(0, (self.waterContacts[udB] or 0) - 1)
+ elseif type(udB) == "table" and udB.type == "water" and self:_isTrackedEntity(udA) then
+ self.waterContacts[udA] = math.max(0, (self.waterContacts[udA] or 0) - 1)
+ end
if udA == "ground" and self:_isTrackedEntity(udB) then
self.groundContacts[udB] = math.max(0, (self.groundContacts[udB] or 0) - 1)
if self:_isPlayerLike(udB) then
@@ -223,7 +268,6 @@ local function drawTileLayer(layer, mapTileW, mapTileH, tileGidInfo, viewMinX, v
minRow, maxRow = 0, h - 1
end
- -- Collect tiles by image for SpriteBatch (reduces 770 draw calls to 1-2 batch draws)
local batches = {}
local fallbacks = {}
local data = layer.data
@@ -284,6 +328,27 @@ function World:draw()
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
+ love.graphics.polygon("fill", tri)
+ end
+ end
+end
+
+love.graphics.setColor(1,1,1,1)
+
+for _, surface in ipairs(self.liquidSurfaces) do
+ if surface.draw then
+ surface:draw()
+ end
+end
+
+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
end
@@ -328,9 +393,29 @@ function World:update(dt)
if e.body then
e:syncFromPhysicsBody()
end
+ end
+
+ if self.player then
+ self.player.isInLiquid = false
+ self.player.waterSurfaceContact = (self.waterContacts[self.player] or 0) > 0
+ for _, liquid in ipairs(self.liquidPolygons) do
+ if liquid:containsEntity(self.player) then self.player.isInLiquid = true break end
+ end
+ end
+
+
+ for _, e in ipairs(self.entities) do
if e.update then e:update(dt) end
end
+ for _, liquid in ipairs(self.liquidPolygons) do
+ if liquid.update then liquid:update(dt) end
+ end
+
+ for _, surface in ipairs(self.liquidSurfaces) do
+ if surface.update then surface:update(dt) end
+ end
+
if self.camera then
self.camera:setTarget(self.player)
self.camera:update(dt)