diff --git a/arena.lua b/arena.lua index 992f9ca..e9275cc 100644 --- a/arena.lua +++ b/arena.lua @@ -250,6 +250,7 @@ function Arena:on_enter(from, level, units, passives) self.healer_level = class_levels.healer self.psyker_level = class_levels.psyker self.conjurer_level = class_levels.conjurer + self.sorcerer_level = class_levels.sorcerer self.t:every(0.375, function() local p = random:table(star_positions) @@ -732,6 +733,13 @@ function Arena:quit() steam.userStats.storeStats() end + if self.sorcerer_level >= 3 then + state.achievement_sorcerers_win = true + system.save_state() + steam.userStats.setAchievement('SORCERERS_WIN') + steam.userStats.storeStats() + end + local units = self.player:get_all_units() local all_units_level_2 = true for _, unit in ipairs(units) do diff --git a/assets/images/sorcerer.png b/assets/images/sorcerer.png new file mode 100644 index 0000000..f984e86 Binary files /dev/null and b/assets/images/sorcerer.png differ diff --git a/assets/sounds/Buff 5.ogg b/assets/sounds/Buff 5.ogg new file mode 100644 index 0000000..1a983c2 Binary files /dev/null and b/assets/sounds/Buff 5.ogg differ diff --git a/assets/sounds/Magical Impact 26.ogg b/assets/sounds/Magical Impact 26.ogg new file mode 100644 index 0000000..22eb405 Binary files /dev/null and b/assets/sounds/Magical Impact 26.ogg differ diff --git a/enemies.lua b/enemies.lua index d02d97e..19d2a06 100644 --- a/enemies.lua +++ b/enemies.lua @@ -180,7 +180,8 @@ function Seeker:init(args) elseif self.headbutter then self.color = orange[0]:clone() self.last_headbutt_time = 0 - self.t:every(function() return math.distance(self.x, self.y, main.current.player.x, main.current.player.y) < 64 and love.timer.getTime() - self.last_headbutt_time > 10 end, function() + local n = math.remap(current_new_game_plus, 0, 5, 1, 0.5) + self.t:every(function() return math.distance(self.x, self.y, main.current.player.x, main.current.player.y) < 64 and love.timer.getTime() - self.last_headbutt_time > 10*n end, function() if self.headbutt_charging or self.headbutting then return end self.headbutt_charging = true self.t:tween(2, self.color, {r = fg[0].r, b = fg[0].b, g = fg[0].g}, math.cubic_in_out, function() @@ -191,19 +192,20 @@ function Seeker:init(args) self.last_headbutt_time = love.timer.getTime() self:set_damping(0) self:apply_steering_impulse(300, self:angle_to_object(main.current.player), 0.75) - self.t:after(0.75, function() + self.t:after(0.5, function() self.headbutting = false end) end) end) elseif self.tank then self.color = yellow[0]:clone() - self.buff_hp_m = 1.25 + (0.025*self.level) + self.buff_hp_m = 1.25 + (0.025*self.level) + (0.2*current_new_game_plus) self:calculate_stats() self.hp = self.max_hp elseif self.shooter then self.color = fg[0]:clone() - self.t:after({2, 4}, function() + local n = math.remap(current_new_game_plus, 0, 5, 1, 0.5) + self.t:after({2*n, 4*n}, function() self.shooting = true self.t:every({3, 5}, function() for i = 1, 3 do @@ -434,7 +436,7 @@ function Seeker:hit(damage, projectile) trigger:after(0.01, function() local n = 8 + current_new_game_plus*2 for i = 1, n do - EnemyProjectile{group = main.current.main, x = self.x, y = self.y, color = blue[0], r = (i-1)*math.pi/(n/2), v = 150 + 5*self.level, dmg = 2*self.dmg} + EnemyProjectile{group = main.current.main, x = self.x, y = self.y, color = blue[0], r = (i-1)*math.pi/(n/2), v = 120 + 5*self.level, dmg = 2*self.dmg} end end) end @@ -442,8 +444,8 @@ function Seeker:hit(damage, projectile) if self.spawner then critter1:play{pitch = random:float(0.95, 1.05), volume = 0.35} trigger:after(0.01, function() - for i = 1, random:int(3, 6) do - EnemyCritter{group = main.current.main, x = self.x, y = self.y, color = purple[0], r = random:float(0, 2*math.pi), v = 5 + 0.1*self.level, dmg = self.dmg, projectile = projectile} + for i = 1, random:int(5, 8) do + EnemyCritter{group = main.current.main, x = self.x, y = self.y, color = purple[0], r = random:float(0, 2*math.pi), v = 10 + 0.1*self.level, dmg = 2*self.dmg, projectile = projectile} end end) end diff --git a/main.lua b/main.lua index 8d6ef09..f96dd62 100644 --- a/main.lua +++ b/main.lua @@ -18,11 +18,13 @@ function init() input:bind('enter', {'space', 'return', 'fleft', 'fdown', 'fright'}) local s = {tags = {sfx}} + illusion1 = Sound('Buff 5.ogg', s) thunder1 = Sound('399656__bajko__sfx-thunder-blast.ogg', s) flagellant1 = Sound('Whipping Horse 3.ogg', s) bard2 = Sound('376532__womb-affliction__flute-trill.ogg', s) bard1 = Sound('Magical Impact 12.ogg', s) frost1 = Sound('Frost Bolt 20.ogg', s) + arcane1 = Sound('Magical Impact 26.ogg', s) pyro1 = Sound('Fire bolt 5.ogg', s) pyro2 = Sound('Explosion Fireworks_01.ogg', s) dot1 = Sound('Magical Swoosh 18.ogg', s) @@ -125,6 +127,7 @@ function init() forcer = Image('forcer') swarmer = Image('swarmer') voider = Image('voider') + sorcerer = Image('sorcerer') ouroboros_technique_r = Image('ouroboros_technique_r') ouroboros_technique_l = Image('ouroboros_technique_l') wall_echo = Image('wall_echo') @@ -184,6 +187,7 @@ function init() ['forcer'] = yellow[0], ['swarmer'] = orange[0], ['voider'] = purple[0], + ['sorcerer'] = blue2[0], } class_color_strings = { @@ -200,6 +204,7 @@ function init() ['forcer'] = 'yellow', ['swarmer'] = 'orange', ['voider'] = 'purple', + ['sorcerer'] = 'blue2', } character_names = { @@ -244,6 +249,13 @@ function init() ['priest'] = 'Priest', ['infestor'] = 'Infestor', ['flagellant'] = 'Flagellant', + ['arcanist'] = 'Arcanist', + ['illusionist'] = 'Illusionist', + ['witch'] = 'Witch', + ['silencer'] = 'Silencer', + ['vulcanist'] = 'Vulcanist', + ['warden'] = 'Warden', + ['psychic'] = 'Psychic', } character_colors = { @@ -288,6 +300,13 @@ function init() ['priest'] = green[0], ['infestor'] = orange[0], ['flagellant'] = fg[0], + ['arcanist'] = blue2[0], + ['illusionist'] = blue2[0], + ['witch'] = purple[0], + ['silencer'] = blue2[0], + ['vulcanist'] = red[0], + ['warden'] = yellow[0], + ['psychic'] = fg[0], } character_color_strings = { @@ -332,6 +351,13 @@ function init() ['priest'] = 'green', ['infestor'] = 'orange', ['flagellant'] = 'fg', + ['arcanist'] = 'blue2', + ['illusionist'] = 'blue2', + ['witch'] = 'purple', + ['silencer'] = 'blue2', + ['vulcanist'] = 'red', + ['warden'] = 'yellow', + ['psychic'] = 'fg', } character_classes = { @@ -376,6 +402,13 @@ function init() ['priest'] = {'healer'}, ['infestor'] = {'curser', 'swarmer'}, ['flagellant'] = {'psyker', 'enchanter'}, + ['arcanist'] = {'sorcerer'}, + ['illusionist'] = {'sorcerer', 'conjurer'}, + ['witch'] = {'sorcerer', 'voider'}, + ['silencer'] = {'sorcerer', 'curser'}, + ['vulcanist'] = {'sorcerer', 'nuker'}, + ['warden'] = {'sorcerer', 'forcer'}, + ['psychic'] = {'sorcerer', 'psyker'}, } character_class_strings = { @@ -420,6 +453,13 @@ function init() ['priest'] = '[green]Healer', ['infestor'] = '[purple]Curser, [orange]Swarmer', ['flagellant'] = '[fg]Psyker, [blue]Enchanter', + ['arcanist'] = '[blue2]Sorcerer', + ['illusionist'] = '[blue2]Sorcerer, [orange]Conjurer', + ['witch'] = '[blue2]Sorcerer, [purple]Voider', + ['silencer'] = '[blue2]Sorcerer, [purple]Curser', + ['vulcanist'] = '[blue2]Sorcerer, [red]Nuker', + ['warden'] = '[blue2]Sorcerer, [yellow]Forcer', + ['psychic'] = '[blue2]Sorcerer, [fg]Psyker', } get_character_stat_string = function(character, level) @@ -468,7 +508,7 @@ function init() ['corruptor'] = function(lvl) return '[fg]spawn [yellow]3[fg] small critters if the corruptor kills an enemy' end, ['beastmaster'] = function(lvl) return '[fg]spawn [yellow]2[fg] small critters if the beastmaster crits' end, ['launcher'] = function(lvl) return '[fg]all nearby enemies are pushed after [yellow]4[fg] seconds, taking [yellow]' .. 2*get_character_stat('launcher', lvl, 'dmg') .. '[fg] damage on wall hit' end, - ['jester'] = function(lvl) return "[fg]curses [yellow]5[fg] nearby enemies for [yellow]6[fg] seconds, they will explode into [yellow]3[fg] knives on death" end, + ['jester'] = function(lvl) return "[fg]curses [yellow]4[fg] nearby enemies for [yellow]6[fg] seconds, they will explode into [yellow]3[fg] knives on death" end, ['assassin'] = function(lvl) return '[fg]throws a piercing knife that deals [yellow]' .. get_character_stat('assassin', lvl, 'dmg') .. '[fg] damage + [yellow]' .. get_character_stat('assassin', lvl, 'dmg')/2 .. '[fg] damage per second' end, ['host'] = function(lvl) return '[fg]periodically spawn [yellow]1[fg] small critter' end, @@ -481,6 +521,13 @@ function init() ['priest'] = function(lvl) return '[fg]heals all allies for [yellow]20%[fg] their max HP' end, ['infestor'] = function(lvl) return '[fg]curses all nearby enemies for [yellow]6[fg] seconds, they will release [yellow]2[fg] critters on death' end, ['flagellant'] = function(lvl) return '[fg]deals [yellow]' .. 2*get_character_stat('flagellant', lvl, 'dmg') .. '[fg] damage to self and grants [yellow]+4%[fg] damage to all allies per cast' end, + ['arcanist'] = function(lvl) return '[fg]launches a slow moving orb that launches projectiles, each dealing [yellow]' .. get_character_stat('arcanist', lvl, 'dmg') .. '[fg] damage' end, + ['illusionist'] = function(lvl) return '[fg]launches a projectile that deals [yellow]' .. get_character_stat('illusionist', lvl, 'dmg') .. '[fg] damage and creates copies that do the same' end, + ['witch'] = function(lvl) return '[fg]creates an area that ricochets around the arena and deals [yellow]' .. get_character_stat('witch', lvl, 'dmg') .. '[fg] damage per second' end, + ['silencer'] = function(lvl) return '[fg]curses [yellow]5[fg] nearby enemies for [yellow]6[fg] seconds, preventing them from using special attacks' end, + ['vulcanist'] = function(lvl) return '[fg]creates a volcano that explodes the nearby area [yellow]5[fg] times, dealing [yellow]' .. get_character_stat('vulcanist', lvl, 'dmg') .. 'AoE [fg]damage' end, + ['warden'] = function(lvl) return '[fg]creates a force field around a random unit that prevents enemies from entering' end, + ['psychic'] = function(lvl) return '[fg]creates a small area that deals [yellow]' .. get_character_stat('psychic', lvl, 'dmg') .. '[fg] damage' end, } character_effect_names = { @@ -525,6 +572,13 @@ function init() ['priest'] = '[green]Divine Intervention', ['infestor'] = '[orange]Infestation', ['flagellant'] = '[red]Zealotry', + ['arcanist'] = '[blue2]Arcane Orb', + ['illusionist'] = '[blue2]Mirror Image', + ['witch'] = '[purple]Death Pool', + ['silencer'] = '[blue2]Arcane Curse', + ['vulcanist'] = '[red]Lava Burst', + ['warden'] = '[yellow]Magnetic Field', + ['psychic'] = '[fg]Mental Strike' } character_effect_names_gray = { @@ -569,6 +623,13 @@ function init() ['priest'] = '[light_bg]Divine Intervention', ['infestor'] = '[light_bg]Infestation', ['flagellant'] = '[light_bg]Zealotry', + ['arcanist'] = '[light_bg]Arcane Orb', + ['illusionist'] = '[light_bg]Mirror Image', + ['witch'] = '[light_bg]Death Pool', + ['silencer'] = '[light_bg]Arcane Curse', + ['vulcanist'] = '[light_bg]Lava Burst', + ['warden'] = '[light_bg]Magnetic Field', + ['psychic'] = '[light_bg]Mental Strike' } character_effect_descriptions = { @@ -613,6 +674,13 @@ function init() ['priest'] = function() return '[fg]picks [yellow]3[fg] units at random and grants them a buff that prevents death once' end, ['infestor'] = function() return '[fg][yellow]triples[fg] the number of critters released' end, ['flagellant'] = function() return '[fg]deals [yellow]' .. 2*get_character_stat('flagellant', 3, 'dmg') .. '[fg] damage to all allies and grants [yellow]+12%[fg] damage to all allies per cast' end, + ['arcanist'] = function() return '[yellow]100%[fg] increased attack speed for the orb and [yellow]2[fg] projectiles are released per cast' end, + ['illusionist'] = function() return '[yellow]doubles[fg] the number of copies created and they release [yellow]12[fg] projectiles on death that pierce and ricochet once' end, + ['witch'] = function() return '[fg]the area periodically releases projectiles, each dealing [yellow]' .. get_character_stat('witch', 3, 'dmg') .. '[fg] damage and chaining once' end, + ['silencer'] = function() return '[fg]the curse also deals [yellow]' .. get_character_stat('silencer', 3, 'dmg') .. '[fg] damage per second' end, + ['vulcanist'] = function() return '[fg]the volcano spawn also deals [yellow]' .. get_character_stat('vulcanist', 3, 'dmg') .. 'AoE [fg] damage and [yellow]doubles[fg] the number of explosions' end, + ['warden'] = function() return '[fg]creates the force field around [yellow]2[fg] additional random units' end, + ['psychic'] = function() return '[fg]the attack can happen from any distance and deals [yellow]double[fg] damage' end, } character_effect_descriptions_gray = { @@ -657,6 +725,13 @@ function init() ['priest'] = function() return '[light_bg]picks 3 units at random and grants them a buff that prevents death once' end, ['infestor'] = function() return '[light_bg]triples the number of critters released' end, ['flagellant'] = function() return '[light_bg]deals ' .. 2*get_character_stat('flagellant', 3, 'dmg') .. ' damage to all allies and grants +12% damage to all allies per cast' end, + ['arcanist'] = function() return '[light_bg]100% increased attack speed for the orb and 2 projectiles are released per cast' end, + ['illusionist'] = function() return '[light_bg]doubles the number of copies created and they release 12 projectiles on death' end, + ['witch'] = function() return '[light_bg]the area periodically releases projectiles, each dealing ' .. get_character_stat('witch', 3, 'dmg') .. ' damage and chaining once' end, + ['silencer'] = function() return '[light_bg]the curse also deals ' .. get_character_stat('silencer', 3, 'dmg') .. ' damage per second' end, + ['vulcanist'] = function() return '[light_bg]the volcano spawn also deals ' .. get_character_stat('vulcanist', 3, 'dmg') .. 'AoE damage and doubles the number of explosions' end, + ['warden'] = function() return '[light_bg]creates the force field around 2 additional random units' end, + ['psychic'] = function() return '[light_bg]the attack can happen from any distance and deals double damage' end, } character_stats = { @@ -701,6 +776,13 @@ function init() ['priest'] = function(lvl) return get_character_stat_string('priest', lvl) end, ['infestor'] = function(lvl) return get_character_stat_string('infestor', lvl) end, ['flagellant'] = function(lvl) return get_character_stat_string('flagellant', lvl) end, + ['arcanist'] = function(lvl) return get_character_stat_string('arcanist', lvl) end, + ['illusionist'] = function(lvl) return get_character_stat_string('illusionist', lvl) end, + ['witch'] = function(lvl) return get_character_stat_string('witch', lvl) end, + ['silencer'] = function(lvl) return get_character_stat_string('silencer', lvl) end, + ['vulcanist'] = function(lvl) return get_character_stat_string('vulcanist', lvl) end, + ['warden'] = function(lvl) return get_character_stat_string('warden', lvl) end, + ['psychic'] = function(lvl) return get_character_stat_string('psychic', lvl) end, } class_stat_multipliers = { @@ -717,14 +799,28 @@ function init() ['forcer'] = {hp = 1.25, dmg = 1.1, aspd = 0.9, area_dmg = 0.75, area_size = 0.75, def = 1.2, mvspd = 1}, ['swarmer'] = {hp = 1.2, dmg = 1, aspd = 1.25, area_dmg = 1, area_size = 1, def = 0.75, mvspd = 0.5}, ['voider'] = {hp = 0.75, dmg = 1.3, aspd = 1, area_dmg = 0.8, area_size = 0.75, def = 0.6, mvspd = 0.8}, + ['sorcerer'] = {hp = 0.8, dmg = 1.3, aspd = 1, area_dmg = 1.2, area_size = 1, def = 0.8, mvspd = 1}, ['seeker'] = {hp = 0.5, dmg = 1, aspd = 1, area_dmg = 1, area_size = 1, def = 1, mvspd = 0.3}, ['mini_boss'] = {hp = 1, dmg = 1, aspd = 1, area_dmg = 1, area_size = 1, def = 1, mvspd = 0.3}, ['enemy_critter'] = {hp = 1, dmg = 1, aspd = 1, area_dmg = 1, area_size = 1, def = 1, mvspd = 0.5}, ['saboteur'] = {hp = 1, dmg = 1, aspd = 1, area_dmg = 1, area_size = 1, def = 1, mvspd = 1.4}, } - local ylb1 = function(lvl) return lvl >= 2 and 'fg' or (lvl >= 1 and 'yellow' or 'light_bg') end - local ylb2 = function(lvl) return (lvl >= 2 and 'yellow' or 'light_bg') end + local ylb1 = function(lvl) + if lvl == 3 then return 'fg' + elseif lvl == 2 then return 'fg' + elseif lvl == 1 then return 'yellow' + else return 'light_bg' end + end + local ylb2 = function(lvl) + if lvl == 3 then return 'fg' + elseif lvl == 2 then return 'yellow' + else return 'light_bg' end + end + local ylb3 = function(lvl) + if lvl == 3 then return 'yellow' + else return 'light_bg' end + end class_descriptions = { ['ranger'] = function(lvl) return '[' .. ylb1(lvl) .. ']3[' .. ylb2(lvl) .. ']/6 [fg]- [' .. ylb1(lvl) .. ']8%[' .. ylb2(lvl) .. ']/16% [fg]chance to release a barrage on attack to allied rangers' end, ['warrior'] = function(lvl) return '[' .. ylb1(lvl) .. ']3[' .. ylb2(lvl) .. ']/6 [fg]- [' .. ylb1(lvl) .. ']+25[' .. ylb2(lvl) .. ']/+50 [fg]defense to allied warriors' end, @@ -739,6 +835,9 @@ function init() ['forcer'] = function(lvl) return '[' .. ylb1(lvl) .. ']2[' .. ylb2(lvl) .. ']/4 [fg]- [' .. ylb1(lvl) .. ']+25%[' .. ylb2(lvl) .. ']/+50% [fg]knockback force to all allies' end, ['swarmer'] = function(lvl) return '[' .. ylb1(lvl) .. ']2[' .. ylb2(lvl) .. ']/4 [fg]- [' .. ylb1(lvl) .. ']+1[' .. ylb2(lvl) .. ']/+3 [fg]hits to critters' end, ['voider'] = function(lvl) return '[' .. ylb1(lvl) .. ']2[' .. ylb2(lvl) .. ']/4 [fg]- [' .. ylb1(lvl) .. ']+15%[' .. ylb2(lvl) .. ']/+25% [fg]damage over time to allied voiders' end, + ['sorcerer'] = function(lvl) + return '[' .. ybl1(lvl) .. ']2[' .. ylb2(lvl) .. ']/4[' .. ylb3(lvl) .. '/6 [fg]- sorcerers repeat their attacks once every [' .. ylb1(lvl) .. ']4/[' .. ylb2(lvl) .. ']3/[' .. ylb3(lvl) .. ']2[fg] attacks' + end, } tier_to_characters = { @@ -809,6 +908,7 @@ function init() local forcers = 0 local swarmers = 0 local voiders = 0 + local sorcerers = 0 for _, unit in ipairs(units) do for _, unit_class in ipairs(character_classes[unit.character]) do if unit_class == 'ranger' then rangers = rangers + 1 end @@ -824,10 +924,11 @@ function init() if unit_class == 'forcer' then forcers = forcers + 1 end if unit_class == 'swarmer' then swarmers = swarmers + 1 end if unit_class == 'voider' then voiders = voiders + 1 end + if unit_class == 'sorcerer' then sorcerers = sorcerers + 1 end end end return {ranger = rangers, warrior = warriors, healer = healers, mage = mages, nuker = nukers, conjurer = conjurers, rogue = rogues, - enchanter = enchanters, psyker = psykers, curser = cursers, forcer = forcers, swarmer = swarmers, voider = voiders} + enchanter = enchanters, psyker = psykers, curser = cursers, forcer = forcers, swarmer = swarmers, voider = voiders, sorcerer = sorcerers} end get_class_levels = function(units) @@ -841,6 +942,11 @@ function init() if number_of_units >= 4 then return 2 elseif number_of_units >= 2 then return 1 else return 0 end + elseif class == 'sorcerer' then + if number_of_units >= 6 then return 3 + elseif number_of_units >= 4 then return 2 + elseif number_of_units >= 2 then return 1 + else return 0 end end end return { @@ -857,6 +963,7 @@ function init() forcer = units_to_class_level(units_per_class.forcer, 'forcer'), swarmer = units_to_class_level(units_per_class.swarmer, 'swarmer'), voider = units_to_class_level(units_per_class.voider, 'voider'), + sorcerer = units_to_class_level(units_per_class.sorcerer, 'sorcerer'), } end @@ -1181,20 +1288,17 @@ function init() if run.level and run.level > 0 then main_song_instance = _G[random:table{'song1', 'song2', 'song3', 'song4', 'song5'}]:play{volume = 0.5} end + main_song_instance = _G[random:table{'song1', 'song2', 'song3', 'song4', 'song5'}]:play{volume = 0.5} + --[[ main:add(BuyScreen'buy_screen') main:go_to('buy_screen', run.level or 0, run.units or {}, passives) - - --[[ - main:add(Arena'arena') - main:go_to('arena', 23, { - {character = 'wizard', level = 1}, - {character = 'spellblade', level = 1}, - {character = 'chronomancer', level = 1}, - {character = 'lich', level = 1}, - {character = 'psykino', level = 1}, - }, passives) ]]-- + + main:add(Arena'arena') + main:go_to('arena', 16, { + {character = 'witch', level = 3}, + }, passives) trigger:every(2, function() if debugging_memory then diff --git a/objects.lua b/objects.lua index eb5c042..e3e32ed 100644 --- a/objects.lua +++ b/objects.lua @@ -262,6 +262,10 @@ function Unit:calculate_stats(first_run) self.base_hp = 100*math.pow(2, self.level-1) self.base_dmg = 10*math.pow(2, self.level-1) self.base_mvspd = 75 + elseif self:is(Illusion) then + self.base_hp = 100*math.pow(2, self.level-1) + self.base_dmg = 10*math.pow(2, self.level-1) + self.base_mvspd = 15 elseif self:is(EnemyCritter) or self:is(Critter) then local x = self.level local y = {0, 1, 3, 3, 4, 6, 5, 6, 9, 7, 8, 12, 10, 11, 15, 12, 13, 18, 16, 17, 21, 17, 20, 24, 25} diff --git a/player.lua b/player.lua index 9b2f906..f2ca948 100644 --- a/player.lua +++ b/player.lua @@ -73,6 +73,32 @@ function Player:init(args) end end, nil, nil, 'heal') + elseif self.character == 'arcanist' then + self.attack_sensor = Circle(self.x, self.y, 128) + 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), {pierce = 1000, v = 40}) + end + end, nil, nil, 'shoot') + + elseif self.character == 'illusionist' then + 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') + self.t:every(8, function() + self.t:every(0.25, function() + SpawnEffect{group = main.current.effects, x = self.x, y = self.y, color = self.color, action = function(x, y) + illusion1:play{pitch = random:float(0.95, 1.05), volume = 0.5} + Illusion{group = main.current.main, x = x, y = y, parent = self, level = self.level, conjurer_buff_m = self.conjurer_buff_m or 1, crit = (self.level == 3) and random:bool(50)} + end} + end, self.level == 3 and 2 or 1) + end) + elseif self.character == 'outlaw' then self.attack_sensor = Circle(self.x, self.y, 96) 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() @@ -100,7 +126,7 @@ function Player:init(args) elseif self.character == 'saboteur' then self.t:every(8, function() self.t:every(0.25, function() - SpawnEffect{group = main.current.effects, x = self.x, y = self.y, action = function(x, y) + SpawnEffect{group = main.current.effects, x = self.x, y = self.y, color = self.color, action = function(x, y) Saboteur{group = main.current.main, x = x, y = y, parent = self, level = self.level, conjurer_buff_m = self.conjurer_buff_m or 1, crit = (self.level == 3) and random:bool(50)} end} end, 2) @@ -202,6 +228,11 @@ function Player:init(args) end) end + elseif self.character == 'witch' then + self.t:every(6, function() + self:dot_attack(24, {duration = random:float(12, 16), homing = true}) + end, nil, nil, 'attack') + elseif self.character == 'barbarian' then self.t:every(8, function() self:attack(96, {stun = 4}) @@ -267,7 +298,7 @@ function Player:init(args) elseif self.character == 'jester' then self.t:every({6, 10}, function() buff1:play{pitch = random:float(0.9, 1.1), volume = 0.5} - local enemies = table.first(table.shuffle(main.current.main:get_objects_by_classes(main.current.enemies)), 5) + local enemies = table.first(table.shuffle(main.current.main:get_objects_by_classes(main.current.enemies)), self.level == 3 and 8 or 4) for _, enemy in ipairs(enemies) do if self:distance_to_object(enemy) < 128 then enemy:curse('jester', 6*(self.hex_duration_m or 1), self.level == 3, self) @@ -1158,7 +1189,7 @@ function Player:shoot(r, mods) dual_gunner2:play{pitch = random:float(0.95, 1.05), volume = 0.3} elseif self.character == 'archer' or self.character == 'hunter' or self.character == 'barrager' or self.character == 'corruptor' then archer1:play{pitch = random:float(0.95, 1.05), volume = 0.35} - elseif self.character == 'wizard' or self.character == 'lich' then + elseif self.character == 'wizard' or self.character == 'lich' or self.character == 'arcanist' or self.character == 'illusionist' 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' or self.character == 'spellblade' or self.character == 'jester' or self.character == 'assassin' or self.character == 'beastmaster' then _G[random:table{'scout1', 'scout2'}]:play{pitch = random:float(0.95, 1.05), volume = 0.35} @@ -1173,6 +1204,10 @@ function Player:shoot(r, mods) frost1:play{pitch = random:float(0.95, 1.05), volume = 0.3} end + if self.character == 'arcanist' then + arcane1:play{pitch = random:float(0.95, 1.05), volume = 0.3} + end + if self.chance_to_barrage and random:bool(self.chance_to_barrage) then self:barrage(r, 3) end @@ -1205,7 +1240,8 @@ function Player:dot_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, rs = self.area_size_m*(area or 64), color = self.color, dmg = self.area_dmg_m*self.dmg, character = self.character, level = self.level} + local t = {group = main.current.effects, x = mods.x or self.x, y = mods.y or self.y, r = self.r, rs = self.area_size_m*(area or 64), color = self.color, dmg = self.area_dmg_m*self.dmg, + character = self.character, level = self.level, parent = self} DotArea(table.merge(t, mods)) dot1:play{pitch = random:float(0.9, 1.1), volume = 0.5} @@ -1233,6 +1269,7 @@ Projectile:implement(GameObject) Projectile:implement(Physics) function Projectile:init(args) self:init_game_object(args) + self.hfx:add('hit', 1) self:set_as_rectangle(10, 4, 'dynamic', 'projectile') self.pierce = args.pierce or 0 self.chain = args.chain or 0 @@ -1291,6 +1328,23 @@ function Projectile:init(args) self.t:every(0.08, function() HitParticle{group = main.current.effects, x = self.x, y = self.y, color = self.color} end) + + elseif self.character == 'arcanist' then + self.dmg = 0.2*self.dmg + self.t:every(0.08, function() HitParticle{group = main.current.effects, x = self.x, y = self.y, color = self.color, r = self.r + math.pi + random:float(-math.pi/6, math.pi/6), v = random:float(10, 25), parent = self} end) + self.t:every(self.parent.level == 3 and 0.54 or 0.8, function() + local enemies = table.head(self:get_objects_in_shape(Circle(self.x, self.y, 128), main.current.enemies), self.level == 3 and 2 or 1) + for _, enemy in ipairs(enemies) do + self.hfx:use('hit', 0.5) + local r = self:angle_to_object(enemy) + local t = {group = main.current.main, x = self.x + 8*math.cos(r), y = self.y + 8*math.sin(r), v = 250, r = r, color = self.parent.color, dmg = self.parent.dmg, pierce = 1000, character = 'arcanist_projectile', + parent = self.parent, level = self.parent.level} + Projectile(table.merge(t, mods or {})) + end + end) + + elseif self.character == 'witch' and self.level == 3 then + self.chain = 1 end if self.parent.divine_machine_arrow and table.any(self.parent.classes, function(v) return v == 'ranger' end) then @@ -1381,7 +1435,12 @@ function Projectile:draw() elseif self.character == 'lich' then graphics.push(self.x, self.y, self.r, self.spring.x, self.spring.x) - graphics.circle(self.x, self.y, 4 + random:float(-1, 1), self.color) + graphics.circle(self.x, self.y, 3 + random:float(-1, 1), self.color) + graphics.pop() + + elseif self.character == 'arcanist' then + graphics.push(self.x, self.y, self.r, self.hfx.hit.x, self.hfx.hit.x) + graphics.circle(self.x, self.y, 4, self.hfx.hit.f and fg[0] or self.color) graphics.pop() else @@ -1451,7 +1510,16 @@ function Projectile:on_collision_enter(other, contact) if self.character == 'spellblade' then magic_area1:play{pitch = random:float(0.95, 1.05), volume = 0.075} end - elseif self.character == 'wizard' or self.character == 'lich' then + elseif self.character == 'illusionist_death' then + if self.ricochet <= 0 then + self:die(x, y, r, random:int(2, 3)) + magic_area1:play{pitch = random:float(0.95, 1.05), volume = 0.075} + else + local r = Unit.bounce(self, nx, ny) + self.r = r + self.ricochet = self.ricochet - 1 + end + elseif self.character == 'wizard' or self.character == 'lich' or self.character == 'arcanist' or self.character == 'arcanist_projectile' or self.character == 'illusionist' or self.character == 'witch' 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 @@ -1511,8 +1579,10 @@ function Projectile:on_trigger_enter(other, contact) if self.character == 'spellblade' then magic_area1:play{pitch = random:float(0.95, 1.05), volume = 0.15} end - elseif self.character == 'wizard' or self.character == 'lich' then + elseif self.character == 'wizard' or self.character == 'lich' or self.character == 'arcanist' or self.character == 'illusionist' or self.character == 'illusionist_death' or self.character == 'witch' then magic_area1:play{pitch = random:float(0.95, 1.05), volume = 0.15} + elseif self.character == 'arcanist_projectile' then + magic_area1:play{pitch = random:float(0.95, 1.05), volume = 0.075} else hit3:play{pitch = random:float(0.95, 1.05), volume = 0.35} end @@ -1732,11 +1802,13 @@ end DotArea = Object:extend() DotArea:implement(GameObject) +DotArea:implement(Physics) function DotArea:init(args) self:init_game_object(args) self.shape = Circle(self.x, self.y, self.rs) + self.closest_sensor = Circle(self.x, self.y, 128) - if self.character == 'plague_doctor' or self.character == 'pyromancer' then + if self.character == 'plague_doctor' or self.character == 'pyromancer' or self.character == 'witch' then self.t:every(0.2, function() local enemies = main.current.main:get_objects_in_shape(self.shape, main.current.enemies) if #enemies > 0 then self.spring:pull(0.05, 200, 10) end @@ -1810,6 +1882,23 @@ function DotArea:init(args) end, nil, nil, 'dot') end + if self.character == 'witch' then + self.v = random:float(40, 80) + self.r = random:table{math.pi/4, 3*math.pi/4, -math.pi/4, -3*math.pi/4} + if self.level == 3 then + self.t:every(1, function() + local enemies = main.current.main:get_objects_in_shape(self.closest_sensor, main.current.enemies) + if enemies and #enemies > 0 then + wizard1:play{pitch = random:float(0.95, 1.05), volume = 0.05} + local r = self:angle_to_object(enemies[1]) + HitCircle{group = main.current.effects, x = self.x, y = self.y, rs = 6} + local t = {group = main.current.main, x = self.x, y = self.y, v = 250, r = r, color = self.parent.color, dmg = self.parent.dmg, character = 'witch', parent = self.parent, level = self.parent.level} + Projectile(table.merge(t, mods or {})) + end + end) + end + end + self.color = fg[0] self.color_transparent = Color(args.color.r, args.color.g, args.color.b, 0.08) self.rs = 0 @@ -1842,6 +1931,17 @@ function DotArea:update(dt) self.shape:move_to(self.x, self.y) end end + + if self.character == 'witch' then + self.x, self.y = self.x + self.v*math.cos(self.r)*dt, self.y + self.v*math.sin(self.r)*dt + if self.x >= main.current.x2 - self.shape.rs/2 or self.x <= main.current.x1 + self.shape.rs/2 then + self.r = math.pi - self.r + end + if self.y >= main.current.y2 - self.shape.rs/2 or self.y <= main.current.y1 + self.shape.rs/2 then + self.r = 2*math.pi - self.r + end + self.shape:move_to(self.x, self.y) + end end @@ -2217,6 +2317,85 @@ end +Illusion = Object:extend() +Illusion:implement(GameObject) +Illusion:implement(Physics) +Illusion:implement(Unit) +function Illusion:init(args) + self:init_game_object(args) + self:init_unit() + self:set_as_rectangle(8, 8, 'dynamic', 'player') + self:set_restitution(0.5) + + self.color = character_colors.illusionist + self.character = 'illusionist' + self.classes = {'sorcerer', 'conjurer'} + self:calculate_stats(true) + self:set_as_steerable(self.v, 2000, 4*math.pi, 4) + + 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 + wizard1:play{pitch = random:float(0.95, 1.05), volume = 0.05} + local r = self:angle_to_object(closest_enemy) + 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.parent.color, dmg = self.parent.dmg, character = 'illusionist', + parent = self.parent, level = self.parent.level} + Projectile(table.merge(t, mods or {})) + end + end, nil, nil, 'shoot') + + self.t:after(12*(self.parent.conjurer_buff_m or 1), 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 + + if self.parent.level == 3 then + shoot1:play{pitch = random:float(0.95, 1.05), volume = 0.2} + for i = 1, 12 do + Projectile{group = main.current.main, x = self.x, y = self.y, color = self.color, r = (i-1)*math.pi/6, v = 200, dmg = self.parent.dmg, character = 'illusionist_death', + parent = self.parent, level = self.parent.level, pierce = 1, ricochet = 1} + end + end + end) +end + + +function Illusion: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 self.target and self.target.dead then self.target = random:table(self.group:get_objects_by_classes(main.current.enemies)) end + if not self.seek_f then return end + if not self.target then + self:seek_point(gw/2, gh/2) + self:wander(50, 200, 50) + self:rotate_towards_velocity(1) + self:steering_separate(32, {Illusion, Seeker, Player}) + else + self:seek_point(self.target.x, self.target.y) + self:wander(50, 200, 50) + self:rotate_towards_velocity(1) + self:steering_separate(32, {Illusion, Seeker, Player}) + end + self.r = self:get_angle() + + self.attack_sensor:move_to(self.x, self.y) +end + + +function Illusion: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 + + + Critter = Object:extend() Critter:implement(GameObject) diff --git a/shared.lua b/shared.lua index ba5cea8..05d271c 100644 --- a/shared.lua +++ b/shared.lua @@ -12,6 +12,7 @@ function shared_init() green = ColorRamp(Color'#8bbf40', 0.025), red = ColorRamp(Color'#e91d39', 0.025), purple = ColorRamp(Color'#8e559e', 0.025), + blue2 = ColorRamp(Color'#4778ba', 0.025), } for name, color in pairs(colors) do _G[name] = color @@ -765,7 +766,11 @@ end function HitParticle:draw() graphics.push(self.x, self.y, self.r) - graphics.rectangle(self.x, self.y, self.w, self.h, 2, 2, self.color) + if self.parent and not self.parent.dead then + graphics.rectangle(self.x, self.y, self.w, self.h, 2, 2, self.parent.hfx.hit.f and fg[0] or self.color) + else + graphics.rectangle(self.x, self.y, self.w, self.h, 2, 2, self.color) + end graphics.pop() end diff --git a/todo b/todo index a8553c5..4f2029a 100644 --- a/todo +++ b/todo @@ -1,37 +1,68 @@ -Hide cursor during waves -Mouse follow control? -Item reroll for 15 gold -Option to have an arrow at the head of the snake - https://imgur.com/a/poXVsoN -Option to turn off camera movement -Headbutter damage should falloff faster after it strikes -https://i.imgur.com/QN0Ntq2.png -https://i.imgur.com/YfhqDYr.png -https://i.imgur.com/ps4OA7o.png -Remove level 3 units from rotation -Fix highlight colors and highlight reserve -Fix bug where quitting on level 1 jumps to level 2 -Fix lock bug after death/win - https://i.imgur.com/iUyOtLk.png -Options menu from buy screen -Fix fullscreen with different resolutions that don't scale properly -Volume slider +Sorcerer update + Options + Option to have an arrow at the head of the snake - https://imgur.com/a/poXVsoN + Option to turn off camera movement + Options menu from buy screen + Volume slider + Fix fullscreen with different resolutions that don't scale properly + QoL + Item reroll for 15 gold + Fix highlight colors and highlight reserve + Change cursers to trigger only near enemies + Rename tutorial to guide or manual + Add visuals for defensive ouroboros + Unlock automatically on shop enter + Bug fixes + Headbutter damage should falloff faster after it strikes + https://i.imgur.com/QN0Ntq2.png + https://i.imgur.com/YfhqDYr.png + https://i.imgur.com/ps4OA7o.png + https://i.imgur.com/j1LS3zt.png + https://i.imgur.com/XXJn4uW.png + Fix bug where quitting on level 1 jumps to level 2 + Fix lock bug after death/win - https://i.imgur.com/iUyOtLk.png + Fix enemies still spawning after arena clear (this happens with the extra enemy spawns that were blocked earlier) + Fix death + win at the same time bug + Balance + Buff tanks, maybe add a simple forcer ability to them + Buff 24/25 HP again + Buff headbutter (+ trigger range) + New + Sorcerer = sorcerers repeat their attacks once every 4/3/2 attacks + * Arcanist (tier 1 sorcerer) - launches a slow piercing orb that launches other piercing projectiles, Lv.3 effect - 50% increased attack speed for the orb and 2 projectiles are released per cast + * Sorcerer + Conjurer = Illusionist - launches a projectile that deals X damage and creates copies that do the same, Lv.3 effect - doubles the number of copies created and they release additional projectiles on death + * Sorcerer + Voider = Witch - creates an area that ricochets around the arena and deals X damage over time, Lv.3 effect - the area periodically releases projectiles that chains once + Sorcerer + Curser = Silencer - curses 5 nearby enemies for 6 seconds, preventing them from using special attacks, Lv.3 effect - the curse also deals X damage over time + Sorcerer + Nuker = Vulcanist - creates a volcano that explodes the nearby area 5 times, Lv.3 effect - the volcano spawn also deals damage and doubles the number of explosions + Sorcerer + Forcer = Warden - creates a force field around a random unit that prevents enemies from entering, Lv.3 effect - creates the force field around 2 other random units + Sorcerer + Psyker = Psychic - creates a small area that deals X damage, Lv.3 effect - the attack can happen from any distance and repeats twice -Sorcerer = sorcerers repeat their attacks once every 3/2 attacks -Sorcerer + Conjurer = Illusionist - launches a projectile that deals X damage and creates copies that do the same, Lv.3 effect - doubles the number of copies created and they release additional projectiles on death -Sorcerer + Voider = Witch - creates an area that deals X damage over time and seeks nearby enemies, Lv.3 effect - the area periodically releases homing projectiles that pierce 2 times -Sorcerer + Curser = Linker - links 3 enemies together, they share damage taken, Lv.3 effect - link 6 enemies instead and damage shared is doubled -Sorcerer + Nuker = Vulcanist - creates a volcano that explodes the nearby area 5 times, Lv.3 effect - the volcano spawn also deals damage and doubles the number of explosions -Sorcerer + Forcer = Warden - creates a force field around a random unit that prevents enemies from entering, Lv.3 effect - creates the force field around 2 other random units +Sorcerer update patch notes + Decreased projectile speed for the exploder (blue enemy) + Changed shooters (white enemy) to scale with NG+ difficulty + Increased damage and movement speed for enemy swarmers (small purple enemies) + Added tooltips for "restart run" and "tutorial" buttons on the shop + Fixed text that was going outside the screen for the Priest and the Assassin + Fixed bug where Divine Punishment would continue triggering after death + Removed cooldown visuals on snake for units that don't have cooldowns (Squire, Chronomancer and Psykeeper) + Slightly decreased base game's difficulty + Fixed NG+ difficulty only being able to be lowered, now you can lower or increase it (up to your maximum acquired value) -Patch Notes: -Decreased projectile speed for the exploder (blue enemy) -Added tooltips for "restart run" and "tutorial" buttons on the shop -Fixed text that was going outside the screen for the Priest and the Assassin -Fixed bug where Divine Punishment would continue triggering after death -Removed cooldown visuals on snake for units that don't have cooldowns (Squire, Chronomancer and Psykeeper) -Slightly decreased base game's difficulty -Fixed NG+ difficulty only being able to be lowered, now you can lower or increase it (up to your maximum acquired value) +--- Future ideas: -Chaos related class = Invoker - shoots a projectile with random properties, Lv.3 effect - ??? +Chaos related classes + Invoker - shoots a projectile with random properties, Lv.3 effect - ??? +Trappers: + Triangler - drops a trap and the 3rd trap will trigger the area, dealing X AoE damage 2-3 times Bench? - https://i.imgur.com/B1gNVKk.png Balance option for when there are more sets - https://i.imgur.com/JMynwbL.png +Negative effect: colliding with yourself kills one of your units +Go through this later https://i.imgur.com/4t7NA32.png <- lots of good improvements +Remove level 3 units from rotation +Hide cursor during waves +Mouse follow control? + +Roguelite update: +Slay the Spire-like node selection map (copy code from SHOOTRX repo as this is already implemented there) +Units die permanently when they die