Skip to content

Instantly share code, notes, and snippets.

@a327ex
Created January 31, 2020 14:55
Show Gist options
  • Save a327ex/5be6cd49d2b75b8d983b2986ca936f1c to your computer and use it in GitHub Desktop.
Save a327ex/5be6cd49d2b75b8d983b2986ca936f1c to your computer and use it in GitHub Desktop.
function init()
white = {1, 1, 1, 1}
black = {0, 0, 0, 1}
red = {1.0, 0.1, 0.2, 1}
combine = g.newShader("combine.frag")
aesthetic = g.newShader("aesthetic.frag")
aesthetic_canvas = g.newCanvas(gw, gh)
displacement_canvas = g.newCanvas(gw, gh)
game_canvas = g.newCanvas(gw, gh)
shockwave = g.newImage("res/shockwave_displacement.png")
new_animation("hit1", 96, 47)
new_animation("smoke1", 50, 50, {1})
new_animation("firehit1", 200, 200)
new_animation("firehit2", 200, 200)
new_animation("firehit3", 200, 200)
new_animation("radial1", 200, 200)
new_animation("radial2", 200, 200)
new_animation("fire1", 192, 108, {1, 3, 5, 7, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19})
new_animation("disappear1", 100, 100)
new_animation("disappear2", 100, 100)
new_animation("transition1", 480, 270)
new_animation("transition2", 480, 270)
new_animation("transition3", 480, 270)
new_animation("transition4", 480, 270)
displacements = {}
projectiles = {}
effects = {}
enemies = {}
ui = {}
ammo_bars = {}
hp_bars = {}
ui_effects = {}
cover = {}
x1, x2 = gw/2 - gw/4, gw/2 + gw/4
y1, y2 = 0, gh
w = x2 - x1
h = y2 - y1
director = Director()
timer:every({0.1, 0.8}, function() table.insert(displacements, DisplacementBlock(rng:float(x1, x2), rng:float(y1, y2), rng:float(w/8, w/2), rng:float(h/16, h/8), 0.05, 0.2, rng:float(1, 4))) end)
timer:after(3, function()
table.insert(cover, BlackRectangle(0, 0, x1, y2))
table.insert(cover, BlackRectangle(x2, 0, gw-x2, y2))
table.insert(ui, UILine(40, x1, y1 - 64, x1, y2 + 64))
table.insert(ui, UILine(40, x2, y1 - 64, x2, y2 + 64))
main_menu()
end)
end
function main_menu()
timer:after(1, function()
local x, y = x1 + w/2, h/2 + 20
table.insert(effects, RuneWord(x1 + 1.5*w/10, y1 + 100, w/10, 110, "RUNEHACK"))
timer:after(0.25, function()
table.insert(effects, RuneButton(x, y, 60, "new_run", 0, -1, x, y + 57, function() to_new_run() end))
timer:after(0.25, function()
table.insert(effects, RuneButton(x - 30, y + 90, 60, "options", -1, 0, x, y + 57, function() to_options() end))
timer:after(0.25, function()
table.insert(effects, RuneButton(x + 30, y + 90, 60, "quit", 1, 0, x, y + 57, function() to_quit() end))
end)
end)
end)
end)
end
function to_new_run()
table.insert(effects, AnimatedEffect(gw/2, gh/2, "transition1", 0.02, "once", 0, 2, 2, nil, 10))
displace_screen(0.04, get_animation_frames("transition1")/2)
timer:after(0.03*get_animation_frames("transition1"), function()
for _, o in ipairs(effects) do if o:is(RuneButton) or o:is(RuneWord) then o.dead = true end end
for _, o in ipairs(ui) do if o:is(RuneText) then o.dead = true end end
table.insert(effects, AnimatedEffect(gw/2, gh/2, "transition2", 0.02, "once", 0, 2, 2, nil, 10))
displace_screen(0.04, get_animation_frames("transition2")/2)
timer:after(0.02*math.floor(get_animation_frames("transition2")/2), function()
new_run()
end)
end)
end
function new_run()
create_stage_objects()
director:new_round(1, 8, "passives")
player.can_shoot = true
end
function create_stage_objects()
if not player then player = Player(gw/2, gh - 6) end
local h = (gh - 55)
local bh = h/player.max_ammo
for i = 1, player.max_ammo do
timer:after((i-1)*0.03, function()
table.insert(ammo_bars, UIBar(x2 + 24, 55 + (i-1)*(bh), bh - 4, "ammo"))
end)
end
player_ammo_ui = PlayerAmmoUI(x2 + 24, 22)
local hh = (gh/2.5)/player.max_hp
for i = 1, player.max_hp do
timer:after((i-1)*0.045, function()
table.insert(hp_bars, UIBar(x1 - 24, 55 + (i-1)*(hh), hh - 4, "hp"))
end)
end
player_hp_ui = PlayerHPUI(x1 - 24, 22)
timer:after(0.045*(player.max_hp + 1), function() player_resource_ui = PlayerResourceUI(x1 - 24, 22 + gh/2) end)
end
function destroy_stage_objects()
player.can_shoot = false
player.visible = false
for i = 1, #ammo_bars do ammo_bars[i]:spend() end
for i = 1, #hp_bars do hp_bars[i]:spend2() end
player_ammo_ui:destroy()
player_hp_ui:destroy()
player_resource_ui:destroy()
ammo_bars = {}
hp_bars = {}
player_ammo_ui = nil
player_hp_ui = nil
player_resource_ui = nil
end
function to_reward(reward_type, round_info)
table.insert(effects, AnimatedEffect(gw/2, gh/2, "transition3", 0.02, "stay", 0, 2, 2, nil, 10, 3.5))
displace_screen(0.04, get_animation_frames("transition3")/2)
timer:after(0.03*get_animation_frames("transition3"), function()
local info_texts = {}
table.insert(info_texts, "ROUND SUMMARY")
table.insert(info_texts, "ENEMIES KILLED: " .. round_info.enemies)
table.insert(info_texts, "HP LOST: " .. round_info.hp)
table.insert(info_texts, "RESOURCES GAINED: " .. round_info.resources)
for i = 1, 4 do
timer:after((i-1)*0.25, function()
table.insert(ui, TextLine(x1 + 40, y1 + 100 + i*font_medium:getHeight(), info_texts[i], font_medium, black, white))
end)
end
destroy_stage_objects()
timer:after(2.5, function()
table.insert(effects, AnimatedEffect(gw/2, gh/2, "transition4", 0.02, "once", 0, 2, 2, nil, 10))
displace_screen(0.04, get_animation_frames("transition4")/2)
timer:after(0.02*math.floor(get_animation_frames("transition4")/2), function()
for _, o in ipairs(ui) do if o:is(TextLine) then o.dead = true end end
reward(reward_type)
end)
end)
end)
end
function displace_screen(interval, amount)
for i = 1, amount do
timer:after((i-1)*interval, function()
table.insert(displacements, DisplacementBlock(rng:float(x1, x2), rng:float(y1, y2), rng:float(w/4, w/2), rng:float(h/16, h/8), 0.05, 0.2, rng:float(1, 4)))
end)
end
end
function reward(type)
local passives = {"double", "triple", "volley", "spread", "burst", "homing", "speed", "accel", "decel", "ricochet", "scatter", "split", "chain", "pierce", "fork", "cross", "blast", "burn", "finale", "weaken", "glitch", "slow", "haste", "stun", "sphere", "spawner"}
if type == "passives" then
local x, y = x1 + w/2, h/2 + 20
local p1 = table.remove(passives, rng:int(1, #passives))
local p2 = table.remove(passives, rng:int(1, #passives))
local p3 = table.remove(passives, rng:int(1, #passives))
timer:after(0.25, function()
table.insert(effects, PassiveRuneButton(x, y, 60, p1, x, y + 57, function() choose_passive(p1) end))
timer:after(0.25, function()
table.insert(effects, PassiveRuneButton(x - 30, y + 90, 60, p2, x, y + 57, function() choose_passive(p2) end))
timer:after(0.25, function()
table.insert(effects, PassiveRuneButton(x + 30, y + 90, 60, p3, x, y + 57, function() choose_passive(p3) end))
end)
end)
end)
end
end
function choose_passive(type)
table.insert(effects, AnimatedEffect(gw/2, gh/2, "transition1", 0.02, "once", 0, 2, 2, nil, 10))
for i = 1, get_animation_frames("transition1") do timer:after((i-1)*0.02, function() table.insert(displacements, DisplacementBlock(rng:float(x1, x2), rng:float(y1, y2), rng:float(0, x2-x1), rng:float(0, y2-y1), 0.05, 0.2, rng:float(1, 4))) end) end
timer:after(0.25, function() table.insert(ui, TransitionRune(x1 + w/2, y1 + h/2, w/2, w/2, type)) end)
timer:after(0.03*get_animation_frames("transition1"), function()
for _, o in ipairs(effects) do if o:is(PassiveRuneButton) then o.dead = true end end
for _, o in ipairs(ui) do if o:is(RuneText) then o.dead = true end end
table.insert(effects, AnimatedEffect(gw/2, gh/2, "transition2", 0.02, "once", 0, 2, 2, nil, 10))
for i = 1, get_animation_frames("transition2") do timer:after((i-1)*0.02, function() table.insert(displacements, DisplacementBlock(rng:float(x1, x2), rng:float(y1, y2), rng:float(0, x2-x1), rng:float(0, y2-y1), 0.05, 0.2, rng:float(1, 4))) end) end
timer:after(0.02*math.floor(get_animation_frames("transition2")/2), function()
for _, o in ipairs(ui) do if o:is(TransitionRune) then o.dead = true end end
player.mods[type] = true
player:reset()
create_stage_objects()
director:new_round(director.difficulty + 1, 8, "passives")
end)
end)
end
Director = Class:extend()
function Director:new()
self.difficulty_points = {}
self.difficulty_points[1] = 12
for i = 2, 1024, 4 do
self.difficulty_points[i] = self.difficulty_points[i-1] + 4
self.difficulty_points[i+1] = self.difficulty_points[i]
self.difficulty_points[i+2] = math.floor(self.difficulty_points[i+1]/1.5)
self.difficulty_points[i+3] = math.floor(self.difficulty_points[i+2]*2)
end
self.round_timer = 0
self.running = false
self.round_killed_enemies = 0
self.round_hp_lost = 0
self.round_resources_gained = 0
end
function Director:update(dt)
if not self.running then return end
self.round_timer = self.round_timer + dt
for i, e in ipairs(self.enemy_spawn_times) do
if self.round_timer > e then
table.insert(enemies, _G[self.enemy_list[i].name](rng:float(x1 + 32, x2 - 32), y1 - 32, self.enemy_list[i].hp_multiplier or 1))
table.remove(self.enemy_spawn_times, i)
break
end
end
if self.round_timer > self.round_duration then
self.round_timer = math.min(self.round_timer, self.round_duration)
self.round_over = true
end
if self.round_over and #enemies <= 0 then
self.round_over = nil
self.running = false
self.round_timer = 0
local round_info = {enemies = self.round_killed_enemies, hp = self.round_hp_lost, resources = self.round_resources_gained}
self.round_killed_enemies = 0
self.round_hp_lost = 0
self.round_resources_gained = 0
timer:after(0.5, function()
to_reward(self.round_reward, round_info)
end)
end
end
function Director:draw()
if not self.running then return end
g.setLineWidth(6)
g.line(x1 + (x2-x1)*(self.round_timer/self.round_duration), y1, x2, y1)
g.setLineWidth(2)
for _, e in ipairs(self.enemy_spawn_times) do
g.line(x1 + (x2-x1)*(e/self.round_duration), y1, x1 + (x2-x1)*(e/self.round_duration), y1 + 6)
end
end
function Director:new_round(difficulty, duration, reward)
self.running = true
self.difficulty = difficulty
self.round_duration = duration
self.round_difficulty = difficulty
self.round_reward = reward
local points = self.difficulty_points[difficulty]
self.enemy_list = {}
while points > 0 do
local enemy, points_spent = self:get_next_enemy(difficulty, points)
points = points - points_spent
table.insert(self.enemy_list, enemy)
end
self.enemy_spawn_times = {}
for i = 1, #self.enemy_list do self.enemy_spawn_times[i] = rng:float(0, duration) end
table.sort(self.enemy_spawn_times, function(a, b) return a < b end)
end
function Director:get_next_enemy(difficulty, points)
if difficulty == 1 then
return {name = "Rock", hp_multiplier = 1}, 2
elseif difficulty == 2 then
return {name = "Rock", hp_multiplier = 1.5}, 2
elseif difficulty == 3 then
return {name = "Rock", hp_multiplier = 2}, 2
end
end
function Director:killed_enemy()
self.round_killed_enemies = self.round_killed_enemies + 1
end
function Director:lost_hp()
self.round_hp_lost = self.round_hp_lost + 1
end
function Director:gained_resource(n)
self.round_resources_gained = self.round_resources_gained + n
end
function to_options()
end
function to_quit()
end
function update(dt)
if player then player:update(dt) end
update_objects(displacements, dt)
update_objects(projectiles, dt)
update_objects(enemies, dt)
update_objects(effects, dt)
update_objects(cover, dt)
update_objects(ammo_bars, dt)
update_objects(hp_bars, dt)
update_objects(ui_effects, dt)
update_objects(ui, dt)
if player_ammo_ui then player_ammo_ui:update(dt) end
if player_hp_ui then player_hp_ui:update(dt) end
if player_resource_ui then player_resource_ui:update(dt) end
director:update(dt)
end
function draw()
g.setCanvas(displacement_canvas)
g.clear()
g.setColor(0.5, 0.5, 0.5, 1)
g.rectangle("fill", 0, 0, gw, gh)
draw_objects(displacements)
g.setCanvas()
g.setCanvas(game_canvas)
g.clear()
g.setColor(white)
camera:attach()
draw_objects(projectiles)
draw_objects(enemies)
table.sort(effects, function(a, b) return (a.z or 0) < (b.z or 0) end)
draw_objects(effects)
if player then player:draw() end
draw_objects(cover)
draw_objects(ammo_bars)
draw_objects(hp_bars)
if player_ammo_ui then player_ammo_ui:draw() end
if player_hp_ui then player_hp_ui:draw() end
if player_resource_ui then player_resource_ui:draw() end
draw_objects(ui_effects)
table.sort(ui, function(a, b) return (a.z or 0) < (b.z or 0) end)
draw_objects(ui)
director:draw()
camera:detach()
g.setCanvas()
-- Apply aesthetic
g.setCanvas(aesthetic_canvas)
g.clear()
aesthetic:send("displacement_map", displacement_canvas)
g.setShader(aesthetic)
draw_canvas(game_canvas, 0, 0, 0, 1, 1)
g.setShader()
g.setColor(white)
g.setCanvas()
draw_canvas(aesthetic_canvas, 0, 0, 0, sx, sy)
end
PassiveRuneButton = Class:extend()
function PassiveRuneButton:new(x, y, w, type, px, py, action)
self.x, self.y = x, y
self.px, self.py = px or x, py or y
self.w = w
self.type = type
self.vs = {-w/2, -w/2, 0, -2*w/2, w/2, -w/2, w/2, w/2, 0, 2*w/2, -w/2, w/2}
self.scale_spring = Spring(1)
self.polygon = to_polygon(self.x, self.y, self.vs)
self.visible = true
self.scale_spring:pull(0.3)
self.lw = 6
self.action = action
timer:tween(0.25, self, {lw = 2}, linear)
timer:everyi(0.05, function() self.visible = not self.visible end, 10, function() self.visible = true end)
self:displace()
end
function PassiveRuneButton:update(dt)
self.scale_spring:update(dt)
local mx, my = camera:get_mouse_position()
if mlib.polygon.checkPoint(mx, my, self.polygon) then
if not self.hot then
self.hot = true
self.scale_spring:pull(0.25)
timer:everyi(0.05, function() self.visible = not self.visible end, 3, function() self.visible = true end)
self.rune_text = RuneText(x1 + 40, y1 + 40, self.type)
table.insert(ui, self.rune_text)
self:displace()
end
else
if self.hot then
self.hot = false
self.scale_spring:pull(0.1)
timer:everyi(0.05, function() self.visible = not self.visible end, 6, function() self.visible = true end)
self.rune_text:die()
self.rune_text = nil
else
if self.rune_text then
self.rune_text:die()
self.rune_text = nil
end
end
end
if self.hot and mouse_pressed(1) then
self.scale_spring:pull(0.3)
self.action()
end
end
function PassiveRuneButton:draw()
if not self.visible then return end
push(self.px, self.py, 0, self.scale_spring.x, self.scale_spring.x)
if self.hot then
self.z = 1
polygonf(self.polygon, white)
polygon(self.polygon, 3*self.lw, black)
draw_rune(self.x, self.y, self.w, self.w, self.type, 4, black)
else
self.z = 0
polygon(self.polygon, self.lw, color)
draw_rune(self.x, self.y, self.w, self.w, self.type, nil, white)
end
pop()
g.setColor(white)
end
function PassiveRuneButton:displace(m)
for i = 1, rng:int(4, 6) do
table.insert(displacements, DisplacementBlock(self.x, self.y + rng:float(-0.75*self.w, 0.75*self.w), rng:float(0.75*self.w, 2*self.w), rng:float(0.1*self.w, 0.25*self.w), 0.05*(m or 1), 0.25*(m or 1)))
end
end
RuneButton = Class:extend()
function RuneButton:new(x, y, w, type, tx, ty, px, py, action)
self.x, self.y = x, y
self.px, self.py = px or x, py or y
self.w = w
self.tx, self.ty = tx, ty
self.type = type
self.vs = {-w/2, -w/2, 0, -2*w/2, w/2, -w/2, w/2, w/2, 0, 2*w/2, -w/2, w/2}
self.scale_spring = Spring(1)
self.polygon = to_polygon(self.x, self.y, self.vs)
self.visible = true
self.scale_spring:pull(0.3)
self.lw = 6
self.action = action
timer:tween(0.25, self, {lw = 2}, linear)
timer:everyi(0.05, function() self.visible = not self.visible end, 10, function() self.visible = true end)
self:displace()
end
function RuneButton:update(dt)
self.scale_spring:update(dt)
local mx, my = camera:get_mouse_position()
if mlib.polygon.checkPoint(mx, my, self.polygon) then
if not self.hot then
self.hot = true
self.scale_spring:pull(0.25)
timer:everyi(0.05, function() self.visible = not self.visible end, 3, function() self.visible = true end)
local w, h
h = font_medium:getHeight()
if self.type == "new_run" then w = font_medium:getWidth("NEW RUN")
elseif self.type == "options" then w = font_medium:getWidth("OPTIONS")
elseif self.type == "quit" then w = font_medium:getWidth("QUIT") end
local x, y
if self.tx == -1 then x = self.x - self.w - w + w/4
else x = self.x + self.tx*self.w - w/2 end
self.rune_text = RuneText(x, self.y + self.ty*1.5*self.w - h/2, self.type)
table.insert(ui, self.rune_text)
self:displace()
end
else
if self.hot then
self.hot = false
self.scale_spring:pull(0.1)
timer:everyi(0.05, function() self.visible = not self.visible end, 6, function() self.visible = true end)
self.rune_text:die()
self.rune_text = nil
else
if self.rune_text then
self.rune_text:die()
self.rune_text = nil
end
end
end
if self.hot and mouse_pressed(1) then
self.scale_spring:pull(0.3)
self.action()
end
end
function RuneButton:draw()
if not self.visible then return end
push(self.px, self.py, 0, self.scale_spring.x, self.scale_spring.x)
if self.hot then
self.z = 1
polygonf(self.polygon, white)
polygon(self.polygon, 3*self.lw, black)
draw_rune(self.x, self.y, self.w, self.w, self.type, 4, black)
else
self.z = 0
polygon(self.polygon, self.lw, color)
draw_rune(self.x, self.y, self.w, self.w, self.type, nil, white)
end
pop()
g.setColor(white)
end
function RuneButton:displace(m)
for i = 1, rng:int(4, 6) do
table.insert(displacements, DisplacementBlock(self.x, self.y + rng:float(-0.75*self.w, 0.75*self.w), rng:float(0.75*self.w, 2*self.w), rng:float(0.1*self.w, 0.25*self.w), 0.05*(m or 1), 0.25*(m or 1)))
end
end
UILine = Class:extend()
function UILine:new(s, x1, y1, x2, y2)
self.points = {}
self.visible = true
timer:after(0.1, function() timer:everyi(0.075, function() self.visible = not self.visible end, 5, function() self.visible = true end) end)
local j = 0
local d = distance(x1, y1, x2, y2)
local r = math.atan2(y2 - y1, x2 - x1)
local n = math.ceil(d/s)
local j = 0
for i = 1, n do
timer:after(j*0.04, function()
table.insert(self.points, x1 + (i-1)*s*math.cos(r))
table.insert(self.points, y1 + (i-1)*s*math.sin(r))
end)
j = j + 1
end
end
function UILine:update(dt)
end
function UILine:draw()
if not self.visible then return end
if #self.points >= 4 then
g.setLineWidth(2)
g.line(self.points)
g.setLineWidth(1)
end
end
Player = Class:extend()
function Player:new(x, y)
self.timer = Timer()
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.last_r = 0
self.scale_spring = Spring(1)
self.timer:everyi(2, function() self.timer:tween(1, self, {sx = 1.025, sy = 1.025}, cubic_in, function() self.timer:tween(1, self, {sx = 1, sy = 1}, linear) end) end)
self.rso = 0; self.timer:everyi({0.1, 0.2}, function() self.rso = rng:float(-1, 1) end)
self.visible = true
self.attack_spring = Spring(1)
self.attack_timer = 100
self.attack_pulse_timer = 0
self.attack_cd = 0.15
self.shape = HC.circle(self.x, self.y, 16)
self.shape.parent = self
self.base_ammo = 15
self.max_ammo = self.base_ammo
self.ammo = self.max_ammo
self.max_hp = 10
self.hp = self.max_hp
self.resource = 0
self.can_shoot = false
self.reloading = false
self.reload_timer = 0
self.reload_cd = 0.6
self.reload_spring = Spring(1)
self.ammo_spend_accumulator = 0
self.mods = {}
self.mods.double = false
self.mods.triple = false
self.mods.volley = false
self.mods.spread = false
self.mods.burst = false
self.burst_amount = 3
self.mods.homing = false
self.mods.speed = false
self.mods.accel = false
self.mods.decel = false
self.mods.ricochet = false
self.ricochet_amount = 2
self.mods.scatter = false
self.scatter_amount = 1
self.mods.split = false
self.mods.chain = false
self.chain_amount = 3
self.mods.pierce = false
self.pierce_amount = 1
self.mods.fork = false
self.mods.cross = false
self.mods.blast = false
self.blast_meter = 0
self.blast_cd = 4
self.blast_timer = 4
self.mods.burn = false
self.mods.finale = false
self.finale_count = 0
self.finale_amount = 5
self.mods.weaken = false
self.mods.glitch = false
self.mods.slow = false
self.mods.haste = false
self.mods.stun = false
self.mods.sphere = false
self.mods.spawner = false
end
function Player:reset()
self.reloading = false
self.visible = true
self.ammo = self.max_ammo
self.can_shoot = true
end
function Player:change_resource(n)
self.resource = self.resource + n
player_resource_ui:jiggle()
director:gained_resource(n)
end
function Player:spend_ammo(a)
if self.ammo <= 0 then return end
self.ammo_spend_accumulator = self.ammo_spend_accumulator + a
while self.ammo_spend_accumulator >= 1 and self.ammo > 0 do
self.ammo_spend_accumulator = self.ammo_spend_accumulator - 1
ammo_bars[self.ammo]:spend()
self.ammo = self.ammo - 1
if self.ammo < 0 then self.ammo = 0 end
player_ammo_ui:jiggle()
end
return true
end
function Player:shoot(x, y)
local mods = copy(self.mods)
local v = rng:float(600, 700)
local lo = rng:float(-4, 4)
local r = self.r
if self.mods.spread then r = r + rng:float(-math.pi/16, math.pi/16) end
if self.mods.sphere then
table.insert(effects, CircleEffect(x + lo*math.cos(r + math.pi/2), y + lo*math.sin(r + math.pi/2), 8))
self.attack_spring:pull(0.25)
end
for i = 1, 4 do table.insert(effects, EllipseParticle(x, y, self.r + rng:float(-math.pi/2, math.pi/2), rng:float(100, 400))) end
table.insert(effects, ShootCapsule(x, y, self.r + rng:float(-math.pi/4, math.pi/4), rng:float(100, 300)))
table.insert(effects, ShootCircle(x + lo*math.cos(r + math.pi/2), y + lo*math.sin(r + math.pi/2)))
local finale = false
if self.mods.finale then
self.finale_count = self.finale_count + 1
if self.finale_count > self.finale_amount then
self.finale_count = 0
finale = true
table.insert(effects, CircleEffect(self.x, self.y, self.s*self.rs))
self.attack_spring:pull(0.25)
end
end
local mods = copy(self.mods)
if self.blasting then mods.blasting = true end
if finale then
mods.final = true
mods.pierce = true
mods.pierce_amount = 12
end
table.insert(projectiles, Projectile(x + lo*math.cos(r + math.pi/2), y + lo*math.sin(r + math.pi/2), v, r, mods))
if self.blasting then
self.blast_meter = self.blast_meter - 10
if self.blast_meter <= 0 then
self.blast_meter = 0
self.blasting = false
self.blast_timer = 0
end
table.insert(effects, CircleEffect(self.x, self.y, self.s*self.rs))
self.attack_spring:pull(0.25)
end
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)
-- double, triple and volley change shooting_positions
self.shooting_positions = {}
local s = self.s*1.5*self.rs
if self.mods.volley then
if self.mods.double and self.mods.triple then
local ofs = {-64, -56, -48, -40, -32, -16, -8, 0, 8, 16, 32, 40, 48, 56, 64}
for i = 1, #ofs do table.insert(self.shooting_positions, {x = self.x + s*math.cos(self.r) + ofs[i]*math.cos(self.r + math.pi/2), y = self.y + s*math.sin(self.r) + ofs[i]*math.sin(self.r + math.pi/2)}) end
elseif self.mods.double and not self.mods.triple then
local ofs = {-28, -20, -4, 4, 20, 28}
for i = 1, #ofs do table.insert(self.shooting_positions, {x = self.x + s*math.cos(self.r) + ofs[i]*math.cos(self.r + math.pi/2), y = self.y + s*math.sin(self.r) + ofs[i]*math.sin(self.r + math.pi/2)}) end
elseif self.mods.triple and not self.mods.double then
local ofs = {-32, -24, -16, -8, 0, 8, 16, 24, 32}
for i = 1, #ofs do table.insert(self.shooting_positions, {x = self.x + s*math.cos(self.r) + ofs[i]*math.cos(self.r + math.pi/2), y = self.y + s*math.sin(self.r) + ofs[i]*math.sin(self.r + math.pi/2)}) end
else
local ofs = {-24, 0, 24}
for i = 1, #ofs do table.insert(self.shooting_positions, {x = self.x + s*math.cos(self.r) + ofs[i]*math.cos(self.r + math.pi/2), y = self.y + s*math.sin(self.r) + ofs[i]*math.sin(self.r + math.pi/2)}) end
end
elseif self.mods.triple then
if self.mods.double then
table.insert(self.shooting_positions, {x = self.x + s*math.cos(self.r - math.pi/4), y = self.y + s*math.sin(self.r - math.pi/4)})
table.insert(self.shooting_positions, {x = self.x + s*math.cos(self.r - math.pi/8), y = self.y + s*math.sin(self.r - math.pi/8)})
table.insert(self.shooting_positions, {x = self.x + s*math.cos(self.r), y = self.y + s*math.sin(self.r)})
table.insert(self.shooting_positions, {x = self.x + s*math.cos(self.r + math.pi/8), y = self.y + s*math.sin(self.r + math.pi/8)})
table.insert(self.shooting_positions, {x = self.x + s*math.cos(self.r + math.pi/4), y = self.y + s*math.sin(self.r + math.pi/4)})
else
table.insert(self.shooting_positions, {x = self.x + s*math.cos(self.r - math.pi/8), y = self.y + s*math.sin(self.r - math.pi/8)})
table.insert(self.shooting_positions, {x = self.x + s*math.cos(self.r), y = self.y + s*math.sin(self.r)})
table.insert(self.shooting_positions, {x = self.x + s*math.cos(self.r + math.pi/8), y = self.y + s*math.sin(self.r + math.pi/8)})
end
elseif self.mods.double then
table.insert(self.shooting_positions, {x = self.x + s*math.cos(self.r - math.pi/18), y = self.y + s*math.sin(self.r - math.pi/18)})
table.insert(self.shooting_positions, {x = self.x + s*math.cos(self.r + math.pi/18), y = self.y + s*math.sin(self.r + math.pi/18)})
else
table.insert(self.shooting_positions, {x = self.x + s*math.cos(self.r), y = self.y + s*math.sin(self.r)})
end
local cdm = 1
if self.mods.double then cdm = cdm*1.25 end
if self.mods.triple then cdm = cdm*1.67 end
if self.mods.volley then cdm = cdm*2 end
if self.mods.spread then cdm = cdm*0.8 end
if self.mods.burst then cdm = cdm*3 end
if self.blasting then cdm = cdm*4 end
if self.mods.sphere then cdm = cdm*4 end
local am = 1
if self.mods.double then am = am*0.5 end
if self.mods.triple then am = am*0.5 end
if self.mods.double and self.mods.triple then am = am*3 end
if self.mods.volley then am = am*0.5 end
if self.mods.volley and self.mods.double then am = am*2 end
if self.mods.volley and self.mods.triple then am = am*2 end
if self.mods.spread then am = am*0.25 end
if self.mods.burst then am = am*0.5 end
if self.mods.sphere then am = am*4 end
self.attack_timer = self.attack_timer + dt
if m.isDown(1) and not m.isDown(2) then
if self.attack_timer > self.attack_cd*cdm and self.can_shoot then
self.attack_timer = 0
self.scale_spring:pull(0.1)
if self.mods.burst then
for i = 1, self.burst_amount do
timer:after((i-1)*(math.min(self.attack_cd*cdm/4, 0.12)), function()
for _, sp in ipairs(self.shooting_positions) do
if self:spend_ammo(am) then
self:shoot(sp.x, sp.y)
end
end
end)
end
else
for _, sp in ipairs(self.shooting_positions) do
if self:spend_ammo(am) then
self:shoot(sp.x, sp.y)
end
end
end
end
self.attack_pulse_timer = self.attack_pulse_timer + dt
if self.attack_pulse_timer > 4*self.attack_cd*cdm then
self.attack_pulse_timer = 0
self.attack_spring:pull(0.25)
end
end
if not self.blasting then
self.blast_timer = self.blast_timer + dt
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) and self.can_shoot 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) and self.can_shoot 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 and self.can_shoot 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, #ammo_bars 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()
if not self.visible then return end
if self.reloading then
if self.reload_text then
if not self.reload_text.visible then return end
end
end
push(self.x, self.y, 0, self.s*self.sx, self.s*self.sy)
if self.mods.blast then circlef(self.x, self.y, remap(self.blast_meter, 0, 50, 0, self.rs + self.rso)) end
circle(self.x, self.y, self.rs + self.rso, 2)
pop()
for _, sp in ipairs(self.shooting_positions) do circlef(sp.x, sp.y, 3) end
end
function Player:hit(damage)
hp_bars[self.hp]:spend2()
self.hp = self.hp - 1
director:lost_hp()
slow(0.5, 0.5)
flash(2, black)
end
function get_closest_unhit_enemy(enemies_hit, x, y)
local min_d, min_i = 1000000, 0
for i, enemy in ipairs(enemies) do
if not any(enemies_hit, enemy.id) then
local d = distance(x, y, enemy.x, enemy.y)
if d < min_d then
min_d = d
min_i = i
end
end
end
return enemies[min_i]
end
function get_close_enemies(x, y, d)
local out = {}
for _, e in ipairs(enemies) do
if distance(e.x, e.y, x, y) < d then
table.insert(out, e)
end
end
return out
end
Projectile = Class:extend()
function Projectile:new(x, y, v, r, mods, enemies_hit)
self.timer = Timer()
self.mods = mods
self.x, self.y = x, y
self.v, self.r = v, r
if self.mods.sphere then self.v = v/8 end
self.vx, self.vy = self.v*math.cos(self.r), self.v*math.sin(self.r)
self.sx, self.sy = 1, 1
self.w, self.h = 16, 4
if self.mods.blasting then self.w, self.h = 24, 6 end
if self.mods.final then self.w, self.h = 24, 6 end
self.shape = HC.rectangle(self.x - self.w/2, self.y - self.h/2, self.w, self.h)
self.shape.parent = self
self.enemies_hit = enemies_hit or {}
self.damage = rng:int(40, 60)
if self.mods.blasting then self.damage = 4*self.damage end
if self.mods.final then self.damage = 3*self.damage end
self.accel_vm = 1
self.decel_vm = 1
self.speed_vm = 1
self.scatter_vm = 1
if self.mods.accel then self.accel_vm = 0; self.timer:tween(0.5, self, {accel_vm = 1.5}, linear) end
if self.mods.decel then self.decel_vm = 1.5; self.timer:tween(0.5, self, {decel_vm = 0.25}, linear) end
if self.mods.speed then self.speed_vm = 1.5 end
if self.mods.ricochet then self.ricochet_amount = mods.ricochet_amount or player.ricochet_amount end
if self.mods.scatter then self.scatter_amount = mods.scatter_amount or player.scatter_amount end
if self.mods.chain then self.chain_amount = mods.chain_amount or player.chain_amount end
if self.mods.pierce then self.pierce_amount = mods.pierce_amount or player.pierce_amount end
if self.mods.sphere then self.sphere_cd = 0.3; self.sphere_timer = 0; self.sphere_range = 128 end
if self.mods.spawner then
self.spawner_timer = 0
self.spawner_cd = rng:float(0.1, 0.3)
if self.mods.sphere then self.spawner_cd = self.spawner_cd*8 end
if self.mods.accel then self.spawner_cd = self.spawner_cd*0.75 end
if self.mods.decel then self.spawner_cd = self.spawner_cd*2 end
if self.mods.speed then self.spawner_cd = self.spawner_cd*0.5 end
end
end
function Projectile:update(dt)
self.timer:update(dt)
local vx, vy = 0, 0
if self.mods.homing then
local target = get_closest_unhit_enemy(self.enemies_hit, self.x, self.y)
if target then
local phx, phy = normalize(self.vx, self.vy)
local r = math.atan2(target.y - self.y, target.x - self.x)
local tthx, tthy = normalize(math.cos(r), math.sin(r))
local fhx, fhy = normalize(phx + 0.1*tthx, phy + 0.1*tthy)
self.homing_vx, self.homing_vy = self.v*fhx, self.v*fhy
else self.homing_vx, self.homing_vy = nil, nil end
self.vx = (self.homing_vx or self.v*math.cos(self.r))*self.accel_vm*self.decel_vm*self.speed_vm*self.scatter_vm
self.vy = (self.homing_vy or self.v*math.sin(self.r))*self.accel_vm*self.decel_vm*self.speed_vm*self.scatter_vm
else
self.vx = self.v*math.cos(self.r)*self.accel_vm*self.decel_vm*self.speed_vm*self.scatter_vm
self.vy = self.v*math.sin(self.r)*self.accel_vm*self.decel_vm*self.speed_vm*self.scatter_vm
end
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 < x1 then self:wall(0, x1, nil, math.pi - self.r); self.vx = -self.vx; self.x = x1 + 2; self.r = math.pi - self.r end
if self.x > x2 then self:wall(math.pi, x2, nil, math.pi - self.r); self.vx = -self.vx; self.x = x2 - 2; self.r = math.pi - self.r end
if self.y < 0 then self:wall(math.pi/2, nil, nil, 2*math.pi - self.r); self.vy = -self.vy; self.y = 2; self.r = 2*math.pi - self.r end
if self.y > gh then self:wall(-math.pi/2, nil, nil, 2*math.pi - self.r); self.vy = -self.vy; self.y = gh - 2; self.r = 2*math.pi - self.r end
self.shape:moveTo(self.x, self.y)
self.shape:setRotation(self.r)
if self.mods.sphere then
self.sphere_timer = self.sphere_timer + dt
if self.sphere_timer > self.sphere_cd then
self.sphere_timer = 0
local targets = get_close_enemies(self.x, self.y, self.sphere_range)
if targets then
local target = rng:table(targets)
if target then
target:hit(self.damage)
table.insert(effects, LightningLine(self.x, self.y, target.x, target.y + target.v/10))
table.insert(effects, HitEffect(target.x, target.y + target.v/10))
for i = 1, 2 do table.insert(effects, ExplosionParticle(target.x, target.y, rng:float(0, 2*math.pi), rng:float(100, 300))) end
end
end
end
end
if self.mods.spawner then
self.spawner_timer = self.spawner_timer + dt
if self.spawner_timer > self.spawner_cd then
self.spawner_timer = 0
table.insert(projectiles, SecondaryProjectile(self.x, self.y, self.v, self.r - math.pi/2))
table.insert(projectiles, SecondaryProjectile(self.x, self.y, self.v, self.r + math.pi/2))
for i = 1, 4 do table.insert(effects, EllipseParticle(self.x, self.y, rng:float(0, 2*math.pi), rng:float(50, 200))) end
table.insert(effects, ShootCircle(self.x, self.y))
end
end
end
function Projectile:draw()
local s = 1
if self.mods.blasting or self.mods.sphere then s = rng:float(1, 1.1) end
push(self.x, self.y, self.r, s*self.sx, s*self.sy)
if self.mods.sphere then
circlef(self.x, self.y, self.w/4 + rng:float(-1, 1))
circle(self.x, self.y, self.sphere_range, 2, {1, 1, 1, 0.04})
else rectf(self.x, self.y, self.w, self.h, nil, nil, white) end
pop()
end
function Projectile:wall(r, x, y, rr)
if (self.mods.ricochet and self.ricochet_amount > 0) or (self.mods.scatter and self.scatter_amount > 0) then
if self.mods.ricochet then self.ricochet_amount = self.ricochet_amount - 1 end
if self.mods.scatter then self.scatter_amount = self.scatter_amount - 1; self.scatter_vm = self.scatter_vm*1.5 end
for i = 1, 2 do table.insert(effects, EllipseParticle(x or self.x, y or 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(x or self.x, y or self.y))
else
self.dead = true
for i = 1, 2 do table.insert(effects, EllipseParticle(x or self.x, y or 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(x or self.x, y or self.y))
if self.mods.scatter then
local v = self.v*self.accel_vm*self.decel_vm*self.speed_vm*self.scatter_vm
for i = 1, rng:int(2, 8) do
table.insert(projectiles, SecondaryProjectile(self.x, self.y, 1.25*v, rr + rng:float(-math.pi/4, math.pi/4)))
end
end
if self.mods.split then
local v = self.v*self.accel_vm*self.decel_vm*self.speed_vm*self.scatter_vm
local mods = copy(self.mods)
mods.split = false
table.insert(projectiles, Projectile(self.x, self.y, v, r + math.pi/4, mods))
table.insert(projectiles, Projectile(self.x, self.y, v, r - math.pi/4, mods))
end
end
end
function Projectile:hit(enemy)
if any(self.enemies_hit, enemy.id) then return end
if self.mods.chain or self.mods.pierce or self.mods.sphere then
table.insert(self.enemies_hit, enemy.id)
if self.mods.chain then
self.chain_amount = self.chain_amount - 1
table.insert(effects, CircleEffect2(enemy.x, enemy.y, enemy.w))
for i = 1, 2 do table.insert(effects, ExplosionParticle(self.x, self.y, rng:float(0, 2*math.pi), rng:float(100, 300))) end
local target = get_closest_unhit_enemy(self.enemies_hit, self.x, self.y)
if target then self.r = math.atan2((target.y + 0.5*target.v) - self.y, target.x - self.x)
else self:die() end
if self.chain_amount < 0 then self:die() end
elseif self.mods.pierce then
self.pierce_amount = self.pierce_amount - 1
table.insert(effects, CircleEffect2(enemy.x, enemy.y, enemy.w))
for i = 1, 2 do table.insert(effects, ExplosionParticle(self.x, self.y, rng:float(0, 2*math.pi), rng:float(100, 300))) end
if self.pierce_amount < 0 then self:die() end
elseif self.mods.sphere then
table.insert(effects, CircleEffect2(enemy.x, enemy.y, enemy.w))
for i = 1, 2 do table.insert(effects, ExplosionParticle(self.x, self.y, rng:float(0, 2*math.pi), rng:float(100, 300))) end
end
else self:die() end
if self.mods.fork then
local v = self.v*self.accel_vm*self.decel_vm*self.speed_vm*self.scatter_vm
local mods = copy(self.mods)
mods.fork = false
table.insert(projectiles, Projectile(self.x, self.y, v, self.r - math.pi/4, mods, {enemy.id}))
table.insert(projectiles, Projectile(self.x, self.y, v, self.r + math.pi/4, mods, {enemy.id}))
end
if self.mods.cross then
local v = self.v*self.accel_vm*self.decel_vm*self.speed_vm*self.scatter_vm
local mods = copy(self.mods)
mods.cross = false
table.insert(projectiles, Projectile(self.x, self.y, v, self.r - math.pi/2, mods, {enemy.id}))
table.insert(projectiles, Projectile(self.x, self.y, v, self.r - 2*math.pi/2, mods, {enemy.id}))
table.insert(projectiles, Projectile(self.x, self.y, v, self.r - 3*math.pi/2, mods, {enemy.id}))
table.insert(projectiles, Projectile(self.x, self.y, v, self.r - 4*math.pi/2, mods, {enemy.id}))
end
if self.mods.blast then
if not player.blasting and player.blast_timer > player.blast_cd then
player.blast_meter = player.blast_meter + 5
if player.blast_meter > 50 then
player.blasting = true
player.blast_meter = 50
table.insert(effects, InfoText(player.x, player.y - 36, "BLAST!", 0.6))
end
end
if self.mods.blasting then
table.insert(effects, ExplosionCircle(enemy.x, enemy.y))
table.insert(displacements, Shockwave(enemy.x, enemy.y))
for i = 1, rng:int(8, 12) do table.insert(effects, ExplosionParticle(enemy.x, enemy.y, rng:float(0, 2*math.pi), rng:float(300, 600))) end
local targets = get_close_enemies(enemy.x, enemy.y, 64)
for i, t in ipairs(targets) do
timer:after((i-1)*0.05, function()
t:hit(2*self.damage)
table.insert(effects, ExplosionCircle(enemy.x, enemy.y, 0.5))
for i = 1, rng:int(4, 6) do table.insert(effects, ExplosionParticle(enemy.x, enemy.y, rng:float(0, 2*math.pi), rng:float(200, 400))) end
end)
end
end
end
if self.mods.weaken then if rng:bool(17) then enemy:weaken() end end
if self.mods.glitch then if rng:bool(17) then enemy:glitch() end end
if self.mods.slow then if rng:bool(17) then enemy:slow() end end
if self.mods.haste then if rng:bool(17) then enemy:hasten() end end
if self.mods.stun then if rng:bool(17) then enemy:stun() end end
return true
end
function Projectile:die()
self.dead = true
for i = 1, 2 do table.insert(effects, EllipseParticle(x or self.x, y or 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(x or self.x, y or self.y))
end
function Projectile:die2()
self.dead = true
for i = 1, 2 do table.insert(effects, EllipseParticle(self.x, self.y, rng:float(0, 2*math.pi), rng:float(100, 200))) end
table.insert(effects, ShootCircle(self.x, self.y))
end
Enemy = Class:extend()
function Enemy:enemy_new(v, hp)
self.timer = Timer()
self.v = v
self.hp = hp
self.vm = 1
self.hit_spring = Spring(1)
self.weak_spring = Spring(1)
end
function Enemy:enemy_update(dt)
self.timer:update(dt)
self.hit_spring:update(dt)
self.weak_spring:update(dt)
self.vm = 1
if self.slowed then self.vm = 0.5 end
if self.haste then self.vm = 1.5 end
if self.stunned then self.vm = 0 end
end
function Enemy:collisions(dt)
if not self.shape then return end
for other in pairs(HC.neighbors(self.shape)) do
if other.parent:is(Projectile) then
local collides = self.shape:collidesWith(other)
if collides then
if other.parent:hit(self) then
self:hit(other.parent.damage, other.parent.r, other.parent)
table.insert(effects, HitEffect(other.parent.x, other.parent.y))
end
end
elseif other.parent:is(SecondaryProjectile) 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.dead = true
table.insert(effects, DeathCircle(self.x, self.y, 2*self.w, white))
table.insert(effects, CircleEffect(self.x, self.y, self.w, white))
player:hit(1)
flash(1, black)
end
end
function Enemy:enemy_draw()
push(self.x, self.y, 0, self.weak_spring.x*self.hit_spring.x*self.sx, self.weak_spring.x*self.hit_spring.x*self.sy)
if self.weak then
local weak_o = 0.125*self.w*math.sin(10*time)
local s = {1, 1, 1, self.weak_a}
line(self.x - self.weak_s - weak_o, self.y - self.weak_s - weak_o, math.pi/4, self.weak_s/4, 2, s)
line(self.x + self.weak_s + weak_o, self.y - self.weak_s - weak_o, 3*math.pi/4, self.weak_s/4, 2, s)
line(self.x + self.weak_s + weak_o, self.y + self.weak_s + weak_o, math.pi/4, self.weak_s/4, 2, s)
line(self.x - self.weak_s - weak_o, self.y + self.weak_s + weak_o, 3*math.pi/4, self.weak_s/4, 2, s)
g.setColor(1, 1, 1, 1)
end
pop()
end
function Enemy:hit(damage, r, projectile)
if self.dead then return end
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")
if self.weak then damage = 2*damage end
self.hp = self.hp - damage
if self.hp <= 0 then
self.dead = true
director:killed_enemy()
player:change_resource(math.ceil(rng:int(2, 4)*self.hpm))
if self.stun_effect then self.stun_effect:die() end
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 projectile then
if projectile.mods.burn then
table.insert(effects, AnimatedEffect(self.x + 6, self.y - 6, "radial1", 0.01, nil, nil))
local targets = get_close_enemies(self.x, self.y, 104)
for i, t in ipairs(targets) do
timer:after((i-1)*0.05, function()
t:burn()
end)
end
end
end
end
end
function Enemy:burn()
self.burning = true
self.timer:everyi(0.4, function()
self.hit_spring:pull(0.1)
self:hit(20)
table.insert(effects, AnimatedEffect(self.x + 3, self.y - 3, "radial1", 0.01, nil, nil, 0.5, 0.5))
end, 5, function() self.burning = false end, "burn")
end
function Enemy:weaken()
self.weak = true
self.weak_s = 1.6*self.w
self.weak_a = 1
self.weak_spring:pull(0.1)
self.timer:tween(0.1, self, {weak_s = 1.3*self.w}, linear, function()
self.timer:after(10, function()
self.timer:tween(0.2, self, {weak_s = 2.6*self.w, weak_a = 0}, linear, function()
self.weak = false
end, "weaktt")
end, "weaka")
end, "weakt")
end
function Enemy:glitch()
self.glitched = true
self.timer:everyi(0.3, function()
self:hit(40)
for j = 1, 3 do
self.timer:after((j-1)*(0.3/3), function()
table.insert(displacements, DisplacementBlock(self.x + rng:float(-self.w, self.w), self.y + rng:float(-self.w, self.w), rng:float(self.w/2, 2*self.w), rng:float(self.w/2, 2*self.w), 0.15, 0.3))
table.insert(effects, Block(self.x + rng:float(-self.w, self.w), self.y + rng:float(-self.w, self.w), rng:float(self.w/2, 1.5*self.w), rng:float(self.w/2, 1.5*self.w), 0.15, 0.3))
end)
end
end, 20, nil, "glitche")
self.timer:after(6, function() self.glitched = false end, "glitcha")
end
function Enemy:slow()
self.slowed = true
self.timer:everyi(0.5, function()
self:hit(0)
for i = 1, 6 do table.insert(effects, EllipseParticle(self.x, self.y, rng:float(0, 2*math.pi), rng:float(150, 300))) end
table.insert(effects, AnimatedEffect(self.x, self.y + 4, rng:table({"disappear1", "disappear2"}), 0.02, nil, math.pi/2, 1, 1))
end, 15, function() self.slowed = false end, "slowe")
end
function Enemy:hasten()
self.haste = true
self.timer:everyi(0.5, function()
self:hit(0)
for i = 1, 6 do table.insert(effects, EllipseParticle(self.x, self.y, rng:float(0, 2*math.pi), rng:float(150, 300))) end
table.insert(effects, AnimatedEffect(self.x, self.y + 4, rng:table({"disappear1", "disappear2"}), 0.02, nil, 0, 1, 1))
end, 15, function() self.haste = false end, "hastee")
end
function Enemy:stun()
if self.stunned then return end
self.stunned = true
self:hit(0)
self.stun_effect = StunEffect(self.x, self.y, self)
table.insert(effects, self.stun_effect)
self.timer:after(3, function()
self.stunned = false
self.stun_effect:die()
end, "stuna")
end
Rock = Class:extend()
Rock:implement(Enemy)
function Rock:new(x, y, hpm)
self.id = rng:uid()
self.x, self.y = x, y
self.w = 16
self.r = 0
self.sx, self.sy = 1, 1
self.d = 1
self.lw = 2
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.hpm = hpm or 1
self:enemy_new(50, 250*self.hpm)
end
function Rock:update(dt)
self:enemy_update(dt)
self.r = self.r + self.vr*dt
self.y = self.y + self.v*self.vm*dt
self.shape:moveTo(self.x, self.y)
self.shape:setRotation(self.r)
self:collisions(dt)
end
function Rock:draw()
if self.y > gh + 64 then return end
if self.hit_flash then self.lw = 4 else self.lw = 2 end
push(self.x, self.y, self.r, self.hit_spring.x*self.sx, self.hit_spring.x*self.sy)
polygon(to_polygon(self.x, self.y, self.vs), self.lw, white)
pop()
self:enemy_draw()
end
SecondaryProjectile = Class:extend()
function SecondaryProjectile:new(x, y, v, r)
self.x, self.y = x, y
self.v, self.r = v, r
self.vx, self.vy = self.v*math.cos(self.r), self.v*math.sin(self.r)
self.sx, self.sy = 1, 1
self.w, self.h = 9, 3
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(10, 20)
end
function SecondaryProjectile:update(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 < x1 then self:die(x1, self.y) end
if self.x > x2 then self:die(x2, self.y) end
if self.y < 0 then self:die(self.x, 0) end
if self.y > gh then self:die(self.x, gh) end
self.shape:moveTo(self.x, self.y)
self.shape:setRotation(self.r)
end
function SecondaryProjectile: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 SecondaryProjectile:die(x, y)
self.dead = true
table.insert(effects, ShootCircle(x or self.x, y or self.y, 8))
end
RuneWord = Class:extend()
function RuneWord:new(x, y, w, h, word)
self.letters = {}
for i = 1, #word do table.insert(self.letters, word:sub(i, i)) end
self.rune_letters = {}
for i = 1, #self.letters do
timer:after((i-1)*0.08, function()
table.insert(self.rune_letters, RuneLetter(x + (i-1)*w, y, w, h, self.letters[i]))
end)
end
end
function RuneWord:update(dt)
for _, rl in ipairs(self.rune_letters) do rl:update(dt) end
end
function RuneWord:draw()
for _, rl in ipairs(self.rune_letters) do rl:draw() end
end
RuneLetter = Class:extend()
function RuneLetter:new(x, y, w, h, type)
self.x, self.y = x, y
self.w, self.h = w, h
self.type = type
self.scale_spring = Spring(1)
self.sx, self.sy = 1.5, 1.5
self.fg_color = {0, 0, 0, 1}
self.bg_color = {1, 1, 1, 1}
self.visible = true
timer:everyi(0.05, function() self.visible = not self.visible end, 3, function() self.visible = true end)
timer:tween(0.15, self, {sx = 1, sy = 1}, linear, function() self.sx, self.sy = 1, 1 end)
timer:after(0.15, function()
self.bg_color = {0, 0, 0, 0}
self.fg_color = {1, 1, 1, 1}
end)
self:displace()
end
function RuneLetter:update(dt)
self.scale_spring:update(dt)
local mx, my = camera:get_mouse_position()
if mx > self.x - self.w/2 and mx < self.x + self.w/2 and my > self.y - self.h/2 and my < self.y + self.h/2 then
if not self.hot then
self.hot = true
self.scale_spring:pull(0.3)
self.fg_color = {0, 0, 0, 1}
self.bg_color = {1, 1, 1, 1}
timer:everyi(0.033, function() self.visible = not self.visible end, 3, function() self.visible = true end)
self:displace()
end
else
if self.hot then
self.hot = false
self.scale_spring:pull(0.15)
self.fg_color = {1, 1, 1, 1}
self.bg_color = {0, 0, 0, 0}
timer:everyi(0.033, function() self.visible = not self.visible end, 3, function() self.visible = true end)
end
end
end
function RuneLetter:draw()
if not self.visible then return end
push(self.x, self.y, 0, self.sx*self.scale_spring.x, self.sy*self.scale_spring.x)
rectf(self.x, self.y, self.w, self.h, nil, nil, self.bg_color)
draw_rune(self.x, self.y, self.w, self.h, self.type, 3, self.fg_color)
pop()
end
function RuneLetter:displace(m)
for i = 1, rng:int(4, 6) do
table.insert(displacements, DisplacementBlock(self.x, self.y + rng:float(-0.75*self.w, 0.75*self.w), rng:float(0.75*self.w, 2*self.w), rng:float(0.1*self.w, 0.25*self.w), 0.05*(m or 1), 0.25*(m or 1)))
end
end
TransitionRune = Class:extend()
function TransitionRune:new(x, y, w, h, type)
self.timer = Timer()
self.x, self.y = x, y
self.w, self.h = w, h
self.type = type
self.scale_spring = Spring(1)
self.scale_spring:pull(0.5)
self.lw = 8
self.timer:everyi({0.02, 0.07}, function() self.lw = rng:float(6, 10) end)
self.timer:everyi({0.04, 0.14}, function()
table.insert(displacements, DisplacementBlock(self.x, self.y + rng:float(-0.75*self.w, 0.75*self.w), rng:float(0.75*self.w, 2*self.w), rng:float(0.1*self.w, 0.25*self.w), 0.05, 0.25, rng:float(1, 4)))
end)
end
function TransitionRune:update(dt)
self.timer:update(dt)
self.scale_spring:update(dt)
end
function TransitionRune:draw()
push(self.x, self.y, 0, self.scale_spring.x, self.scale_spring.x)
draw_rune(self.x, self.y, self.w, self.h, self.type, self.lw, black)
pop()
end
Rune = Class:extend()
function Rune:new(x, y, w, h, type)
self.x, self.y = x, y
self.w, self.h = w, h
self.type = type
self.scale_spring = Spring(1)
end
function Rune:update(dt)
self.scale_spring:update(dt)
local mx, my = camera:get_mouse_position()
if mx > self.x - self.w/2 and mx < self.x + self.w/2 and my > self.y - self.h/2 and my < self.y + self.h/2 then
if not self.hot then
self.hot = true
self.scale_spring:pull(0.5)
self.rune_text = RuneText(x1 + 50, 50, self.type)
table.insert(ui, self.rune_text)
end
else
if self.hot then
self.hot = false
self.scale_spring:pull(0.2)
self.rune_text:die()
self.rune_text = nil
else
if self.rune_text then
self.rune_text:die()
self.rune_text = nil
end
end
end
end
function draw_rune(x, y, w, h, type, lw, color)
g.setLineWidth(lw or 2)
g.setColor(color or white)
local x1, y1 = x - w/2, y - h/2
local x2, y2 = x + w/2, y + h/2
if type == "double" then
g.line(x1 + w/3 + h/16, y1 + h/4, x1 + w/3 + h/16, y2 - h/4)
g.line(x2 - w/3 - h/16, y1 + h/4 + h/12, x2 - w/3 - h/16, y2 - h/4)
elseif type == "triple" then
g.line(x1 + w/4 + w/12, y1 + h/4, x1 + w/4 + w/12, y2 - h/4)
g.line(x, y - h/8, x, y + h/8)
g.line(x2 - w/4 - w/12, y1 + h/4 + h/16, x2 - w/4 - w/12, y2 - h/4 - h/16)
elseif type == "volley" then
g.line(x1 + w/3, y1 + h/4, x1 + w/3, y2 - h/4)
g.line(x, y1 + h/4, x, y2 - h/4)
g.line(x2 - w/3, y1 + h/4, x2 - w/3, y2 - h/4)
elseif type == "spread" then
g.line(x, y2 - h/4, x1 + w/4, y1 + h/3)
g.line(x, y2 - h/4, x, y1 + h/3)
g.line(x, y2 - h/4, x2 - w/4, y1 + h/3)
elseif type == "burst" then
g.line(x1 + w/3, y, x, y1 + h/4, x2 - w/3, y)
g.line(x1 + w/3 + w/12, y + h/6 - h/12 + h/24, x, y1 + h/4 + h/4 - h/12 + h/24, x2 - w/3 - w/12, y + h/6 - h/12 + h/24)
g.line(x1 + w/3 + w/12 + w/24, y + h/6 + h/12, x, y + h/12 + h/12, x2 - w/3 - w/12 - w/24, y + h/6 + h/12)
elseif type == "homing" then
g.line(x1 + w/3, y1 + h/4, x2 - w/3, y, x1 + w/3, y2 - h/4)
g.line(x1 + w/3, y1 + h/4 + h/6, x1 + w/3 + w/6, y1 + h/4)
elseif type == "speed" then
g.line(x - w/4, y, x + w/4, y)
g.line(x + w/4 - w/8, y + h/8, x + w/4, y, x + w/4 - w/8, y - h/8)
elseif type == "accel" then
g.line(x - w/4, y, x + w/4, y)
g.line(x + w/4 - w/8, y + h/8, x + w/4, y, x + w/4 - w/8, y - h/8)
g.line(x + w/4 - w/8 - w/8, y + h/8, x + w/4 - w/8, y, x + w/4 - w/8 - w/8, y - h/8)
elseif type == "decel" then
g.line(x - w/4, y, x + w/4, y)
g.line(x - w/4 + w/8, y + h/8, x - w/4, y, x - w/4 + w/8, y - h/8)
g.line(x - w/4 + w/8 + w/8, y + h/8, x - w/4 + w/8, y, x - w/4 + w/8 + w/8, y - h/8)
elseif type == "ricochet" then
g.line(x + w/10, y1 + h/4, x - w/8, y - h/12, x + w/8, y + h/12, x - w/10, y2 - h/4)
elseif type == "scatter" then
g.line(x, y2 - h/4, x, y - h/8)
g.line(x1 + w/4, y - h/8, x1 + w/4 + w/8, y1 + h/4, x, y - h/8, x2 - w/4 - w/8, y1 + h/4, x2 - w/4, y - h/8)
elseif type == "split" then
g.line(x, y1 + h/4, x, y2 - h/4)
g.line(x - w/6, y, x, y1 + h/4, x + w/6, y)
g.line(x - w/5, y1 + h/4, x + w/5, y1 + h/4)
elseif type == "chain" then
g.line(x + w/10, y1 + h/4, x - w/8, y, x + w/8, y, x - w/10, y2 - h/4)
elseif type == "pierce" then
g.line(x, y1 + h/4, x, y2 - h/4)
g.line(x - w/6, y, x, y1 + h/4, x + w/6, y)
elseif type == "fork" then
g.line(x, y2 - h/4, x, y)
g.line(x1 + w/3, y1 + h/4, x, y)
g.line(x2 - w/3, y1 + h/4, x, y)
elseif type == "cross" then
g.line(x1 + w/3, y, x2 - w/3, y)
g.line(x, y1 + h/3, x, y2 - h/3)
elseif type == "blast" then
g.line(x, y - h/12, x + w/12, y, x, y + h/12, x - w/12, y, x, y - h/12)
g.line(x1 + w/4, y, x1 + w/4 + w/10, y)
g.line(x2 - w/4, y, x2 - w/4 - w/10, y)
g.line(x, y1 + h/4, x, y1 + h/4 + h/10)
g.line(x, y2 - h/4, x, y2 - h/4 - h/10)
elseif type == "burn" then
g.line(x, y - h/10, x + w/10, y, x, y + h/10, x - w/10, y, x, y - h/10)
g.line(x - w/10, y - h/10, x - w/10, y - h/10 - h/12)
g.line(x + w/10, y - h/10, x + w/10, y - h/10 - h/12)
g.line(x, y - h/10 - h/12, x, y - h/5 - h/12)
elseif type == "finale" then
g.line(x, y1 + h/4, x, y2 - h/4)
g.line(x1 + w/3, y, x, y1 + h/4, x2 - w/3, y)
g.line(x1 + w/3 + w/12, y + h/6 - h/12 + h/24, x, y1 + h/4 + h/4 - h/12 + h/24, x2 - w/3 - w/12, y + h/6 - h/12 + h/24)
g.line(x1 + w/3 + w/12 + w/24, y + h/6 + h/12, x, y + h/12 + h/12, x2 - w/3 - w/12 - w/24, y + h/6 + h/12)
elseif type == "weaken" then
g.line(x1 + w/4, y1 + h/4, x, y, x1 + w/4, y2 - h/4)
g.line(x + w/8, y - h/8, x, y, x + w/8, y + h/8)
elseif type == "glitch" then
g.line(x1 + w/4, y - h/4, x + w/12, y - h/4)
g.line(x - w/8, y - h/8, x + w/4, y - h/8)
g.line(x - w/12, y + h/8, x + w/12, y + h/8)
g.line(x - w/24, y + h/4, x + w/12, y + h/4)
g.line(x + w/12, y - h/8, x + w/12, y + h/4)
elseif type == "slow" then
g.line(x1 + w/4, y + h/8, x - w/8, y - h/8, x + w/8, y + h/8, x + w/4, y - h/8)
elseif type == "haste" then
g.line(x1 + w/3, y1 + h/4, x2 - w/3, y, x1 + w/3, y2 - h/4)
g.line(x1 + w/3 - w/6 + w/8, y1 + h/4 + h/8, x2 - w/3 - w/3 + w/8, y, x1 + w/3 - w/6 + w/8, y2 - h/4 - h/8)
elseif type == "stun" then
g.line(x1 + w/3, y1 + h/3, x2 - w/3, y1 + h/3, x2 - w/3, y2 - h/3, x1 + w/3, y2 - h/3, x1 + w/3, y1 + h/3)
g.line(x2 - w/3, y1 + h/3, x1 + w/3, y2 - h/3)
elseif type == "sphere" then
g.line(x, y - h/8, x + w/8, y, x, y + h/8, x - w/8, y, x, y - h/8)
g.line(x, y - h/8, x, y - h/4, x + w/8, y - h/4)
g.line(x + w/8, y, x + w/4, y, x + w/4, y + h/8)
g.line(x, y + h/8, x, y + h/4, x - w/8, y + h/4)
g.line(x - w/8, y, x - w/4, y, x - w/4, y - h/8)
elseif type == "spawner" then
g.line(x, y1 + h/4, x, y2 - h/4)
g.line(x, y1 + h/4 + 1*h/10, x - w/8, y1 + h/4 + 1*h/10)
g.line(x, y1 + h/4 + 2*h/10, x + w/8, y1 + h/4 + 2*h/10)
g.line(x, y1 + h/4 + 3*h/10, x - w/8, y1 + h/4 + 3*h/10)
g.line(x, y1 + h/4 + 4*h/10, x + w/8, y1 + h/4 + 4*h/10)
elseif type == "new_run" then
g.line(x1 + w/4, y, x2 - w/4, y)
g.line(x2 - w/4 - w/4, y - h/4, x2 - w/4, y, x2 - w/4 - w/4, y + h/4)
elseif type == "options" then
g.line(x1 + w/4, y2 - h/4, x + w/8, y - h/8, x, y1 + h/4, x - w/8, y - h/8, x2 - w/4, y2 - h/4)
elseif type == "quit" then
g.line(x1 + w/4, y1 + h/4, x2 - w/4, y2 - h/4)
g.line(x1 + w/4, y2 - h/4, x2 - w/4, y1 + h/4)
elseif type == "R" then
g.line(x - w/4, y + h/4, x - w/4, y - h/4, x + w/4, y - h/8, x - w/4, y, x + w/4, y + h/4)
elseif type == "U" then
g.line(x - w/4, y - h/4, x - w/4, y + h/12, x + w/4, y + h/4, x + w/4, y - h/4)
elseif type == "N" then
g.line(x - w/4, y - h/4, x - w/4, y + h/4)
g.line(x + w/4, y - h/4, x + w/4, y + h/4)
g.line(x - w/4, y1 + h/4 + h/16, x + w/4, y2 - h/4 - h/16)
elseif type == "E" then
g.line(x + w/4, y, x - w/4, y - h/4, x - w/4, y + h/4, x, y + h/8)
g.line(x - w/4, y - h/8, x, y)
elseif type == "H" then
g.line(x - w/4, y - h/4, x - w/4, y + h/4)
g.line(x + w/4, y - h/4, x + w/4, y + h/4)
g.line(x - w/4, y - h/12, x + w/4, y + h/12)
elseif type == "A" then
g.line(x1 + w/4, y + h/4, x, y - h/4, x2 - w/4, y + h/4)
g.line(x - w/10, y, x + w/6, y + h/8)
elseif type == "C" then
g.line(x + w/4, y - h/4, x - w/4, y, x + w/4, y + h/4)
elseif type == "K" then
g.line(x + w/4, y - h/4, x - w/4, y, x + w/4, y + h/4)
g.line(x - w/4, y - h/4, x - w/4, y + h/4)
end
g.setLineWidth(1)
end
function Rune:draw()
push(self.x, self.y, 0, self.scale_spring.x, self.scale_spring.x)
draw_rune(self.x, self.y, self.w, self.h, self.type)
pop()
end
RuneText = Class:extend()
function RuneText:new(x, y, type)
self.timer = Timer()
self.x, self.y = x, y
self.type = type
local sh = font_small:getHeight()
local h = font_medium:getHeight()
self.texts = {}
if self.type == "double" then
self.texts[1] = TextLine(self.x, self.y, "DOUBLE")
self.texts[2] = TextLine(self.x, self.y + sh + h, "2 projectiles")
self.texts[3] = TextLine(self.x, self.y + sh + 2*h, "20% decreased attack speed")
self.texts[4] = TextLine(self.x, self.y + sh + 3*h, "100% decreased ammo spent")
elseif self.type == "triple" then
self.texts[1] = TextLine(self.x, self.y, "TRIPLE")
self.texts[2] = TextLine(self.x, self.y + sh + h, "3 projectiles")
self.texts[3] = TextLine(self.x, self.y + sh + 2*h, "40% decreased attack speed")
self.texts[4] = TextLine(self.x, self.y + sh + 3*h, "100% decreased ammo spent")
elseif self.type == "volley" then
self.texts[1] = TextLine(self.x, self.y, "VOLLEY")
self.texts[2] = TextLine(self.x, self.y + sh + h, "3 projectiles in a wider area")
self.texts[3] = TextLine(self.x, self.y + sh + 2*h, "50% decreased attack speed")
self.texts[4] = TextLine(self.x, self.y + sh + 3*h, "100% decreased ammo spent")
elseif self.type == "spread" then
self.texts[1] = TextLine(self.x, self.y, "SPREAD")
self.texts[2] = TextLine(self.x, self.y + sh + h, "Projectiles at random angles")
self.texts[3] = TextLine(self.x, self.y + sh + 2*h, "25% increased attack speed")
self.texts[4] = TextLine(self.x, self.y + sh + 3*h, "300% decreased ammo spent")
elseif self.type == "burst" then
self.texts[1] = TextLine(self.x, self.y, "BURST")
self.texts[2] = TextLine(self.x, self.y + sh + h, "3 quick waves of projectiles")
self.texts[3] = TextLine(self.x, self.y + sh + 2*h, "66% decreased attack speed")
self.texts[4] = TextLine(self.x, self.y + sh + 3*h, "100% decreased ammo spent")
elseif self.type == "homing" then
self.texts[1] = TextLine(self.x, self.y, "HOMING")
self.texts[2] = TextLine(self.x, self.y + sh + h, "Projectile seeks targets")
elseif self.type == "speed" then
self.texts[1] = TextLine(self.x, self.y, "SPEED")
self.texts[2] = TextLine(self.x, self.y + sh + h, "Projectile is faster")
self.texts[3] = TextLine(self.x, self.y + sh + 2*h, "50% increased movement speed")
elseif self.type == "accel" then
self.texts[1] = TextLine(self.x, self.y, "ACCEL")
self.texts[2] = TextLine(self.x, self.y + sh + h, "Projectile accelerates")
self.texts[3] = TextLine(self.x, self.y + sh + 2*h, "Starts at 0% speed")
self.texts[4] = TextLine(self.x, self.y + sh + 3*h, "Accelerates to 150% speed")
elseif self.type == "decel" then
self.texts[1] = TextLine(self.x, self.y, "DECEL")
self.texts[2] = TextLine(self.x, self.y + sh + h, "Projectile decelerates")
self.texts[3] = TextLine(self.x, self.y + sh + 2*h, "Starts at 150% speed")
self.texts[4] = TextLine(self.x, self.y + sh + 3*h, "Decelerates to 25% speed")
elseif self.type == "ricochet" then
self.texts[1] = TextLine(self.x, self.y, "RICOCHET")
self.texts[2] = TextLine(self.x, self.y + sh + h, "Projectile ricochets twice")
self.texts[3] = TextLine(self.x, self.y + sh + 2*h + sh, "Ricochet hit: only applies to walls", font_small)
elseif self.type == "scatter" then
self.texts[1] = TextLine(self.x, self.y, "SCATTER")
self.texts[2] = TextLine(self.x, self.y + sh + h, "Projectile ricochets and splits into secondary projectiles")
self.texts[3] = TextLine(self.x, self.y + sh + 2*h + sh, "Ricochet hit: only applies to walls", font_small)
self.texts[4] = TextLine(self.x, self.y + sh + 2*h + 2*sh, "Secondary projectile: doesn't inherit passives", font_small)
elseif self.type == "split" then
self.texts[1] = TextLine(self.x, self.y, "SPLIT")
self.texts[2] = TextLine(self.x, self.y + sh + h, "Projectile splits into 2 projectiles")
self.texts[3] = TextLine(self.x, self.y + sh + 2*h + sh, "Split hit: only applies to walls", font_small)
elseif self.type == "chain" then
self.texts[1] = TextLine(self.x, self.y, "CHAIN")
self.texts[2] = TextLine(self.x, self.y + sh + h, "Projectile seeks next target after hit")
self.texts[3] = TextLine(self.x, self.y + sh + 2*h + sh, "Chain hit: only applies to enemies", font_small)
elseif self.type == "pierce" then
self.texts[1] = TextLine(self.x, self.y, "PIERCE")
self.texts[2] = TextLine(self.x, self.y + sh + h, "Projectile pierces target once")
self.texts[3] = TextLine(self.x, self.y + sh + 2*h + sh, "Pierce hit: only applies to enemies", font_small)
elseif self.type == "fork" then
self.texts[1] = TextLine(self.x, self.y, "FORK")
self.texts[2] = TextLine(self.x, self.y + sh + h, "Projectile forks into 2 projectiles")
self.texts[3] = TextLine(self.x, self.y + sh + 2*h + sh, "Fork hit: only applies to enemies", font_small)
elseif self.type == "cross" then
self.texts[1] = TextLine(self.x, self.y, "CROSS")
self.texts[2] = TextLine(self.x, self.y + sh + h, "Projectile splits into 4 projectiles")
self.texts[3] = TextLine(self.x, self.y + sh + 2*h + sh, "Cross hit: only applies to enemies", font_small)
elseif self.type == "blast" then
self.texts[1] = TextLine(self.x, self.y, "BLAST")
self.texts[2] = TextLine(self.x, self.y + sh + h, "Projectiles explode after build up")
self.texts[3] = TextLine(self.x, self.y + sh + 2*h, "75% decreased attack speed")
self.texts[4] = TextLine(self.x, self.y + sh + 3*h + sh, "Blast meter hit: only builds from enemies", font_small)
self.texts[5] = TextLine(self.x, self.y + sh + 3*h + 2*sh, "Explosion hit: deals AoE damage", font_small)
elseif self.type == "burn" then
self.texts[1] = TextLine(self.x, self.y, "BURN")
self.texts[2] = TextLine(self.x, self.y + sh + h, "Inflict nearby enemies with burn on kill")
self.texts[3] = TextLine(self.x, self.y + sh + 2*h + sh, "Burn: damage over time", font_small)
elseif self.type == "finale" then
self.texts[1] = TextLine(self.x, self.y, "FINALE")
self.texts[2] = TextLine(self.x, self.y + sh + h, "Every 5th projectile is final")
self.texts[3] = TextLine(self.x, self.y + sh + 2*h + sh, "Final: pierces and deals extra damage", font_small)
elseif self.type == "weaken" then
self.texts[1] = TextLine(self.x, self.y, "WEAKEN")
self.texts[2] = TextLine(self.x, self.y + sh + h, "Projectile has 17% chance of inflicting weak")
self.texts[3] = TextLine(self.x, self.y + sh + 2*h + sh, "Weak: enemy takes double damage for 10 seconds", font_small)
elseif self.type == "glitch" then
self.texts[1] = TextLine(self.x, self.y, "GLITCH")
self.texts[2] = TextLine(self.x, self.y + sh + h, "Projectile has 17% chance of inflicting glitch")
self.texts[3] = TextLine(self.x, self.y + sh + 2*h + sh, "Glitch: enemy takes damage over 6 seconds", font_small)
elseif self.type == "slow" then
self.texts[1] = TextLine(self.x, self.y, "SLOW")
self.texts[2] = TextLine(self.x, self.y + sh + h, "Projectile has 17% chance of inflicting slow")
self.texts[3] = TextLine(self.x, self.y + sh + 2*h + sh, "Slow: enemy has 100% decreased movement speed", font_small)
elseif self.type == "haste" then
self.texts[1] = TextLine(self.x, self.y, "HASTE")
self.texts[2] = TextLine(self.x, self.y + sh + h, "Projectile has 17% chance of inflicting haste")
self.texts[3] = TextLine(self.x, self.y + sh + 2*h + sh, "Haste: enemy has 50% increased movement speed", font_small)
elseif self.type == "stun" then
self.texts[1] = TextLine(self.x, self.y, "STUN")
self.texts[2] = TextLine(self.x, self.y + sh + h, "Projectile has 17% chance of inflicting stun")
self.texts[3] = TextLine(self.x, self.y + sh + 2*h + sh, "Stun: enemy can't move", font_small)
elseif self.type == "sphere" then
self.texts[1] = TextLine(self.x, self.y, "BALL LIGHTNING")
self.texts[2] = TextLine(self.x, self.y + sh + h, "Projectile pierces and discharges electricity")
self.texts[3] = TextLine(self.x, self.y + sh + 2*h, "75% decreased attack speed")
self.texts[4] = TextLine(self.x, self.y + sh + 3*h, "300% increased ammo spent")
elseif self.type == "spawner" then
self.texts[1] = TextLine(self.x, self.y, "SPAWNER")
self.texts[2] = TextLine(self.x, self.y + sh + h, "Projectile spawns secondary projectiles")
self.texts[3] = TextLine(self.x, self.y + sh + 2*h + sh, "Secondary projectile: doesn't inherit passives", font_small)
elseif self.type == "new_run" then
self.texts[1] = TextLine(self.x, self.y, "NEW RUN")
elseif self.type == "options" then
self.texts[1] = TextLine(self.x, self.y, "OPTIONS")
elseif self.type == "quit" then
self.texts[1] = TextLine(self.x, self.y, "QUIT")
end
self.visible = true
self.timer:after(0.2, function() self.timer:everyi(0.05, function() self.visible = not self.visible end, 10, function() self.visible = true end) end)
end
function RuneText:update(dt)
self.timer:update(dt)
update_objects(self.texts, dt)
end
function RuneText:draw()
if not self.visible then return end
draw_objects(self.texts)
end
function RuneText:die()
self.timer:everyi(0.04, function() self.visible = not self.visible end, 5, function() self.dead = true end)
end
TextLine = Class:extend()
function TextLine:new(x, y, text, font, color, bg_color)
self.timer = Timer()
self.x, self.y = x, y
self.font = font or font_medium
self.w = self.font:getWidth(text)
self.h = self.font:getHeight()
self.color = color or white
self.bg_color = bg_color or black
self.characters = {}
self.visuals = {}
for i = 1, #text do table.insert(self.characters, {c = text:sub(i, i), visible = false}); self.visuals[i] = 4 end
for i = 1, #self.characters do
self.timer:after((i-1)*0.02, function()
if rng:bool(25) then
self.visuals[i] = rng:int(1, 4)
if self.visuals[i-1] then self.visuals[i-1] = rng:int(1, 4) end
if self.visuals[i-2] then self.visuals[i-2] = rng:int(1, 4) end
self.timer:after(rng:float(0.07, 0.2), function() self.visuals[i] = 1 end)
else
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
end
if rng:bool(50) then
local random_characters = "0123456789abcdefghijklmnopqrstuvwyxzABCDEFGHIJKLMNOPQRSTUVWYXZ"
local r = rng:int(1, #random_characters)
local c = self.characters[i].c
self.characters[i].c = random_characters:sub(r, r)
self.characters[i].visible = true
self.timer:after(rng:float(0.07, 0.2), function() self.characters[i].c = c end)
else
self.characters[i].visible = true
end
end)
end
self.timer:after(0.02*#self.characters + 0.2, function()
self.visuals = {}
for i = 1, #self.characters do self.visuals[i] = 1 end
self.timer:every({0.4, 1}, function()
for i = 1, rng:int(2, 3) do table.insert(displacements, DisplacementBlock(self.x, self.y + rng:float(-0.75*self.h, 0.75*self.h), rng:float(0.75*self.w, 2*self.w), rng:float(0.25*self.h, 0.5*self.h), 0.05, 0.25)) end
local c = rng:int(1, #self.characters)
local v = rng:int(1, #self.visuals)
local random_characters = "0123456789abcdefghijklmnopqrstuvwyxzABCDEFGHIJKLMNOPQRSTUVWYXZ"
local r = rng:int(1, #random_characters)
local pc = self.characters[c].c
self.characters[c].c = random_characters:sub(r, r)
self.timer:after(rng:float(0.07, 0.2), function() self.characters[c].c = pc; self.visuals[v] = 1 end)
self.visuals[v] = rng:int(1, 4)
end)
end)
--self.timer:after(2, function() self.dead = true end)
end
function TextLine:update(dt)
self.timer:update(dt)
end
function TextLine:draw()
local w, h = 0, 0
local x, y = self.x, self.y
g.setFont(self.font)
g.setColor(self.color)
for i = 1, #self.characters do
if self.characters[i].visible then
local cw, ch = self.font:getWidth(self.characters[i].c), self.font:getHeight()
if self.visuals[i] == 1 then
g.setColor(self.color)
g.print(self.characters[i].c, x + w, y + h)
elseif self.visuals[i] == 2 then
g.setColor(self.color)
rectf(x + w + cw/2, y + h + ch/2, cw, ch)
g.setColor(self.bg_color)
g.print(self.characters[i].c, x + w, y + h)
elseif self.visuals[i] == 3 then
g.setColor(self.color)
rectf(x + w + cw/2, y + h + ch/2, cw, ch)
elseif self.visuals[i] == 4 then
g.setColor(self.bg_color)
rectf(x + w + cw/2, y + h + ch/2, cw, ch)
end
end
w = w + self.font:getWidth(self.characters[i].c)
end
end
LightningLine = Class:extend()
function LightningLine:new(x1, y1, x2, y2)
self.timer = Timer()
self.lines = {}
self.x1, self.y1 = x1, y1
self.x2, self.y2 = x2, y2
table.insert(self.lines, {x1 = self.x1, y1 = self.y1, x2 = self.x2, y2 = self.y2})
table.insert(effects, ShootCircle(x1, y1, 8))
self.lw = 3
self.generations = 4
self.max_offset = 12
self:generate()
self.timer:after(0.1, function() self.dead = true end)
end
function LightningLine:update(dt)
self.timer:update(dt)
end
function LightningLine:generate()
local offset_amount = self.max_offset
local lines = self.lines
for j = 1, self.generations do
for i = #lines, 1, -1 do
local x1, y1 = lines[i].x1, lines[i].y1
local x2, y2 = lines[i].x2, lines[i].y2
table.remove(lines, i)
local x, y = (x1 + x2)/2, (y1 + y2)/2
local px, py = perpendicular(normalize(x2 - x1, y2 - y1))
x = x + px*rng:float(-offset_amount, offset_amount)
y = y + py*rng:float(-offset_amount, offset_amount)
table.insert(lines, {x1 = x1, y1 = y1, x2 = x, y2 = y})
table.insert(lines, {x1 = x, y1 = y, x2 = x2, y2 = y2})
end
offset_amount = offset_amount/2
end
end
function LightningLine:draw()
for i, line in ipairs(self.lines) do
g.setLineWidth(self.lw)
g.line(line.x1, line.y1, line.x2, line.y2)
end
g.setLineWidth(1)
g.setColor(1, 1, 1, 1)
end
StunEffect = Class:extend()
function StunEffect:new(x, y, enemy)
self.x, self.y = x, y
self.sx, self.sy = 4, 4
self.a = 0
self.vs = enemy.vs
self.enemy = enemy
self.scale_spring = Spring(1)
timer:tween(0.2, self, {a = 1, sx = 1, sy = 1}, linear, function()
self.sx, self.sy = 1, 1
self.a = 1
self.scale_spring:pull(-0.5)
for i = 1, 2 do table.insert(effects, DustParticle(self.x, self.y, white)) end
end)
end
function StunEffect:update(dt)
self.scale_spring:update(dt)
self.r = self.enemy.r
end
function StunEffect:draw()
push(self.x, self.y, self.r, self.scale_spring.x*self.sx, self.scale_spring.x*self.sy)
polygon(to_polygon(self.x, self.y, self.vs), 3, {1, 1, 1, self.a})
pop()
end
function StunEffect:die()
table.insert(effects, CircleEffect(self.x, self.y, self.enemy.w))
for i = 1, 6 do table.insert(effects, EllipseParticle(self.x, self.y, rng:float(0, 2*math.pi), rng:float(150, 300))) end
timer:tween(0.2, self, {a = 0, sx = 4, sy = 4}, linear, function() self.dead = true end)
end
Block = Class:extend()
function Block:new(x, y, w, h, d1, d2)
self.x, self.y = x, y
self.w, self.h = w, h
timer:after({d1 or 0.05, d2 or 0.4}, function() self.dead = true end)
end
function Block:update(dt)
end
function Block:draw()
g.setColor(1, 1, 1, 1)
g.rectangle("fill", self.x - self.w/2, self.y - self.h/2, self.w, self.h)
end
DisplacementBlock = Class:extend()
function DisplacementBlock:new(x, y, w, h, d1, d2, m)
self.x, self.y = x, y
self.w, self.h = w, h
self.r = rng:float(-32*(m or 1), 32*(m or 1))/255
self.color = {0.5 + self.r, 0.5 + self.r, 0.5 + self.r}
local n = rng:float(d1 or 0.05, d2 or 0.4)
timer:after(n, function() self.dead = true end)
if rng:bool(50) then timer:tween(n, self, {r = 0}, linear) end
end
function DisplacementBlock:update(dt)
self.color = {0.5 + self.r, 0.5 + self.r, 0.5 + self.r}
end
function DisplacementBlock:draw()
g.setColor(self.color)
g.rectangle("fill", self.x - self.w/2, self.y - self.h/2, self.w, self.h)
g.setColor(white)
end
SlowParticle = Class:extend()
function SlowParticle:new(x, y)
self.x, self.y = x, y
self.sx, self.sy = 0, 0
self.r = math.pi/2
self.v = rng:float(100, 175)
self.rs = rng:float(3, 6)
timer:tween(rng:float(0.04, 0.06), self, {sx = 0.75, sy = 0.75}, cubic_in_out, function()
timer:tween(rng:float(0.3, 0.5), self, {sx = 0, sy = 0}, linear, function() self.dead = true end)
end)
end
function SlowParticle:update(dt)
self.x = self.x + self.v*math.cos(self.r)*dt
self.y = self.y + self.v*math.sin(self.r)*dt
end
function SlowParticle:draw()
push(self.x, self.y, 0, self.sx, self.sy)
circlef(self.x, self.y, self.rs)
pop()
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)
self.timer:everyi(2, function() self.timer:tween(1, self, {sx = 1.1, sy = 1.1}, cubic_in, function() self.timer:tween(1, self, {sx = 1, sy = 1}, linear) end) end)
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
function PlayerHPUI:destroy()
self.dead = true
table.insert(ui_effects, ShootCircle(self.x, self.y))
table.insert(ui_effects, FadingShootCapsule(self.x, self.y, rng:float(-math.pi/4, 0), rng:float(50, 150)))
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)
self.timer:after(0.1, function()
self.timer:everyi(2, function() self.timer:tween(1, self, {sx = 1.1, sy = 1.1}, cubic_in, function() self.timer:tween(1, self, {sx = 1, sy = 1}, linear) end) end)
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
function PlayerAmmoUI:destroy()
self.dead = true
table.insert(ui_effects, ShootCircle(self.x, self.y))
table.insert(ui_effects, FadingShootCapsule(self.x, self.y, rng:float(-math.pi/4, 0), rng:float(50, 150)))
end
PlayerResourceUI = Class:extend()
function PlayerResourceUI: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)
self.timer:tween(0.1, self, {sx = 1, sy = 1}, cubic_in, function() self.sx, self.sy = 1, 1 end)
self.timer:after(0.1, function()
self.timer:everyi(2, function() self.timer:tween(1, self, {sx = 1.1, sy = 1.1}, cubic_in, function() self.timer:tween(1, self, {sx = 1, sy = 1}, linear) end) end)
end)
local oy = 28
local w = 12
self.vs = {self.x - w/2, self.y - w/2 + oy, self.x, self.y - w + oy, self.x + w/2, self.y - w/2 + oy, self.x + w/2, self.y + w/2 + oy, self.x, self.y + w + oy, self.x - w/2, self.y + w/2 + oy}
end
function PlayerResourceUI:update(dt)
self.timer:update(dt)
self.scale_spring:update(dt)
self.resource = player.resource
end
function PlayerResourceUI:draw()
push(self.x, self.y, 0, self.scale_spring.x*self.sx, self.scale_spring.x*self.sy)
draw_text(self.resource, self.x, self.y, 0, 1, 1, font_medium)
pop()
push(self.x, self.y + 28, 0, self.scale_spring.x*self.sx, self.scale_spring.x*self.sy)
polygonf(self.vs)
pop()
end
function PlayerResourceUI:jiggle()
self.scale_spring:pull(0.5)
end
function PlayerResourceUI:destroy()
self.dead = true
table.insert(ui_effects, ShootCircle(self.x, self.y))
table.insert(ui_effects, FadingShootCapsule(self.x, self.y, rng:float(-math.pi/4, 0), rng:float(50, 150)))
table.insert(ui_effects, ShootCircle(self.x, self.y + 28))
table.insert(ui_effects, FadingShootCapsule(self.x, self.y + 28, rng:float(-math.pi/4, 0), rng:float(50, 150)))
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")
self.timer:after(0.1, function()
self.timer:everyi(2, function() self.timer:tween(1, self, {sx = 1.1, sy = 1.1}, cubic_in, function() self.timer:tween(1, self, {sx = 1, sy = 1}, linear) end) end)
end)
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(ui_effects, ShootCircle(self.x, self.y, self.h/2))
table.insert(ui_effects, FadingShootCapsule(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(ui_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(ui_effects, DeathParticle(self.x, self.y, rng:float(0, 2*math.pi), rng:float(100, 400))) end
end
CircleEffect2 = Class:extend()
function CircleEffect2:new(x, y, r)
self.x, self.y = x, y
self.r = r
self.a = 1
self.lw = 6
timer:tween(0.2, self, {r = 4*self.r, lw = 1}, linear, function() self.dead = true end)
timer:after(0.15, function() timer:tween(0.05, self, {a = 0}, linear) end)
end
function CircleEffect2:update(dt)
end
function CircleEffect2:draw()
g.setColor(1, 1, 1, self.a)
g.setLineWidth(self.lw)
g.circle("line", self.x, self.y, self.r)
g.setLineWidth(1)
g.setColor(1, 1, 1, 1)
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
ExplosionCircle = Class:extend()
function ExplosionCircle:new(x, y, s)
self.x, self.y = x, y
self.rs = 0
self.scale_spring = Spring(1)
camera:shake(3*(s or 1), 0.5*(s or 1))
timer:tween(0.1, self, {rs = (s or 1)*36}, cubic_in_out, function()
self.scale_spring:pull(0.2)
timer:tween(0.2, self, {rs = 0}, linear, function() self.dead = true end)
end)
end
function ExplosionCircle:update(dt)
self.scale_spring:update(dt)
end
function ExplosionCircle:draw()
push(self.x, self.y, 0, self.scale_spring.x, self.scale_spring.x)
g.setColor(white)
circlef(self.x, self.y, 1*self.rs)
g.setColor(1, 1, 1, 0.062)
circlef(self.x, self.y, 2*self.rs)
g.setColor(1, 1, 1, 1)
pop()
end
Shockwave = Class:extend()
function Shockwave:new(x, y)
self.timer = Timer()
self.x, self.y = x, y
self.sx, self.sy = 0.05, 0.05
self.a = 1
self.timer:tween(0.5, self, {sx = 0.75, sy = 0.75, a = 0}, linear, function() self.dead = true end)
end
function Shockwave:update(dt)
self.timer:update(dt)
end
function Shockwave:draw()
g.setColor(1, 1, 1, self.a)
g.draw(shockwave, self.x, self.y, 0, self.sx, self.sy, shockwave:getWidth()/2, shockwave:getHeight()/2)
g.setColor(1, 1, 1, 1)
end
InfoText = Class:extend()
function InfoText:new(x, y, text, duration, r, glitch)
self.timer = Timer()
self.x, self.y = x, y
self.r = r or 0
self.sx, self.sy = 1, 1
self.characters = {}
self.visuals = {}
for i = 1, #text do table.insert(self.visuals, 1) end
self.visible = true
self.font = font_medium
self.w, self.h = self.font:getWidth(text), self.font:getHeight()
self.scale_spring = Spring(1)
self.scale_spring:pull(0.15)
self.t = 0
local characters = {}
for i = 1, #text do table.insert(characters, text:sub(i, i)) end
for i = 1, #characters 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; self.dead = 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 InfoText:update(dt)
self.timer:update(dt)
self.scale_spring:update(dt)
end
function InfoText: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
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
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
ChargeParticle = Class:extend()
function ChargeParticle:new(x, y, rs)
local r = rng:float(0, 2*math.pi)
local d = rng:float(0.75*rs, 1.25*rs)
self.x, self.y = x + d*math.cos(r), y + d*math.sin(r)
self.rs = rng:float(6, 10)
timer:tween(rng:float(0.5, 1.5), self, {x = x, y = y}, linear, function() self.dead = true end)
end
function ChargeParticle:update(dt)
end
function ChargeParticle:draw()
circlef(self.x, self.y, self.rs, white)
end
BurnParticle = Class:extend()
function BurnParticle:new(x, y)
self.x, self.y = x, y
self.animation = Animation(1, get_animation_frames("smoke1"), "once")
self.sx, self.sy = 0, 0
self.r = rng:float(0, 2*math.pi)
self.v = rng:float(50, 150)
self.rs = 0
self.vr = rng:float(0, 2*math.pi)
timer:tween(rng:float(0.04, 0.06), self, {sx = 0.5, sy = 0.5}, cubic_in_out, function()
timer:tween(rng:float(0.2, 0.4), self, {sx = 0, sy = 0}, linear, function() self.dead = true end)
end)
end
function BurnParticle:update(dt)
self.animation:update(dt)
self.x = self.x + self.v*math.cos(self.r)*dt
self.y = self.y + self.v*math.sin(self.r)*dt
self.rs = self.rs + self.vr*dt
end
function BurnParticle:draw()
draw_animation("smoke1", self.animation:get_current_frame(), self.x, self.y, self.rs, 3*self.sx, 3*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, r, sx, sy, action, z, duration)
self.x, self.y = x, y
self.r, self.sx, self.sy = r or 0, sx or 1, sy or 1
self.z = z or 0
self.name = name
self.delay = delay
self.loop_mode = loop_mode or "once"
self.action = action
if self.loop_mode == "once" then
self.animation = Animation(delay, get_animation_frames(name), self.loop_mode, {[0] = function()
self.dead = true
if self.action then self.action() end
end})
elseif self.loop_mode == "stay" then self.animation = Animation(delay, get_animation_frames(name), "once", {[0] = function() if self.action then self.action() end end})
else self.animation = Animation(delay, get_animation_frames(name), self.loop_mode) end
if duration then timer:after(duration, function() self.dead = true end) 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
ExplosionParticle = Class:extend()
function ExplosionParticle: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 ExplosionParticle:update(dt)
self.x = self.x + self.v*math.cos(self.r)*dt
self.y = self.y + self.v*math.sin(self.r)*dt
self.w = remap(self.v, 0, 400, 0, 9)
self.h = remap(self.v, 0, 400, 0, 3)
end
function ExplosionParticle:draw()
push(self.x, self.y, math.atan2(self.v*math.sin(self.r), 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(ui_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
FadingShootCapsule = Class:extend()
function FadingShootCapsule: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)
self.color = {1, 1, 1, 1}
timer:after(0.1, function()
timer:tween({0.2, 0.5}, self.color, {[4] = 0}, linear, function() self.dead = true end)
end)
end
function FadingShootCapsule: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(ui_effects, ShootCircle(self.x, self.y))
end
end
function FadingShootCapsule:draw()
push(self.x, self.y, self.r)
rectf(self.x, self.y, self.w, self.h, nil, nil, self.color)
pop()
end
BlackRectangle = Class:extend()
function BlackRectangle:new(x, y, w, h)
self.x, self.y, self.w, self.h = x, y, w, h
end
function BlackRectangle:update(dt)
end
function BlackRectangle:draw()
rectf(self.x + self.w/2, self.y + self.h/2, self.w, self.h, nil, nil, black)
g.setColor(white)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment