From d2dab6c8efcf126503c004e935fda9190533729c Mon Sep 17 00:00:00 2001 From: cursed22bc Date: Tue, 10 Mar 2026 22:29:39 +0200 Subject: particles --- bloodParticle.lua | 82 +++++++++++++++++++++++++++++++++ dustParticle.lua | 135 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ world.lua | 47 ++++++++++++++++++- 3 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 bloodParticle.lua create mode 100644 dustParticle.lua diff --git a/bloodParticle.lua b/bloodParticle.lua new file mode 100644 index 0000000..12aa2af --- /dev/null +++ b/bloodParticle.lua @@ -0,0 +1,82 @@ +local BloodParticle = {} +BloodParticle.__index = BloodParticle + +local SHRINK_RATE = 6 +local GRAVITY = 300 +local PARTICLE_COUNT = 32 +local BURST_SPEED_MIN = 40 +local BURST_SPEED_MAX = 160 + +local BLOOD_COLORS = { + {0.22, 0.38, 0.22, 1}, + {0.18, 0.30, 0.18, 1}, + {0.28, 0.44, 0.28, 1}, + {0.14, 0.22, 0.14, 1}, +} + +local GLOW_COLOR = {0.05, 0.7, 0.05, 0.0} -- meh + +function BloodParticle.new(x, y, radius, color, vx, vy) + local self = setmetatable({}, BloodParticle) + self.x = x + self.y = y + self.radius = radius + self.initialRadius = radius + self.color = color + self.vx = vx or 0 + self.vy = vy or 0 + self.stuck = false + self.kill = false + return self +end + +function BloodParticle:update(dt, isSolidAt) + if self.stuck or self.kill then return end + + self.x = self.x + self.vx * dt + self.y = self.y + self.vy * dt + self.vy = self.vy + GRAVITY * dt + + self.radius = self.radius - SHRINK_RATE * dt + + local hitWall = isSolidAt and isSolidAt(self.x, self.y) + if hitWall or self.radius <= self.initialRadius * 0.5 then + self.stuck = true + self.radius = math.max(self.radius, self.initialRadius * 0.35) + self.vx, self.vy = 0, 0 + return + end + + if self.radius <= 0 then + self.kill = true + end +end + +function BloodParticle:draw() + if self.kill then return end + + love.graphics.setBlendMode("add") + love.graphics.setColor(GLOW_COLOR) + love.graphics.circle("fill", self.x, self.y, self.radius * 2.2) + love.graphics.setBlendMode("alpha") + + love.graphics.setColor(self.color) + love.graphics.circle("fill", self.x, self.y, self.radius) + love.graphics.setColor(1, 1, 1, 1) +end + +function BloodParticle.burst(x, y) + local particles = {} + for _ = 1, PARTICLE_COUNT do + local angle = math.random() * math.pi * 2 + local speed = BURST_SPEED_MIN + math.random() * (BURST_SPEED_MAX - BURST_SPEED_MIN) + local vx = math.cos(angle) * speed + local vy = math.sin(angle) * speed - 40 + local radius = 1.5 + math.random() * 8.0 + local color = BLOOD_COLORS[math.random(#BLOOD_COLORS)] + table.insert(particles, BloodParticle.new(x, y, radius, color, vx, vy)) + end + return particles +end + +return BloodParticle diff --git a/dustParticle.lua b/dustParticle.lua new file mode 100644 index 0000000..8e7edd4 --- /dev/null +++ b/dustParticle.lua @@ -0,0 +1,135 @@ +local DustParticle = {} +DustParticle.__index = DustParticle + +local MAX_DUST = 30 +local DRIFT_SPEED_MIN = 4 +local DRIFT_SPEED_MAX = 12 +local WOBBLE_AMP_MIN = 3 +local WOBBLE_AMP_MAX = 8 +local WOBBLE_FREQ_MIN = 0.8 +local WOBBLE_FREQ_MAX = 2.5 +local GLOW_FREQ_MIN = 1.5 +local GLOW_FREQ_MAX = 4.0 +local GLOW_RADIUS_MIN = 2 +local GLOW_RADIUS_MAX = 5 +local MARGIN = 16 + +local DUST_COLORS = { + {1.0, 0.95, 0.75}, + {0.85, 0.9, 1.0}, + {1.0, 0.85, 0.6}, + {0.75, 0.85, 1.0}, +} + +function DustParticle.new(x, y) + local self = setmetatable({}, DustParticle) + self.x = x + self.y = y + + local angle = math.random() * math.pi * 2 + local speed = DRIFT_SPEED_MIN + math.random() * (DRIFT_SPEED_MAX - DRIFT_SPEED_MIN) + self.dx = math.cos(angle) * speed + self.dy = math.sin(angle) * speed + + self.perpX = -math.sin(angle) + self.perpY = math.cos(angle) + + self.wobbleAmp = WOBBLE_AMP_MIN + math.random() * (WOBBLE_AMP_MAX - WOBBLE_AMP_MIN) + self.wobbleFreq = WOBBLE_FREQ_MIN + math.random() * (WOBBLE_FREQ_MAX - WOBBLE_FREQ_MIN) + self.wobblePhase = math.random() * math.pi * 2 + + self.glowFreq = GLOW_FREQ_MIN + math.random() * (GLOW_FREQ_MAX - GLOW_FREQ_MIN) + self.glowPhase = math.random() * math.pi * 2 + + self.baseAlpha = 0.15 + math.random() * 0.25 + self.color = DUST_COLORS[math.random(#DUST_COLORS)] + self.time = 0 + return self +end + +function DustParticle:update(dt) + self.time = self.time + dt + local wobble = math.sin(self.time * self.wobbleFreq + self.wobblePhase) * self.wobbleAmp + self.x = self.x + (self.dx + self.perpX * wobble * 0.3) * dt + self.y = self.y + (self.dy + self.perpY * wobble * 0.3) * dt +end + +function DustParticle:draw() + local glowT = math.sin(self.time * self.glowFreq + self.glowPhase) + local glowRadius = GLOW_RADIUS_MIN + (GLOW_RADIUS_MAX - GLOW_RADIUS_MIN) * (glowT * 0.5 + 0.5) + local alpha = self.baseAlpha * (0.6 + 0.4 * (glowT * 0.5 + 0.5)) + + love.graphics.setBlendMode("add") + love.graphics.setColor(self.color[1], self.color[2], self.color[3], alpha * 0.4) + love.graphics.circle("fill", self.x, self.y, glowRadius) + love.graphics.setColor(self.color[1], self.color[2], self.color[3], alpha) + love.graphics.circle("fill", self.x, self.y, 1) + love.graphics.setBlendMode("alpha") + love.graphics.setColor(1, 1, 1, 1) +end + +function DustParticle:isOutOfBounds(viewX, viewY, viewW, viewH) + return self.x < viewX - MARGIN or self.x > viewX + viewW + MARGIN + or self.y < viewY - MARGIN or self.y > viewY + viewH + MARGIN +end + +local DustSystem = {} +DustSystem.__index = DustSystem + +function DustSystem.new() + local self = setmetatable({}, DustSystem) + self.particles = {} + self.spawned = false + return self +end + +function DustSystem:update(dt, camX, camY, camW, camH) + if not self.spawned then + for _ = 1, MAX_DUST do + local x = camX + math.random() * camW + local y = camY + math.random() * camH + table.insert(self.particles, DustParticle.new(x, y)) + end + self.spawned = true + end + + for _, p in ipairs(self.particles) do + p:update(dt) + end + + for i = #self.particles, 1, -1 do + local p = self.particles[i] + if p:isOutOfBounds(camX, camY, camW, camH) then + local edge = math.random(4) + local x, y + if edge == 1 then + x = camX - MARGIN * 0.5 + y = camY + math.random() * camH + elseif edge == 2 then + x = camX + camW + MARGIN * 0.5 + y = camY + math.random() * camH + elseif edge == 3 then + x = camX + math.random() * camW + y = camY - MARGIN * 0.5 + else + x = camX + math.random() * camW + y = camY + camH + MARGIN * 0.5 + end + self.particles[i] = DustParticle.new(x, y) + end + end + + while #self.particles < MAX_DUST do + local x = camX + math.random() * camW + local y = camY + math.random() * camH + table.insert(self.particles, DustParticle.new(x, y)) + end +end + +function DustSystem:draw() + for _, p in ipairs(self.particles) do + p:draw() + end +end + +return DustSystem diff --git a/world.lua b/world.lua index a4d7f12..0678804 100644 --- a/world.lua +++ b/world.lua @@ -4,6 +4,8 @@ local Player = require("player") local Camera = require("camera") local Textbox = require("textbox") local Enemy = require("enemy") +local BloodParticle = require("bloodParticle") +local DustSystem = require("dustParticle") local World = {} World.__index = World @@ -39,6 +41,9 @@ function World:new() self.contactKind = {} self.pendingPlayerDeath = nil self.playerTextbox = Textbox:new() + self.bloodParticles = {} + self.bloodSplatters = {} + self.dustSystem = DustSystem.new() return self end @@ -395,6 +400,21 @@ function World:isTileSolidAtPixel(x, y) return gid and gid ~= 0 end +function World:spawnBlood(x, y) + local newParticles = BloodParticle.burst(x, y) + for _, p in ipairs(newParticles) do + table.insert(self.bloodParticles, p) + end +end + +function World:drawBloodSplatters() + for _, s in ipairs(self.bloodSplatters) do + love.graphics.setColor(s.color) + love.graphics.circle("fill", s.x, s.y, s.radius) + end + love.graphics.setColor(1, 1, 1, 1) +end + local function drawTileLayer(layer, mapTileW, mapTileH, tileGidInfo, viewMinX, viewMinY, viewMaxX, viewMaxY) if not layer or not layer.visible or not layer.data then return end local w = layer.width or 0 @@ -485,6 +505,7 @@ function World:draw() love.graphics.setCanvas(self.refractionCanvas) love.graphics.clear(0, 0, 0, 1) drawTileLayer(self.tilemap:getFargroundLayer(), mapTileW, mapTileH, tileGidInfo, viewMinX, viewMinY, viewMaxX, viewMaxY) + self:drawBloodSplatters() drawTileLayer(self.tilemap:getDecorationBackgroundLayer(), mapTileW, mapTileH, tileGidInfo, viewMinX, viewMinY, viewMaxX, viewMaxY) drawTileLayer(self.tilemap:getBackgroundLayer(), mapTileW, mapTileH, tileGidInfo, viewMinX, viewMinY, viewMaxX, viewMaxY) drawTileLayer(self.tilemap:getGroundLayer(), mapTileW, mapTileH, tileGidInfo, viewMinX, viewMinY, viewMaxX, viewMaxY) @@ -494,6 +515,9 @@ function World:draw() for _, e in ipairs(self.entities) do if e.draw then e:draw() else World.drawEntityDefault(self, e) end end + for _, p in ipairs(self.bloodParticles) do + p:draw() + end drawTileLayer(self.tilemap:getForegroundLayer(), mapTileW, mapTileH, tileGidInfo, viewMinX, viewMinY, viewMaxX, viewMaxY) drawTileLayer(self.tilemap:getDecorationForegroundLayer(), mapTileW, mapTileH, tileGidInfo, viewMinX, viewMinY, viewMaxX, viewMaxY) love.graphics.setCanvas(mainCanvas) @@ -544,6 +568,7 @@ function World:draw() drawTileLayer(self.tilemap:getDecorationBackgroundLayer(), mapTileW, mapTileH, tileGidInfo, viewMinX, viewMinY, viewMaxX, viewMaxY) drawTileLayer(self.tilemap:getGroundLayer(), mapTileW, mapTileH, tileGidInfo, viewMinX, viewMinY, viewMaxX, viewMaxY) drawTileLayer(self.tilemap:getFargroundLayer(), mapTileW, mapTileH, tileGidInfo, viewMinX, viewMinY, viewMaxX, viewMaxY) + self:drawBloodSplatters() drawTileLayer(self.tilemap:getDecorationBackgroundLayer(), mapTileW, mapTileH, tileGidInfo, viewMinX, viewMinY, viewMaxX, viewMaxY) love.graphics.setColor(0.25, 0.5, 0.9, 0.6) @@ -569,13 +594,17 @@ function World:draw() if not useRefraction then for _, e in ipairs(self.entities) do - print(e) if e.draw then e:draw() else World.drawEntityDefault(self, e) end end + for _, p in ipairs(self.bloodParticles) do + p:draw() + end drawTileLayer(self.tilemap:getForegroundLayer(), mapTileW, mapTileH, tileGidInfo, viewMinX, viewMinY, viewMaxX, viewMaxY) drawTileLayer(self.tilemap:getDecorationForegroundLayer(), mapTileW, mapTileH, tileGidInfo, viewMinX, viewMinY, viewMaxX, viewMaxY) end + self.dustSystem:draw() + if DEBUG then World.drawPhysicsBodyOutlines(self.entities) World.drawPhysicsBodyOutlines(self.groundEntities) @@ -619,6 +648,9 @@ function World:update(dt) self.physicsWorld:update(PHYSICS_DT) if self.pendingPlayerDeath and self.player then + local px = self.player.x + self.player.width / 2 + local py = self.player.y + self.player.height / 2 + self:spawnBlood(px, py) self.player:die(self.pendingPlayerDeath.nx, self.pendingPlayerDeath.ny) self.pendingPlayerDeath = nil end @@ -659,6 +691,18 @@ function World:update(dt) if spike.update then spike:update(dt) end end + local solidCheck = function(x, y) return self:isTileSolidAtPixel(x, y) end + for i = #self.bloodParticles, 1, -1 do + local p = self.bloodParticles[i] + p:update(dt, solidCheck) + if p.stuck then + table.insert(self.bloodSplatters, { x = p.x, y = p.y, radius = p.radius, color = p.color }) + table.remove(self.bloodParticles, i) + elseif p.kill then + table.remove(self.bloodParticles, i) + 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 @@ -674,6 +718,7 @@ function World:update(dt) if self.camera then self.camera:setTarget(self.player) self.camera:update(dt) + self.dustSystem:update(dt, self.camera.x, self.camera.y, self.camera.width, self.camera.height) end if self.playerTextbox.active and self.player then -- cgit v1.2.3