diff --git a/arena.lua b/arena.lua index 46c677d..b45a493 100644 --- a/arena.lua +++ b/arena.lua @@ -50,13 +50,18 @@ 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 = 'elementor'} - --[[ - self.player:add_follower(Player{group = self.main, character = 'archer'}) + 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:add_follower(Player{group = self.main, character = 'cleric'}) + 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'}) + self.player:add_follower(Player{group = self.main, character = 'blade'}) + self.player:add_follower(Player{group = self.main, character = 'elementor'}) ]]-- self.win_condition = random:table{'time', 'enemy_kill', 'wave'} diff --git a/assets/sounds/Buff 8.ogg b/assets/sounds/Buff 8.ogg new file mode 100644 index 0000000..0baf6e4 Binary files /dev/null and b/assets/sounds/Buff 8.ogg differ diff --git a/assets/sounds/Click.ogg b/assets/sounds/Click.ogg new file mode 100644 index 0000000..296caec Binary files /dev/null and b/assets/sounds/Click.ogg differ diff --git a/assets/sounds/Explosion Flesh_01.ogg b/assets/sounds/Explosion Flesh_01.ogg new file mode 100644 index 0000000..bbf947d Binary files /dev/null and b/assets/sounds/Explosion Flesh_01.ogg differ diff --git a/assets/sounds/Explosion Flesh_02.ogg b/assets/sounds/Explosion Flesh_02.ogg new file mode 100644 index 0000000..d57d8ad Binary files /dev/null and b/assets/sounds/Explosion Flesh_02.ogg differ diff --git a/assets/sounds/Male Jump 1.ogg b/assets/sounds/Male Jump 1.ogg new file mode 100644 index 0000000..e167589 Binary files /dev/null and b/assets/sounds/Male Jump 1.ogg differ diff --git a/assets/sounds/Male Jump 2.ogg b/assets/sounds/Male Jump 2.ogg new file mode 100644 index 0000000..0015fd5 Binary files /dev/null and b/assets/sounds/Male Jump 2.ogg differ diff --git a/assets/sounds/Male Jump 3.ogg b/assets/sounds/Male Jump 3.ogg new file mode 100644 index 0000000..9243977 Binary files /dev/null and b/assets/sounds/Male Jump 3.ogg differ diff --git a/assets/sounds/Spark 1.ogg b/assets/sounds/Spark 1.ogg new file mode 100644 index 0000000..abff271 Binary files /dev/null and b/assets/sounds/Spark 1.ogg differ diff --git a/assets/sounds/Spark 2.ogg b/assets/sounds/Spark 2.ogg new file mode 100644 index 0000000..a428c8a Binary files /dev/null and b/assets/sounds/Spark 2.ogg differ diff --git a/assets/sounds/Spark 3.ogg b/assets/sounds/Spark 3.ogg new file mode 100644 index 0000000..06e7808 Binary files /dev/null and b/assets/sounds/Spark 3.ogg differ diff --git a/devlog.md b/devlog.md index ff436f1..21c548a 100644 --- a/devlog.md +++ b/devlog.md @@ -120,26 +120,26 @@ Ideaguyed the entire roster for the demo and implemented a few of them. | Character | Description | Trigger Range | Effect Range | | --- | --- | --- | --- | -| Vagrant | shoots a projectile | medium | nil | -| Scout | throws a knife that chains 3 times | small | nil | -| Cleric | heals every unit when any one drops below 50% HP | nil | nil | +| Vagrant | shoots a projectile | medium | | +| Scout | throws a knife that chains 3 times | small | | +| Cleric | heals every unit when any one drops below 50% HP | | | | Swordsman | deals physical damage in an area around the unit | small | medium | -| Archer | shoots an arrow that pierces | very long | nil | +| Archer | shoots an arrow that pierces | very long | | | Wizard | shoots a projectile that deals AoE damage | long | very small | -| Outlaw | throws a fan of 5 knives | medium | nil | +| Outlaw | throws a fan of 5 knives | medium | | | Blade | shoots multiple blades that deal AoE damage on contact | small | small | | Elementor | deals massive AoE damage to a random target | long | medium | -| Ninja | creates clones that roam and shoot shurikens | nil | very small | +| Ninja | creates clones that roam and shoot shurikens | | very small | | Linker | links nearby enemies together making them share damage taken | medium | small | | Sage | shoots a slow projectile that draws enemies in | medium | medium | -| Squire | improves damage and defense for adjacent units as well as healing them periodically | nil | nil | +| Squire | improves damage and defense for adjacent units as well as healing them periodically | | | | Cannoneer | shoots a projectile that deals massive AoE damage | long | medium | -| Dual Gunner | shoots two parallel projectiles | medium | nil | +| Dual Gunner | shoots two parallel projectiles | medium | | | Hunter | shoots an arrow with a chance to summon a pet | long | small | -| Chronomancer | dramatically improves attack speed for adjacent units | nil | nil | +| Chronomancer | dramatically improves attack speed for adjacent units | | | | Spellblade | knives orbit you and hoam towards nearby enemies | small | small | -| Psykeeper | all damage taken is stored and distributed as healing | nil | nil | -| Gambler | drops a sentry that uses random attacks | nil | medium | +| Psykeeper | all damage taken is stored and distributed as healing | | | +| Gambler | drops a sentry that uses random attacks | | medium | ### Character Classes @@ -195,3 +195,12 @@ Ideaguyed the entire roster for the demo and implemented a few of them. | Psy | 1.5 | 1.0 | 1.0 | 1.0 | 1.0 | 0.5 | 1.0 | I've implemented up to Elementor today and ATM in the process of doing Ninja, but today seems like a particularly low energy day so I'm just going to play some games instead. + +# Day 7 - 23/02/21 + +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~~ diff --git a/main.lua b/main.lua index 8d3411d..501f193 100644 --- a/main.lua +++ b/main.lua @@ -41,8 +41,17 @@ function init() pop1 = Sound('Pop sounds 10.ogg', s) heal1 = Sound('Buff 3.ogg', s) spawn1 = Sound('Buff 13.ogg', s) - alert1 = Sound('Alert sounds 3.ogg', s) + alert1 = Sound('Click.ogg', s) elementor1 = Sound('Wind Bolt 18.ogg', s) + saboteur_hit1 = Sound('Explosion Flesh_01.ogg', s) + saboteur_hit2 = Sound('Explosion Flesh_02.ogg', s) + saboteur1 = Sound('Male Jump 1.ogg', s) + saboteur2 = Sound('Male Jump 2.ogg', s) + saboteur3 = Sound('Male Jump 3.ogg', s) + spark1 = Sound('Spark 1.ogg', s) + spark2 = Sound('Spark 2.ogg', s) + spark3 = Sound('Spark 3.ogg', s) + stormweaver1 = Sound('Buff 8.ogg', s) main = Main() main:add(Arena'arena') diff --git a/objects.lua b/objects.lua index f4645db..7117c63 100644 --- a/objects.lua +++ b/objects.lua @@ -1,3 +1,71 @@ +LightningLine = Object:extend() +LightningLine:implement(GameObject) +function LightningLine:init(args) + self:init_game_object(args) + self.lines = {} + table.insert(self.lines, {x1 = self.src.x, y1 = self.src.y, x2 = self.dst.x, y2 = self.dst.y}) + self.w = 3 + self.generations = 3 + self.max_offset = 8 + self:generate() + self.t:tween(0.1, self, {w = 1}, math.linear, function() self.dead = true end) + self.color = blue[0] + HitCircle{group = main.current.effects, x = self.src.x, y = self.src.y, rs = 6, color = fg[0], duration = 0.1} + for i = 1, 2 do HitParticle{group = main.current.effects, x = self.src.x, y = self.src.y, color = blue[0]} end + HitCircle{group = main.current.effects, x = self.dst.x, y = self.dst.y, rs = 6, color = fg[0], duration = 0.1} + HitParticle{group = main.current.effects, x = self.dst.x, y = self.dst.y, color = blue[0]} +end + + +function LightningLine:update(dt) + self:update_game_object(dt) +end + + +function LightningLine:draw() + graphics.polyline(self.color, self.w, unpack(self.points)) +end + + +function LightningLine:generate() + local offset_amount = self.max_offset + local lines = self.lines + + for j = 1, self.generations do + for i = #self.lines, 1, -1 do + local x1, y1 = self.lines[i].x1, self.lines[i].y1 + local x2, y2 = self.lines[i].x2, self.lines[i].y2 + table.remove(self.lines, i) + + local x, y = (x1+x2)/2, (y1+y2)/2 + local p = Vector(x2-x1, y2-y1):normalize():perpendicular() + x = x + p.x*random:float(-offset_amount, offset_amount) + y = y + p.y*random:float(-offset_amount, offset_amount) + table.insert(self.lines, {x1 = x1, y1 = y1, x2 = x, y2 = y}) + table.insert(self.lines, {x1 = x, y1 = y, x2 = x2, y2 = y2}) + end + offset_amount = offset_amount/2 + end + + self.points = {} + while #self.lines > 0 do + local min_d, min_i = 1000000, 0 + for i, line in ipairs(self.lines) do + local d = math.distance(self.src.x, self.src.y, line.x1, line.y1) + if d < min_d then + min_d = d + min_i = i + end + end + local line = table.remove(self.lines, min_i) + table.insert(self.points, line.x1) + table.insert(self.points, line.y1) + end +end + + + + WallKnife = Object:extend() WallKnife:implement(GameObject) WallKnife:implement(Physics) @@ -68,6 +136,7 @@ function Unit:init_unit() 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} end @@ -97,6 +166,12 @@ function Unit:show_heal(n) end +function Unit:show_infused(n) + self.infused_bar.hidden = false + self.t:after(n, function() self.infused_bar.hidden = true end, 'infused_bar') +end + + function Unit:calculate_damage(dmg) if self.def >= 0 then dmg = dmg*(100/(100+self.def)) else dmg = dmg*(2 - 100/(100+self.def)) end @@ -152,7 +227,8 @@ function Unit:calculate_stats(first_run) if class == 'warrior' then self.class_dmg_m = self.class_dmg_m*1.1 elseif class == 'ranger' then self.class_dmg_m = self.class_dmg_m*1.2 elseif class == 'rogue' then self.class_dmg_m = self.class_dmg_m*1.3 - elseif class == 'mage' then self.class_dmg_m = self.class_dmg_m*1.4 end + elseif class == 'mage' then self.class_dmg_m = self.class_dmg_m*1.4 + elseif class == 'ninja_clone' then self.class_dmg_m = self.class_dmg_m*1.5 end end self.dmg = (self.base_dmg + self.class_dmg_a + self.buff_dmg_a)*self.class_dmg_m*self.buff_dmg_m @@ -195,7 +271,8 @@ function Unit:calculate_stats(first_run) elseif class == 'ranger' then self.class_mvspd_m = self.class_mvspd_m*1.2 elseif class == 'rogue' then self.class_mvspd_m = self.class_mvspd_m*1.4 elseif class == 'enchanter' then self.class_mvspd_m = self.class_mvspd_m*1.2 - elseif class == 'seeker' then self.class_mvspd_m = self.class_mvspd_m*0.3 end + elseif class == 'seeker' then self.class_mvspd_m = self.class_mvspd_m*0.3 + elseif class == 'saboteur' then self.class_mvspd_m = self.class_mvspd_m*1.4 end end self.v = (self.base_mvspd + self.class_mvspd_a + self.buff_mvspd_a)*self.class_mvspd_m*self.buff_mvspd_m end @@ -203,6 +280,35 @@ end +InfusedBar = Object:extend() +InfusedBar:implement(GameObject) +InfusedBar:implement(Parent) +function InfusedBar: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) +end + + +function InfusedBar:update(dt) + self:update_game_object(dt) + self:follow_parent_exclusively() +end + + +function InfusedBar: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) diff --git a/player.lua b/player.lua index 1fe3543..ac53477 100644 --- a/player.lua +++ b/player.lua @@ -137,18 +137,37 @@ function Player:init(args) end end, nil, nil, 'attack') - elseif self.character == 'ninja' then + elseif self.character == 'saboteur' then self.color = red[0] self:set_as_rectangle(9, 9, 'dynamic', 'player') self.visual_shape = 'rectangle' - self.classes = {'rogue', 'conjurer'} + self.classes = {'rogue', 'conjurer', 'nuker'} self.t:every(8, function() self.t:every(0.25, function() - SpawnEffect{group = self.effects, x = self.x, y = self.y, action = function(x, y) - NinjaClone{group = self.main, x = x, y = y, parent = self} + SpawnEffect{group = main.current.effects, x = self.x, y = self.y, action = function(x, y) + Saboteur{group = main.current.main, x = x, y = y, parent = self} end} - end, 3) + end, 4) + end) + + elseif self.character == 'stormweaver' then + self.color = blue[0] + self:set_as_rectangle(9, 9, 'dynamic', 'player') + 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 + local leader = (self.leader and self) or self.parent + if self.leader then followers = self.followers else followers = self.parent.followers end + for _, f in ipairs(followers) do + if f.character ~= 'swordsman' and f.character ~= 'cleric' and f.character ~= 'elementor' and f.character ~= 'saboteur' then + f:chain_infuse(4) + end + end end) end self:calculate_stats(true) @@ -244,7 +263,7 @@ function Player:on_collision_enter(other, contact) pop1:play{pitch = r, volume = 0.2} for i, f in ipairs(self.followers) do - trigger:after(i*0.002*self.v, function() + trigger:after(i*(10.6/self.v), function() f.hfx:use('hit', 0.5, 200, 10, 0.1) player_hit_wall1:play{pitch = r + 0.025*i, volume = 0.1} pop1:play{pitch = r + 0.05*i, volume = 0.2} @@ -293,6 +312,13 @@ function Player:heal(amount) end +function Player:chain_infuse(duration) + self.chain_infused = true + self:show_infused(duration or 2) + self.t:after(duration or 2, function() self.chain_infused = false end, 'chain_infuse') +end + + function Player:add_follower(unit) table.insert(self.followers, unit) unit.parent = self @@ -341,10 +367,11 @@ end function Player:attack(area, mods) + mods = mods or {} camera:shake(2, 0.5) self.hfx:use('shoot', 0.25) local t = {group = main.current.effects, x = mods.x or self.x, y = mods.y or self.y, r = self.r, w = self.area_size_m*(area or 64), color = self.color, dmg = self.area_dmg_m*self.dmg, character = self.character} - Area(table.merge(t, mods or {})) + Area(table.merge(t, mods)) if self.character == 'swordsman' then _G[random:table{'swordsman1', 'swordsman2'}]:play{pitch = random:float(0.9, 1.1), volume = 0.75} @@ -365,6 +392,7 @@ function Projectile:init(args) self.pierce = args.pierce or 0 self.chain = args.chain or 0 self.chain_enemies_hit = {} + self.infused_enemies_hit = {} end @@ -454,6 +482,7 @@ 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 hit2:play{pitch = random:float(0.95, 1.05), volume = 0.35} elseif self.character == 'wizard' then @@ -461,7 +490,22 @@ function Projectile:on_trigger_enter(other, contact) else hit3:play{pitch = random:float(0.95, 1.05), volume = 0.35} end + other:hit(self.dmg) + + if self.parent.chain_infused then + local src = other + for i = 1, 2 do + _G[random:table{'spark1', 'spark2', 'spark3'}]:play{pitch = random:float(0.9, 1.1), volume = 0.3} + table.insert(self.infused_enemies_hit, src) + local dst = src:get_random_object_in_shape(Circle(src.x, src.y, 64), main.current.enemies, self.infused_enemies_hit) + if dst then + dst:hit(self.dmg/(i+1)) + LightningLine{group = main.current.effects, src = src, dst = dst} + src = dst + end + end + end end end @@ -486,6 +530,8 @@ function Area:init(args) elseif self.character == 'blade' then blade_hit1:play{pitch = random:float(0.9, 1.1), volume = 0.35} 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} end end @@ -524,30 +570,54 @@ end -NinjaClone = Object:extend() -NinjaClone:implement(GameObject) -NinjaClone:implement(Physics) -function NinjaClone:init(args) +Saboteur = Object:extend() +Saboteur:implement(GameObject) +Saboteur:implement(Physics) +Saboteur:implement(Unit) +function Saboteur:init(args) self:init_game_object(args) self:init_unit() - self:set_as_rectangle(8, 8, 'dynamic', 'enemy') + self:set_as_rectangle(8, 8, 'dynamic', 'player') self:set_restitution(0.5) self.color = red[0] - self.classes = {'ninja_clone'} + self.character = 'saboteur' + self.classes = {'saboteur', 'rogue', 'nuker'} self:calculate_stats(true) self:set_as_steerable(self.v, 2000, 4*math.pi, 4) + + _G[random:table{'saboteur1', 'saboteur2', 'saboteur3'}]:play{pitch = random:float(0.8, 1.2), volume = 0.2} + self.target = random:table(self.group:get_objects_by_classes(main.current.enemies)) end -function NinjaClone:update(dt) +function Saboteur:update(dt) self:update_game_object(dt) self:calculate_stats() + + if not self.target then self.target = random:table(self.group:get_objects_by_classes(main.current.enemies)) end + if not self.target then return end + if self.target.dead then self.target = random:table(self.group:get_objects_by_classes(main.current.enemies)) end + if not self.target then return end + + self:seek_point(self.target.x, self.target.y) + self:rotate_towards_velocity(0.5) + self.r = self:get_angle() end -function NinjaClone:draw() +function Saboteur: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 Saboteur:on_collision_enter(other, contact) + if table.any(main.current.enemies, function(v) return other:is(v) end) then + camera:shake(4, 0.5) + local t = {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, character = self.character} + Area(table.merge(t, mods or {})) + self.dead = true + end +end diff --git a/todo b/todo index 6fba923..4b56aca 100644 --- a/todo +++ b/todo @@ -7,9 +7,8 @@ Wizard: shoots a projectile at any nearby enemy and deals AoE damage on contact, Outlaw: throws a leque of knives at nearby enemies, medium range Blade: shoots multiple blades at nearby enemies, each dealing AoE damage on contact, small range, AoE has small range Elementor: deals massive AoE damage to a random target, long range, AoE has medium range - -Ninja: creates clones that roam and shoot projectiles at nearby enemies, very small range -Linker: links nearby enemies together making them share damage taken, 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 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 @@ -39,9 +38,9 @@ Wizard [mage] Outlaw [rogue, warrior] Blade [warrior, nuker] Elementor [mage, nuker] +Saboteur [rogue, conjurer, nuker] -Ninja [rogue, conjurer] -Linker [enchanter, nuker] +Linker [enchanter] Sage [mage, nuker] Squire [warrior, healer, enchanter] Cannoneer [ranger, nuker]