diff --git a/arena.lua b/arena.lua index 1c5964a..f5c3dfa 100644 --- a/arena.lua +++ b/arena.lua @@ -24,7 +24,7 @@ function Arena:on_enter(from, level, units, passives) steam.friends.setRichPresence('text', 'Arena - Level ' .. self.level) self.floor = Group() - self.main = Group():set_as_physics_world(32, 0, 0, {'player', 'enemy', 'projectile', 'enemy_projectile', 'force_field'}) + self.main = Group():set_as_physics_world(32, 0, 0, {'player', 'enemy', 'projectile', 'enemy_projectile', 'force_field', 'ghost'}) self.post_main = Group() self.effects = Group() self.ui = Group() @@ -39,10 +39,19 @@ function Arena:on_enter(from, level, units, passives) self.main:disable_collision_between('enemy_projectile', 'enemy_projectile') self.main:disable_collision_between('player', 'force_field') self.main:disable_collision_between('projectile', 'force_field') + self.main:disable_collision_between('ghost', 'player') + self.main:disable_collision_between('ghost', 'projectile') + self.main:disable_collision_between('ghost', 'enemy') + self.main:disable_collision_between('ghost', 'enemy_projectile') + self.main:disable_collision_between('ghost', 'ghost') + self.main:disable_collision_between('ghost', 'force_field') self.main:enable_trigger_between('projectile', 'enemy') self.main:enable_trigger_between('enemy_projectile', 'player') self.main:enable_trigger_between('player', 'enemy_projectile') + self.main:enable_trigger_between('player', 'ghost') + self.main:enable_trigger_between('ghost', 'player') + self.gold_picked_up = 0 self.damage_dealt = 0 self.damage_taken = 0 self.main_slow_amount = 1 @@ -257,6 +266,7 @@ function Arena:on_enter(from, level, units, passives) self.psyker_level = class_levels.psyker self.conjurer_level = class_levels.conjurer self.sorcerer_level = class_levels.sorcerer + self.mercenary_level = class_levels.mercenary self.t:every(0.375, function() local p = random:table(star_positions) @@ -348,12 +358,9 @@ function Arena:update(dt) trigger:tween(0.25, _G, {slow_amount = 1}, math.linear, function() slow_amount = 1 self.paused = false - self.paused_t1.dead = true - self.paused_t2.dead = true - self.paused_t1 = nil - self.paused_t2 = nil - self.ng_t.dead = true - self.ng_t = nil + if self.paused_t1 then self.paused_t1.dead = true; self.paused_t1 = nil end + if self.paused_t2 then self.paused_t2.dead = true; self.paused_t2 = nil end + if self.ng_t then self.ng_t.dead = true; self.ng_t = nil end if self.resume_button then self.resume_button.dead = true; self.resume_button = nil end if self.restart_button then self.restart_button.dead = true; self.restart_button = nil end if self.mouse_button then self.mouse_button.dead = true; self.mouse_button = nil end @@ -527,12 +534,9 @@ function Arena:update(dt) trigger:tween(0.25, _G, {slow_amount = 1}, math.linear, function() slow_amount = 1 self.paused = false - self.paused_t1.dead = true - self.paused_t2.dead = true - self.paused_t1 = nil - self.paused_t2 = nil - self.ng_t.dead = true - self.ng_t = nil + if self.paused_t1 then self.paused_t1.dead = true; self.paused_t1 = nil end + if self.paused_t2 then self.paused_t2.dead = true; self.paused_t2 = nil end + if self.ng_t then self.ng_t.dead = true; self.ng_t = nil end if self.resume_button then self.resume_button.dead = true; self.resume_button = nil end if self.restart_button then self.restart_button.dead = true; self.restart_button = nil end if self.mouse_button then self.mouse_button.dead = true; self.mouse_button = nil end @@ -608,6 +612,7 @@ function Arena:quit() self.quitting = true if self.level == 25 then if not self.win_text and not self.win_text2 then + input:set_mouse_visible(true) self.won = true locked_state = nil system.save_run() @@ -800,6 +805,13 @@ function Arena:quit() steam.userStats.storeStats() end + if self.mercenary_level >= 2 then + state.achievement_mercenaries_win = true + system.save_state() + steam.userStats.setAchievement('MERCENARIES_WIN') + steam.userStats.storeStats() + end + local units = self.player:get_all_units() local all_units_level_2 = true for _, unit in ipairs(units) do @@ -836,6 +848,7 @@ function Arena:quit() self:gain_gold() self.t:after(3, function() if self.level % 3 == 0 then + input:set_mouse_visible(true) self.arena_clear_text.dead = true trigger:tween(1, _G, {slow_amount = 0}, math.linear, function() slow_amount = 0 end, 'slow_amount') trigger:tween(4, camera, {x = gw/2, y = gh/2, r = 0}, math.linear, function() camera.x, camera.y, camera.r = gw/2, gh/2, 0 end) @@ -1058,9 +1071,10 @@ end function Arena:gain_gold() + local merchant = self.player:get_unit'merchant' self.gold_gained = random:int(level_to_gold_gained[self.level][1], level_to_gold_gained[self.level][2]) - self.interest = math.min(math.floor(gold/5), 5) - gold = gold + self.gold_gained + self.interest + self.interest = math.min(math.floor(gold/5), 5) + (merchant and math.floor(gold/10) or 0) + gold = gold + self.gold_gained + self.gold_picked_up + self.interest end @@ -1074,30 +1088,30 @@ function Arena:transition() main:go_to('buy_screen', self.level, self.units, self.passives) t.t:after(0.1, function() t.text:set_text({ - {text = '[nudge_down, bg]gold gained: ' .. tostring(self.gold_gained or 0), font = pixul_font, alignment = 'center', height_multiplier = 1.5}, + {text = '[nudge_down, bg]gold gained: ' .. tostring(self.gold_gained or 0) .. ' + ' .. tostring(self.gold_picked_up or 0), font = pixul_font, alignment = 'center', height_multiplier = 1.5}, {text = '[wavy_lower, bg]interest: 0', font = pixul_font, alignment = 'center', height_multiplier = 1.5}, {text = '[wavy_lower, bg]total: 0', font = pixul_font, alignment = 'center'} }) _G[random:table{'coins1', 'coins2', 'coins3'}]:play{pitch = random:float(0.95, 1.05), volume = 0.5} t.t:after(0.2, function() t.text:set_text({ - {text = '[wavy_lower, bg]gold gained: ' .. tostring(self.gold_gained or 0), font = pixul_font, alignment = 'center', height_multiplier = 1.5}, + {text = '[wavy_lower, bg]gold gained: ' .. tostring(self.gold_gained or 0) .. ' + ' .. tostring(self.gold_picked_up or 0), font = pixul_font, alignment = 'center', height_multiplier = 1.5}, {text = '[nudge_down, bg]interest: ' .. tostring(self.interest or 0), font = pixul_font, alignment = 'center', height_multiplier = 1.5}, {text = '[wavy_lower, bg]total: 0', font = pixul_font, alignment = 'center'} }) _G[random:table{'coins1', 'coins2', 'coins3'}]:play{pitch = random:float(0.95, 1.05), volume = 0.5} t.t:after(0.2, function() t.text:set_text({ - {text = '[wavy_lower, bg]gold gained: ' .. tostring(self.gold_gained or 0), font = pixul_font, alignment = 'center', height_multiplier = 1.5}, + {text = '[wavy_lower, bg]gold gained: ' .. tostring(self.gold_gained or 0) .. ' + ' .. tostring(self.gold_picked_up or 0), font = pixul_font, alignment = 'center', height_multiplier = 1.5}, {text = '[wavy_lower, bg]interest: ' .. tostring(self.interest or 0), font = pixul_font, alignment = 'center', height_multiplier = 1.5}, - {text = '[nudge_down, bg]total: ' .. tostring((self.gold_gained or 0) + (self.interest or 0)), font = pixul_font, alignment = 'center'} + {text = '[nudge_down, bg]total: ' .. tostring((self.gold_gained or 0) + (self.interest or 0) + (self.gold_picked_up or 0)), font = pixul_font, alignment = 'center'} }) _G[random:table{'coins1', 'coins2', 'coins3'}]:play{pitch = random:float(0.95, 1.05), volume = 0.5} end) end) end) end, text = Text({ - {text = '[wavy_lower, bg]gold gained: 0', font = pixul_font, alignment = 'center', height_multiplier = 1.5}, + {text = '[wavy_lower, bg]gold gained: 0 + 0', font = pixul_font, alignment = 'center', height_multiplier = 1.5}, {text = '[wavy_lower, bg]interest: 0', font = pixul_font, alignment = 'center', height_multiplier = 1.5}, {text = '[wavy_lower, bg]total: 0', font = pixul_font, alignment = 'center'} }, global_text_tags)} diff --git a/assets/images/mercenary.png b/assets/images/mercenary.png new file mode 100644 index 0000000..fbed7b2 Binary files /dev/null and b/assets/images/mercenary.png differ diff --git a/assets/sounds/Coins - Gears - Slot.ogg b/assets/sounds/Coins - Gears - Slot.ogg new file mode 100644 index 0000000..75c8961 Binary files /dev/null and b/assets/sounds/Coins - Gears - Slot.ogg differ diff --git a/assets/sounds/Collect 2.ogg b/assets/sounds/Collect 2.ogg new file mode 100644 index 0000000..c26445e Binary files /dev/null and b/assets/sounds/Collect 2.ogg differ diff --git a/assets/sounds/Collect 4.ogg b/assets/sounds/Collect 4.ogg new file mode 100644 index 0000000..99189ea Binary files /dev/null and b/assets/sounds/Collect 4.ogg differ diff --git a/assets/sounds/Collect 5.ogg b/assets/sounds/Collect 5.ogg new file mode 100644 index 0000000..4f3de27 Binary files /dev/null and b/assets/sounds/Collect 5.ogg differ diff --git a/assets/sounds/Damage 2.ogg b/assets/sounds/Damage 2.ogg new file mode 100644 index 0000000..8104a0c Binary files /dev/null and b/assets/sounds/Damage 2.ogg differ diff --git a/assets/sounds/Damage 3.ogg b/assets/sounds/Damage 3.ogg new file mode 100644 index 0000000..599c73e Binary files /dev/null and b/assets/sounds/Damage 3.ogg differ diff --git a/assets/sounds/Damage 5.ogg b/assets/sounds/Damage 5.ogg new file mode 100644 index 0000000..0c555eb Binary files /dev/null and b/assets/sounds/Damage 5.ogg differ diff --git a/assets/sounds/Shadow Punch 2.ogg b/assets/sounds/Shadow Punch 2.ogg new file mode 100644 index 0000000..741bbf0 Binary files /dev/null and b/assets/sounds/Shadow Punch 2.ogg differ diff --git a/buy_screen.lua b/buy_screen.lua index 782594f..3fc91fb 100644 --- a/buy_screen.lua +++ b/buy_screen.lua @@ -781,7 +781,8 @@ function RerollButton:init(args) self.text = Text({{text = '[bg10]reroll: [yellow]2', font = pixul_font, alignment = 'center'}}, global_text_tags) elseif self.parent:is(Arena) then self.shape = Rectangle(self.x, self.y, 60, 16) - if self.parent.level == 3 then + local merchant = self.parent.player:get_unit'merchant' + if self.parent.level == 3 or (merchant and merchant.level == 3) then self.free_reroll = true self.text = Text({{text = '[bg10]reroll: [yellow]0', font = pixul_font, alignment = 'center'}}, global_text_tags) else @@ -847,7 +848,11 @@ end function RerollButton:draw() graphics.push(self.x, self.y, 0, self.spring.x, self.spring.y) - graphics.rectangle(self.x, self.y, self.shape.w, self.shape.h, 4, 4, self.selected and fg[0] or bg[1]) + if self.parent:is(Arena) then + graphics.rectangle(self.x, self.y, self.shape.w, self.shape.h, 4, 4, self.selected and fg[0] or bg[-2]) + else + graphics.rectangle(self.x, self.y, self.shape.w, self.shape.h, 4, 4, self.selected and fg[0] or bg[1]) + end self.text:draw(self.x, self.y + 1) graphics.pop() end @@ -1077,6 +1082,7 @@ end function CharacterPart:get_sale_price() + if not character_tiers[self.character] then return 0 end local total = 0 total = total + ((self.level == 1 and character_tiers[self.character]) or (self.level == 2 and 2*character_tiers[self.character]) or (self.level == 3 and 6*character_tiers[self.character]) or 0) if self.reserve then diff --git a/enemies.lua b/enemies.lua index bbd63a0..6ff1211 100644 --- a/enemies.lua +++ b/enemies.lua @@ -253,6 +253,8 @@ function Seeker:init(args) if player and player.temporal_chains then self.temporal_chains_mvspd_m = 0.8 end + + self.usurer_count = 0 end @@ -435,6 +437,14 @@ function Seeker:hit(damage, projectile) HitCircle{group = main.current.effects, x = self.x, y = self.y, rs = 12}:scale_down(0.3):change_color(0.5, self.color) _G[random:table{'enemy_die1', 'enemy_die2'}]:play{pitch = random:float(0.9, 1.1), volume = 0.5} + if main.current.mercenary_level > 0 then + if random:bool((main.current.mercenary_level == 2 and 20) or (main.current.mercenary_level == 1 and 10) or 0) then + trigger:after(0.01, function() + Gold{group = main.current.main, x = self.x, y = self.y} + end) + end + end + if self.boss then slow(0.25, 1) magic_die1:play{pitch = random:float(0.95, 1.05), volume = 0.5} @@ -594,6 +604,17 @@ function Seeker:curse(curse, duration, arg1, arg2, arg3) elseif curse == 'silencer' then self.silenced = true self.t:after(duration*curse_m, function() self.silenced = false end, 'silencer_curse') + elseif curse == 'usurer' then + if arg1 then + self.usurer_count = self.usurer_count + 1 + if self.usurer_count == 3 then + usurer1:play{pitch = random:float(0.95, 1.05), volume = 1} + rogue_crit1:play{pitch = random:float(0.95, 1.05), volume = 1} + camera:shake(4, 0.4) + self.usurer_count = 0 + self:hit(50*arg2.dmg) + end + end end end @@ -628,6 +649,7 @@ function EnemyCritter:init(args) self:push(args.v, args.r) self.invulnerable_to = args.projectile self.t:after(0.5, function() self.invulnerable_to = false end) + self.usurer_count = 0 end @@ -778,6 +800,14 @@ function EnemyCritter:curse(curse, duration, arg1, arg2, arg3) elseif curse == 'silencer' then self.silenced = true self.t:after(duration*curse_m, function() self.silenced = false end, 'silencer_curse') + elseif curse == 'usurer' then + if arg1 then + self.usurer_count = self.usurer_count + 1 + if self.usurer_count == 3 then + self.usurer_count = 0 + self:hit(10*arg2.dmg) + end + end end end diff --git a/engine/datastructures/table.lua b/engine/datastructures/table.lua index 8b4c624..d0c140b 100644 --- a/engine/datastructures/table.lua +++ b/engine/datastructures/table.lua @@ -418,6 +418,16 @@ function table.first(t, n) end +function table.first2(t, n) + if n == 1 then return {t[1]} end + local out = {} + for i = 1, (n or 1) do + table.push(out, t[i]) + end + return out +end + + -- Returns the last n values, same as tail -- t = {4, 3, 2, 1} -- table.last(t) -> 1 diff --git a/main.lua b/main.lua index dfcb3d8..bf16014 100644 --- a/main.lua +++ b/main.lua @@ -9,7 +9,6 @@ require 'media' function init() - print('Initializing engine...') shared_init() input:bind('move_left', {'a', 'left', 'dpleft', 'm1'}) @@ -18,8 +17,11 @@ function init() input:bind('move_down', {'s', 'down', 'dpdown'}) input:bind('enter', {'space', 'return', 'fleft', 'fdown', 'fright'}) - print('Loading sounds...') local s = {tags = {sfx}} + gambler1 = Sound('Collect 5.ogg', s) + usurer1 = Sound('Shadow Punch 2.ogg', s) + gold1 = Sound('Collect 5.ogg', s) + gold2 = Sound('Coins - Gears - Slot.ogg', s) psychic1 = Sound('Magical Impact 13.ogg', s) fire1 = Sound('Fire bolt 3.ogg', s) fire2 = Sound('Fire bolt 5.ogg', s) @@ -111,7 +113,6 @@ function init() rogue_crit1 = Sound('Dagger Stab (Flesh) 4.ogg', s) rogue_crit2 = Sound('Sword hits another sword 6.ogg', s) - print('Loading songs...') song1 = Sound('Kubbi - Ember - 01 Pathfinder.ogg', {tags = {music}}) song2 = Sound('Kubbi - Ember - 02 Ember.ogg', {tags = {music}}) song3 = Sound('Kubbi - Ember - 03 Firelight.ogg', {tags = {music}}) @@ -119,7 +120,6 @@ function init() song5 = Sound('Kubbi - Ember - 05 Compass.ogg', {tags = {music}}) death_song = Sound('Kubbi - Ember - 09 Formed by Glaciers.ogg', {tags = {music}}) - print('Loading images...') lock_image = Image('lock') speed_booster_elite = Image('speed_booster_elite') exploder_elite = Image('exploder_elite') @@ -140,6 +140,7 @@ function init() swarmer = Image('swarmer') voider = Image('voider') sorcerer = Image('sorcerer') + mercenary = Image('mercenary') ouroboros_technique_r = Image('ouroboros_technique_r') ouroboros_technique_l = Image('ouroboros_technique_l') wall_echo = Image('wall_echo') @@ -185,7 +186,6 @@ function init() star = Image('star') arrow = Image('arrow') - print('Initializing game...') class_colors = { ['warrior'] = yellow[0], ['ranger'] = green[0], @@ -201,6 +201,7 @@ function init() ['swarmer'] = orange[0], ['voider'] = purple[0], ['sorcerer'] = blue2[0], + ['mercenary'] = yellow2[0], } class_color_strings = { @@ -218,6 +219,7 @@ function init() ['swarmer'] = 'orange', ['voider'] = 'purple', ['sorcerer'] = 'blue2', + ['mercenary'] = 'yellow2', } character_names = { @@ -270,6 +272,11 @@ function init() ['vulcanist'] = 'Vulcanist', ['warden'] = 'Warden', ['psychic'] = 'Psychic', + ['treasure_hunter'] = 'Treasure Hunter', + ['merchant'] = 'Merchant', + ['usurer'] = 'Usurer', + ['gambler'] = 'Gambler', + ['thief'] = 'Thief', } character_colors = { @@ -322,6 +329,11 @@ function init() ['vulcanist'] = red[0], ['warden'] = yellow[0], ['psychic'] = fg[0], + ['treasure_hunter'] = yellow2[0], + ['merchant'] = yellow2[0], + ['usurer'] = purple[0], + ['gambler'] = yellow2[0], + ['thief'] = red[0], } character_color_strings = { @@ -374,6 +386,11 @@ function init() ['vulcanist'] = 'red', ['warden'] = 'yellow', ['psychic'] = 'fg', + ['treasure_hunter'] = 'yellow2', + ['merchant'] = 'yellow2', + ['usurer'] = 'purple', + ['gambler'] = 'yellow2', + ['thief'] = 'red', } character_classes = { @@ -426,6 +443,11 @@ function init() ['vulcanist'] = {'sorcerer', 'nuker'}, ['warden'] = {'sorcerer', 'forcer'}, ['psychic'] = {'sorcerer', 'psyker'}, + ['treasure_hunter'] = {'mercenary'}, + ['merchant'] = {'mercenary'}, + ['usurer'] = {'curser', 'mercenary', 'voider'}, + ['gambler'] = {'mercenary', 'sorcerer'}, + ['thief'] = {'rogue', 'mercenary'}, } character_class_strings = { @@ -478,6 +500,11 @@ function init() ['vulcanist'] = '[blue2]Sorcerer, [red]Nuker', ['warden'] = '[blue2]Sorcerer, [yellow]Forcer', ['psychic'] = '[blue2]Sorcerer, [fg]Psyker', + ['treasure_hunter'] = '[yellow2]Mercenary', + ['merchant'] = '[yellow2]Mercenary', + ['usurer'] = '[purple]Curser, [yellow2]Mercenary, [purple]Curser', + ['gambler'] = '[yellow2]Mercenary, [blue2]Sorcerer', + ['thief'] = '[red]Rogue, [yellow2]Mercenary', } get_character_stat_string = function(character, level) @@ -547,6 +574,11 @@ function init() ['vulcanist'] = function(lvl) return '[fg]creates a volcano that explodes the nearby area [yellow]4[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') .. ' AoE[fg] damage' end, + ['treasure_hunter'] = function(lvl) return '[fg]picking up gold releases [yellow]4[fg] homing projectiles that each deal [yellow]' .. get_character_stat('treasure_hunter', lvl, 'dmg') .. ' [fg]damage' end, + ['merchant'] = function(lvl) return '[fg]gain [yellow]+1[fg] interest for every [yellow]10[fg] gold' end, + ['usurer'] = function(lvl) return '[fg]curses [yellow]3[fg] nearby enemies indefinitely with [yellow]debt[fg], dealing [yellow]' .. get_character_stat('usurer', lvl, 'dmg') .. '[fg] damage per second' end, + ['gambler'] = function(lvl) return '[fg]deal [yellow]2X[fg] damage to a single random enemy where X is how much gold you have' end, + ['thief'] = function(lvl) return '[fg]throws a knife that deals [yellow]' .. 2*get_character_stat('thief', lvl, 'dmg') .. '[fg] damage and chains [yellow]5[fg] times' end, } character_effect_names = { @@ -598,7 +630,12 @@ function init() ['silencer'] = '[blue2]Arcane Curse', ['vulcanist'] = '[red]Lava Burst', ['warden'] = '[yellow]Magnetic Field', - ['psychic'] = '[fg]Mental Strike' + ['psychic'] = '[fg]Mental Strike', + ['treasure_hunter'] = '[yellow2]Golden Bolts', + ['merchant'] = '[yellow2]Item Shop', + ['usurer'] = '[purple]Bankruptcy', + ['gambler'] = '[yellow2]Multicast', + ['thief'] = '[red]Ultrakill', } character_effect_names_gray = { @@ -650,7 +687,12 @@ function init() ['silencer'] = '[light_bg]Arcane Curse', ['vulcanist'] = '[light_bg]Lava Burst', ['warden'] = '[light_bg]Magnetic Field', - ['psychic'] = '[light_bg]Mental Strike' + ['psychic'] = '[light_bg]Mental Strike', + ['treasure_hunter'] = '[light_bg]Golden Bolts', + ['merchant'] = '[light_bg]Item Shop', + ['usurer'] = '[light_bg]Bankruptcy', + ['gambler'] = '[light_bg]Multicast', + ['thief'] = '[light_bg]Ultrakill', } character_effect_descriptions = { @@ -703,6 +745,11 @@ function init() ['vulcanist'] = function() return '[fg]the number and speed of explosions is [yellow]doubled[fg]' end, ['warden'] = function() return '[fg]creates the force field around [yellow]2[fg] units' end, ['psychic'] = function() return '[fg]the attack can happen from any distance and repeats once' end, + ['treasure_hunter'] = function() return '[fg]release [yellow]8[fg] homing projectiles instead and they pierce twice' end, + ['merchant'] = function() return '[fg]your first item reroll is always free' end, + ['usurer'] = function() return '[fg]if the same enemy is cursed [yellow]3[fg] times it takes [yellow]' .. 10*get_character_stat('usurer', 3, 'dmg') .. '[fg] damage' end, + ['gambler'] = function() return '[yellow]60/40/20%[fg] chance to cast the attack [yellow]2/3/4[fg] times' end, + ['thief'] = function() return '[fg]if the knife crits it deals [yellow]' .. 10*get_character_stat('thief', 3, 'dmg') .. '[fg] damage, chains [yellow]10[fg] times and grants [yellow]1[fg] gold' end, } character_effect_descriptions_gray = { @@ -755,6 +802,11 @@ function init() ['vulcanist'] = function() return '[light_bg]the number and speed of explosions is doubled' end, ['warden'] = function() return '[light_bg]creates the force field around 2 units' end, ['psychic'] = function() return '[light_bg]the attack can happen from any distance and repeats once' end, + ['treasure_hunter'] = function() return '[light_bg]release 8 homing projectiles instead and they pierce twice' end, + ['merchant'] = function() return '[light_bg]your first item reroll is always free' end, + ['usurer'] = function() return '[light_bg]if the same enemy is cursed 3 times it takes ' .. 10*get_character_stat('usurer', 3, 'dmg') .. ' damage' end, + ['gambler'] = function() return '[light_bg]60/40/20% chance to cast the attack 2/3/4 times' end, + ['thief'] = function() return '[light_bg]if the coin crits it deals ' .. 10*get_character_stat('thief', 3, 'dmg') .. ' damage, chains 10 times and grants 1 gold' end, } character_stats = { @@ -807,6 +859,11 @@ function init() ['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, + ['treasure_hunter'] = function(lvl) return get_character_stat_string('treasure_hunter', lvl) end, + ['merchant'] = function(lvl) return get_character_stat_string('merchant', lvl) end, + ['usurer'] = function(lvl) return get_character_stat_string('usurer', lvl) end, + ['gambler'] = function(lvl) return get_character_stat_string('gambler', lvl) end, + ['thief'] = function(lvl) return get_character_stat_string('thief', lvl) end, } class_stat_multipliers = { @@ -824,6 +881,7 @@ function init() ['swarmer'] = {hp = 1.2, dmg = 1, aspd = 1.25, area_dmg = 1, area_size = 1, def = 0.75, mvspd = 0.75}, ['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}, + ['mercenary'] = {hp = 1, dmg = 1, aspd = 1, area_dmg = 1, area_size = 1, def = 1, 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}, @@ -862,17 +920,18 @@ function init() ['sorcerer'] = function(lvl) return '[' .. ylb1(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, + ['mercenary'] = function(lvl) return '[' .. ylb1(lvl) .. ']2[' .. ylb2(lvl) .. ']/4 [fg]- [' .. ylb1(lvl) .. ']+10%[' .. ylb2(lvl) .. ']/+20% [fg]chance for enemies to drop gold on death' end, } tier_to_characters = { - [1] = {'vagrant', 'swordsman', 'magician', 'archer', 'scout', 'cleric', 'arcanist'}, - [2] = {'wizard', 'saboteur', 'sage', 'squire', 'dual_gunner', 'hunter', 'chronomancer', 'barbarian', 'cryomancer', 'beastmaster', 'launcher', 'jester', 'carver', 'psychic', 'witch', 'silencer', 'outlaw'}, - [3] = {'elementor', 'stormweaver', 'spellblade', 'psykeeper', 'engineer', 'juggernaut', 'pyromancer', 'host', 'assassin', 'bane', 'barrager', 'infestor', 'flagellant', 'illusionist'}, - [4] = {'priest', 'highlander', 'psykino', 'fairy', 'blade', 'plague_doctor', 'cannoneer', 'vulcanist', 'warden', 'corruptor'}, + [1] = {'vagrant', 'swordsman', 'magician', 'archer', 'scout', 'cleric', 'arcanist', 'treasure_hunter'}, + [2] = {'wizard', 'saboteur', 'sage', 'squire', 'dual_gunner', 'hunter', 'chronomancer', 'barbarian', 'cryomancer', 'beastmaster', 'launcher', 'jester', 'carver', 'psychic', 'witch', 'silencer', 'outlaw', 'merchant'}, + [3] = {'elementor', 'stormweaver', 'spellblade', 'psykeeper', 'engineer', 'juggernaut', 'pyromancer', 'host', 'assassin', 'bane', 'barrager', 'infestor', 'flagellant', 'illusionist', 'usurer', 'gambler'}, + [4] = {'priest', 'highlander', 'psykino', 'fairy', 'blade', 'plague_doctor', 'cannoneer', 'vulcanist', 'warden', 'corruptor', 'thief'}, } - non_attacking_characters = {'cleric', 'stormweaver', 'squire', 'chronomancer', 'sage', 'psykeeper', 'bane', 'carver', 'fairy', 'priest', 'flagellant'} - non_cooldown_characters = {'squire', 'chronomancer', 'psykeeper'} + non_attacking_characters = {'cleric', 'stormweaver', 'squire', 'chronomancer', 'sage', 'psykeeper', 'bane', 'carver', 'fairy', 'priest', 'flagellant', 'merchant', 'treasure_hunter'} + non_cooldown_characters = {'squire', 'chronomancer', 'psykeeper', 'merchant', 'treasure_hunter'} character_tiers = { ['vagrant'] = 1, @@ -881,7 +940,6 @@ function init() ['archer'] = 1, ['scout'] = 1, ['cleric'] = 1, - ['arcanist'] = 1, ['outlaw'] = 2, ['blade'] = 4, ['elementor'] = 3, @@ -918,12 +976,18 @@ function init() ['priest'] = 4, ['infestor'] = 3, ['flagellant'] = 3, - ['psychic'] = 2, + ['arcanist'] = 1, + ['illusionist'] = 3, ['witch'] = 2, ['silencer'] = 2, - ['illusionist'] = 3, - ['warden'] = 4, ['vulcanist'] = 4, + ['warden'] = 4, + ['psychic'] = 2, + ['treasure_hunter'] = 1, + ['merchant'] = 2, + ['usurer'] = 3, + ['gambler'] = 3, + ['thief'] = 4, } get_number_of_units_per_class = function(units) @@ -941,6 +1005,7 @@ function init() local swarmers = 0 local voiders = 0 local sorcerers = 0 + local mercenaries = 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 @@ -957,10 +1022,11 @@ function init() 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 + if unit_class == 'mercenary' then mercenaries = mercenaries + 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, sorcerer = sorcerers} + enchanter = enchanters, psyker = psykers, curser = cursers, forcer = forcers, swarmer = swarmers, voider = voiders, sorcerer = sorcerers, mercenary = mercenaries} end get_class_levels = function(units) @@ -970,7 +1036,7 @@ function init() if number_of_units >= 6 then return 2 elseif number_of_units >= 3 then return 1 else return 0 end - elseif class == 'healer' or class == 'conjurer' or class == 'enchanter' or class == 'psyker' or class == 'curser' or class == 'forcer' or class == 'swarmer' or class == 'voider' then + elseif class == 'healer' or class == 'conjurer' or class == 'enchanter' or class == 'psyker' or class == 'curser' or class == 'forcer' or class == 'swarmer' or class == 'voider' or class == 'mercenary' then if number_of_units >= 4 then return 2 elseif number_of_units >= 2 then return 1 else return 0 end @@ -996,6 +1062,7 @@ function init() 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'), + mercenary = units_to_class_level(units_per_class.mercenary, 'mercenary'), } end @@ -1022,6 +1089,7 @@ function init() ['swarmer'] = function(units) return 2, 4, nil, get_number_of_units_per_class(units).swarmer end, ['voider'] = function(units) return 2, 4, nil, get_number_of_units_per_class(units).voider end, ['sorcerer'] = function(units) return 2, 4, 6, get_number_of_units_per_class(units).sorcerer end, + ['mercenary'] = function(units) return 2, 4, nil, get_number_of_units_per_class(units).mercenary end, } passive_names = { @@ -1323,22 +1391,16 @@ function init() 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:go_to('buy_screen', 7, run.units or {}, {'unleash'}) --[[ main:add(Arena'arena') - main:go_to('arena', 17, { - {character = 'arcanist', level = 2}, - {character = 'silencer', level = 2}, - {character = 'warden', level = 3}, - {character = 'chronomancer', level = 1}, - {character = 'witch', level = 3}, - {character = 'illusionist', level = 3}, - {character = 'psychic', level = 2}, - {character = 'vulcanist', level = 3}, + main:go_to('arena', 13, { + {character = 'thief', level = 3}, + {character = 'scout', level = 1}, + {character = 'beastmaster', level = 1}, }, passives) ]]-- diff --git a/player.lua b/player.lua index 647c4d5..1c10f58 100644 --- a/player.lua +++ b/player.lua @@ -47,7 +47,6 @@ function Player:init(args) self:attack(32, {x = enemy.x, y = enemy.y}) end end, nil, nil, 'attack') - if self.level == 3 then self.t:every(12, function() self.magician_invulnerable = true @@ -55,6 +54,60 @@ function Player:init(args) end) end + elseif self.character == 'gambler' then + self.sorcerer_count = 0 + local cast = function(pitch_a) + local enemy = table.shuffle(main.current.main:get_objects_by_classes(main.current.enemies))[1] + if enemy then + gambler1:play{pitch = pitch_a, volume = math.remap(gold, 0, 50, 0, 0.8)} + enemy:hit(2*gold) + if main.current.sorcerer_level > 0 then + self.sorcerer_count = self.sorcerer_count + 1 + if self.sorcerer_count >= ((main.current.sorcerer_level == 3 and 2) or (main.current.sorcerer_level == 2 and 3) or (main.current.sorcerer_level == 1 and 4)) then + self.sorcerer_count = 0 + self.t:after(0.25, function() + local enemy = table.shuffle(main.current.main:get_objects_by_classes(main.current.enemies))[1] + if enemy then + gambler1:play{pitch = pitch_a + 0.05, volume = math.remap(gold, 0, 50, 0, 0.8)} + enemy:hit(2*gold) + end + end) + end + end + end + end + self.t:every(2, function() + cast(1) + if self.level == 3 then + if random:bool(60) then + if random:bool(40) then + if random:bool(20) then + self.t:after(0.25, function() + cast(1.1) + self.t:after(0.25, function() + cast(1.2) + self.t:after(0.25, function() + cast(1.3) + end) + end) + end) + else + self.t:after(0.25, function() + cast(1.1) + self.t:after(0.25, function() + cast(1.2) + end) + end) + end + else + self.t:after(0.25, function() + cast(1.1) + end) + end + end + end + end, nil, nil, 'attack') + elseif self.character == 'archer' then 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() @@ -73,6 +126,15 @@ function Player:init(args) end end, nil, nil, 'shoot') + elseif self.character == 'thief' then + self.attack_sensor = Circle(self.x, self.y, 64) + 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 = (self.level == 3 and 10 or 5)}) + end + end, nil, nil, 'shoot') + elseif self.character == 'cleric' then self.t:every(6, function() local all_units = self:get_all_units() @@ -261,12 +323,12 @@ function Player:init(args) if x == 0 and y == 0 then x, y = gw/2, gh/2 end x, y = x + self.x, y + self.y x, y = x/2, y/2 - trigger:every_immediate(0.1, function() + main.current.t:every_immediate(0.1, function() local check_circle = Circle(x, y, 2) local objects = main.current.main:get_objects_in_shape(check_circle, {Player, Seeker, EnemyCritter, Critter, Illusion, Saboteur, Pet, Turret}) if #objects == 0 then Volcano{group = main.current.main, x = x, y = y, color = self.color, parent = self, rs = 24, level = self.level} - trigger:cancel('volcano_spawn') + main.current.t:cancel('volcano_spawn') end end, nil, nil, 'volcano_spawn') end @@ -431,9 +493,10 @@ function Player:init(args) elseif self.character == 'jester' then self.attack_sensor = Circle(self.x, self.y, 96) + self.wide_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() 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)), 6) + local enemies = table.first(table.shuffle(self:get_objects_in_shape(self.wide_attack_sensor, main.current.enemies)), 6) 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) @@ -443,26 +506,39 @@ function Player:init(args) end end, nil, nil, 'attack') + elseif self.character == 'usurer' then + self.attack_sensor = Circle(self.x, self.y, 96) + self.wide_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() + buff1:play{pitch = random:float(0.9, 1.1), volume = 0.5} + local enemies = table.first2(table.shuffle(self:get_objects_in_shape(self.wide_attack_sensor, main.current.enemies)), 3) + for _, enemy in ipairs(enemies) do + enemy:curse('usurer', 10000, self.level == 3, self) + enemy:apply_dot(self.dmg*(self.dot_dmg_m or 1)*(main.current.chronomancer_dot or 1), 10000) + HitCircle{group = main.current.effects, x = self.x, y = self.y, rs = 6, color = purple[0], duration = 0.1} + LightningLine{group = main.current.effects, src = self, dst = enemy, color = purple[0]} + end + end, nil, nil, 'attack') + elseif self.character == 'silencer' then self.sorcerer_count = 0 self.attack_sensor = Circle(self.x, self.y, 96) + self.wide_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 curse = 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)), 6) + local enemies = table.first(table.shuffle(self:get_objects_in_shape(self.wide_attack_sensor, main.current.enemies)), 6) for _, enemy in ipairs(enemies) do - if self:distance_to_object(enemy) < 128 then - enemy:curse('silencer', 6*(self.hex_duration_m or 1), self.level == 3, self) - if self.level == 3 then - local curse_m = 1 - if main.current.curser_level == 2 then curse_m = 1.5 - elseif main.current.curser_level == 1 then curse_m = 1.25 - else curse_m = 1 end - enemy:apply_dot(self.dmg*(self.dot_dmg_m or 1)*(main.current.chronomancer_dot or 1), 6*(self.hex_duration_m or 1)*(curse_m or 1)) - end - HitCircle{group = main.current.effects, x = self.x, y = self.y, rs = 6, color = blue2[0], duration = 0.1} - LightningLine{group = main.current.effects, src = self, dst = enemy, color = blue2[0]} + enemy:curse('silencer', 6*(self.hex_duration_m or 1), self.level == 3, self) + if self.level == 3 then + local curse_m = 1 + if main.current.curser_level == 2 then curse_m = 1.5 + elseif main.current.curser_level == 1 then curse_m = 1.25 + else curse_m = 1 end + enemy:apply_dot(self.dmg*(self.dot_dmg_m or 1)*(main.current.chronomancer_dot or 1), 6*(self.hex_duration_m or 1)*(curse_m or 1)) end + HitCircle{group = main.current.effects, x = self.x, y = self.y, rs = 6, color = blue2[0], duration = 0.1} + LightningLine{group = main.current.effects, src = self, dst = enemy, color = blue2[0]} end end curse() @@ -508,15 +584,14 @@ function Player:init(args) elseif self.character == 'bane' then self.attack_sensor = Circle(self.x, self.y, 96) + self.wide_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() 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)), 6) + local enemies = table.first(table.shuffle(self:get_objects_in_shape(self.wide_attack_sensor, main.current.enemies)), 6) for _, enemy in ipairs(enemies) do - if self:distance_to_object(enemy) < 128 then - enemy:curse('bane', 6*(self.hex_duration_m or 1), self.level == 3, self) - HitCircle{group = main.current.effects, x = self.x, y = self.y, rs = 6, color = purple[0], duration = 0.1} - LightningLine{group = main.current.effects, src = self, dst = enemy, color = purple[0]} - end + enemy:curse('bane', 6*(self.hex_duration_m or 1), self.level == 3, self) + HitCircle{group = main.current.effects, x = self.x, y = self.y, rs = 6, color = purple[0], duration = 0.1} + LightningLine{group = main.current.effects, src = self, dst = enemy, color = purple[0]} end end, nil, nil, 'attack') @@ -603,24 +678,24 @@ function Player:init(args) local unit_2 = random:table_remove(units) if unit_1 then illusion1:play{pitch = random:float(0.95, 1.05), volume = 0.5} - trigger:every_immediate(0.1, function() + main.current.t:every_immediate(0.1, function() local check_circle = Circle(unit_1.x, unit_1.y, 6) local objects = main.current.main:get_objects_in_shape(check_circle, {Seeker, EnemyCritter}) if #objects == 0 then ForceField{group = main.current.main, x = unit_1.x, y = unit_1.y, parent = unit_1} - trigger:cancel('warden_force_field_1') + main.current.t:cancel('warden_force_field_1') end end, nil, nil, 'warden_force_field_1') end if unit_2 then illusion1:play{pitch = random:float(0.95, 1.05), volume = 0.5} ForceField{group = main.current.main, x = unit_2.x, y = unit_2.y, parent = unit_2} - trigger:every_immediate(0.1, function() + main.current.t:every_immediate(0.1, function() local check_circle = Circle(unit_2.x, unit_2.y, 6) local objects = main.current.main:get_objects_in_shape(check_circle, {Seeker, EnemyCritter}) if #objects == 0 then ForceField{group = main.current.main, x = unit_2.x, y = unit_2.y, parent = unit_2} - trigger:cancel('warden_force_field_2') + main.current.t:cancel('warden_force_field_2') end end, nil, nil, 'warden_force_field_2') end @@ -628,12 +703,12 @@ function Player:init(args) local unit = random:table(self:get_all_units()) if unit then illusion1:play{pitch = random:float(0.95, 1.05), volume = 0.5} - trigger:every_immediate(0.1, function() + main.current.t:every_immediate(0.1, function() local check_circle = Circle(unit.x, unit.y, 6) local objects = main.current.main:get_objects_in_shape(check_circle, {Seeker, EnemyCritter}) if #objects == 0 then ForceField{group = main.current.main, x = unit.x, y = unit.y, parent = unit} - trigger:cancel('warden_force_field_0') + main.current.t:cancel('warden_force_field_0') end end, nil, nil, 'warden_force_field_0') end @@ -671,16 +746,15 @@ function Player:init(args) end, nil, nil, 'heal') elseif self.character == 'infestor' then - self.attack_sensor = Circle(self.x, self.y, 128) + self.attack_sensor = Circle(self.x, self.y, 96) + self.wide_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() 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)), 8) + local enemies = table.first(table.shuffle(self:get_objects_in_shape(self.wide_attack_sensor, main.current.enemies)), 8) for _, enemy in ipairs(enemies) do - if self:distance_to_object(enemy) < 128 then - enemy:curse('infestor', 6*(self.hex_duration_m or 1), (self.level == 3 and 6 or 2), self.dmg, self) - HitCircle{group = main.current.effects, x = self.x, y = self.y, rs = 6, color = orange[0], duration = 0.1} - LightningLine{group = main.current.effects, src = self, dst = enemy, color = orange[0]} - end + enemy:curse('infestor', 6*(self.hex_duration_m or 1), (self.level == 3 and 6 or 2), self.dmg, self) + HitCircle{group = main.current.effects, x = self.x, y = self.y, rs = 6, color = orange[0], duration = 0.1} + LightningLine{group = main.current.effects, src = self, dst = enemy, color = orange[0]} end end, nil, nil, 'attack') @@ -923,6 +997,7 @@ function Player:update(dt) if class_levels.swarmer >= 1 then number_of_active_sets = number_of_active_sets + 1 end if class_levels.voider >= 1 then number_of_active_sets = number_of_active_sets + 1 end if class_levels.sorcerer >= 1 then number_of_active_sets = number_of_active_sets + 1 end + if class_levels.mercenary >= 1 then number_of_active_sets = number_of_active_sets + 1 end self.vagrant_dmg_m = 1 + 0.1*number_of_active_sets self.vagrant_aspd_m = 1 + 0.1*number_of_active_sets end @@ -988,6 +1063,7 @@ function Player:update(dt) if class_levels.swarmer >= 1 then number_of_active_sets = number_of_active_sets + 1 end if class_levels.voider >= 1 then number_of_active_sets = number_of_active_sets + 1 end if class_levels.sorcerer >= 1 then number_of_active_sets = number_of_active_sets + 1 end + if class_levels.mercenary >= 1 then number_of_active_sets = number_of_active_sets + 1 end if main.current.psyker_level == 2 then self.psyker_dmg_m = 1 + 0.2*number_of_active_sets self.psyker_aspd_m = 1 + 0.2*number_of_active_sets @@ -1039,6 +1115,7 @@ function Player:update(dt) self:calculate_stats() if self.attack_sensor then self.attack_sensor:move_to(self.x, self.y) end + if self.wide_attack_sensor then self.wide_attack_sensor:move_to(self.x, self.y) end if self.gun_kata_sensor then self.gun_kata_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) @@ -1371,6 +1448,14 @@ function Player:shoot(r, mods) end end + if self.character == 'thief' then + dmg_m = dmg_m*2 + if self.level == 3 and crit then + dmg_m = dmg_m*10 + main.current.gold_picked_up = main.current.gold_picked_up + 1 + end + end + if crit and mods.spawn_critters_on_crit then critter1:play{pitch = random:float(0.95, 1.05), volume = 0.5} trigger:after(0.01, function() @@ -1450,7 +1535,8 @@ function Player:shoot(r, mods) archer1:play{pitch = random:float(0.95, 1.05), volume = 0.35} elseif self.character == 'wizard' or self.character == 'lich' or self.character == 'arcanist' 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 + 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' or + self.character == 'thief' then _G[random:table{'scout1', 'scout2'}]:play{pitch = random:float(0.95, 1.05), volume = 0.35} if self.character == 'spellblade' then wizard1:play{pitch = random:float(0.95, 1.05), volume = 0.15} @@ -1609,6 +1695,12 @@ function Projectile:init(args) elseif self.character == 'witch' and self.level == 3 then self.chain = 1 + + elseif self.character == 'treasure_hunter' then + self.homing = true + if self.level == 3 then + self.pierce = 2 + end end if self.parent.divine_machine_arrow and table.any(self.parent.classes, function(v) return v == 'ranger' end) then @@ -1764,7 +1856,8 @@ function Projectile:on_collision_enter(other, contact) self.ricochet = self.ricochet - 1 end _G[random:table{'arrow_hit_wall1', 'arrow_hit_wall2'}]:play{pitch = random:float(0.9, 1.1), volume = 0.2} - elseif self.character == 'scout' or self.character == 'outlaw' or self.character == 'blade' or self.character == 'spellblade' or self.character == 'jester' or self.character == 'beastmaster' or self.character == 'witch' then + elseif self.character == 'scout' or self.character == 'outlaw' or self.character == 'blade' or self.character == 'spellblade' or self.character == 'jester' or self.character == 'beastmaster' or self.character == 'witch' or + self.character == 'thief' 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) @@ -1789,7 +1882,7 @@ function Projectile:on_collision_enter(other, contact) 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' or self.character == 'dual_gunner' then + elseif self.character == 'engineer' or self.character == 'dual_gunner' or self.character == 'treasure_hunter' 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 @@ -1838,7 +1931,7 @@ function Projectile:on_trigger_enter(other, contact) end 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' or - self.character == 'jester' or self.character == 'assassin' or self.character == 'barrager' or self.character == 'beastmaster' or self.character == 'witch' then + self.character == 'jester' or self.character == 'assassin' or self.character == 'barrager' or self.character == 'beastmaster' or self.character == 'witch' or self.character == 'treasure_hunter' or self.character == 'thief' 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} @@ -2427,6 +2520,7 @@ Volcano:implement(GameObject) Volcano:implement(Physics) function Volcano:init(args) self:init_game_object(args) + if not self.group.world then self.dead = true; return end self:set_as_rectangle(9, 9, 'static', 'player') self:set_restitution(0.5) self.hfx:add('hit', 1) @@ -2777,6 +2871,81 @@ function Illusion:draw() end +Gold = Object:extend() +Gold:implement(GameObject) +Gold:implement(Physics) +function Gold:init(args) + self:init_game_object(args) + self:set_as_rectangle(3, 3, 'dynamic', 'ghost') + self:set_restitution(0.5) + local r = random:float(0, 2*math.pi) + local f = random:float(2, 4) + self:apply_impulse(f*math.cos(r), f*math.sin(r)) + self:apply_angular_impulse(random:table{random:float(-6*math.pi, -2*math.pi), random:float(2*math.pi, 6*math.pi)}) + self:set_damping(2.5) + self:set_angular_damping(5) + self.color = yellow2[0] + self.hfx:add('hit', 1) + gold1:play{pitch = random:float(0.95, 1.05), volume = 0.5} +end + + +function Gold:update(dt) + self:update_game_object(dt) + self.r = self:get_angle() +end + + +function Gold: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, 1, 1, self.hfx.hit.f and fg[0] or self.color) + graphics.pop() +end + +function Gold:on_trigger_enter(other, contact) + if other:is(Player) then + main.current.gold_picked_up = main.current.gold_picked_up + 1 + self.dead = true + HitCircle{group = main.current.effects, x = self.x, y = self.y, rs = 4, color = fg[0], duration = 0.1} + for i = 1, 2 do HitParticle{group = main.current.effects, x = self.x, y = self.y, color = self.color} end + _G[random:table{'gold2', 'coins1', 'coins2', 'coins3'}]:play{pitch = random:float(0.9, 1.1), volume = 0.3} + + local units = other:get_all_units() + local th + for _, unit in ipairs(units) do + if unit.character == 'treasure_hunter' then + th = unit + end + end + if th then + if th.level == 3 then + trigger:after(0.01, function() + _G[random:table{'scout1', 'scout2'}]:play{pitch = random:float(0.95, 1.05), volume = 0.35} + HitCircle{group = main.current.effects, x = self.x, y = self.y, rs = 6} + local r = random:float(0, 2*math.pi) + for i = 1, 8 do + 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 = yellow2[0], dmg = th.dmg, character = th.character, parent = th, level = th.level} + Projectile(table.merge(t, mods or {})) + r = r + math.pi/4 + end + end) + else + trigger:after(0.01, function() + _G[random:table{'scout1', 'scout2'}]:play{pitch = random:float(0.95, 1.05), volume = 0.35} + HitCircle{group = main.current.effects, x = self.x, y = self.y, rs = 6} + local r = random:float(0, 2*math.pi) + for i = 1, 4 do + 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 = yellow2[0], dmg = th.dmg, character = th.character, parent = th, level = th.level} + Projectile(table.merge(t, mods or {})) + r = r + 2*math.pi/4 + end + end) + end + end + end +end + + Critter = Object:extend() diff --git a/shared.lua b/shared.lua index b1ae221..fbb949d 100644 --- a/shared.lua +++ b/shared.lua @@ -13,6 +13,7 @@ function shared_init() red = ColorRamp(Color'#e91d39', 0.025), purple = ColorRamp(Color'#8e559e', 0.025), blue2 = ColorRamp(Color'#4778ba', 0.025), + yellow2 = ColorRamp(Color'#f59f10', 0.025), } for name, color in pairs(colors) do _G[name] = color @@ -494,6 +495,7 @@ global_text_tags = { red = TextTag{draw = function(c, i, text) graphics.set_color(red[0]) end}, orange = TextTag{draw = function(c, i, text) graphics.set_color(orange[0]) end}, yellow = TextTag{draw = function(c, i, text) graphics.set_color(yellow[0]) end}, + yellow2 = TextTag{draw = function(c, i, text) graphics.set_color(yellow2[0]) end}, green = TextTag{draw = function(c, i, text) graphics.set_color(green[0]) end}, purple = TextTag{draw = function(c, i, text) graphics.set_color(purple[0]) end}, blue = TextTag{draw = function(c, i, text) graphics.set_color(blue[0]) end}, @@ -501,6 +503,7 @@ global_text_tags = { bg = TextTag{draw = function(c, i, text) graphics.set_color(bg[0]) end}, bg3 = TextTag{draw = function(c, i, text) graphics.set_color(bg[3]) end}, bg10 = TextTag{draw = function(c, i, text) graphics.set_color(bg[10]) end}, + bgm2 = TextTag{draw = function(c, i, text) graphics.set_color(bg[-2]) end}, light_bg = TextTag{draw = function(c, i, text) graphics.set_color(bg[5]) end}, fg = TextTag{draw = function(c, i, text) graphics.set_color(fg[0]) end}, fgm5 = TextTag{draw = function(c, i, text) graphics.set_color(fg[-5]) end}, @@ -511,6 +514,8 @@ global_text_tags = { bluem5 = TextTag{draw = function(c, i, text) graphics.set_color(blue[-5]) end}, blue25 = TextTag{draw = function(c, i, text) graphics.set_color(blue2[5]) end}, blue2m5 = TextTag{draw = function(c, i, text) graphics.set_color(blue2[-5]) end}, + yellow25 = TextTag{draw = function(c, i, text) graphics.set_color(yellow2[5]) end}, + yellow2m5 = TextTag{draw = function(c, i, text) graphics.set_color(yellow2[-5]) end}, redm5 = TextTag{draw = function(c, i, text) graphics.set_color(red[-5]) end}, orangem5 = TextTag{draw = function(c, i, text) graphics.set_color(orange[-5]) end}, purplem5 = TextTag{draw = function(c, i, text) graphics.set_color(purple[-5]) end}, diff --git a/todo b/todo index c715cf0..3c0c200 100644 --- a/todo +++ b/todo @@ -1,24 +1,24 @@ Shop Update New units - Assists (2/4) - +25/50% assist buff effectiveness - Ringmaster (tier 4 assist, nuker) - Absorber (tier 2 assist, warrior) - Pardoner (tier 3 assist, mercenary) - Oracle (tier 1 assist) - Seraph (tier 2 assist, healer) - Mercenaries (3/3) - enemies occasionally drop pieces of gold - Treasure Hunter (tier 1 mercenary) - Merchant (tier 2 mercenary) - Pardoner (tier 3 assist, mercenary) - Gambler (tier 4 mercenary, rogue) + * Mercenaries (2/4) - +10/20% chance for enemies to drop gold on death + * Treasure Hunter (tier 1 mercenary) - picking up gold releases 4 homing projectiles that deal X damage, Lv.3 - release 8 homing projectiles instead and they pierce twice + * Merchant (tier 2 mercenary) - gain +1 interest for every 10 gold, Lv.3 - your first item reroll is always free + * Usurer (tier 3 curser, mercenary, voider) - curses 3 nearby enemies indefinitely with debt, dealing X damage over time, Lv.3 - if the same enemy is cursed 3 times it takes 50X damage + * Gambler (tier 3 mercenary, sorcerer) - deal 2X damage to a single random enemy where X is how much gold you have, Lv.3 effect - 60%/40%/20% chance to cast the attack 2/3/4 times + * Thief (tier 4 mercenary, rogue) - throws a knife that deals 2X damage and chains 5 times, Lv.3 - if the knife crits it deals 10X damage, chains 10 times and grants 1 gold Shop changes Owned units highlighted in shop Fix highlight colors and highlight reserve Shop level up - Remove level 3 units from rotation Item changes + Sorcerer items + Mercenary items + + Unit changes + Launcher - remove + Beastmaster QoL * Added sorcerer achievement @@ -28,7 +28,7 @@ Shop Update * Added mouse follow control mode * The cursor is now invisible during waves, unless mouse follow mode is enabled Options menu from buy screen - Add visuals for defensive ouroboros, divine intervention, fairy buff + Add visuals for tank attack Balance * Decreased level 25 boss movement speed @@ -38,34 +38,43 @@ Shop Update * Fixed a bug where enemy critters would sometimes be unkillable * Fixed a rare crash involving broken enemy critter state * Fixed text for the illusionist's Lv.3 effect going outside the screen - * Fixed a rare crash when hovering over your owned in the shop + * Fixed a crash when hovering over the Lich in the shop due to playing with a save before the sorcerer update * Fixed a crash when warden's force field would spawn on top of enemies + * Fixed a crash when a volcano would spawn on top of enemies * Fixed a crash when a pet would spawn on top of enemies * Fixed a bug where the maximum number of units would be wrong on certain conditions + * Fixed a crash when clicking too fast after unpausing the game Fix bug where quitting on level 2 arena goes back to level 1 shop Fix fullscreen with different resolutions that don't scale properly Fix enemies still spawning after arena clear (this happens with the extra enemy spawns that were blocked earlier) + https://i.imgur.com/ieVqYNI.png + https://i.imgur.com/3JCeFuZ.png + https://i.imgur.com/cvC1TBz.png Sacrifice Update New mechanics Sacrifice units to level items up New items + https://steamcommunity.com/app/915310/discussions/0/3106901028662504698/ Reworked items Items shouldn't just be more powerful versions of other items Items should have drawbacks Items that apply to a position on the snake (a good middle step between applying them to individual units like in Underlords) - New units - Nocturnals/Darks/??? (3/6) - Shadowmancer - Necromancer - Demonologist - Demon - Lich - Lifestealer - Zombie QoL Current items visible on item selection screen Endless mode + Volume slider + Add visuals for defensive ouroboros, divine intervention, fairy buff + +Melee Update + New Units + Guardians - https://i.imgur.com/Ynu5Cdw.png + Assists (2/4) - + Ringmaster (tier 4 assist, nuker) - +15% to all stats to adjacent units, Lv.3 effect - create a cross that deals AoE damage 5 times for 10 seconds + Absorber (tier 2 assist, warrior) - absorbs 50% damage from adjacent units, Lv.3 effect - absorbs 75% damage from adjacent units and gives the absorber +25% defense + Pardoner (tier 3 assist, mercenary) - + Oracle (tier 1 assist) - +10% dodge chance to adjacent units, Lv.3 effect - +20% dodge chance to adjacent units + Seraph (tier 2 assist, healer) - periodically chooses 1 random unit and gives it +100% defense for 6 seconds, Lv.3 - choose 2 units instead --- @@ -83,25 +92,36 @@ Hexblaster? - curser that consumes curses to deal damage 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 +https://i.imgur.com/bxfvA7g.png + Roguelite update: Technical improvements: Spawn tech: spawn every entity in a grid, before spawning check to see if grid position is clear, this will prevent any issues due to entities spawning inside one another Battle stats: DPS, damage taken, etc Key rebinding (for non-QWERTY keyboards) -StS-like map with nodes, node types: - Arena - Elite - Boss - Map (map of bigger size than arena with fixed spawns) - Unit shop - Item shop - Text + reward - Training grounds (upgrade unit) - Tavern (heal units) + StS-like map with nodes, node types: + Arena + Elite + Boss + Map (map of bigger size than arena with fixed spawns) + Unit shop + Item shop + Text + reward + Training grounds (upgrade unit) + Tavern (heal units) + Units die permanently when they die (dead units can be stored in bench to be revived later) + Units can have items attached to them like in Underlords + Unit item ideas: + This unit's projectiles pierce/chain/fork/seek/split/stun/etc + This unit is a [class] + New stat system: + All stats are values from 1 to 10 (can be lower than 1 or higher than due to debuffs/buffs only) that represent consistent internal values between all units + i.e. 3 attack speed means the same internal attack rate value (like say 6 seconds) for the entire game + In general it's better if units don't have hidden internal multipliers on these stats, although sometimes that may be inevitable + Damage: + Crash - damage dealt when bumping into enemies + Projectile - damage dealt by projectiles + AoE - damage dealt in an area + DoT - damage dealt over time -Units die permanently when they die (dead units can be stored in bench to be revived later) -Units can have items attached to them like in Underlords -Unit item ideas: - This unit's projectiles pierce/chain/fork/seek/split/stun/etc