master
a327ex 2021-02-19 22:54:54 -03:00
parent 30b10c0bd4
commit 8f8910fec9
10 changed files with 496 additions and 226 deletions

View File

@ -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

View File

@ -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.

134
enemies.lua 100644
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -2,6 +2,8 @@ require 'engine'
require 'shared'
require 'arena'
require 'objects'
require 'player'
require 'enemies'
function init()

View File

@ -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

261
player.lua 100644
View File

@ -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

23
todo
View File

@ -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