diff --git a/arena.lua b/arena.lua index 41fd480..6e5cfb7 100644 --- a/arena.lua +++ b/arena.lua @@ -4,10 +4,23 @@ Arena:implement(GameObject) function Arena:init(name) self:init_state(name) self:init_game_object() +end + + +function Arena:on_enter(from) self.main = Group():set_as_physics_world(32, 0, 0, {'player', 'enemy', 'projectile', 'enemy_projectile'}) self.effects = Group() self.ui = Group():no_camera() self.main:disable_collision_between('player', 'player') + self.main:disable_collision_between('player', 'projectile') + self.main:disable_collision_between('player', 'enemy_projectile') + self.main:disable_collision_between('projectile', 'projectile') + self.main:disable_collision_between('projectile', 'enemy_projectile') + self.main:disable_collision_between('projectile', 'enemy') + self.main:disable_collision_between('enemy_projectile', 'enemy') + self.main:disable_collision_between('enemy_projectile', 'enemy_projectile') + self.main:enable_trigger_between('projectile', 'enemy') + self.main:enable_trigger_between('enemy_projectile', 'player') self.enemies = {Seeker} @@ -26,19 +39,15 @@ function Arena:init(name) Wall{group = self.main, vertices = math.to_rectangle_vertices(self.x2, -40, gw + 40, gh + 40), color = bg[-1]} Wall{group = self.main, vertices = math.to_rectangle_vertices(self.x1, -40, self.x2, self.y1), color = bg[-1]} Wall{group = self.main, vertices = math.to_rectangle_vertices(self.x1, self.y2, self.x2, gh + 40), color = bg[-1]} - self.player = Player{group = self.main, x = gw/2, y = gh/2, leader = true, character = 'vagrant'} + self.player:add_follower(Player{group = self.main, character = 'swordsman'}) + --[[ self.player:add_follower(Player{group = self.main, character = 'vagrant'}) self.player:add_follower(Player{group = self.main, character = 'vagrant'}) self.player:add_follower(Player{group = self.main, character = 'vagrant'}) self.player:add_follower(Player{group = self.main, character = 'vagrant'}) self.player:add_follower(Player{group = self.main, character = 'vagrant'}) - self.player:add_follower(Player{group = self.main, character = 'vagrant'}) -end - - -function Arena:on_enter(from) - + ]]-- end diff --git a/devlog.md b/devlog.md index 48fcb11..bea9b42 100644 --- a/devlog.md +++ b/devlog.md @@ -43,3 +43,26 @@ Right now basic player and enemy movement works, as well as melee collisions bet * One or a few of the characters * Port over enemy spawn logic from SHOOTRX * Sounds + +# Day 3 - 19/02/21 + +Managed to get the first 4 items of the previous todo list done. Removed the cycle stat because the way projectiles work (they're autoshot) already feels like a cycle so having that in would feel redundant. +I changed it for area damage + area size stats which feel more fundamental. So currently the synergies are: + +* Ranger: yellow, buff attack speed +* Warrior: orange, buff attack damage +* Healer: green, buff healing effectiveness +* Mage: blue, debuff enemy defense +* Void: purple, buff area damage and size + +And the stats are: + +* HP +* Damage +* Area damage +* Area of effect +* Attack speed +* Defense -> if defense >= 0 then dmg_m = 100/(100+defense) else dmg_m = 2-100/(100-defense) + +HP, damage and defense are flat stats, whereas area damage, area of effect and attack speed are multipliers. This is because each character/attack has its own attack speed/area and trying to generalize that +too much wouldn't work well. For tomorrow I'll just try to finish the rest of the todo, which is add more characters, port enemy spawning logic from SHOOTRX and add sounds. diff --git a/enemies.lua b/enemies.lua new file mode 100644 index 0000000..a9ce680 --- /dev/null +++ b/enemies.lua @@ -0,0 +1,134 @@ +Seeker = Object:extend() +Seeker:implement(GameObject) +Seeker:implement(Physics) +Seeker:implement(Unit) +function Seeker:init(args) + self:init_game_object(args) + self:init_unit() + self:set_as_rectangle(14, 6, 'dynamic', 'enemy') + self:set_restitution(0.5) + + self.color = red[0] + self.classes = {'seeker'} + self:calculate_stats(true) + self:set_as_steerable(self.v, 2000, 4*math.pi, 4) +end + + +function Seeker:update(dt) + self:update_game_object(dt) + self:calculate_stats() + + if self.being_pushed then + local v = math.length(self:get_velocity()) + if v < 25 then + self.being_pushed = false + self.steering_enabled = true + self:set_damping(0) + self:set_angular_damping(0) + end + else + local player = main.current.player + self:seek_point(player.x, player.y) + self:wander(50, 100, 20) + self:separate(16, main.current.enemies) + self:rotate_towards_velocity(0.5) + end + self.r = self:get_angle() +end + + +function Seeker:draw() + graphics.push(self.x, self.y, self.r, self.hfx.hit.x, self.hfx.hit.x) + graphics.rectangle(self.x, self.y, self.shape.w, self.shape.h, 3, 3, self.hfx.hit.f and fg[0] or self.color) + graphics.pop() +end + + +function Seeker:on_collision_enter(other, contact) + local x, y = contact:getPositions() + + if other:is(Wall) then + self.hfx:use('hit', 0.15, 200, 10, 0.1) + self:bounce(contact:getNormal()) + + elseif table.any(main.current.enemies, function(v) return other:is(v) end) then + if self.being_pushed and math.length(self:get_velocity()) > 60 then + other:hit(5) + self:hit(10) + other:push(random:float(10, 15), other:angle_to_object(self)) + HitCircle{group = main.current.effects, x = x, y = y, rs = 6, color = fg[0], duration = 0.1} + for i = 1, 2 do HitParticle{group = main.current.effects, x = x, y = y, color = self.color} end + end + end +end + + +function Seeker:push(f, r) + self.being_pushed = true + self.steering_enabled = false + self:apply_impulse(f*math.cos(r), f*math.sin(r)) + self:apply_angular_impulse(random:table{random:float(-12*math.pi, -4*math.pi), random:float(4*math.pi, 12*math.pi)}) + self:set_damping(1.5) + self:set_angular_damping(1.5) +end + + + + +EnemyProjectile = Object:extend() +EnemyProjectile:implement(GameObject) +EnemyProjectile:implement(Physics) +function EnemyProjectile:init(args) + self:init_game_object(args) + self:set_as_rectangle(10, 4, 'dynamic', 'enemy_projectile') +end + + +function EnemyProjectile:update(dt) + self:update_game_object(dt) + + self:set_angle(self.r) + self:move_along_angle(self.v, self.r) +end + + +function EnemyProjectile:draw() + graphics.push(self.x, self.y, self.r) + graphics.rectangle(self.x, self.y, self.shape.w, self.shape.h, 2, 2, self.color) + graphics.pop() +end + + +function EnemyProjectile:die(x, y, r, n) + if self.dead then return end + x = x or self.x + y = y or self.y + n = n or random:int(3, 4) + for i = 1, n do HitParticle{group = main.current.effects, x = x, y = y, r = random:float(0, 2*math.pi), color = self.color} end + HitCircle{group = main.current.effects, x = x, y = y}:scale_down() + self.dead = true +end + + +function EnemyProjectile:on_collision_enter(other, contact) + local x, y = contact:getPositions() + local nx, ny = contact:getNormal() + local r = 0 + if nx == 0 and ny == -1 then r = -math.pi/2 + elseif nx == 0 and ny == 1 then r = math.pi/2 + elseif nx == -1 and ny == 0 then r = math.pi + else r = 0 end + + if other:is(Wall) then + self:die(x, y, r, random:int(2, 3)) + end +end + + +function EnemyProjectile:on_trigger_enter(other, contact) + if other:is(Player) then + self:die(self.x, self.y, nil, random:int(2, 3)) + other:hit(self.dmg) + end +end diff --git a/engine/game/group.lua b/engine/game/group.lua index 48ca53f..763f449 100644 --- a/engine/game/group.lua +++ b/engine/game/group.lua @@ -44,7 +44,7 @@ function Group:init() self.objects.by_id = {} self.objects.by_class = {} self.cells = {} - self.cell_size = 128 + self.cell_size = 64 return self end diff --git a/engine/game/trigger.lua b/engine/game/trigger.lua index 9c55cfe..2bf8e69 100644 --- a/engine/game/trigger.lua +++ b/engine/game/trigger.lua @@ -132,6 +132,7 @@ end -- This is useful when you need the event to happen in a varying interval, like based on the player's attack speed, which might change every frame based on buffs. -- Call this on the update function with the appropriate multiplier. function Trigger:set_every_multiplier(tag, multiplier) + if not self.triggers[tag] then return end self.triggers[tag].multiplier = multiplier or 1 end diff --git a/engine/init.lua b/engine/init.lua index ada5e58..5989623 100644 --- a/engine/init.lua +++ b/engine/init.lua @@ -137,7 +137,7 @@ function engine_run(config) local mx, my = love.mouse.getPosition() mouse:set(mx/sx, my/sy) mouse_dt:set(mouse.x - last_mouse.x, mouse.y - last_mouse.y) - update(dt) + update(fixed_dt) system.update() input.last_key_pressed = nil last_mouse:set(mouse.x, mouse.y) diff --git a/main.lua b/main.lua index d2eefc0..75196af 100644 --- a/main.lua +++ b/main.lua @@ -2,6 +2,8 @@ require 'engine' require 'shared' require 'arena' require 'objects' +require 'player' +require 'enemies' function init() diff --git a/objects.lua b/objects.lua index 7086556..e1ef867 100644 --- a/objects.lua +++ b/objects.lua @@ -1,18 +1,8 @@ Unit = Object:extend() function Unit:init_unit() self.hfx:add('hit', 1) -end - - -function Unit:draw_hp() - graphics.push(self.x, self.y, 0, self.hfx.hit.x, self.hfx.hit.x) - if self.show_hp_bar then - graphics.line(self.x - 0.5*self.shape.w, self.y - self.shape.h, self.x + 0.5*self.shape.w, self.y - self.shape.h, bg[-3], 2) - local n = math.remap(self.hp, 0, self.max_hp, 0, 1) - graphics.line(self.x - 0.5*self.shape.w, self.y - self.shape.h, self.x - 0.5*self.shape.w + n*self.shape.w, self.y - self.shape.h, - self.hfx.hit.f and fg[0] or ((self:is(Player) and green[0]) or (table.any(main.current.enemies, function(v) return self:is(v) end) and red[0])), 2) - end - graphics.pop() + self.hfx:add('shoot', 1) + self.hp_bar = HPBar{group = main.current.effects, parent = self} end @@ -30,8 +20,8 @@ end function Unit:show_hp(n) - self.show_hp_bar = true - self.t:after(n or 2, function() self.show_hp_bar = false end, 'show_hp_bar') + self.hp_bar.hidden = false + self.t:after(n or 2, function() self.hp_bar.hidden = true end, 'hp_bar') end @@ -70,32 +60,31 @@ end function Unit:calculate_stats(first_run) self.base_hp = 100 self.base_dmg = 10 - self.base_aspd = 1 - self.base_cycle = 2 + self.base_aspd_m = 1 + self.base_area_dmg_m = 1 + self.base_area_size_m = 1 self.base_def = 0 self.base_mvspd = 75 self.class_hp_a = 0 self.class_dmg_a = 0 - self.class_aspd_a = 0 - self.class_cycle_a = 0 self.class_def_a = 0 self.class_mvspd_a = 0 self.class_hp_m = 1 self.class_dmg_m = 1 self.class_aspd_m = 1 - self.class_cycle_m = 1 + self.class_area_dmg_m = 1 + self.class_area_size_m = 1 self.class_def_m = 1 self.class_mvspd_m = 1 self.buff_hp_a = 0 self.buff_dmg_a = 0 - self.buff_aspd_a = 0 - self.buff_cycle_a = 0 self.buff_def_a = 0 self.buff_mvspd_a = 0 self.buff_hp_m = 1 self.buff_dmg_m = 1 self.buff_aspd_m = 1 - self.buff_cycle_m = 1 + self.buff_area_dmg_m = 1 + self.buff_area_size_m = 1 self.buff_def_m = 1 self.buff_mvspd_m = 1 @@ -103,7 +92,7 @@ function Unit:calculate_stats(first_run) if class == 'warrior' then self.class_hp_m = self.class_hp_m*1.4 elseif class == 'mage' then self.class_hp_m = self.class_hp_m*0.6 elseif class == 'healer' then self.class_hp_m = self.class_hp_m*1.1 - elseif class == 'cycler' then self.class_hp_m = self.class_hp_m*0.9 + elseif class == 'void' then self.class_hp_m = self.class_hp_m*0.9 elseif class == 'seeker' then self.class_hp_m = self.class_hp_m*0.5 end end self.max_hp = (self.base_hp + self.class_hp_a + self.buff_hp_a)*self.class_hp_m*self.buff_hp_m @@ -119,21 +108,27 @@ function Unit:calculate_stats(first_run) for _, class in ipairs(self.classes) do if class == 'warrior' then self.class_aspd_m = self.class_aspd_m*0.9 elseif class == 'ranger' then self.class_aspd_m = self.class_aspd_m*1.4 - elseif class == 'healer' then self.class_aspd_m = self.class_aspd_m*0.5 end + elseif class == 'healer' then self.class_aspd_m = self.class_aspd_m*0.5 + elseif class == 'void' then self.class_aspd_m = self.class_aspd_m*0.75 end end - self.aspd = 1/((self.base_aspd + self.class_aspd_a + self.buff_aspd_a)*self.class_aspd_m*self.buff_aspd_m) + self.aspd_m = 1/(self.base_aspd_m*self.class_aspd_m*self.buff_aspd_m) for _, class in ipairs(self.classes) do - if class == 'mage' then self.class_cycle_m = self.class_cycle_m*1.25 - elseif class == 'healer' then self.class_cycle_m = self.class_cycle_m*1.1 - elseif class == 'cycler' then self.class_cycle_m = self.class_cycle_m*1.5 end + if class == 'mage' then self.class_area_dmg_m = self.class_area_dmg_m*1.25 + elseif class == 'void' then self.class_area_dmg_m = self.class_area_m*1.5 end end - self.cycle = (self.base_cycle + self.class_cycle_a + self.buff_cycle_a)*self.class_cycle_m*self.buff_cycle_m + self.area_dmg_m = self.base_area_dmg_m*self.class_area_dmg_m*self.buff_area_dmg_m + + for _, class in ipairs(self.classes) do + if class == 'mage' then self.class_area_size_m = self.class_area_size_m*1.2 + elseif class == 'void' then self.class_area_size_m = self.class_area_m*1.3 end + end + self.area_size_m = self.base_area_size_m*self.class_area_size_m*self.buff_area_size_m for _, class in ipairs(self.classes) do if class == 'warrior' then self.class_def_m = self.class_def_m*1.25 elseif class == 'ranger' then self.class_def_m = self.class_def_m*1.1 - elseif class == 'mage' then self.class_def_m = self.class_def_m*1.5 + elseif class == 'mage' then self.class_def_m = self.class_def_m*0.8 elseif class == 'healer' then self.class_def_m = self.class_def_m*1.2 end end self.def = (self.base_def + self.class_def_a + self.buff_def_a)*self.class_def_m*self.buff_def_m @@ -148,193 +143,29 @@ end -Player = Object:extend() -Player:implement(GameObject) -Player:implement(Physics) -Player:implement(Unit) -function Player:init(args) + +HPBar = Object:extend() +HPBar:implement(GameObject) +HPBar:implement(Parent) +function HPBar:init(args) self:init_game_object(args) - self:init_unit() - - if self.character == 'vagrant' then - self.color = blue[0] - self:set_as_rectangle(9, 9, 'dynamic', 'player') - self.visual_shape = 'rectangle' - self.classes = {'ranger', 'warrior', 'mage'} - end - self:calculate_stats(true) - - if self.leader then - self.previous_positions = {} - self.followers = {} - self.t:every(0.01, function() - table.insert(self.previous_positions, 1, {x = self.x, y = self.y, r = self.r}) - if #self.previous_positions > 256 then self.previous_positions[257] = nil end - end) - end + self.hidden = true end -function Player:update(dt) +function HPBar:update(dt) self:update_game_object(dt) - self:calculate_stats() - - if self.leader then - if input.move_left.down then self.r = self.r - 1.66*math.pi*dt end - if input.move_right.down then self.r = self.r + 1.66*math.pi*dt end - self:set_velocity(self.v*math.cos(self.r), self.v*math.sin(self.r)) - - local vx, vy = self:get_velocity() - local hd = math.remap(math.abs(self.x - gw/2), 0, 192, 1, 0) - local vd = math.remap(math.abs(self.y - gh/2), 0, 108, 1, 0) - camera.x = camera.x + math.remap(vx, -100, 100, -24*hd, 24*hd)*dt - camera.y = camera.y + math.remap(vy, -100, 100, -8*vd, 8*vd)*dt - if input.move_right.down then camera.r = math.lerp_angle_dt(0.01, dt, camera.r, math.pi/256) - elseif input.move_left.down then camera.r = math.lerp_angle_dt(0.01, dt, camera.r, -math.pi/256) - elseif input.move_down.down then camera.r = math.lerp_angle_dt(0.01, dt, camera.r, math.pi/256) - elseif input.move_up.down then camera.r = math.lerp_angle_dt(0.01, dt, camera.r, -math.pi/256) - else camera.r = math.lerp_angle_dt(0.005, dt, camera.r, 0) end - - self:set_angle(self.r) - - else - local target_distance = 10.6*self.follower_index - local distance_sum = 0 - local p - local previous = self.parent - for i, point in ipairs(self.parent.previous_positions) do - local distance_to_previous = math.distance(previous.x, previous.y, point.x, point.y) - distance_sum = distance_sum + distance_to_previous - if distance_sum >= target_distance then - p = point - break - end - previous = point - end - - if p then - self:set_position(p.x, p.y) - self.r = p.r - if not self.following then - for i = 1, random:int(3, 4) do HitParticle{group = main.current.effects, x = self.x, y = self.y, color = self.color} end - HitCircle{group = main.current.effects, x = self.x, y = self.y, rs = 10, color = fg[0]}:scale_down(0.3):change_color(0.5, self.color) - self.following = true - end - else - self.r = self:get_angle() - end - end + self:follow_parent_exclusively() end -function Player:draw() - graphics.push(self.x, self.y, self.r, self.hfx.hit.x, self.hfx.hit.x) - if self.visual_shape == 'rectangle' then - graphics.rectangle(self.x, self.y, self.shape.w, self.shape.h, 3, 3, self.hfx.hit.f and fg[0] or self.color) - end +function HPBar:draw() + if self.hidden then return end + local p = self.parent + graphics.push(p.x, p.y, 0, p.hfx.hit.x, p.hfx.hit.x) + graphics.line(p.x - 0.5*p.shape.w, p.y - p.shape.h, p.x + 0.5*p.shape.w, p.y - p.shape.h, bg[-3], 2) + local n = math.remap(p.hp, 0, p.max_hp, 0, 1) + graphics.line(p.x - 0.5*p.shape.w, p.y - p.shape.h, p.x - 0.5*p.shape.w + n*p.shape.w, p.y - p.shape.h, + p.hfx.hit.f and fg[0] or ((p:is(Player) and green[0]) or (table.any(main.current.enemies, function(v) return p:is(v) end) and red[0])), 2) graphics.pop() - self:draw_hp() -end - - -function Player:on_collision_enter(other, contact) - local x, y = contact:getPositions() - - if other:is(Wall) then - self.hfx:use('hit', 0.5, 200, 10, 0.1) - camera:spring_shake(2, math.pi - self.r) - self:bounce(contact:getNormal()) - - elseif table.any(main.current.enemies, function(v) return other:is(v) end) then - other:push(random:float(25, 35), self:angle_to_object(other)) - other:hit(20) - self:hit(20) - HitCircle{group = main.current.effects, x = x, y = y, rs = 6, color = fg[0], duration = 0.1} - for i = 1, 2 do HitParticle{group = main.current.effects, x = x, y = y, color = self.color} end - for i = 1, 2 do HitParticle{group = main.current.effects, x = x, y = y, color = other.color} end - end -end - - -function Player:add_follower(unit) - table.insert(self.followers, unit) - unit.parent = self - unit.follower_index = #self.followers -end - - -Seeker = Object:extend() -Seeker:implement(GameObject) -Seeker:implement(Physics) -Seeker:implement(Unit) -function Seeker:init(args) - self:init_game_object(args) - self:init_unit() - self:set_as_rectangle(14, 6, 'dynamic', 'enemy') - self:set_restitution(0.5) - - self.color = red[0] - self.classes = {'seeker'} - self:calculate_stats(true) - self:set_as_steerable(self.v, 2000, 4*math.pi, 4) -end - - -function Seeker:update(dt) - self:update_game_object(dt) - self:calculate_stats() - - if self.being_pushed then - local v = math.length(self:get_velocity()) - if v < 25 then - self.being_pushed = false - self.steering_enabled = true - self:set_damping(0) - self:set_angular_damping(0) - end - else - local player = main.current.player - self:seek_point(player.x, player.y) - self:wander(50, 100, 20) - self:separate(16, main.current.enemies) - self:rotate_towards_velocity(0.5) - end - self.r = self:get_angle() -end - - -function Seeker:draw() - graphics.push(self.x, self.y, self.r, self.hfx.hit.x, self.hfx.hit.x) - graphics.rectangle(self.x, self.y, self.shape.w, self.shape.h, 3, 3, self.hfx.hit.f and fg[0] or self.color) - graphics.pop() - self:draw_hp() -end - - -function Seeker:on_collision_enter(other, contact) - local x, y = contact:getPositions() - - if other:is(Wall) then - self.hfx:use('hit', 0.15, 200, 10, 0.1) - self:bounce(contact:getNormal()) - - elseif table.any(main.current.enemies, function(v) return other:is(v) end) then - if self.being_pushed and math.length(self:get_velocity()) > 60 then - other:hit(5) - self:hit(10) - other:push(random:float(10, 15), other:angle_to_object(self)) - HitCircle{group = main.current.effects, x = x, y = y, rs = 6, color = fg[0], duration = 0.1} - for i = 1, 2 do HitParticle{group = main.current.effects, x = x, y = y, color = self.color} end - end - end -end - - -function Seeker:push(f, r) - self.being_pushed = true - self.steering_enabled = false - self:apply_impulse(f*math.cos(r), f*math.sin(r)) - self:apply_angular_impulse(random:table{random:float(-12*math.pi, -4*math.pi), random:float(4*math.pi, 12*math.pi)}) - self:set_damping(1.5) - self:set_angular_damping(1.5) end diff --git a/player.lua b/player.lua new file mode 100644 index 0000000..528b010 --- /dev/null +++ b/player.lua @@ -0,0 +1,261 @@ +Player = Object:extend() +Player:implement(GameObject) +Player:implement(Physics) +Player:implement(Unit) +function Player:init(args) + self:init_game_object(args) + self:init_unit() + + if self.character == 'vagrant' then + self.color = blue[0] + self:set_as_rectangle(9, 9, 'dynamic', 'player') + self.visual_shape = 'rectangle' + self.classes = {'ranger', 'warrior', 'mage'} + + self.attack_sensor = Circle(self.x, self.y, 96) + self.t:every(2, function() + local closest_enemy = self:get_closest_object_in_shape(self.attack_sensor, main.current.enemies) + if closest_enemy then + self:shoot(self:angle_to_object(closest_enemy)) + end + end, nil, nil, 'shoot') + + elseif self.character == 'swordsman' then + self.color = orange[0] + self:set_as_rectangle(9, 9, 'dynamic', 'player') + self.visual_shape = 'rectangle' + self.classes = {'warrior'} + + self.attack_sensor = Circle(self.x, self.y, 64) + self.t:every(3, function() + local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies) + if enemies and #enemies > 0 then + self:attack() + end + end, nil, nil, 'attack') + end + self:calculate_stats(true) + + if self.leader then + self.previous_positions = {} + self.followers = {} + self.t:every(0.01, function() + table.insert(self.previous_positions, 1, {x = self.x, y = self.y, r = self.r}) + if #self.previous_positions > 256 then self.previous_positions[257] = nil end + end) + end +end + + +function Player:update(dt) + self:update_game_object(dt) + self:calculate_stats() + + self.attack_sensor:move_to(self.x, self.y) + self.t:set_every_multiplier('shoot', self.aspd_m) + self.t:set_every_multiplier('attack', self.aspd_m) + + if self.leader then + if input.move_left.down then self.r = self.r - 1.66*math.pi*dt end + if input.move_right.down then self.r = self.r + 1.66*math.pi*dt end + self:set_velocity(self.v*math.cos(self.r), self.v*math.sin(self.r)) + + local vx, vy = self:get_velocity() + local hd = math.remap(math.abs(self.x - gw/2), 0, 192, 1, 0) + local vd = math.remap(math.abs(self.y - gh/2), 0, 108, 1, 0) + camera.x = camera.x + math.remap(vx, -100, 100, -24*hd, 24*hd)*dt + camera.y = camera.y + math.remap(vy, -100, 100, -8*vd, 8*vd)*dt + if input.move_right.down then camera.r = math.lerp_angle_dt(0.01, dt, camera.r, math.pi/256) + elseif input.move_left.down then camera.r = math.lerp_angle_dt(0.01, dt, camera.r, -math.pi/256) + elseif input.move_down.down then camera.r = math.lerp_angle_dt(0.01, dt, camera.r, math.pi/256) + elseif input.move_up.down then camera.r = math.lerp_angle_dt(0.01, dt, camera.r, -math.pi/256) + else camera.r = math.lerp_angle_dt(0.005, dt, camera.r, 0) end + + self:set_angle(self.r) + + else + local target_distance = 10.6*self.follower_index + local distance_sum = 0 + local p + local previous = self.parent + for i, point in ipairs(self.parent.previous_positions) do + local distance_to_previous = math.distance(previous.x, previous.y, point.x, point.y) + distance_sum = distance_sum + distance_to_previous + if distance_sum >= target_distance then + p = point + break + end + previous = point + end + + if p then + self:set_position(p.x, p.y) + self.r = p.r + if not self.following then + for i = 1, random:int(3, 4) do HitParticle{group = main.current.effects, x = self.x, y = self.y, color = self.color} end + HitCircle{group = main.current.effects, x = self.x, y = self.y, rs = 10, color = fg[0]}:scale_down(0.3):change_color(0.5, self.color) + self.following = true + end + else + self.r = self:get_angle() + end + end +end + + +function Player:draw() + graphics.push(self.x, self.y, self.r, self.hfx.hit.x*self.hfx.shoot.x, self.hfx.hit.x*self.hfx.shoot.x) + if self.visual_shape == 'rectangle' then + graphics.rectangle(self.x, self.y, self.shape.w, self.shape.h, 3, 3, (self.hfx.hit.f or self.hfx.shoot.f) and fg[0] or self.color) + end + graphics.pop() +end + + +function Player:on_collision_enter(other, contact) + local x, y = contact:getPositions() + + if other:is(Wall) then + self.hfx:use('hit', 0.5, 200, 10, 0.1) + camera:spring_shake(2, math.pi - self.r) + self:bounce(contact:getNormal()) + + elseif table.any(main.current.enemies, function(v) return other:is(v) end) then + other:push(random:float(25, 35), self:angle_to_object(other)) + other:hit(20) + self:hit(20) + HitCircle{group = main.current.effects, x = x, y = y, rs = 6, color = fg[0], duration = 0.1} + for i = 1, 2 do HitParticle{group = main.current.effects, x = x, y = y, color = self.color} end + for i = 1, 2 do HitParticle{group = main.current.effects, x = x, y = y, color = other.color} end + end +end + + +function Player:add_follower(unit) + table.insert(self.followers, unit) + unit.parent = self + unit.follower_index = #self.followers +end + + +function Player:shoot(r) + camera:spring_shake(2, r) + self.hfx:use('shoot', 0.25) + HitCircle{group = main.current.effects, x = self.x + 0.8*self.shape.w*math.cos(r), y = self.y + 0.8*self.shape.w*math.sin(r), rs = 6} + Projectile{group = main.current.main, x = self.x + 1.6*self.shape.w*math.cos(r), y = self.y + 1.6*self.shape.w*math.sin(r), v = 250, r = r, color = self.color, dmg = self.dmg} +end + + +function Player:attack() + camera:shake(2, 0.5) + self.hfx:use('shoot', 0.25) + Area{group = main.current.effects, x = self.x, y = self.y, r = self.r, w = self.area_size_m*64, color = self.color, dmg = self.area_dmg_m*self.dmg} +end + + + + +Projectile = Object:extend() +Projectile:implement(GameObject) +Projectile:implement(Physics) +function Projectile:init(args) + self:init_game_object(args) + self:set_as_rectangle(10, 4, 'dynamic', 'projectile') +end + + +function Projectile:update(dt) + self:update_game_object(dt) + + self:set_angle(self.r) + self:move_along_angle(self.v, self.r) +end + + +function Projectile:draw() + graphics.push(self.x, self.y, self.r) + graphics.rectangle(self.x, self.y, self.shape.w, self.shape.h, 2, 2, self.color) + graphics.pop() +end + + +function Projectile:die(x, y, r, n) + if self.dead then return end + x = x or self.x + y = y or self.y + n = n or random:int(3, 4) + for i = 1, n do HitParticle{group = main.current.effects, x = x, y = y, r = random:float(0, 2*math.pi), color = self.color} end + HitCircle{group = main.current.effects, x = x, y = y}:scale_down() + self.dead = true +end + + +function Projectile:on_collision_enter(other, contact) + local x, y = contact:getPositions() + local nx, ny = contact:getNormal() + local r = 0 + if nx == 0 and ny == -1 then r = -math.pi/2 + elseif nx == 0 and ny == 1 then r = math.pi/2 + elseif nx == -1 and ny == 0 then r = math.pi + else r = 0 end + + if other:is(Wall) then + self:die(x, y, r, random:int(2, 3)) + end +end + + +function Projectile:on_trigger_enter(other, contact) + if table.any(main.current.enemies, function(v) return other:is(v) end) then + self:die(self.x, self.y, nil, random:int(2, 3)) + other:hit(self.dmg) + end +end + + + + +Area = Object:extend() +Area:implement(GameObject) +function Area:init(args) + self:init_game_object(args) + self.shape = Rectangle(self.x, self.y, 1.5*self.w, 1.5*self.w, self.r) + local enemies = main.current.main:get_objects_in_shape(self.shape, main.current.enemies) + for _, enemy in ipairs(enemies) do + enemy:hit(self.dmg) + HitCircle{group = main.current.effects, x = enemy.x, y = enemy.y, rs = 6, color = fg[0], duration = 0.1} + for i = 1, 2 do HitParticle{group = main.current.effects, x = enemy.x, y = enemy.y, color = self.color} end + for i = 1, 2 do HitParticle{group = main.current.effects, x = enemy.x, y = enemy.y, color = enemy.color} end + end + + self.color = fg[0] + self.color_transparent = Color(args.color.r, args.color.g, args.color.b, 0.08) + self.w = 0 + self.hidden = false + self.t:tween(0.05, self, {w = args.w}, math.cubic_in_out, function() self.spring:pull(0.15) end) + self.t:after(0.2, function() + self.color = args.color + self.t:every_immediate(0.05, function() self.hidden = not self.hidden end, 7, function() self.dead = true end) + end) +end + + +function Area:update(dt) + self:update_game_object(dt) +end + + +function Area:draw() + if self.hidden then return end + graphics.push(self.x, self.y, self.r, self.spring.x, self.spring.x) + local w = self.w/2 + local w10 = self.w/10 + local x1, y1 = self.x - w, self.y - w + local x2, y2 = self.x + w, self.y + w + graphics.polyline(self.color, 2, x1, y1 + w10, x1, y1, x1 + w10, y1) + graphics.polyline(self.color, 2, x2 - w10, y1, x2, y1, x2, y1 + w10) + graphics.polyline(self.color, 2, x2 - w10, y2, x2, y2, x2, y2 - w10) + graphics.polyline(self.color, 2, x1, y2 - w10, x1, y2, x1 + w10, y2) + graphics.rectangle((x1+x2)/2, (y1+y2)/2, x2-x1, y2-y1, nil, nil, self.color_transparent) + graphics.pop() +end diff --git a/todo b/todo index b184993..13c7736 100644 --- a/todo +++ b/todo @@ -2,18 +2,27 @@ Vagrant: shoots an ethereal projectile at any nearby enemy that deals physical a Scout: throws a knife at any nearby enemy that deals physical damage and chains, small range Cleric: heals every unit when any one drops below 50% HP Swordsman: deals physical damage in an area around the unit, small range -Archer: shoots an arrow at any nearby enemy in front of the unit, long range +Archer: shoots an arrow at any nearby enemy in front of the unit, very long range Wizard: shoots a projectile at any nearby enemy and deals AoE magical damage on contact, small range +Engineer: drops a turret that shoots secondary projectiles very fast, long range Ranger: yellow, buff attack speed Warrior: orange, buff attack damage Healer: green, buff healing effectiveness -Mage: blue, debuff enemy magic resistance -Cycler: purple, buff cycle speed +Mage: blue, debuff enemy defense +Void: purple, buff area damage and size HP Damage -Attack speed -> stacks additively, capped at minimum 0.125s or +300% -Cycle speed -> stacks additively, capped at minimum 0.5s or +300% -Armor -> if armor >= 0 then dmg_m = 100/(100+armor) else dmg_m = 2-100/(100-armor) -Magic resistance -> if mr >= 0 then dmg_m = 100/(100+mr) else dmg_m = 2-100/(100-mr) +Area damage +Area of effect +Attack speed +Defense -> if defense >= 0 then dmg_m = 100/(100+defense) else dmg_m = 2-100/(100-defense) + +* HP bar should be drawn on top of all player units +* Projectiles +* Areas +* Stats: attack speed, damage +One or a few of the characters +Port over enemy spawn logic from SHOOTRX +Sounds