master
a327ex 2021-02-26 00:52:28 -03:00
parent 57e7b3cab7
commit 7829584241
19 changed files with 634 additions and 117 deletions

View File

@ -50,13 +50,16 @@ function Arena:on_enter(from, level)
WallCover{group = self.post_main, vertices = math.to_rectangle_vertices(self.x1, -40, self.x2, self.y1), color = bg[-1]}
WallCover{group = self.post_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 = 'stormweaver'}
self.player:add_follower(Player{group = self.main, character = 'vagrant'})
self.player:add_follower(Player{group = self.main, character = 'archer'})
self.player:add_follower(Player{group = self.main, character = 'wizard'})
self.player:add_follower(Player{group = self.main, character = 'scout'})
self.player = Player{group = self.main, x = gw/2, y = gh/2, leader = true, character = 'engineer'}
--[[
self.player:add_follower(Player{group = self.main, character = 'sage'})
self.player:add_follower(Player{group = self.main, character = 'archer'})
self.player:add_follower(Player{group = self.main, character = 'spellblade'})
self.player:add_follower(Player{group = self.main, character = 'hunter'})
self.player:add_follower(Player{group = self.main, character = 'cleric'})
self.player:add_follower(Player{group = self.main, character = 'wizard'})
self.player:add_follower(Player{group = self.main, character = 'squire'})
self.player:add_follower(Player{group = self.main, character = 'scout'})
self.player:add_follower(Player{group = self.main, character = 'swordsman'})
self.player:add_follower(Player{group = self.main, character = 'scout'})
self.player:add_follower(Player{group = self.main, character = 'wizard'})

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -201,6 +201,19 @@ I've implemented up to Elementor today and ATM in the process of doing Ninja, bu
Not a lot done today... My sleep schedule is fucked up and I've been unable to focus properly. I managed to get 2 characters done though and also changed their definitions a bit:
Ninja -> Saboteur: calls on other saboteurs to seek targets and explode on contact, AoE has small range
Ninja -> Saboteur: rogue, conjurer, nuker
Linker -> Stormweaver: infuses all projetile attacks with chain lightning, medium range
Linker -> Stormweaver: enchanter, ~~nuker~~
# Day 8-9 - 24-25/02/21
Finished all characters finally. My sleep is so fucked these two days blended together seamlessly. It's so fucking hot and I'm so tired. God damn I fucking hate the summer so fucking much. I hope I can sleep properly today.
Definition changes for one character: Spellblade - knives slowly spiral outwards.
Tomorrow I'll probably do some UI work so the player can buy new characters as he goes from arena to arena, or work on the game's progression in terms of enemy HP and DMG. These are fundamentally the only two things missing
and I have a essentially 1 week to do them, which should be more than enough.
Note: remember to attribute https://freesound.org/people/Hybrid_V/sounds/321215/ for turret_deploy sound in credits.

View File

@ -31,7 +31,7 @@ function Seeker:update(dt)
local player = main.current.player
self:seek_point(player.x, player.y)
self:wander(50, 100, 20)
self:separate(16, main.current.enemies)
self:steering_separate(16, main.current.enemies)
self:rotate_towards_velocity(0.5)
end
self.r = self:get_angle()
@ -61,6 +61,11 @@ function Seeker:on_collision_enter(other, contact)
for i = 1, 2 do HitParticle{group = main.current.effects, x = x, y = y, color = self.color} end
hit2:play{pitch = random:float(0.95, 1.05), volume = 0.35}
end
elseif other:is(Turret) then
_G[random:table{'player_hit1', 'player_hit2'}]:play{pitch = random:float(0.95, 1.05), volume = 0.35}
self:hit(0)
self:push(random:float(2.5, 7), other:angle_to_object(self))
end
end

View File

@ -8,6 +8,9 @@ function Physics:set_as_steerable(max_v, max_f, max_turn_rate, turn_multiplier)
self.steering_enabled = true
self.heading = Vector()
self.side = Vector()
self.steering_force = Vector()
self.applied_force = Vector()
self.applied_impulse = Vector()
self.mass = 1
self.max_v = max_v or 100
self.max_f = max_f or 2000
@ -32,29 +35,33 @@ end
function Physics:steering_update(dt)
if self.steerable and self.steering_enabled then
local steering_force = self:calculate_steering_force(dt):div(self.mass)
self:apply_force(steering_force.x, steering_force.y)
local applied_force = self:calculate_applied_force(dt):div(self.mass)
local applied_impulse = self:calculate_applied_impulse(dt):div(self.mass)
self:apply_force(steering_force.x + applied_force.x, steering_force.y + applied_force.y)
local vx, vy = self:get_velocity()
local v = Vector(vx, vy):truncate(self.max_v)
self:set_velocity(v.x, v.y)
self:set_velocity(v.x + applied_impulse.x, v.y + applied_impulse.y)
if v:length_squared() > 0.00001 then
self.heading = v:clone():normalize()
self.side = self.heading:perpendicular()
end
self.apply_force_f:set(0, 0)
self.apply_impulse_f:set(0, 0)
end
end
function Physics:calculate_steering_force(dt)
local steering_force = Vector(0, 0)
if self.seeking then steering_force:add(self.seek_f) end
if self.fleeing then steering_force:add(self.flee_f) end
if self.pursuing then steering_force:add(self.pursuit_f) end
if self.evading then steering_force:add(self.evade_f) end
if self.wandering then steering_force:add(self.wander_f) end
if self.path_following then steering_force:add(self.path_follow_f) end
if self.separating then steering_force:add(self.separation_f) end
if self.aligning then steering_force:add(self.alignment_f) end
if self.cohesing then steering_force:add(self.cohesion_f) end
self.steering_force:set(0, 0)
if self.seeking then self.steering_force:add(self.seek_f) end
if self.fleeing then self.steering_force:add(self.flee_f) end
if self.pursuing then self.steering_force:add(self.pursuit_f) end
if self.evading then self.steering_force:add(self.evade_f) end
if self.wandering then self.steering_force:add(self.wander_f) end
if self.path_following then self.steering_force:add(self.path_follow_f) end
if self.separating then self.steering_force:add(self.separation_f) end
if self.aligning then self.steering_force:add(self.alignment_f) end
if self.cohesing then self.steering_force:add(self.cohesion_f) end
self.seeking = false
self.fleeing = false
self.pursuing = false
@ -64,7 +71,55 @@ function Physics:calculate_steering_force(dt)
self.separating = false
self.aligning = false
self.cohesing = false
return steering_force:truncate(self.max_f)
return self.steering_force:truncate(self.max_f)
end
function Physics:calculate_applied_force(dt)
self.applied_force:set(0, 0)
if self.applying_force then self.applied_force:add(self.apply_force_f) end
return self.applied_force
end
function Physics:calculate_applied_impulse(dt)
self.applied_impulse:set(0, 0)
if self.applying_impulse then self.applied_impulse:add(self.apply_impulse_f) end
return self.applied_impulse
end
-- Applies force f to the object at the given angle r for duration s
-- This plays along with steering behaviors, whereas the apply_force function simply applies it directly to the body and doesn't work when steering behaviors are enabled
-- self:apply_steering_force(100, math.pi/4)
function Physics:apply_steering_force(f, r, s)
self.applying_force = true
self.apply_force_f:set(f*math.cos(r), f*math.sin(r))
if s then
self.t:after((s or 0.01)/2, function()
self.t:tween((s or 0.01)/2, self.apply_force_f, {x = 0, y = 0}, math.linear, function()
self.applying_force = false
self.apply_force_f:set(0, 0)
end, 'apply_steering_force_2')
end, 'apply_steering_force_1')
end
end
-- Applies impulse f to the object at the given angle r for duration s
-- This plays along with steering behaviors, whereas the apply_impulse function simply applies it directly to the body and doesn't work when steering behaviors are enabled
-- self:apply_steering_impulse(100, math.pi/4, 0.5)
function Physics:apply_steering_impulse(f, r, s)
self.applying_impulse = true
self.apply_impulse_f:set(f*math.cos(r), f*math.sin(r))
if s then
self.t:after((s or 0.01)/2, function()
self.t:tween((s or 0.01)/2, self.apply_impulse_f, {x = 0, y = 0}, math.linear, function()
self.applying_impulse = false
self.apply_impulse_f:set(0, 0)
end, 'apply_steering_impulse_2')
end, 'apply_steering_impulse_1')
end
end

View File

@ -31,6 +31,20 @@ function Trigger:after(delay, action, tag)
end
-- Calls the action every delay seconds if the condition is true.
-- If the condition isn't true when delay seconds are up then it waits and only performs the action and resets the timer when that happens.
-- If times is passed in then it only calls action for that amount of times.
-- If after is passed in then it is called after the last time action is called.
-- If tag is passed in then any other trigger actions with the same tag are automatically cancelled.
-- trigger:cooldown(2, function() return #self:get_objects_in_shape(self.attack_sensor, enemies) > 0 end, function() self:attack() end) -> only attacks when 2 seconds have passed and there are more than 0 enemies around
function Trigger:cooldown(delay, condition, action, times, after, tag)
local times = times or 0
local after = after or function() end
local tag = tag or random:uid()
self.triggers[tag] = {type = "cooldown", timer = 0, unresolved_delay = delay, delay = self:resolve_delay(delay), condition = condition, action = action, times = times, max_times = times, after = after, multiplier = 1}
end
-- Calls the action every delay seconds.
-- Or calls the action once every time the condition becomes true.
-- If times is passed in then it only calls action for that amount of times.
@ -162,6 +176,20 @@ function Trigger:update(dt)
if trigger.type == "run" then
trigger.action()
elseif trigger.type == "cooldown" then
if trigger.timer > trigger.delay*trigger.multiplier and trigger.condition() then
trigger.action()
trigger.timer = 0
trigger.delay = self:resolve_delay(trigger.unresolved_delay)
if trigger.times > 0 then
trigger.times = trigger.times - 1
if trigger.times <= 0 then
trigger.after()
self.triggers[tag] = nil
end
end
end
elseif trigger.type == "after" then
if trigger.timer > trigger.delay then
trigger.action()

View File

@ -148,6 +148,14 @@ function graphics.circle(x, y, r, color, line_width)
end
-- Draws an arc of radius r from angle r1 to angle r2 centered on x, y.
-- If color is passed in then the arc will be filled with that color (color is Color object)
-- If line_width is passed in then the arc will not be filled and will instead be drawn as a set of lines of the given width.
function graphics.arc(arctype, x, y, r, r1, r2, color, line_width)
graphics.shape("arc", color, line_width, arctype, x, y, r, r1, r2)
end
-- Draws a polygon with the given points.
-- If color is passed in then the polygon will be filled with that color (color is Color object)
-- If line_width is passed in then the polygon will not be filled and will instead be drawn as a set of lines of the given width.

View File

@ -52,6 +52,15 @@ function init()
spark2 = Sound('Spark 2.ogg', s)
spark3 = Sound('Spark 3.ogg', s)
stormweaver1 = Sound('Buff 8.ogg', s)
cannoneer1 = Sound('Cannon shots 1.ogg', s)
cannoneer2 = Sound('Cannon shots 7.ogg', s)
cannon_hit_wall1 = Sound('Cannon impact sounds (Hitting ship) 4.ogg', s)
pet1 = Sound('Wolf barks 5.ogg', s)
turret1 = Sound('Sci Fi Machine Gun 7.ogg', s)
turret2 = Sound('Sniper Shot_09.ogg', s)
turret_hit_wall1 = Sound('Concrete 6.ogg', s)
turret_hit_wall2 = Sound('Concrete 7.ogg', s)
turret_deploy = Sound('321215__hybrid-v__sci-fi-weapons-deploy.ogg', s)
main = Main()
main:add(Arena'arena')

View File

@ -135,8 +135,7 @@ function Unit:init_unit()
self.hfx:add('hit', 1)
self.hfx:add('shoot', 1)
self.hp_bar = HPBar{group = main.current.effects, parent = self}
self.heal_bar = HealBar{group = main.current.effects, parent = self}
self.infused_bar = InfusedBar{group = main.current.effects, parent = self}
self.effect_bar = EffectBar{group = main.current.effects, parent = self}
end
@ -156,19 +155,36 @@ end
function Unit:show_hp(n)
self.hp_bar.hidden = false
self.hp_bar.color = red[0]
self.t:after(n or 2, function() self.hp_bar.hidden = true end, 'hp_bar')
end
function Unit:show_heal(n)
self.heal_bar.hidden = false
self.t:after(n or 4, function() self.heal_bar.hidden = true end, 'heal_bar')
self.effect_bar.hidden = false
self.effect_bar.color = green[0]
self.t:after(n or 4, function() self.effect_bar.hidden = true end, 'effect_bar')
end
function Unit:show_infused(n)
self.infused_bar.hidden = false
self.t:after(n, function() self.infused_bar.hidden = true end, 'infused_bar')
self.effect_bar.hidden = false
self.effect_bar.color = blue[0]
self.t:after(n or 4, function() self.effect_bar.hidden = true end, 'effect_bar')
end
function Unit:show_squire(n)
self.effect_bar.hidden = false
self.effect_bar.color = purple[0]
self.t:after(n or 4, function() self.effect_bar.hidden = false end, 'effect_bar')
end
function Unit:show_chronomancer(n)
self.effect_bar.hidden = false
self.effect_bar.color = purple[0]
self.t:after(n or 2, function() self.effect_bar.hidden = false end, 'effect_bar')
end
@ -198,17 +214,19 @@ function Unit:calculate_stats(first_run)
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_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_area_dmg_m = 1
self.buff_area_size_m = 1
self.buff_def_m = 1
self.buff_mvspd_m = 1
if first_run then
self.buff_hp_a = 0
self.buff_dmg_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_area_dmg_m = 1
self.buff_area_size_m = 1
self.buff_def_m = 1
self.buff_mvspd_m = 1
end
for _, class in ipairs(self.classes) do
if class == 'warrior' then self.class_hp_m = self.class_hp_m*1.4
@ -280,59 +298,30 @@ end
InfusedBar = Object:extend()
InfusedBar:implement(GameObject)
InfusedBar:implement(Parent)
function InfusedBar:init(args)
EffectBar = Object:extend()
EffectBar:implement(GameObject)
EffectBar:implement(Parent)
function EffectBar:init(args)
self:init_game_object(args)
self.hidden = true
self.color = blue[0]
self.color_transparent = Color(self.color.r, self.color.g, self.color.b, 0.2)
self.color = fg[0]
end
function InfusedBar:update(dt)
function EffectBar:update(dt)
self:update_game_object(dt)
self:follow_parent_exclusively()
end
function InfusedBar:draw()
function EffectBar: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.rectangle(p.x, p.y, 1.25*p.shape.w, 1.25*p.shape.h, 2, 2, self.color_transparent)
graphics.rectangle(p.x, p.y, 1.25*p.shape.w, 1.25*p.shape.h, 2, 2, self.color, 1)
graphics.pop()
end
HealBar = Object:extend()
HealBar:implement(GameObject)
HealBar:implement(Parent)
function HealBar:init(args)
self:init_game_object(args)
self.hidden = true
self.color = green[0]
self.color_transparent = Color(self.color.r, self.color.g, self.color.b, 0.2)
end
function HealBar:update(dt)
self:update_game_object(dt)
self:follow_parent_exclusively()
end
function HealBar: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.rectangle(p.x, p.y, 1.25*p.shape.w, 1.25*p.shape.h, 2, 2, self.color_transparent)
graphics.rectangle(p.x, p.y, 1.25*p.shape.w, 1.25*p.shape.h, 2, 2, self.color, 1)
graphics.push(p.x, p.y, p.r, p.hfx.hit.x, p.hfx.hit.x)
graphics.rectangle(p.x, p.y, 3, 3, 1, 1, self.color)
graphics.pop()
]]--
end

View File

@ -13,7 +13,7 @@ function Player:init(args)
self.classes = {'ranger', 'warrior', 'psy'}
self.attack_sensor = Circle(self.x, self.y, 96)
self.t:every(2, function()
self.t:cooldown(2, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, 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))
@ -27,11 +27,8 @@ function Player:init(args)
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(96)
end
self.t:cooldown(3, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, function()
self:attack(96)
end, nil, nil, 'attack')
elseif self.character == 'wizard' then
@ -41,10 +38,10 @@ function Player:init(args)
self.classes = {'mage'}
self.attack_sensor = Circle(self.x, self.y, 128)
self.t:every(2, function()
self.t:cooldown(2, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, 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), {wizard = self})
self:shoot(self:angle_to_object(closest_enemy))
end
end, nil, nil, 'shoot')
@ -55,7 +52,7 @@ function Player:init(args)
self.classes = {'ranger'}
self.attack_sensor = Circle(self.x, self.y, 160)
self.t:every(2, function()
self.t:cooldown(2, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, 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), {pierce = 1000})
@ -69,7 +66,7 @@ function Player:init(args)
self.classes = {'rogue'}
self.attack_sensor = Circle(self.x, self.y, 64)
self.t:every(2, function()
self.t:cooldown(2, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, 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), {chain = 3})
@ -102,7 +99,7 @@ function Player:init(args)
self.classes = {'warrior', 'rogue'}
self.attack_sensor = Circle(self.x, self.y, 96)
self.t:every(3, function()
self.t:cooldown(3, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, 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))
@ -116,11 +113,8 @@ function Player:init(args)
self.classes = {'warrior', 'rogue'}
self.attack_sensor = Circle(self.x, self.y, 64)
self.t:every(4, function()
local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies)
if enemies and #enemies > 0 then
self:shoot()
end
self.t:cooldown(4, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, function()
self:shoot()
end, nil, nil, 'shoot')
elseif self.character == 'elementor' then
@ -130,7 +124,7 @@ function Player:init(args)
self.classes = {'mage', 'nuker'}
self.attack_sensor = Circle(self.x, self.y, 128)
self.t:every(12, function()
self.t:cooldown(12, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, function()
local enemy = self:get_random_object_in_shape(self.attack_sensor, main.current.enemies)
if enemy then
self:attack(128, {x = enemy.x, y = enemy.y})
@ -157,7 +151,6 @@ function Player:init(args)
self.visual_shape = 'rectangle'
self.classes = {'enchanter'}
self.attack_sensor = Circle(self.x, self.y, 96)
self.t:every(8, function()
stormweaver1:play{pitch = random:float(0.95, 1.05), volume = 0.5}
local followers
@ -169,6 +162,142 @@ function Player:init(args)
end
end
end)
elseif self.character == 'sage' then
self.color = purple[0]
self:set_as_rectangle(9, 9, 'dynamic', 'player')
self.visual_shape = 'rectangle'
self.classes = {'mage', 'nuker'}
self.attack_sensor = Circle(self.x, self.y, 96)
self.t:cooldown(12, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, 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)
elseif self.character == 'squire' then
self.color = green[0]
self:set_as_rectangle(9, 9, 'dynamic', 'player')
self.visual_shape = 'rectangle'
self.classes = {'warrior', 'healer', 'enchanter'}
self.t:every(8, function()
self.applying_buff = true
local followers
local leader = (self.leader and self) or self.parent
if self.leader then followers = self.followers else followers = self.parent.followers end
local next_character = followers[self.follower_index + 1]
local previous_character = followers[self.follower_index - 1]
if next_character then next_character:squire_buff(8) end
if previous_character then previous_character:squire_buff(8) end
self.t:after(8, function() self.applying_buff = false end, 'squire_buff_apply')
heal1:play{pitch = random:float(0.95, 1.05), volume = 0.5}
if next_character then next_character:heal(0.1*next_character.max_hp) end
if previous_character then previous_character:heal(0.1*previous_character.max_hp) end
end)
elseif self.character == 'cannoneer' then
self.color = yellow[0]
self:set_as_rectangle(9, 9, 'dynamic', 'player')
self.visual_shape = 'rectangle'
self.classes = {'ranger', 'nuker'}
self.attack_sensor = Circle(self.x, self.y, 128)
self.t:cooldown(6, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, 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 == 'dual_gunner' then
self.color = yellow[0]
self:set_as_rectangle(9, 9, 'dynamic', 'player')
self.visual_shape = 'rectangle'
self.classes = {'ranger', 'rogue'}
self.attack_sensor = Circle(self.x, self.y, 96)
self.t:cooldown(2, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, 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 == 'hunter' then
self.color = orange[0]
self:set_as_rectangle(9, 9, 'dynamic', 'player')
self.visual_shape = 'rectangle'
self.classes = {'ranger', 'rogue'}
self.attack_sensor = Circle(self.x, self.y, 160)
self.t:cooldown(2, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, 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 == 'chronomancer' then
self.color = purple[0]
self:set_as_rectangle(9, 9, 'dynamic', 'player')
self.visual_shape = 'rectangle'
self.classes = {'mage', 'enchanter'}
self.t:every(2, function()
local followers
local leader = (self.leader and self) or self.parent
if self.leader then followers = self.followers else followers = self.parent.followers end
local next_character = followers[self.follower_index + 1]
local previous_character = followers[self.follower_index - 1]
if next_character then next_character:chronomancer_buff(2) end
if previous_character then previous_character:chronomancer_buff(2) end
end)
elseif self.character == 'spellblade' then
self.color = blue[0]
self:set_as_rectangle(9, 9, 'dynamic', 'player')
self.visual_shape = 'rectangle'
self.classes = {'mage', 'rogue'}
self.t:every(2, function()
self:shoot(random:float(0, 2*math.pi))
end, nil, nil, 'shoot')
elseif self.character == 'psykeeper' then
self.color = fg[0]
self:set_as_rectangle(9, 9, 'dynamic', 'player')
self.visual_shape = 'rectangle'
self.classes = {'healer', 'psy'}
self.psykeeper_heal = 0
self.t:every(8, function()
local followers
local leader = (self.leader and self) or self.parent
if self.leader then followers = self.followers else followers = self.parent.followers end
if self.psykeeper_heal > 0 then
local heal_amount = math.floor(self.psykeeper_heal/(#followers+1))
if self.leader then self:heal(heal_amount) else self.parent:heal(heal_amount) end
for _, f in ipairs(followers) do f:heal(heal_amount) end
heal1:play{pitch = random:float(0.95, 1.05), volume = 0.5}
self.psykeeper_heal = 0
end
end)
elseif self.character == 'engineer' then
self.color = orange[0]
self:set_as_rectangle(9, 9, 'dynamic', 'player')
self.visual_shape = 'rectangle'
self.classes = {'conjurer'}
self.t:every(8, function()
SpawnEffect{group = main.current.effects, x = self.x, y = self.y, color = orange[0], action = function(x, y)
Turret{group = main.current.main, x = x, y = y, parent = self}
end}
end)
end
self:calculate_stats(true)
@ -184,13 +313,55 @@ end
function Player:update(dt)
if self.attack_sensor then self.enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies) end
self:update_game_object(dt)
if self.character == 'squire' then
local followers
local leader = (self.leader and self) or self.parent
if self.leader then followers = self.followers else followers = self.parent.followers end
local next_character = followers[self.follower_index + 1]
local previous_character = followers[self.follower_index - 1]
if self.applying_buff then
if next_character then
next_character.squire_dmg_a = 10
next_character.squire_def_a = 25
end
if previous_character then
previous_character.squire_dmg_a = 10
previous_character.squire_def_a = 25
end
else
if next_character then
next_character.squire_dmg_a = 0
next_character.squire_def_a = 0
end
if previous_character then
previous_character.squire_dmg_a = 0
previous_character.squire_def_a = 0
end
end
elseif self.character == 'chronomancer' then
local followers
local leader = (self.leader and self) or self.parent
if self.leader then followers = self.followers else followers = self.parent.followers end
local next_character = followers[self.follower_index + 1]
local previous_character = followers[self.follower_index - 1]
if next_character then next_character.chronomancer_aspd_m = 1.25 end
if previous_character then previous_character.chronomancer_aspd_m = 1.25 end
end
self.buff_dmg_a = self.squire_dmg_a or 0
self.buff_def_a = self.squire_def_a or 0
self.buff_aspd_m = self.chronomancer_aspd_m or 1
self:calculate_stats()
if self.attack_sensor then self.attack_sensor:move_to(self.x, self.y) end
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
@ -210,7 +381,7 @@ function Player:update(dt)
self:set_angle(self.r)
else
local target_distance = 10.6*self.follower_index
local target_distance = 10.4*self.follower_index
local distance_sum = 0
local p
local previous = self.parent
@ -246,7 +417,6 @@ function Player:draw()
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()
-- self.attack_sensor:draw(self.color, 2)
end
@ -292,6 +462,8 @@ function Player:hit(damage)
_G[random:table{'player_hit1', 'player_hit2'}]:play{pitch = random:float(0.95, 1.05), volume = 0.5}
camera:shake(4, 0.5)
if self.character == 'psykeeper' then self.psykeeper_heal = self.psykeeper_heal + actual_damage end
if self.hp <= 0 then
slow(0.25, 1)
self.dead = true
@ -319,6 +491,16 @@ function Player:chain_infuse(duration)
end
function Player:squire_buff(duration)
self:show_squire(duration or 2)
end
function Player:chronomancer_buff(duration)
self:show_chronomancer(duration or 2)
end
function Player:add_follower(unit)
table.insert(self.followers, unit)
unit.parent = self
@ -338,6 +520,7 @@ function Player:shoot(r, mods)
Projectile(table.merge(t, mods or {}))
r = r + math.pi/8
end
elseif self.character == 'blade' then
local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies)
if enemies and #enemies > 0 then
@ -348,20 +531,41 @@ function Player:shoot(r, mods)
Projectile(table.merge(t, mods or {}))
end
end
elseif self.character == 'sage' then
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}
local t = {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 = 25, r = r, color = self.color, dmg = self.dmg, pierce = 1000, character = 'sage', parent = self}
Projectile(table.merge(t, mods or {}))
elseif self.character == 'dual_gunner' then
HitCircle{group = main.current.effects, x = self.x + 0.8*self.shape.w*math.cos(r) + 4*math.cos(r - math.pi/2), y = self.y + 0.8*self.shape.w*math.sin(r) + 4*math.sin(r - math.pi/2), rs = 6}
HitCircle{group = main.current.effects, x = self.x + 0.8*self.shape.w*math.cos(r) + 4*math.cos(r + math.pi/2), y = self.y + 0.8*self.shape.w*math.sin(r) + 4*math.sin(r + math.pi/2), rs = 6}
local t1 = {group = main.current.main, x = self.x + 1.6*self.shape.w*math.cos(r) + 4*math.cos(r - math.pi/2) , y = self.y + 1.6*self.shape.w*math.sin(r) + 4*math.sin(r - math.pi/2),
v = 250, r = r, color = self.color, dmg = self.dmg, character = self.character, parent = self}
local t2 = {group = main.current.main, x = self.x + 1.6*self.shape.w*math.cos(r) + 4*math.cos(r + math.pi/2) , y = self.y + 1.6*self.shape.w*math.sin(r) + 4*math.sin(r + math.pi/2),
v = 250, r = r, color = self.color, dmg = self.dmg, character = self.character, parent = self}
Projectile(table.merge(t1, mods or {}))
Projectile(table.merge(t2, mods or {}))
else
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}
local t = {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, character = self.character, parent = self}
Projectile(table.merge(t, mods or {}))
end
if self.character == 'vagrant' then
if self.character == 'vagrant' or self.character == 'dual_gunner' then
shoot1:play{pitch = random:float(0.95, 1.05), volume = 0.3}
elseif self.character == 'archer' then
elseif self.character == 'archer' or self.character == 'hunter' then
archer1:play{pitch = random:float(0.95, 1.05), volume = 0.5}
elseif self.character == 'wizard' then
wizard1:play{pitch = random:float(0.95, 1.05), volume = 0.15}
elseif self.character == 'scout' or self.character == 'outlaw' or self.character == 'blade' then
elseif self.character == 'scout' or self.character == 'outlaw' or self.character == 'blade' or self.character == 'spellblade' then
_G[random:table{'scout1', 'scout2'}]:play{pitch = random:float(0.95, 1.05), volume = 0.5}
if self.character == 'spellblade' then
wizard1:play{pitch = random:float(0.95, 1.05), volume = 0.15}
end
elseif self.character == 'cannoneer' then
_G[random:table{'cannoneer1', 'cannoneer2'}]:play{pitch = random:float(0.95, 1.05), volume = 0.5}
end
end
@ -393,21 +597,71 @@ function Projectile:init(args)
self.chain = args.chain or 0
self.chain_enemies_hit = {}
self.infused_enemies_hit = {}
if self.character == 'sage' then
self.dmg = 0
self.pull_sensor = Circle(self.x, self.y, 64*self.parent.area_size_m)
self.rs = 0
self.t:tween(0.05, self, {rs = self.shape.w/2.5}, math.cubic_in_out, function() self.spring:pull(0.15) end)
self.t:after(4, function()
self.t:every_immediate(0.05, function() self.hidden = not self.hidden end, 7, function() self:die() end)
end)
self.color_transparent = Color(args.color.r, args.color.g, args.color.b, 0.08)
self.t:every(0.08, function()
HitParticle{group = main.current.effects, x = self.x, y = self.y, color = self.color}
end)
self.vr = 0
self.dvr = random:float(-math.pi/4, math.pi/4)
elseif self.character == 'spellblade' then
self.pierce = 1000
self.orbit_r = 0
self.orbit_vr = 8*math.pi
self.t:tween(6.25, self, {orbit_vr = math.pi}, math.expo_out, function()
self.t:tween(12.25, self, {orbit_vr = 0}, math.linear)
end)
end
end
function Projectile:update(dt)
self:update_game_object(dt)
if self.character == 'spellblade' then
self.orbit_r = self.orbit_r + self.orbit_vr*dt
end
self:set_angle(self.r)
self:move_along_angle(self.v, self.r)
self:move_along_angle(self.v, self.r + (self.orbit_r or 0))
if self.character == 'sage' then
self.pull_sensor:move_to(self.x, self.y)
local enemies = self:get_objects_in_shape(self.pull_sensor, main.current.enemies)
for _, enemy in ipairs(enemies) do
enemy:apply_steering_force(math.remap(self:distance_to_object(enemy), 0, 100, 250, 50), enemy:angle_to_object(self))
end
self.vr = self.vr + self.dvr*dt
end
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()
if self.character == 'sage' then
if self.hidden then return end
graphics.push(self.x, self.y, self.r + self.vr, self.spring.x, self.spring.x)
graphics.circle(self.x, self.y, self.rs + random:float(-1, 1), self.color)
graphics.circle(self.x, self.y, self.pull_sensor.rs, self.color_transparent)
local lw = math.remap(self.pull_sensor.rs, 32, 256, 2, 4)
for i = 1, 4 do graphics.arc('open', self.x, self.y, self.pull_sensor.rs, (i-1)*math.pi/2 + math.pi/4 - math.pi/8, (i-1)*math.pi/2 + math.pi/4 + math.pi/8, self.color, lw) end
graphics.pop()
else
graphics.push(self.x, self.y, self.r + (self.orbit_r or 0))
graphics.rectangle(self.x, self.y, self.shape.w, self.shape.h, 2, 2, self.color)
graphics.pop()
end
end
@ -424,6 +678,8 @@ function Projectile:die(x, y, r, n)
Area{group = main.current.effects, x = self.x, y = self.y, r = self.r, w = self.parent.area_size_m*32, color = self.color, dmg = self.parent.area_dmg_m*self.dmg, character = self.character}
elseif self.character == 'blade' then
Area{group = main.current.effects, x = self.x, y = self.y, r = self.r, w = self.parent.area_size_m*64, color = self.color, dmg = self.parent.area_dmg_m*self.dmg, character = self.character}
elseif self.character == 'cannoneer' then
Area{group = main.current.effects, x = self.x, y = self.y, r = self.r, w = self.parent.area_size_m*96, color = self.color, dmg = self.parent.area_dmg_m*self.dmg, character = self.character}
end
end
@ -438,20 +694,29 @@ function Projectile:on_collision_enter(other, contact)
else r = 0 end
if other:is(Wall) then
if self.character == 'archer' then
if self.character == 'archer' or self.character == 'hunter' then
self:die(x, y, r, 0)
_G[random:table{'arrow_hit_wall1', 'arrow_hit_wall2'}]:play{pitch = random:float(0.9, 1.1), volume = 0.2}
WallArrow{group = main.current.main, x = x, y = y, r = self.r, color = self.color}
elseif self.character == 'scout' or self.character == 'outlaw' or self.character == 'blade' then
elseif self.character == 'scout' or self.character == 'outlaw' or self.character == 'blade' or self.character == 'spellblade' then
self:die(x, y, r, 0)
knife_hit_wall1:play{pitch = random:float(0.9, 1.1), volume = 0.2}
local r = Unit.bounce(self, nx, ny)
trigger:after(0.01, function()
WallKnife{group = main.current.main, x = x, y = y, r = r, v = self.v*0.1, color = self.color}
end)
if self.character == 'spellblade' then
magic_area1:play{pitch = random:float(0.95, 1.05), volume = 0.075}
end
elseif self.character == 'wizard' then
self:die(x, y, r, random:int(2, 3))
magic_area1:play{pitch = random:float(0.95, 1.05), volume = 0.075}
elseif self.character == 'cannoneer' then
self:die(x, y, r, random:int(2, 3))
cannon_hit_wall1:play{pitch = random:float(0.95, 1.05), volume = 0.1}
elseif self.character == 'engineer' then
self:die(x, y, r, random:int(2, 3))
_G[random:table{'turret_hit_wall1', 'turret_hit_wall2'}]:play{pitch = random:float(0.9, 1.1), volume = 0.2}
else
self:die(x, y, r, random:int(2, 3))
proj_hit_wall1:play{pitch = random:float(0.9, 1.1), volume = 0.2}
@ -461,6 +726,8 @@ end
function Projectile:on_trigger_enter(other, contact)
if self.character == 'sage' then return end
if table.any(main.current.enemies, function(v) return other:is(v) end) then
if self.pierce <= 0 and self.chain <= 0 then
self:die(self.x, self.y, nil, random:int(2, 3))
@ -482,9 +749,11 @@ function Projectile:on_trigger_enter(other, contact)
HitParticle{group = main.current.effects, x = self.x, y = self.y, color = other.color}
end
if self.character == 'archer' or self.character == 'scout' or self.character == 'outlaw' or self.character == 'blade' then
if self.character == 'archer' or self.character == 'scout' or self.character == 'outlaw' or self.character == 'blade' or self.character == 'hunter' or self.character == 'spellblade' or self.character == 'engineer' then
hit2:play{pitch = random:float(0.95, 1.05), volume = 0.35}
if self.character == 'spellblade' then
magic_area1:play{pitch = random:float(0.95, 1.05), volume = 0.15}
end
elseif self.character == 'wizard' then
magic_area1:play{pitch = random:float(0.95, 1.05), volume = 0.15}
else
@ -493,6 +762,14 @@ function Projectile:on_trigger_enter(other, contact)
other:hit(self.dmg)
if self.character == 'hunter' and random:bool(40) then
trigger:after(0.01, function()
SpawnEffect{group = main.current.effects, x = self.parent.x, y = self.parent.y, color = orange[0], action = function(x, y)
Pet{group = main.current.main, x = x, y = y, r = self.parent:angle_to_object(other), v = 150, parent = self.parent}
end}
end)
end
if self.parent.chain_infused then
local src = other
for i = 1, 2 do
@ -532,6 +809,8 @@ function Area:init(args)
hit2:play{pitch = random:float(0.95, 1.05), volume = 0.2}
elseif self.character == 'saboteur' then
_G[random:table{'saboteur_hit1', 'saboteur_hit2'}]:play{pitch = random:float(0.95, 1.05), volume = 0.2}
elseif self.character == 'cannoneer' then
_G[random:table{'saboteur_hit1', 'saboteur_hit2'}]:play{pitch = random:float(0.95, 1.05), volume = 0.075}
end
end
@ -559,10 +838,11 @@ function Area:draw()
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)
local lw = math.remap(w, 32, 256, 2, 4)
graphics.polyline(self.color, lw, x1, y1 + w10, x1, y1, x1 + w10, y1)
graphics.polyline(self.color, lw, x2 - w10, y1, x2, y1, x2, y1 + w10)
graphics.polyline(self.color, lw, x2 - w10, y2, x2, y2, x2, y2 - w10)
graphics.polyline(self.color, lw, 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
@ -570,6 +850,133 @@ end
Turret = Object:extend()
Turret:implement(GameObject)
Turret:implement(Physics)
function Turret:init(args)
self:init_game_object(args)
self:set_as_rectangle(14, 6, 'static', 'player')
self:set_restitution(0.5)
self.hfx:add('hit', 1)
self.color = orange[0]
self.attack_sensor = Circle(self.x, self.y, 96)
turret_deploy:play{pitch = 1.2, volume = 0.2}
self.t:every({3.5, 4.5}, function()
self.t:every({0.1, 0.2}, function()
self.hfx:use('hit', 0.25, 200, 10)
HitCircle{group = main.current.effects, x = self.x + 0.8*self.shape.w*math.cos(self.r), y = self.y + 0.8*self.shape.w*math.sin(self.r), rs = 6}
local t = {group = main.current.main, x = self.x + 1.6*self.shape.w*math.cos(self.r), y = self.y + 1.6*self.shape.w*math.sin(self.r), v = 200, r = self.r, color = self.color, dmg = self.parent.dmg,
character = self.parent.character, parent = self.parent}
Projectile(table.merge(t, mods or {}))
turret1:play{pitch = random:float(0.95, 1.05), volume = 0.35}
turret2:play{pitch = random:float(0.95, 1.05), volume = 0.35}
end, 3)
end)
self.t:after(24, function()
local n = n or random:int(3, 4)
for i = 1, n do HitParticle{group = main.current.effects, x = self.x, y = self.y, r = random:float(0, 2*math.pi), color = self.color} end
HitCircle{group = main.current.effects, x = self.x, y = self.y}:scale_down()
self.dead = true
end)
end
function Turret:update(dt)
self:update_game_object(dt)
local closest_enemy = self:get_closest_object_in_shape(self.attack_sensor, main.current.enemies)
if closest_enemy then
self:rotate_towards_object(closest_enemy, 0.2)
self.r = self:get_angle()
end
end
function Turret: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
Pet = Object:extend()
Pet:implement(GameObject)
Pet:implement(Physics)
function Pet:init(args)
self:init_game_object(args)
self:set_as_rectangle(8, 8, 'dynamic', 'projectile')
self:set_restitution(0.5)
self.hfx:add('hit', 1)
self.color = orange[0]
self.pierce = 6
pet1:play{pitch = random:float(0.95, 1.05), volume = 0.35}
end
function Pet:update(dt)
self:update_game_object(dt)
self:set_angle(self.r)
self:move_along_angle(self.v, self.r)
end
function Pet: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 Pet: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
local 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
hit2:play{pitch = random:float(0.95, 1.05), volume = 0.35}
end
end
function Pet:on_trigger_enter(other)
if table.any(main.current.enemies, function(v) return other:is(v) end) then
if self.pierce <= 0 then
camera:shake(2, 0.5)
other:hit(self.parent.dmg)
other:push(35, self:angle_to_object(other))
self.dead = true
local n = 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()
else
camera:shake(2, 0.5)
other:hit(self.parent.dmg)
other:push(35, self:angle_to_object(other))
self.pierce = self.pierce - 1
end
self.hfx:use('hit', 0.25)
HitCircle{group = main.current.effects, x = self.x, y = self.y, rs = 6, color = fg[0], duration = 0.1}
HitParticle{group = main.current.effects, x = self.x, y = self.y, color = self.color}
HitParticle{group = main.current.effects, x = self.x, y = self.y, color = other.color}
end
end
Saboteur = Object:extend()
Saboteur:implement(GameObject)
Saboteur:implement(Physics)

12
todo
View File

@ -9,15 +9,15 @@ Blade: shoots multiple blades at nearby enemies, each dealing AoE damage on cont
Elementor: deals massive AoE damage to a random target, long range, AoE has medium range
Saboteur: calls on other saboteurs to seek targets and explode on contact, AoE has small range
Stormweaver: infuses all projectile attacks with chain lightning, small range
Sage: shoots a slow projectile that draws enemies in, medium range, AoE has medium range
Sage: shoots a slow projectile that draws enemies in, medium range, AoE has small range
Squire: improves damage and defense for adjacent units, as well as healing them periodically
Cannoneer: shoots a projectile at any nearby enemy and deals massive AoE damage on contact, long range, AoE has medium range
Dual Gunner: shoots two parallel projectiles at any nearby enemy, medium range
Hunter: shoots an arrow at any nearby enemy with a chance to summon a pet that will trample through enemies knocking them away, arrow has long range, pet has small range
Chronomancer: dramatically improves attack speed for adjacent units
Spellblade: knives orbit you and hoam towards nearby enemies, small range
Spellblade: knives spiral outwards and pierce enemies
Psykeeper: all damage taken is stored and distributed as healing
Gambler: drops a sentry that uses random attacks, medium range
Engineer: drops a sentry that uses random attacks, medium range
Ranger: yellow, chance to release a barrage
Warrior: orange, increased defense
@ -39,8 +39,7 @@ Outlaw [rogue, warrior]
Blade [warrior, nuker]
Elementor [mage, nuker]
Saboteur [rogue, conjurer, nuker]
Linker [enchanter]
Stormweaver [enchanter]
Sage [mage, nuker]
Squire [warrior, healer, enchanter]
Cannoneer [ranger, nuker]
@ -49,7 +48,8 @@ Hunter [ranger, conjurer]
Chronomancer [mage, enchanter]
Spellblade [mage, rogue]
Psykeeper [healer, psy]
Gambler [conjurer]
Engineer [conjurer]
Ranger [2, 4] (5)
Warrior [2, 4] (5)