Day 3
parent
30b10c0bd4
commit
8f8910fec9
23
arena.lua
23
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
|
||||
|
||||
|
||||
|
|
23
devlog.md
23
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.
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
2
main.lua
2
main.lua
|
@ -2,6 +2,8 @@ require 'engine'
|
|||
require 'shared'
|
||||
require 'arena'
|
||||
require 'objects'
|
||||
require 'player'
|
||||
require 'enemies'
|
||||
|
||||
|
||||
function init()
|
||||
|
|
251
objects.lua
251
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
|
||||
|
|
|
@ -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
23
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
|
||||
|
|
Loading…
Reference in New Issue