Skip to content

Instantly share code, notes, and snippets.

@a327ex
Created January 10, 2020 16:26
Show Gist options
  • Select an option

  • Save a327ex/1b2ac2a19cac37617cd9f605a115aec0 to your computer and use it in GitHub Desktop.

Select an option

Save a327ex/1b2ac2a19cac37617cd9f605a115aec0 to your computer and use it in GitHub Desktop.

Revisions

  1. a327ex created this gist Jan 10, 2020.
    828 changes: 828 additions & 0 deletions lmao.lua
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,828 @@
    function init()
    white = {1, 1, 1, 1}
    black = {0, 0, 0, 1}
    red = {1.0, 0.1, 0.2, 1}

    doodle = g.newShader("doodle.frag")
    doodle:send("doodle_max_offset", {0.0020, 0.0020})
    doodle:send("doodle_noise_scale", {6, 8})
    doodle:send("doodle_frame_count", 8)
    doodle:send("doodle_frame_time", 0.2)
    combine = g.newShader("combine.frag")
    doodle_canvas = g.newCanvas(gw, gh)
    game_canvas = g.newCanvas(gw, gh)

    new_animation("hit1", 96, 47)
    new_animation("smoke1", 50, 50, {1})

    player = Player(gw/2, gh - 6)
    projectiles = {}
    effects = {}
    enemies = {}
    ui = {}
    ammo_bars = {}
    hp_bars = {}

    timer:every(2, function()
    table.insert(enemies, Enemy1(rng:float(60, gw - 60), -60))
    end)

    local h = (gh - 55)
    local bh = h/player.max_ammo
    for i = 1, player.max_ammo do table.insert(ammo_bars, UIBar(gw - 24, 50 + (i-1)*(bh), bh - 3, "ammo")) end
    player_ammo_ui = PlayerAmmoUI(gw - 24, 22)

    local hh = (gh/2)/player.max_hp
    for i = 1, player.max_hp do table.insert(hp_bars, UIBar(24, 55 + (i-1)*(hh), hh - 3, "hp")) end
    player_hp_ui = PlayerHPUI(24, 22)
    end

    function update(dt)
    doodle:send("time", time)
    player:update(dt)
    update_objects(projectiles, dt)
    update_objects(enemies, dt)
    update_objects(effects, dt)
    update_objects(ammo_bars, dt)
    update_objects(hp_bars, dt)
    player_ammo_ui:update(dt)
    player_hp_ui:update(dt)
    end

    function draw()
    g.setCanvas(game_canvas)
    g.clear()
    g.setColor(white)
    camera:attach()
    draw_objects(projectiles)
    draw_objects(enemies)
    draw_objects(effects)
    draw_objects(ammo_bars)
    draw_objects(hp_bars)
    player_ammo_ui:draw()
    player_hp_ui:draw()
    player:draw()
    camera:detach()
    g.setCanvas()

    g.setCanvas(doodle_canvas)
    g.clear()
    g.setColor(white)
    g.setShader(doodle)
    g.draw(game_canvas, 0, 0, 0, 1, 1)
    g.setShader()
    g.setCanvas()

    g.setColor(1, 1, 1, 1)
    g.setBlendMode("alpha", "premultiplied")
    g.draw(doodle_canvas, 0, 0, 0, sx, sy)
    g.setBlendMode("alpha")
    end

    Player = Class:extend()

    function Player:new(x, y)
    self.x, self.y = x, y
    self.rs = 16
    self.sx, self.sy = 1, 1
    self.e1x, self.e1y = -self.rs/2.5, -self.rs/2.5
    self.e2x, self.e2y = self.rs/2.5, -self.rs/2.5
    self.e1ox, self.e1oy = 0, 0
    self.e2ox, self.e2oy = 0, 0
    self.r = 0

    self.scale_spring = Spring(1)
    timer:everyi(2, function() timer:tween(1, self, {sx = 1.025, sy = 1.025}, cubic_in, function() timer:tween(1, self, {sx = 1, sy = 1}, linear) end) end)
    self.rso = 0; timer:everyi({0.1, 0.2}, function() self.rso = rng:float(-1, 1) end)

    self.attack_spring = Spring(1)
    self.attack_timer = 0
    self.attack_pulse_timer = 0
    self.attack_cd = 0.06

    self.shape = HC.circle(self.x, self.y, 16)
    self.shape.parent = self

    self.max_ammo = 35
    self.ammo = self.max_ammo
    self.max_hp = 10
    self.hp = self.max_hp

    self.reloading = false
    self.reload_timer = 0
    self.reload_cd = 0.6
    self.reload_spring = Spring(1)
    end

    function Player:update(dt)
    self.scale_spring:update(dt)
    self.attack_spring:update(dt)
    self.reload_spring:update(dt)
    self.s = self.scale_spring.x*self.attack_spring.x*self.reload_spring.x
    self.r = angle_to_mouse(self.x, self.y)

    if m.isDown(1) and not m.isDown(2) then
    self.attack_timer = self.attack_timer + dt
    if self.attack_timer > self.attack_cd and self.ammo > 0 then
    self.attack_timer = 0

    local r = rng:float(-6, 6)
    local v = rng:float(600, 700)
    table.insert(projectiles, Projectile(self.x + self.s*1.5*self.rs*math.cos(self.r) + r*math.cos(self.r + math.pi/2), self.y + self.s*1.5*self.rs*math.sin(self.r) + r*math.sin(self.r + math.pi/2), v*math.cos(self.r), v*math.sin(self.r)))
    self.scale_spring:pull(0.1)
    for i = 1, 4 do table.insert(effects, EllipseParticle(self.x + self.s*1.5*self.rs*math.cos(self.r), self.y + self.s*1.5*self.rs*math.sin(self.r), self.r + rng:float(-math.pi/2, math.pi/2), rng:float(100, 400))) end
    table.insert(effects, ShootCircle(self.x + self.s*1.5*self.rs*math.cos(self.r), self.y + self.s*1.5*self.rs*math.sin(self.r)))
    table.insert(effects, ShootCapsule(self.x + self.s*1.5*self.rs*math.cos(self.r), self.y + self.s*1.5*self.rs*math.sin(self.r), self.r + rng:float(-math.pi/4, math.pi/4), rng:float(100, 300)))

    player_ammo_ui:jiggle()
    ammo_bars[self.ammo]:spend()
    self.ammo = self.ammo - 1
    end

    self.attack_pulse_timer = self.attack_pulse_timer + dt
    if self.attack_pulse_timer > 4*self.attack_cd then
    self.attack_pulse_timer = 0
    self.attack_spring:pull(0.25)
    end
    end

    local r = math.deg(self.r)
    local mx = camera:get_mouse_position()
    local rd = 1
    if mx > self.x then rd = -1 else rd = 1 end

    if mouse_pressed(2) then
    self.reloading = true
    self.reload_spring:pull(0.4)
    self.reload_text = ReloadText(self.x + rd*62, self.y - 14, self.reload_cd, rd*math.pi/32)
    table.insert(effects, self.reload_text)
    end

    if mouse_released(2) then
    self.reloading = false
    self.reload_spring:pull(0.2)
    if self.reload_text then
    self.reload_text:die()
    self.reload_text = nil
    end
    end

    if self.reloading then
    self.reload_timer = self.reload_timer + dt
    if self.reload_text then self.reload_text.t = self.reload_timer/self.reload_cd end
    player_ammo_ui.t = self.reload_timer/self.reload_cd
    if self.reload_timer > self.reload_cd then
    for i = 1, self.max_ammo do ammo_bars[i]:refresh() end
    player_ammo_ui:refresh()
    table.insert(effects, CircleEffect(self.x, self.y, self.s*self.rs))

    self.reload_spring:pull(0.4)
    self.reload_timer = 0
    self.ammo = self.max_ammo
    if self.reload_text then
    self.reload_text:die()
    self.reload_text = nil
    end
    end
    else self.reload_timer = 0 end

    self.shape:moveTo(self.x, self.y)
    end

    function Player:draw()
    push(self.x, self.y, 0, self.s*self.sx, self.s*self.sy)
    circle(self.x, self.y, self.rs + self.rso, 2)
    pop()

    circlef(self.x + self.s*1.5*self.rs*math.cos(self.r), self.y + self.s*1.5*self.rs*math.sin(self.r), 3)
    end

    function Player:hit(damage)
    self.hp = self.hp - 1
    slow(0.5, 0.5)
    flash(2, black)
    end

    PlayerHPUI = Class:extend()

    function PlayerHPUI:new(x, y)
    self.timer = Timer()
    self.x, self.y = x, y
    self.sx, self.sy = 1.25, 1.25
    self.scale_spring = Spring(1)
    end

    function PlayerHPUI:update(dt)
    self.timer:update(dt)
    self.scale_spring:update(dt)
    self.hp = player.hp
    self.max_hp = player.max_hp
    end

    function PlayerHPUI:draw()
    push(self.x, self.y, 0, self.scale_spring.x*self.sx, self.scale_spring.x*self.sy)
    draw_text(self.hp, self.x, self.y, 0, 1, 1, font_medium)
    pop()
    end

    function PlayerHPUI:refresh()
    self.scale_spring:pull(0.5)
    end

    function PlayerHPUI:jiggle()
    self.scale_spring:pull(0.2)
    end

    PlayerAmmoUI = Class:extend()

    function PlayerAmmoUI:new(x, y)
    self.timer = Timer()
    self.x, self.y = x, y
    self.sx, self.sy = 1.25, 1.25
    self.oy = 0
    self.scale_spring = Spring(1)
    self.timer:tween(0.1, self, {sx = 1, sy = 1}, cubic_in, function() self.sx, self.sy = 1, 1 end)
    end

    function PlayerAmmoUI:update(dt)
    self.timer:update(dt)
    self.scale_spring:update(dt)
    self.ammo = player.ammo
    self.max_ammo = player.max_ammo
    end

    function PlayerAmmoUI:draw()
    if player.reloading then
    if player.reload_text then
    if not player.reload_text.visible then return end
    end
    end

    push(self.x, self.y + self.oy, 0, self.scale_spring.x*self.sx, self.scale_spring.x*self.sy)
    draw_text(self.ammo, self.x, self.y + self.oy, 0, 1, 1, font_medium)
    pop()

    if player.reloading then
    local x = self.x
    local y1
    for i = 1, #ammo_bars do
    if ammo_bars[i].spent then
    y1 = ammo_bars[i].y
    break
    end
    end
    local y2 = ammo_bars[#ammo_bars].y
    if y1 then
    g.setLineWidth(4)
    g.line(x, y1, x, y1 + (y2-y1)*self.t)
    g.setLineWidth(1)
    end
    end
    end

    function PlayerAmmoUI:refresh()
    self.t = 0
    self.scale_spring:pull(0.5)
    end

    function PlayerAmmoUI:jiggle()
    self.scale_spring:pull(0.2)
    end

    UIBar = Class:extend()

    function UIBar:new(x, y, h, type)
    self.type = type
    self.timer = Timer()
    self.x, self.y = x, y
    self.sx, self.sy = 1.25, 1.25
    self.w = 16
    self.ow = 16
    self.h = h
    self.spent = false
    self.scale_spring = Spring(1)
    self.timer:tween(0.1, self, {sx = 1, sy = 1}, cubic_in, function() self.sx, self.sy = 1, 1 end, "refreshs")
    end

    function UIBar:update(dt)
    self.timer:update(dt)
    self.scale_spring:update(dt)
    end

    function UIBar:draw()
    if self.type == "ammo" then
    if player.reloading then
    if player.reload_text then
    if not player.reload_text.visible then return end
    end
    end
    end

    push(self.x, self.y, 0, self.sx*self.scale_spring.x, self.sy*self.scale_spring.x)
    if self.fake_spent then rect(self.x, self.y, self.w, self.h, nil, nil, 2, white)
    else rectf(self.x, self.y, self.w, self.h, nil, nil, white) end
    pop()
    end

    function UIBar:refresh()
    self.spent = false
    self.fake_spent = false
    self.scale_spring:pull(0.4)
    self.timer:tween(0.05, self, {w = self.ow}, linear, function() self.w = self.ow end, "refresh")
    end

    function UIBar:spend()
    self.spent = true
    self.scale_spring:pull(0.4)
    self.timer:tween(0.05, self, {w = 0}, linear, function() self.w = 0 end, "spend")
    table.insert(effects, ShootCircle(self.x, self.y))
    table.insert(effects, ShootCapsule(self.x, self.y, rng:float(-math.pi/4, 0), rng:float(50, 150)))
    end

    function UIBar:fake_spend()
    self.fake_spent = true
    self.scale_spring:pull(0.4)
    end

    function UIBar:spend2()
    self.spent = true
    self.scale_spring:pull(0.4)
    table.insert(effects, ShootCircle(self.x, self.y, 24))
    self.timer:tween(0.1, self, {w = 0}, linear, function() self.w = 0 end, "spend")
    for i = 1, 8 do table.insert(effects, DeathParticle(self.x, self.y, rng:float(0, 2*math.pi), rng:float(100, 400))) end
    end

    CircleEffect = Class:extend()

    function CircleEffect:new(x, y, r, color)
    self.x, self.y = x, y
    self.r = r
    self.lw = 8
    self.color = color or white
    timer:tween(0.15, self, {r = 4*self.r, lw = 1}, linear, function() self.dead = true end)
    end

    function CircleEffect:update(dt)

    end

    function CircleEffect:draw()
    circle(self.x, self.y, self.r, self.lw, self.color)
    end

    ReloadText = Class:extend()

    function ReloadText:new(x, y, duration, r)
    self.timer = Timer()
    self.x, self.y = x, y
    self.r = r
    self.sx, self.sy = 1, 1
    self.characters = {}
    self.visuals = {1, 1, 1, 1, 1, 1}
    self.visible = true
    self.font = font_medium
    self.w, self.h = self.font:getWidth("RELOAD"), self.font:getHeight()
    self.scale_spring = Spring(1)
    self.scale_spring:pull(0.15)
    self.t = 0

    local characters = {"R", "E", "L", "O", "A", "D"}
    for i = 1, 6 do
    self.timer:after((i-1)*(0.15/6), function()
    self.visuals[i] = 3
    if self.visuals[i-1] then self.visuals[i-1] = 2 end
    if self.visuals[i-2] then self.visuals[i-2] = 1 end
    table.insert(self.characters, characters[i])
    end)
    end
    self.timer:after(0.15, function()
    self.visuals = {1, 1, 1, 1, 1, 1}
    self.timer:every(0.05, function() self.visible = not self.visible end, math.floor((duration - 0.15)/0.05))
    self.timer:after((duration - 0.15), function() self.visible = true end)
    self.timer:every(0.035, function()
    local random_characters = "0123456789abcdefghijklmnopqrstuvwyxzABCDEFGHIJKLMNOPQRSTUVWYXZ"
    for i, c in ipairs(self.characters) do
    if rng:bool(10) then
    local r = rng:int(1, #random_characters)
    self.characters[i] = random_characters:sub(r, r)
    end
    if rng:bool(10) then self.visuals[i] = rng:int(1, 4) end
    end
    end, math.floor((duration - 0.15)/0.035))
    end)
    end

    function ReloadText:update(dt)
    self.timer:update(dt)
    self.scale_spring:update(dt)
    end

    function ReloadText:draw()
    if not self.visible then return end

    local w, h = 0, 0
    local x, y = self.x - self.w/2, self.y - self.h/2
    g.setFont(self.font)
    push(self.x, self.y, self.r, self.scale_spring.x*self.sx, self.scale_spring.x*self.sy)
    for i = 1, #self.characters do
    local cw, ch = self.font:getWidth(self.characters[i]), self.font:getHeight()
    if self.visuals[i] == 1 then
    g.setColor(white)
    g.print(self.characters[i], x + w, y + h)
    elseif self.visuals[i] == 2 then
    g.setColor(white)
    rectf(x + w + cw/2, y + h + ch/2, cw, ch)
    g.setColor(black)
    g.print(self.characters[i], x + w, y + h)
    elseif self.visuals[i] == 3 then
    g.setColor(white)
    rectf(x + w + cw/2, y + h + ch/2, cw, ch)
    elseif self.visuals[i] == 4 then
    g.setColor(black)
    rectf(x + w + cw/2, y + h + ch/2, cw, ch)
    end
    w = w + self.font:getWidth(self.characters[i])
    end

    g.setColor(white)
    g.rectangle("fill", x, y - 5, self.w*self.t, 3)
    pop()
    end

    function ReloadText:die()
    self.dead = true
    end

    RefreshEffect = Class:extend()

    function RefreshEffect:new(x, y, w, h)
    self.x, self.y = x, y
    self.w, self.h = w, h
    self.oy = h/3
    timer:tween(0.15, self, {h = 0}, linear, function() self.dead = true end)
    end

    function RefreshEffect:update(dt)

    end

    function RefreshEffect:draw()
    g.rectangle("fill", self.x - self.w/2, self.y - self.oy, self.w, self.h)
    end

    Projectile = Class:extend()

    function Projectile:new(x, y, vx, vy, opts)
    for k, v in pairs(opts or {}) do self[k] = v end
    self.x, self.y = x, y
    self.vx, self.vy = vx, vy
    self.sx, self.sy = 1, 1
    self.w, self.h = 16, 4

    self.shape = HC.rectangle(self.x - self.w/2, self.y - self.h/2, self.w, self.h)
    self.shape.parent = self

    self.damage = rng:int(40, 60)
    end

    function Projectile:update(dt)
    self.vy = self.vy + 300*dt
    self.x = self.x + self.vx*dt
    self.y = self.y + self.vy*dt
    self.r = math.atan2(self.vy, self.vx)

    if self.x < 0 then self:die(0) end
    if self.x > gw then self:die(math.pi) end
    if self.y < 0 then self:die(math.pi/2) end
    if self.y > gh then self:die(-math.pi/2) end

    self.shape:moveTo(self.x, self.y)
    self.shape:setRotation(self.r)
    end

    function Projectile:draw()
    push(self.x, self.y, self.r, self.sx, self.sy)
    rectf(self.x, self.y, self.w, self.h, nil, nil, white)
    pop()
    end

    function Projectile:die(r)
    self.dead = true
    for i = 1, 2 do table.insert(effects, EllipseParticle(self.x, self.y, (r or math.atan2(-self.vy, -self.vx)) + rng:float(-math.pi/4, math.pi/4), rng:float(100, 300))) end
    table.insert(effects, ShootCircle(self.x, self.y))
    end

    Enemy1 = Class:extend()

    function Enemy1:new(x, y)
    self.timer = Timer()
    self.x, self.y = x, y
    self.w = 16
    self.r = 0
    self.sx, self.sy = 1, 1
    self.d = 1
    self.v = 100
    self.vs = {}
    local r = 0
    for i = 1, 8 do
    local w = rng:float(0.95, 1.05)
    self.vs[2*(i-1)+1] = self.w*w*math.cos(r)
    self.vs[2*i] = self.w*w*math.sin(r)
    r = r + math.pi/4
    end
    self.vr = rng:float(-4*math.pi, 4*math.pi)

    self.shape = HC.polygon(unpack(to_polygon(self.x, self.y, self.vs)))
    self.shape.parent = self

    self.hp = 800
    end

    function Enemy1:update(dt)
    self.timer:update(dt)
    self.y = self.y + self.d*self.v*dt
    self.r = self.r + self.vr*dt

    self.shape:moveTo(self.x, self.y)
    self.shape:setRotation(self.r)

    for other in pairs(HC.neighbors(self.shape)) do
    if other.parent:is(Projectile) then
    local collides = self.shape:collidesWith(other)
    if collides then
    self:hit(other.parent.damage, other.parent.r)
    other.parent:die()
    table.insert(effects, HitEffect(other.parent.x, other.parent.y))
    end
    end
    end

    if self.y > gh then
    self.fast = true
    self.d = -1
    self.v = 150
    self.y = gh - 2
    table.insert(effects, DeathCircle(self.x, self.y, 2*self.w, red))
    table.insert(effects, CircleEffect(self.x, self.y, self.w, red))
    for i = player.max_hp, 1, -1 do
    if not hp_bars[i].spent and not hp_bars[i].fake_spent then
    hp_bars[i]:fake_spend()
    break
    end
    end

    flash(1, black)
    end

    if self.y < 0 and self.fast then
    self.dead = true
    player:hit(1)
    table.insert(effects, DeathCircle(self.x, self.y, 2*self.w, red))
    table.insert(effects, CircleEffect(self.x, self.y, self.w, red))
    for i = player.max_hp, 1, -1 do
    if hp_bars[i].fake_spent and not hp_bars[i].spent then
    hp_bars[i]:spend2()
    break
    end
    end
    end
    end

    function Enemy1:draw()
    if self.y > gh + 64 then return end

    local color = red
    if self.hit_flash then color = white end
    push(self.x, self.y, self.r, self.sx, self.sy)
    polygon(to_polygon(self.x, self.y, self.vs), 2, color)
    pop()
    end

    function Enemy1:hit(damage, r)
    self.sx, self.sy = 1.35, 1.35
    self.hit_flash = true
    self.timer:tween(0.1, self, {sx = 1, sy = 1}, linear, function() self.hit_flash = false end, "hit")

    self.hp = self.hp - damage
    if self.hp <= 0 then
    self.dead = true
    table.insert(effects, DeathCircle(self.x, self.y, 2*self.w))
    for i = 1, 4 do table.insert(effects, DustParticle(self.x, self.y, white)) end
    if self.fast then
    for i = 1, player.max_hp do
    if hp_bars[i].fake_spent then
    hp_bars[i]:refresh()
    break
    end
    end
    end
    end
    end

    DamageNumber = Class:extend()

    function DamageNumber:new(x, y, vx, vy, t)
    self.x, self.y = x, y
    self.sx, self.sy = 1.00, 1.00
    self.vx, self.vy = vx, vy
    self.t = t
    self.scale_spring = Spring(1)
    self.scale_spring:pull(0.25)
    self.r = 0
    -- if vx > 0 then self.vr = rng:float(2*math.pi, 4*math.pi) else self.vr = rng:float(-4*math.pi, -2*math.pi) end

    timer:after(0.25, function()
    timer:tween(0.05, self, {sx = 0, sy = 0}, linear, function() self.dead = true end)
    end)
    end

    function DamageNumber:update(dt)
    self.scale_spring:update(dt)
    self.vy = self.vy + 400*dt
    self.x = self.x + self.vx*dt
    self.y = self.y + self.vy*dt
    -- self.r = self.r + self.vr*dt
    end

    function DamageNumber:draw()
    push(self.x, self.y, self.r, 1.25*self.scale_spring.x*self.sx, 1.25*self.scale_spring.x*self.sy)
    draw_text(self.t, self.x, self.y, 0, 1, 1, font_small)
    pop()
    end

    HitEffect = Class:extend()

    function HitEffect:new(x, y)
    self.x, self.y = x, y
    self.r = rng:float(0, 2*math.pi)
    self.animation = Animation(0.025, get_animation_frames("hit1"), "once", {[0] = function() self.dead = true end})
    self.sx, self.sy = 1.2, 1.2
    timer:tween(0.025*get_animation_frames("hit1"), self, {sx = 1, sy = 1}, linear)
    end

    function HitEffect:update(dt)
    self.animation:update(dt)
    end

    function HitEffect:draw()
    draw_animation("hit1", self.animation:get_current_frame(), self.x, self.y, self.r, 1.35*self.sx, 1.35*self.sy)
    end

    DustParticle = Class:extend()

    function DustParticle:new(x, y, color)
    self.x, self.y = x, y
    self.animation = Animation(1, get_animation_frames("smoke1"), "once")
    self.color = color or red
    self.sx, self.sy = 0, 0
    self.v = rng:float(100, 220)
    self.r = rng:float(0, 2*math.pi)
    self.rs = 0
    self.vr = rng:float(0, 2*math.pi)

    timer:after(0.1, function() self.color = color or white end)
    timer:tween(rng:float(0.04, 0.06), self, {sx = 0.7, sy = 0.7}, cubic_in_out, function()
    timer:tween(rng:float(0.3, 0.4), self, {sx = 0, sy = 0, v = 0}, linear, function() self.dead = true end)
    end)
    end

    function DustParticle:update(dt)
    self.animation:update(dt)
    self.rs = self.rs + self.vr*dt
    self.x, self.y = self.x + self.v*math.cos(self.r)*dt, self.y + self.v*math.sin(self.r)*dt
    end

    function DustParticle:draw()
    g.setShader(combine)
    g.setColor(self.color)
    draw_animation("smoke1", self.animation:get_current_frame(), self.x, self.y, self.rs, 3.5*self.sx, 3.5*self.sy)
    g.setShader()
    g.setColor(white)
    end

    AnimatedEffect = Class:extend()

    function AnimatedEffect:new(x, y, name, delay, loop_mode)
    self.x, self.y = x, y
    self.r, self.sx, self.sy = 0, 1, 1
    self.name = name
    self.delay = delay
    self.loop_mode = loop_mode or "once"
    self.animation = Animation(delay, get_animation_frames(name), self.loop_mode, {[0] = function() self.dead = true end})
    end

    function AnimatedEffect:update(dt)
    self.animation:update(dt)
    end

    function AnimatedEffect:draw()
    draw_animation(self.name, self.animation:get_current_frame(), self.x, self.y, self.r, self.sx, self.sy)
    end

    EllipseParticle = Class:extend()

    function EllipseParticle:new(x, y, r, v)
    self.x, self.y = x, y
    self.r = r
    self.v = v
    self.w, self.h = 9, 3
    timer:tween({0.2, 0.5}, self, {v = 0}, linear, function() self.dead = true end)
    end

    function EllipseParticle:update(dt)
    self.x = self.x + self.v*math.cos(self.r)*dt
    self.y = self.y + (self.v*math.sin(self.r) + 100)*dt
    self.w = remap(self.v, 0, 400, 0, 9)
    self.h = remap(self.v, 0, 400, 0, 3)
    end

    function EllipseParticle:draw()
    push(self.x, self.y, math.atan2(self.v*math.sin(self.r) + 100, self.v*math.cos(self.r)))
    ellipsef(self.x, self.y, self.w, self.h)
    pop()
    end

    DeathCircle = Class:extend()

    function DeathCircle:new(x, y, r, color)
    self.x, self.y = x, y
    self.r = r
    self.color = color or white
    timer:tween(0.26, self, {r = 0}, cubic_in_out, function() self.dead = true end)
    end

    function DeathCircle:update(dt)

    end

    function DeathCircle:draw()
    circlef(self.x, self.y, self.r, self.color)
    end

    DeathParticle = Class:extend()

    function DeathParticle:new(x, y, r, v, color)
    self.x, self.y = x, y
    self.r = r
    self.v = v
    self.w, self.h = 14, 4.5
    self.color = color or white
    timer:tween({0.2, 0.4}, self, {v = 0}, linear, function() self.dead = true end)
    end

    function DeathParticle:update(dt)
    self.x = self.x + self.v*math.cos(self.r)*dt
    self.y = self.y + (self.v*math.sin(self.r) + 0)*dt
    self.w = remap(self.v, 0, 400, 0, 14)
    self.h = remap(self.v, 0, 400, 0, 4.5)
    end

    function DeathParticle:draw()
    push(self.x, self.y, math.atan2(self.v*math.sin(self.r) + 0, self.v*math.cos(self.r)))
    rectf(self.x, self.y, self.w, self.h, nil, nil, self.color)
    pop()
    end

    ShootCircle = Class:extend()

    function ShootCircle:new(x, y, rs)
    self.x, self.y = x, y
    self.rs = rs or 12
    timer:tween(0.1, self, {rs = 0}, linear, function() self.dead = true end)
    end

    function ShootCircle:update(dt)

    end

    function ShootCircle:draw()
    circlef(self.x, self.y, self.rs)
    end

    ShootCapsule = Class:extend()

    function ShootCapsule:new(x, y, r, v)
    self.x, self.y = x, y
    self.vx, self.vy = v*math.cos(r), v*math.sin(r)
    self.w, self.h = 6, 3
    self.r = 0
    self.vr = rng:float(-4*math.pi, 4*math.pi)
    end

    function ShootCapsule:update(dt)
    self.vy = self.vy + 600*dt
    self.x = self.x + self.vx*dt
    self.y = self.y + self.vy*dt
    self.r = self.r + self.vr*dt

    if self.y > gh or self.y < 0 or self.x > gw or self.x < 0 then
    self.dead = true
    table.insert(effects, ShootCircle(self.x, self.y))
    end
    end

    function ShootCapsule:draw()
    push(self.x, self.y, self.r)
    rectf(self.x, self.y, self.w, self.h)
    pop()
    end