From 30b10c0bd4e50303a1c8011653ab12e86f347037 Mon Sep 17 00:00:00 2001 From: a327ex Date: Fri, 19 Feb 2021 00:50:27 -0300 Subject: [PATCH] Day 2 --- arena.lua | 37 ++-- devlog.md | 15 ++ engine/game/physics.lua | 12 ++ objects.lua | 387 ++++++++++++++++++++++++---------------- shared.lua | 39 +++- 5 files changed, 323 insertions(+), 167 deletions(-) diff --git a/arena.lua b/arena.lua index df9b754..41fd480 100644 --- a/arena.lua +++ b/arena.lua @@ -8,13 +8,18 @@ function Arena:init(name) self.effects = Group() self.ui = Group():no_camera() self.main:disable_collision_between('player', 'player') - self.main:disable_collision_between('player', 'enemy') - self.main:disable_collision_between('enemy', 'enemy') - self.main:enable_trigger_between('player', 'enemy') + + self.enemies = {Seeker} self.x1, self.y1 = gw/2 - 0.8*gw/2, gh/2 - 0.8*gh/2 self.x2, self.y2 = gw/2 + 0.8*gw/2, gh/2 + 0.8*gh/2 - self.spawn_points = {{x = self.x1 + 32, y = self.y1 + 32}, {x = self.x1 + 32, y = self.y2 - 32}, {x = self.x2 - 32, y = self.y1 + 32}, {x = self.x2 - 32, y = self.y2 - 32}, {x = gw/2, y = gh/2}} + self.spawn_points = { + {x = self.x1 + 32, y = self.y1 + 32, r = math.pi/4}, + {x = self.x1 + 32, y = self.y2 - 32, r = -math.pi/4}, + {x = self.x2 - 32, y = self.y1 + 32, r = 3*math.pi/4}, + {x = self.x2 - 32, y = self.y2 - 32, r = -3*math.pi/4}, + {x = gw/2, y = gh/2, r = random:float(0, 2*math.pi)} + } self.spawn_offsets = {{x = -12, y = -12}, {x = 12, y = -12}, {x = 12, y = 12}, {x = -12, y = 12}, {x = 0, y = 0}} Wall{group = self.main, vertices = math.to_rectangle_vertices(-40, -40, self.x1, gh + 40), color = bg[-1]} @@ -22,8 +27,13 @@ function Arena:init(name) 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 = Unit{group = self.main, x = gw/2, y = gh/2, player = true, leader = true, character = 'vagrant'} - -- self.player:add_follower(Unit{group = self.main, player = true, character = 'seeker'}) + 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 = '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 @@ -39,7 +49,7 @@ function Arena:update(dt) self.ui:update(dt*slow_amount) if input.k.pressed then - self:spawn_enemy() + self:spawn_enemy(4) end end @@ -51,10 +61,13 @@ function Arena:draw() end -function Arena:spawn_enemy() +function Arena:spawn_enemy(n) + n = n or 1 local p = table.random(self.spawn_points) - local o = table.random(self.spawn_offsets) - local leader = Unit{group = self.main, x = p.x + o.x, y = p.y + o.y, enemy = true, leader = true, character = 'seeker'} - leader:add_follower(Unit{group = self.main, x = p.x + o.x, y = p.y + o.y, enemy = true, character = 'seeker'}) - leader:add_follower(Unit{group = self.main, x = p.x + o.x, y = p.y + o.y, enemy = true, character = 'seeker'}) + for i = 1, n do + self.t:after((i-1)*0.1, function() + local o = table.random(self.spawn_offsets) + SpawnEffect{group = self.effects, x = p.x + o.x, y = p.y + o.y, action = function(x, y) Seeker{group = self.main, x = x, y = y, character = 'seeker'} end} + end) + end end diff --git a/devlog.md b/devlog.md index d13a1c1..48fcb11 100644 --- a/devlog.md +++ b/devlog.md @@ -28,3 +28,18 @@ Ideaguyed the basics of the game's classes and mechanics, and implemented basic * Cycle speed: stacks additively, starts at 2 and capped at minimum 0.5s or +300% Perhaps I'm overengineering it already with the stats but I wanna see where this goes. From SHOOTRX it seems like figuring out stats earlier is better than later, and these seem like they have enough flexibility. + +# Day 2 - 18/02/21 + +Went through like 3 small refactors of how I was laying out Unit, Player and Enemy classes and how I wanted enemies to behave. +Settled on just copying enemy behavior 100% from SHOOTRX, which is likely the more correct decision since it saves a lot of time. + +Right now basic player and enemy movement works, as well as melee collisions between player and enemy. To do for tomorrow: + +* HP bar should be drawn on top of all player units +* Projectiles +* Areas +* Stats: attack speed, damage, cycle +* One or a few of the characters +* Port over enemy spawn logic from SHOOTRX +* Sounds diff --git a/engine/game/physics.lua b/engine/game/physics.lua index 646686c..3314490 100644 --- a/engine/game/physics.lua +++ b/engine/game/physics.lua @@ -143,6 +143,18 @@ function Physics:set_as_triangle(w, h, body_type, tag) end +function Physics:connect(other, direction) + if not self.joints then self.joints = {} end + local d = Vector(0, 0) + if direction == 'right' then d:set(1, 0) + elseif direction == 'left' then d:set(-1, 0) + elseif direction == 'up' then d:set(0, -1) + elseif direction == 'down' then d:set(0, 1) end + self.joints[direction] = love.physics.newRevoluteJoint(self.body, other.body, self.x + 0.5*d.x*self.shape.w, self.y + 0.5*d.y*self.shape.h) + return self +end + + -- Automatically called by the group instance this game object belongs to whenever it dies. function Physics:destroy() if self.body then diff --git a/objects.lua b/objects.lua index 67f0431..7086556 100644 --- a/objects.lua +++ b/objects.lua @@ -1,169 +1,62 @@ Unit = Object:extend() -Unit:implement(GameObject) -Unit:implement(Physics) -function Unit:init(args) - self:init_game_object(args) - self:set_as_rectangle(9, 9, 'dynamic', (self.player and 'player') or (self.enemy and 'enemy')) - - if self.character == 'vagrant' then - self.color = fg[0] - self.visual_shape = 'rectangle' - self.classes = {'ranger', 'warrior', 'mage'} - elseif self.character == 'seeker' then - self.color = red[0] - self.visual_shape = 'capsule' - self.classes = {'seeker'} - if self.enemy then - self.enemy_behavior = 'seek' - self:calculate_stats() - self:set_as_steerable(self.v) - end - end - - self:calculate_stats() - - self.r = 0 +function Unit:init_unit() self.hfx:add('hit', 1) - self.hp = self.max_hp - - 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 Unit:update(dt) - self:update_game_object(dt) - self:calculate_stats() - - if self.player and 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 - end - - if not self.player and self.leader then - local player = main.current.player - if self.enemy_behavior == 'seek' then - 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 - self.r = self:get_angle() - else - self:seek_point(player.x, player.y) - self:wander(25, 100, 20) - self:rotate_towards_velocity(0.5) - self.r = self:get_angle() - end - end - end - - if not self.leader then - local ds - if self.parent.v >= 80 and self.parent.v <= 90 then ds = 8 end - if self.parent.v >= 20 and self.parent.v <= 30 then ds = 12 end - if not ds then error('undefined unit distance for parent velocity: ' .. self.parent.v) end - - local d = ds*self.follower_index - local p = self.parent.previous_positions[d] - if p then - self:set_position(p.x, p.y) - self.r = p.r - end - end - - self:set_angle(self.r) -end - - -function Unit:on_trigger_enter(other) - if other:is(Unit) and other.enemy then - other:push(math.length(self:get_velocity())/3.5, self:angle_to_object(other)) - end -end - - -function Unit: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:float(-12*math.pi, 12*math.pi)) - self:set_damping(1.5) - self:set_angular_damping(1.5) -end - - -function Unit:draw() - graphics.push(self.x, self.y, self.r, self.hfx.hit.x, self.hfx.hit.x) - if self.visual_shape == 'triangle' then - graphics.triangle(self.x, self.y, self.shape.w, self.shape.h, self.hfx.hit.f and fg[0] or self.color) - elseif self.visual_shape == 'rectangle' then - graphics.rectangle(self.x, self.y, self.shape.w, self.shape.h, 2, 2, self.hfx.hit.f and fg[0] or self.color) - elseif self.visual_shape == 'capsule' then - graphics.rectangle(self.x, self.y, self.shape.w, 0.525*self.shape.h, 2, 2, self.hfx.hit.f and fg[0] or self.color) - end - graphics.pop() - +function Unit:draw_hp() graphics.push(self.x, self.y, 0, self.hfx.hit.x, self.hfx.hit.x) - if self.show_hp then + 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.player and green[0]) or (self.enemy and red[0])), 2) + 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() end -function Unit:on_collision_enter(other, contact) - if other:is(Wall) and self.leader then - self.hfx:use('hit', 0.5, 200, 10, 0.1) - camera:spring_shake(2, math.pi - self.r) - - for i, unit in ipairs(self.followers) do - self.t:after((i-1)*self.v*0.00185, function() - unit.hfx:use('hit', 0.25, 200, 10, 0.1) - end) - end - - local nx, ny = contact:getNormal() - local vx, vy = self:get_velocity() - if nx == 0 then - self:set_velocity(vx, -vy) - self.r = 2*math.pi - self.r - end - if ny == 0 then - self:set_velocity(-vx, vy) - self.r = math.pi - self.r - end +function Unit:bounce(nx, ny) + local vx, vy = self:get_velocity() + if nx == 0 then + self:set_velocity(vx, -vy) + self.r = 2*math.pi - self.r + end + if ny == 0 then + self:set_velocity(-vx, vy) + self.r = math.pi - self.r end end -function Unit:add_follower(unit) - table.insert(self.followers, unit) - unit.parent = self - unit.follower_index = #self.followers +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') +end + + +function Unit:hit(damage) + if self.dead then return end + + self.hfx:use('hit', 0.25, 200, 10) + self:show_hp() + + local actual_damage = self:calculate_damage(damage) + self.hp = self.hp - actual_damage + if self:is(Player) then + if actual_damage >= 20 then + camera:shake(2, 1) + slow(0.25, 1) + else + camera:shake(2, 0.5) + end + end + + if self.hp <= 0 then + self.dead = true + for i = 1, random:int(4, 6) 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 = 12}:scale_down(0.3):change_color(0.5, self.color) + end end @@ -174,7 +67,7 @@ function Unit:calculate_damage(dmg) end -function Unit:calculate_stats() +function Unit:calculate_stats(first_run) self.base_hp = 100 self.base_dmg = 10 self.base_aspd = 1 @@ -214,6 +107,7 @@ function Unit:calculate_stats() 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 + if first_run then self.hp = self.max_hp end for _, class in ipairs(self.classes) do if class == 'warrior' then self.class_dmg_m = self.class_dmg_m*1.1 @@ -251,3 +145,196 @@ function Unit:calculate_stats() end self.v = (self.base_mvspd + self.class_mvspd_a + self.buff_mvspd_a)*self.class_mvspd_m*self.buff_mvspd_m end + + + +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'} + 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() + + 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.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 + 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/shared.lua b/shared.lua index 5d4c39a..3ad55a1 100644 --- a/shared.lua +++ b/shared.lua @@ -291,6 +291,35 @@ end +SpawnEffect = Object:extend() +SpawnEffect:implement(GameObject) +function SpawnEffect:init(args) + self:init_game_object(args) + self.target_color = self.color or red[0] + self.color = fg[0] + self.rs = 0 + self.t:tween(0.1, self, {rs = 6}, math.cubic_in_out, function() + if self.action then self.action(self.x, self.y) end + self.spring:pull(1) + for i = 1, random:int(6, 8) do HitParticle{group = main.current.effects, x = self.x, y = self.y, color = self.target_color, duration = random:float(0.3, 0.5), w = random:float(5, 8), v = random:float(150, 200)} end + self.t:tween(0.25, self, {rs = 0}, math.linear, function() self.dead = true end) + self.t:after(0.15, function() self.color = self.target_color end) + end) +end + + +function SpawnEffect:update(dt) + self:update_game_object(dt) +end + + +function SpawnEffect:draw() + graphics.circle(self.x, self.y, random:float(0.9, 1.1)*self.rs*self.spring.x, self.color) +end + + + + HoverCrosshair = Object:extend() HoverCrosshair:implement(GameObject) function HoverCrosshair:init(args) @@ -489,7 +518,7 @@ end function flash(duration, color) flashing = true flash_color = color or fg[0] - t:after(duration, function() flashing = false end, 'flash') + trigger:after(duration, function() flashing = false end, 'flash') end @@ -498,7 +527,7 @@ function slow(amount, duration, tween_method) duration = duration or 0.5 tween_method = tween_method or math.cubic_in_out slow_amount = amount - t:tween(duration, _G, {slow_amount = 1}, tween_method, function() slow_amount = 1 end, 'slow') + trigger:tween(duration, _G, {slow_amount = 1}, tween_method, function() slow_amount = 1 end, 'slow') end @@ -510,7 +539,7 @@ function HitCircle:init(args) self:init_game_object(args) self.rs = self.rs or 8 self.duration = self.duration or 0.05 - self.color = self.color or white + self.color = self.color or fg[0] self.t:after(self.duration, function() self.dead = true end, 'die') return self end @@ -552,7 +581,7 @@ function HitParticle:init(args) self.duration = self.duration or random:float(0.2, 0.6) self.w = self.w or random:float(3.5, 7) self.h = self.h or self.w/2 - self.color = self.color or white + self.color = self.color or fg[0] self.t:tween(self.duration, self, {w = 2, h = 2, v = 0}, math.cubic_in_out, function() self.dead = true end) end @@ -585,7 +614,7 @@ AnimationEffect:implement(GameObject) function AnimationEffect:init(args) self:init_game_object(args) self.animation = Animation(self.delay, self.frames, 'once', {[0] = function() self.dead = true end}) - self.color = self.color or white + self.color = self.color or fg[0] end