diff --git a/arena.lua b/arena.lua index c09d0cb..7551f6a 100644 --- a/arena.lua +++ b/arena.lua @@ -104,7 +104,7 @@ function Arena:on_enter(from, level, loop, units, passives, shop_level, shop_xp, if self.level == 1000 then self.level_1000_text = Text2{group = self.ui, x = gw/2, y = gh/2, lines = {{text = '[fg, wavy_mid]SNKRX', font = fat_font, alignment = 'center'}}} - + elseif (self.level - (25*self.loop)) % 6 == 0 or self.level % 25 == 0 then self.boss_level = true self.start_time = 3 @@ -353,7 +353,7 @@ function Arena:update(dt) end if not self.paused and not self.died and not self.won then - run_time = run_time + dt + run_time = run_time + dt/4 end if self.shop_text then self.shop_text:update(dt) end @@ -386,9 +386,10 @@ function Arena:update(dt) 'assassination', 'flying_daggers', 'ultimatum', 'magnify', 'echo_barrage', 'unleash', 'reinforce', 'payback', 'enchanted', 'freezing_field', 'burning_field', 'gravity_field', 'magnetism', 'insurance', 'dividends', 'berserking', 'unwavering_stance', 'unrelenting_stance', 'blessing', 'haste', 'divine_barrage', 'orbitism', 'psyker_orbs', 'psychosink', 'rearm', 'taunt', 'construct_instability', 'intimidation', 'vulnerability', 'temporal_chains', 'ceremonial_dagger', 'homing_barrage', 'critical_strike', 'noxious_strike', 'infesting_strike', 'burning_strike', 'lucky_strike', 'healing_strike', 'stunning_strike', - 'silencing_strike', 'culling_strike', 'lightning_strike', 'psycholeak', 'divine_blessing', 'hardening', 'kinetic_strike', + 'silencing_strike', 'culling_strike', 'lightning_strike', 'psycholeak', 'divine_blessing', 'hardening', 'kinetic_strike', 'mine_detector', } - max_units = math.clamp(7 + current_new_game_plus + self.loop, 7, 12) + -- max_units = math.clamp(7 + current_new_game_plus + self.loop, 7, 12) + max_units = math.clamp(7 + current_new_game_plus, 7, 12) main:add(BuyScreen'buy_screen') locked_state = nil system.save_run() @@ -436,7 +437,8 @@ function Arena:quit() end current_new_game_plus = current_new_game_plus + 1 state.current_new_game_plus = current_new_game_plus - max_units = math.clamp(7 + current_new_game_plus + self.loop, 7, 12) + -- max_units = math.clamp(7 + current_new_game_plus + self.loop, 7, 12) + max_units = math.clamp(7 + current_new_game_plus, 7, 12) system.save_run() trigger:tween(1, _G, {slow_amount = 0}, math.linear, function() slow_amount = 0 end, 'slow_amount') @@ -482,12 +484,35 @@ function Arena:quit() SteamFollowButton{group = self.ui, x = gw/2 + 40, y = gh/2 + 58, force_update = true} Button{group = self.ui, x = gw - 40, y = gh - 44, force_update = true, button_text = 'credits', fg_color = 'bg10', bg_color = 'bg', action = function() self:create_credits() end} Button{group = self.ui, x = gw - 39, y = gh - 20, force_update = true, button_text = ' loop ', fg_color = 'bg10', bg_color = 'bg', action = function() self:endless() end} - self.try_loop_text = Text2{group = self.ui, x = gw - 144, y = gh - 20, force_update = true, lines = { - {text = '[bg10]continue run (+difficulty):', font = pixul_font}, + self.try_loop_text = Text2{group = self.ui, x = gw - (self.loop < 5 and 191 or 143), y = gh - 20, force_update = true, lines = { + {text = '[bg10]continue run (+difficulty' .. (self.loop < 5 and ', +1 max passive):' or '):'), font = pixul_font}, }} Button{group = self.ui, x = gw/2 - 50 + 40, y = gh/2 + 12, force_update = true, button_text = 'nimble quest', fg_color = 'bluem5', bg_color = 'blue', action = function(b) open_url(b, 'https://store.steampowered.com/app/259780/Nimble_Quest/') end} Button{group = self.ui, x = gw/2 + 50 + 40, y = gh/2 + 12, force_update = true, button_text = 'dota underlords', fg_color = 'bluem5', bg_color = 'blue', action = function(b) open_url(b, 'https://store.steampowered.com/app/1046930/Dota_Underlords/') end} + if self.loop > 3 and not state.achievement_its_full_of_stars then + UnlocksText{group = self.ui, character = 'astralist'} + state.achievement_its_full_of_stars = true + system.save_state() + elseif self.loop > 2 and not state.achievement_im_snkman then + UnlocksText{group = self.ui, character = 'avenger'} + state.achievement_im_snkman = true + system.save_state() + elseif self.loop > 1 and not state.achievement_lord_of_war then + UnlocksText{group = self.ui, character = 'warmonger'} + state.achievement_lord_of_war = true + system.save_state() + elseif self.loop > 0 and not state.achievement_attack_the_core then + UnlocksText{group = self.ui, character = 'technomancer'} + state.achievement_attack_the_core = true + system.save_state() + elseif not state.achievement_new_game_5 then + UnlocksText{group = self.ui, character = 'physician'} + state.achievement_new_game_5 = true + system.save_state() + steam.userStats.setAchievement('GAME_COMPLETE') + steam.userStats.storeStats() + end else self.win_text2 = Text2{group = self.ui, x = gw/2 + 40, y = gh/2 + 5, force_update = true, lines = { {text = "[fg]you've beaten the game!", font = pixul_font, alignment = 'center', height_multiplier = 1.24}, @@ -509,8 +534,8 @@ function Arena:quit() Button{group = self.ui, x = gw/2 + 40, y = gh/2 + 33, force_update = true, button_text = 'buy the soundtrack!', fg_color = 'greenm5', bg_color = 'green', action = function(b) open_url(b, 'https://kubbimusic.com/album/ember') end} Button{group = self.ui, x = gw - 40, y = gh - 44, force_update = true, button_text = ' loop ', fg_color = 'bg10', bg_color = 'bg', action = function() self:endless() end} RestartButton{group = self.ui, x = gw - 40, y = gh - 20, force_update = true} - self.try_loop_text = Text2{group = self.ui, x = gw - 200, y = gh - 44, force_update = true, lines = { - {text = '[bg10]continue run (+difficulty, +1 max snake size):', font = pixul_font}, + self.try_loop_text = Text2{group = self.ui, x = gw - (self.loop < 5 and 191 or 143), y = gh - 44, force_update = true, lines = { + {text = '[bg10]continue run (+difficulty' .. (self.loop < 5 and ', +1 max passive):' or '):'), font = pixul_font}, }} self.try_ng_text = Text2{group = self.ui, x = gw - 187, y = gh - 20, force_update = true, lines = { {text = '[bg10]new run (+difficulty, +1 max snake size):', font = pixul_font}, @@ -525,14 +550,16 @@ function Arena:quit() steam.userStats.setAchievement('NEW_GAME_1') steam.userStats.storeStats() end - + -- Can never reach this condition since current_new_game_plus is reset to 5 above. + -- Functionality moved to that section. + --[[ if current_new_game_plus == 6 then state.achievement_new_game_5 = true system.save_state() steam.userStats.setAchievement('GAME_COMPLETE') steam.userStats.storeStats() end - + ]]-- if self.ranger_level >= 2 then state.achievement_rangers_win = true system.save_state() @@ -676,14 +703,14 @@ function Arena:quit() self.t:tween(0.7, self, {main_slow_amount = 0}, math.linear, function() self.main_slow_amount = 0 end) end) self.t:after(3, function() - if (self.level-(25*self.loop)) % 3 == 0 and #self.passives < 8 then + if (self.level-(25*self.loop)) % 3 == 0 and #self.passives < 8 + math.min(self.loop, 4) 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(1, _G, {music_slow_amount = 0}, math.linear, function() music_slow_amount = 0 end, 'music_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) self:set_passives() - RerollButton{group = main.current.ui, x = gw - 40, y = gh - 40, parent = self, force_update = true} + RerollButton{group = main.current.ui, x = gw - 40, y = gh - 40, reroll_cost = 5, parent = self, force_update = true} self.shop_text = Text({{text = '[wavy_mid, fg]gold: [yellow]' .. gold, font = pixul_font, alignment = 'center'}}, global_text_tags) self.build_text = Text2{group = self.ui, x = 40, y = 20, force_update = true, lines = {{text = "[wavy_mid, fg]your build", font = pixul_font, alignment = 'center'}}} @@ -859,7 +886,7 @@ function Arena:die() 'assassination', 'flying_daggers', 'ultimatum', 'magnify', 'echo_barrage', 'unleash', 'reinforce', 'payback', 'enchanted', 'freezing_field', 'burning_field', 'gravity_field', 'magnetism', 'insurance', 'dividends', 'berserking', 'unwavering_stance', 'unrelenting_stance', 'blessing', 'haste', 'divine_barrage', 'orbitism', 'psyker_orbs', 'psychosink', 'rearm', 'taunt', 'construct_instability', 'intimidation', 'vulnerability', 'temporal_chains', 'ceremonial_dagger', 'homing_barrage', 'critical_strike', 'noxious_strike', 'infesting_strike', 'burning_strike', 'lucky_strike', 'healing_strike', 'stunning_strike', - 'silencing_strike', 'culling_strike', 'lightning_strike', 'psycholeak', 'divine_blessing', 'hardening', 'kinetic_strike', + 'silencing_strike', 'culling_strike', 'lightning_strike', 'psycholeak', 'divine_blessing', 'hardening', 'kinetic_strike', 'mine_detector', } max_units = math.clamp(7 + current_new_game_plus, 7, 12) main:add(BuyScreen'buy_screen') @@ -936,23 +963,23 @@ function Arena:create_credits() Button{group = self.credits, x = 262, y = 160, button_text = 'InspectorJ', fg_color = 'yellowm5', bg_color = 'yellow', credits_button = true, action = function(b) open_url(b, 'https://freesound.org/people/InspectorJ/sounds/458586/') end} Text2{group = self.credits, x = 70, y = 190, lines = {{text = '[red]playtesters: ', font = pixul_font}}} - Button{group = self.credits, x = 130, y = 190, button_text = 'Jofer', fg_color = 'redm5', bg_color = 'red', credits_button = true, action = function(b) + Button{group = self.credits, x = 130, y = 190, button_text = 'Jofer', fg_color = 'redm5', bg_color = 'red', credits_button = true, action = function(b) open_url(b, 'https://twitter.com/JofersGames') end} - Button{group = self.credits, x = 172, y = 190, button_text = 'ekun', fg_color = 'redm5', bg_color = 'red', credits_button = true, action = function(b) + Button{group = self.credits, x = 172, y = 190, button_text = 'ekun', fg_color = 'redm5', bg_color = 'red', credits_button = true, action = function(b) open_url(b, 'https://twitter.com/ekunenuke') end} - Button{group = self.credits, x = 224, y = 190, button_text = 'cvisy_GN', fg_color = 'redm5', bg_color = 'red', credits_button = true, action = function(b) + Button{group = self.credits, x = 224, y = 190, button_text = 'cvisy_GN', fg_color = 'redm5', bg_color = 'red', credits_button = true, action = function(b) open_url(b, 'https://twitter.com/cvisy_GN') end} - Button{group = self.credits, x = 292, y = 190, button_text = 'Blue Fairy', fg_color = 'redm5', bg_color = 'red', credits_button = true, action = function(b) + Button{group = self.credits, x = 292, y = 190, button_text = 'Blue Fairy', fg_color = 'redm5', bg_color = 'red', credits_button = true, action = function(b) open_url(b, 'https://twitter.com/blue9fairy') end} - Button{group = self.credits, x = 362, y = 190, button_text = 'Phil Blank', fg_color = 'redm5', bg_color = 'red', credits_button = true, action = function(b) + Button{group = self.credits, x = 362, y = 190, button_text = 'Phil Blank', fg_color = 'redm5', bg_color = 'red', credits_button = true, action = function(b) open_url(b, 'https://twitter.com/PhilBlankGames') end} - Button{group = self.credits, x = 440, y = 190, button_text = 'DefineDoddy', fg_color = 'redm5', bg_color = 'red', credits_button = true, action = function(b) + Button{group = self.credits, x = 440, y = 190, button_text = 'DefineDoddy', fg_color = 'redm5', bg_color = 'red', credits_button = true, action = function(b) open_url(b, 'https://twitter.com/DefineDoddy') end} - Button{group = self.credits, x = 140, y = 210, button_text = 'Ge0force', fg_color = 'redm5', bg_color = 'red', credits_button = true, action = function(b) + Button{group = self.credits, x = 140, y = 210, button_text = 'Ge0force', fg_color = 'redm5', bg_color = 'red', credits_button = true, action = function(b) open_url(b, 'https://twitter.com/Ge0forceBE') end} - Button{group = self.credits, x = 193, y = 210, button_text = 'Vlad', fg_color = 'redm5', bg_color = 'red', credits_button = true, action = function(b) + Button{group = self.credits, x = 193, y = 210, button_text = 'Vlad', fg_color = 'redm5', bg_color = 'red', credits_button = true, action = function(b) open_url(b, 'https://twitter.com/thecryru') end} - Button{group = self.credits, x = 258, y = 210, button_text = 'Yongmin Park', fg_color = 'redm5', bg_color = 'red', credits_button = true, action = function(b) + Button{group = self.credits, x = 258, y = 210, button_text = 'Yongmin Park', fg_color = 'redm5', bg_color = 'red', credits_button = true, action = function(b) open_url(b, 'https://twitter.com/yongminparks') end} end @@ -973,7 +1000,7 @@ end function Arena:transition() self.transitioning = true - if not self.lock then locked_state = nil end + -- if not self.lock then locked_state = nil end ui_transition2:play{pitch = random:float(0.95, 1.05), volume = 0.5} TransitionEffect{group = main.transitions, x = self.player.x, y = self.player.y, color = state.dark_transitions and bg[-2] or self.color, transition_action = function(t) if self.level % 2 == 0 and self.shop_level < 5 then @@ -997,7 +1024,7 @@ function Arena:transition() main:go_to('buy_screen', self.level+1, self.loop, self.units, self.passives, self.shop_level, self.shop_xp) t.t:after(0.1, function() t.text:set_text({ - {text = '[nudge_down, ' .. tostring(state.dark_transitions and 'fg' or 'bg') .. ']gold gained: ' .. tostring(self.gold_gained or 0) .. ' + ' .. tostring(self.gold_picked_up or 0), font = pixul_font, + {text = '[nudge_down, ' .. tostring(state.dark_transitions and 'fg' or '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, ' .. tostring(state.dark_transitions and 'fg' or 'bg') .. ']interest: 0', font = pixul_font, alignment = 'center', height_multiplier = 1.5}, {text = '[wavy_lower, ' .. tostring(state.dark_transitions and 'fg' or 'bg') .. ']total: 0', font = pixul_font, alignment = 'center'} @@ -1195,8 +1222,7 @@ function CharacterHP:draw() end graphics.pop() - if state.cooldown_snake then - if table.any(non_cooldown_characters, function(v) return v == self.parent.character end) then return end + if state.cooldown_snake and self.parent.has_cooldown then local p = self.parent graphics.push(p.x, p.y, 0, self.hfx.hit.x, self.hfx.hit.y) if not p.dead then @@ -1211,3 +1237,44 @@ end function CharacterHP:change_hp() self.hfx:use('hit', 0.5) end + + + + +UnlocksText = Object:extend() +UnlocksText:implement(GameObject) +function UnlocksText:init(args) + self:init_game_object(args) + self.x, self.y = gw/2, gh/2 + self.w, self.h = 50, 64 + self.sx, self.sy = 0, 0 + self.character = self.character or 'physician' + self.text = Text({ + {text = "[" .. character_color_strings[self.character] .. "]unlocked the " .. character_names[self.character], font = pixul_font, alignment = 'center'} + }, global_text_tags) + self.force_update = true + + -- auto popup + self.t:after(0.5, function() + self.t:tween(0.1, self, {sx = 1, sy = 1}, math.cubic_in_out, function() + self.t:after(3, function() + self.t:tween(0.05, self, {sx = 0, sy = 0}, math.cubic_in_out, function() + self.dead = true end) + end) + end) + self.spring:pull(0.5) end) + return self +end + + +function UnlocksText:update(dt) + self:update_game_object(dt) +end + + +function UnlocksText:draw() + graphics.push(self.x, self.y, 0, self.sx*self.spring.x, self.sy*self.spring.x) + graphics.rectangle(self.x, self.y, self.w + self.text.w, self.h, 5, 5, bg[-1]) + self.text:draw(self.x, self.y) + graphics.pop() +end diff --git a/assets/images/mine_detector.png b/assets/images/mine_detector.png new file mode 100644 index 0000000..edb133b Binary files /dev/null and b/assets/images/mine_detector.png differ diff --git a/buy_screen.lua b/buy_screen.lua index dc6dfa6..3f31521 100644 --- a/buy_screen.lua +++ b/buy_screen.lua @@ -45,8 +45,10 @@ function BuyScreen:on_enter(from, level, loop, units, passives, shop_level, shop self.passives = passives self.shop_level = shop_level self.shop_xp = shop_xp + self.num_cards = 3 camera.x, camera.y = gw/2, gh/2 - max_units = math.clamp(7 + current_new_game_plus + self.loop, 7, 12) + -- max_units = math.clamp(7 + current_new_game_plus + self.loop, 7, 12) + max_units = math.clamp(7 + current_new_game_plus, 7, 12) input:set_mouse_visible(true) @@ -58,10 +60,10 @@ function BuyScreen:on_enter(from, level, loop, units, passives, shop_level, shop self.ui = Group() self.tutorial = Group() - self.locked = locked_state and locked_state.locked - LockButton{group = self.main, x = 205, y = 18, parent = self} + -- self.locked = locked_state and locked_state.locked + -- LockButton{group = self.main, x = 205, y = 18, parent = self} - self:set_cards(self.shop_level, nil, true) + self:set_cards(self.shop_level, nil, from.name == 'main_menu') self:set_party_and_sets() self:set_items() @@ -77,7 +79,7 @@ function BuyScreen:on_enter(from, level, loop, units, passives, shop_level, shop end self.level_text = Text({{text = '[fg]Lv.' .. tostring(self.level) .. get_elite_str(self.level), font = pixul_font, alignment = 'center'}}, global_text_tags) - RerollButton{group = self.main, x = 150, y = 18, parent = self} + self.reroll_button = RerollButton{group = self.main, x = 150, y = 18, parent = self, reroll_cost = 2} GoButton{group = self.main, x = gw - 90, y = gh - 20, parent = self} LevelButton{group = self.main, x = gw/2, y = 18, parent = self} self.tutorial_button = Button{group = self.main, x = gw/2 + 129, y = 18, button_text = '?', fg_color = 'bg10', bg_color = 'bg', action = function() @@ -133,7 +135,7 @@ function BuyScreen:on_enter(from, level, loop, units, passives, shop_level, shop ui_transition2:play{pitch = random:float(0.95, 1.05), volume = 0.5} ui_switch2:play{pitch = random:float(0.95, 1.05), volume = 0.5} ui_switch1:play{pitch = random:float(0.95, 1.05), volume = 0.5} - locked_state = nil + locked_state = {['locks'] = {false, false, false}, ['cards'] = {}} TransitionEffect{group = main.transitions, x = gw/2, y = gh/2, color = state.dark_transitions and bg[-2] or fg[0], transition_action = function() slow_amount = 1 music_slow_amount = 1 @@ -148,7 +150,7 @@ function BuyScreen:on_enter(from, level, loop, units, passives, shop_level, shop 'assassination', 'flying_daggers', 'ultimatum', 'magnify', 'echo_barrage', 'unleash', 'reinforce', 'payback', 'enchanted', 'freezing_field', 'burning_field', 'gravity_field', 'magnetism', 'insurance', 'dividends', 'berserking', 'unwavering_stance', 'unrelenting_stance', 'blessing', 'haste', 'divine_barrage', 'orbitism', 'psyker_orbs', 'psychosink', 'rearm', 'taunt', 'construct_instability', 'intimidation', 'vulnerability', 'temporal_chains', 'ceremonial_dagger', 'homing_barrage', 'critical_strike', 'noxious_strike', 'infesting_strike', 'burning_strike', 'lucky_strike', 'healing_strike', 'stunning_strike', - 'silencing_strike', 'culling_strike', 'lightning_strike', 'psycholeak', 'divine_blessing', 'hardening', 'kinetic_strike', + 'silencing_strike', 'culling_strike', 'lightning_strike', 'psycholeak', 'divine_blessing', 'hardening', 'kinetic_strike', 'mine_detector', } max_units = math.clamp(7 + current_new_game_plus, 7, 12) main:add(BuyScreen'buy_screen') @@ -170,7 +172,7 @@ function BuyScreen:on_enter(from, level, loop, units, passives, shop_level, shop trigger:tween(1, main_song_instance, {volume = 0.2, pitch = 1}, math.linear) - locked_state = {locked = self.locked, cards = {self.cards[1] and self.cards[1].unit, self.cards[2] and self.cards[2].unit, self.cards[3] and self.cards[3].unit}} + -- locked_state = {locked = self.locked, cards = {self.cards[1] and self.cards[1].unit, self.cards[2] and self.cards[2].unit, self.cards[3] and self.cards[3].unit}} system.save_run(self.level, self.loop, gold, self.units, self.passives, self.shop_level, self.shop_xp, run_passive_pool, locked_state) end @@ -300,6 +302,7 @@ function BuyScreen:buy(character, i) unit.reserve[1] = 0 unit.level = 3 unit.spawn_effect = true + if character == 'physician' then self.reroll_button:adjust_reroll_cost(-1) end else unit.reserve[2] = unit.reserve[2] + 1 unit.reserve[1] = 0 @@ -341,28 +344,79 @@ end function BuyScreen:set_cards(shop_level, dont_spawn_effect, first_call) - if self.cards then for i = 1, 3 do if self.cards[i] then self.cards[i]:die(dont_spawn_effect) end end end - self.cards = {} - local all_units = {} - local unit_1 - local unit_2 - local unit_3 local shop_level = shop_level or 1 local tier_weights = level_to_shop_odds[shop_level] - repeat - unit_1 = random:table(tier_to_characters[random:weighted_pick(unpack(tier_weights))]) - unit_2 = random:table(tier_to_characters[random:weighted_pick(unpack(tier_weights))]) - unit_3 = random:table(tier_to_characters[random:weighted_pick(unpack(tier_weights))]) - all_units = {unit_1, unit_2, unit_3} - until not table.all(all_units, function(v) return table.any(non_attacking_characters, function(u) return v == u end) end) - if first_call and locked_state then - if locked_state.cards[1] then self.cards[1] = ShopCard{group = self.main, x = 60, y = 75, w = 80, h = 90, unit = locked_state.cards[1], parent = self, i = 1} end - if locked_state.cards[2] then self.cards[2] = ShopCard{group = self.main, x = 140, y = 75, w = 80, h = 90, unit = locked_state.cards[2], parent = self, i = 2} end - if locked_state.cards[3] then self.cards[3] = ShopCard{group = self.main, x = 220, y = 75, w = 80, h = 90, unit = locked_state.cards[3], parent = self, i = 3} end + local tier_to_unlocked = table.copy(tier_to_characters) + + -- Reroll only if the run data hasn't been newly loaded, i.e. coming from main menu. + local reroll = first_call == nil or first_call == false + + -- Add unlocked characters to the list of units that can be bought. + if state.achievement_its_full_of_stars then table.insert(tier_to_unlocked[character_tiers['astralist']], 'astralist') end + if state.achievement_im_snkman then table.insert(tier_to_unlocked[character_tiers['avenger']], 'avenger') end + if state.achievement_lord_of_war then table.insert(tier_to_unlocked[character_tiers['warmonger']], 'warmonger') end + if state.achievement_attack_the_core then table.insert(tier_to_unlocked[character_tiers['technomancer']], 'technomancer'); print('core') end + if state.achievement_new_game_5 then table.insert(tier_to_unlocked[character_tiers['physician']], 'physician') end + + -- Remove already maxed characters from the list of units that can be bought. + for _, unit in ipairs(self.units) do + if unit.level > 2 then + table.delete(tier_to_unlocked[character_tiers[unit.character]], unit.character) + end + end + + -- Process the locked_state. + if (locked_state == nil) or (locked_state.locks == nil) or (locked_state.cards == nil) then + + -- It's the first run, a reset run, or corrupted data, so init a fresh locked_state. + locked_state = {['locks'] = {false, false, false}, ['cards'] = {}} + + -- Force a reroll for the newly create locked_state card data. + reroll = true + + elseif type(locked_state.locks) == 'boolean' then + + -- Disable reroll if the cards were locked in the old system. + reroll = reroll and not locked_state.locks + + -- Convert old 'locks' to the new multi lock system. + locked_state.locks = {locked_state.locks, locked_state.locks, locked_state.locks} + else - self.cards[1] = ShopCard{group = self.main, x = 60, y = 75, w = 80, h = 90, unit = unit_1, parent = self, i = 1} - self.cards[2] = ShopCard{group = self.main, x = 140, y = 75, w = 80, h = 90, unit = unit_2, parent = self, i = 2} - self.cards[3] = ShopCard{group = self.main, x = 220, y = 75, w = 80, h = 90, unit = unit_3, parent = self, i = 3} + + -- If the locked_state exists and has valid data, check if all cards are locked and disable reroll accordingly. + local all_locked = true + for i = 1, self.num_cards do + all_locked = all_locked and locked_state.locks[i] == true + end + reroll = reroll and not all_locked + end + + -- Reroll units for unlocked cards, making sure that not all the cards contain non-attacking units. + while reroll do + for i = 1, self.num_cards do + if locked_state.locks[i] ~= true then + locked_state.cards[i] = random:table(tier_to_unlocked[random:weighted_pick(unpack(tier_weights))]) + end + end + reroll = table.all(locked_state.cards, function(v) return table.any(non_attacking_characters, function(u) return v == u end) end) + end + + -- Update the shop cards with the rerolled unit data. + -- Note: there are 3 variables that affect the outcome: locked_state.cards, self.cards and locked_state.locks. + -- 3 combinations have locked_state.cards[i] == self.cards[i] and so require no update. + -- 2 combinations have a nil locked_state.cards[i] and non nil self.cards[i] which is not possible given the code flow. + -- The remaining 3 combinations are handled below and require a new self.cards[i] to be created in each case. + if type(self.cards) ~= 'table' then self.cards = {} end + for i = 1, self.num_cards do + if locked_state.cards[i] then + if self.cards[i] and not locked_state.locks[i] then + self.cards[i]:die(dont_spawn_effect) + self.cards[i] = ShopCard{group = self.main, x = i * 90 - 30, y = 75, w = 88, h = 90, unit = locked_state.cards[i], parent = self, i = i} + elseif self.cards[i] == nil then -- for both cases where locked_state.locks[i] is true or false. + self.cards[i] = ShopCard{group = self.main, x = i * 90 - 30, y = 75, w = 88, h = 90, unit = locked_state.cards[i], parent = self, i = i} + end + end end end @@ -534,6 +588,7 @@ function RestartButton:update(dt) ui_transition2:play{pitch = random:float(0.95, 1.05), volume = 0.5} ui_switch2:play{pitch = random:float(0.95, 1.05), volume = 0.5} ui_switch1:play{pitch = random:float(0.95, 1.05), volume = 0.5} + locked_state = nil TransitionEffect{group = main.transitions, x = gw/2, y = gh/2, color = state.dark_transitions and bg[-2] or fg[0], transition_action = function() slow_amount = 1 music_slow_amount = 1 @@ -548,7 +603,7 @@ function RestartButton:update(dt) 'assassination', 'flying_daggers', 'ultimatum', 'magnify', 'echo_barrage', 'unleash', 'reinforce', 'payback', 'enchanted', 'freezing_field', 'burning_field', 'gravity_field', 'magnetism', 'insurance', 'dividends', 'berserking', 'unwavering_stance', 'unrelenting_stance', 'blessing', 'haste', 'divine_barrage', 'orbitism', 'psyker_orbs', 'psychosink', 'rearm', 'taunt', 'construct_instability', 'intimidation', 'vulnerability', 'temporal_chains', 'ceremonial_dagger', 'homing_barrage', 'critical_strike', 'noxious_strike', 'infesting_strike', 'burning_strike', 'lucky_strike', 'healing_strike', 'stunning_strike', - 'silencing_strike', 'culling_strike', 'lightning_strike', 'psycholeak', 'divine_blessing', 'hardening', 'kinetic_strike', + 'silencing_strike', 'culling_strike', 'lightning_strike', 'psycholeak', 'divine_blessing', 'hardening', 'kinetic_strike', 'mine_detector', } max_units = math.clamp(7 + current_new_game_plus, 7, 12) system.save_state() @@ -700,7 +755,7 @@ function GoButton:update(dt) self.t:after(2, function() self.info_text:deactivate(); self.info_text.dead = true; self.info_text = nil end, 'info_text') else - locked_state = {locked = self.parent.locked, cards = {self.parent.cards[1] and self.parent.cards[1].unit, self.parent.cards[2] and self.parent.cards[2].unit, self.parent.cards[3] and self.parent.cards[3].unit}} + -- locked_state = {locked = self.parent.locked, cards = {self.parent.cards[1] and self.parent.cards[1].unit, self.parent.cards[2] and self.parent.cards[2].unit, self.parent.cards[3] and self.parent.cards[3].unit}} ui_switch2:play{pitch = random:float(0.95, 1.05), volume = 0.5} self.spring:pull(0.2, 200, 10) self.selected = true @@ -748,10 +803,18 @@ function LockButton:init(args) self:init_game_object(args) self.shape = Rectangle(self.x, self.y, 32, 16) self.interact_with_mouse = true + if locked_state.locks[self.parent.i] then + self.text = Text({{text = '[bg10]unlock', font = pixul_font, alignment = 'center'}}, global_text_tags) + self.shape.w = 44 + else + self.text = Text({{text = '[fgm5]lock', font = pixul_font, alignment = 'center'}}, global_text_tags) + end + --[[ if self.parent.locked then self.shape.w = 44 else self.shape.w = 32 end if self.parent.locked then self.text = Text({{text = '[fgm5]' .. tostring(self.parent.locked and 'unlock' or 'lock'), font = pixul_font, alignment = 'center'}}, global_text_tags) else self.text = Text({{text = '[bg10]' .. tostring(self.parent.locked and 'unlock' or 'lock'), font = pixul_font, alignment = 'center'}}, global_text_tags) end + ]]-- end @@ -759,6 +822,23 @@ function LockButton:update(dt) self:update_game_object(dt) if self.selected and input.m1.pressed then + ui_switch2:play{pitch = random:float(0.95, 1.05), volume = 0.5} + self.spring:pull(0.2, 200, 10) + if locked_state.locks[self.parent.i] then + locked_state.locks[self.parent.i] = false + self.text:set_text{{text = '[fgm5]lock', font = pixul_font, alignment = 'center'}} + self.shape.w = 32 + else + locked_state.locks[self.parent.i] = true + self.text:set_text{{text = '[bg10]unlock', font = pixul_font, alignment = 'center'}} + self.shape.w = 44 + end + + -- self is the button, self.parent is the shop card, and self.parent.parent is the BuyScreen. + local p = self.parent.parent + system.save_run(p.level, p.loop, gold, p.units, p.passives, p.shop_level, p.shop_xp, run_passive_pool, locked_state) + + --[[ self.parent.locked = not self.parent.locked if not self.parent.locked then locked_state = nil end if self.parent.locked then @@ -771,6 +851,7 @@ function LockButton:update(dt) self.text:set_text{{text = '[fgm5]' .. tostring(self.parent.locked and 'unlock' or 'lock'), font = pixul_font, alignment = 'center'}} if self.parent.locked then self.shape.w = 44 else self.shape.w = 32 end + ]]-- end end @@ -787,13 +868,13 @@ function LockButton:on_mouse_enter() ui_hover1:play{pitch = random:float(1.3, 1.5), volume = 0.5} pop2:play{pitch = random:float(0.95, 1.05), volume = 0.5} self.selected = true - self.text:set_text{{text = '[fgm5]' .. tostring(self.parent.locked and 'unlock' or 'lock'), font = pixul_font, alignment = 'center'}} + -- self.text:set_text{{text = '[fgm5]' .. tostring(self.parent.locked and 'unlock' or 'lock'), font = pixul_font, alignment = 'center'}} self.spring:pull(0.2, 200, 10) end function LockButton:on_mouse_exit() - if not self.parent.locked then self.text:set_text{{text = '[bg10]' .. tostring(self.parent.locked and 'unlock' or 'lock'), font = pixul_font, alignment = 'center'}} end + -- if not self.parent.locked then self.text:set_text{{text = '[bg10]' .. tostring(self.parent.locked and 'unlock' or 'lock'), font = pixul_font, alignment = 'center'}} end self.selected = false end @@ -964,23 +1045,34 @@ RerollButton:implement(GameObject) function RerollButton:init(args) self:init_game_object(args) self.interact_with_mouse = true + self.reroll_cost = self.reroll_cost or 5 if self.parent:is(BuyScreen) then + for _, unit in ipairs(self.parent.units) do + if unit.character == 'physician' and unit.level == 3 then + self.reroll_cost = self.reroll_cost -1 + if self.reroll_cost < 0 then self.reroll_cost = 0 end + break + end + end self.shape = Rectangle(self.x, self.y, 54, 16) - self.text = Text({{text = '[bg10]reroll: [yellow]2', font = pixul_font, alignment = 'center'}}, global_text_tags) + self.text = Text({{text = '[bg10]reroll: [yellow]' .. self.reroll_cost, font = pixul_font, alignment = 'center'}}, global_text_tags) elseif self.parent:is(Arena) then self.shape = Rectangle(self.x, self.y, 60, 16) local merchant for _, unit in ipairs(self.parent.starting_units) do - if unit.character == 'merchant' then + if unit.character == 'merchant' and unit.level == 3 then merchant = unit - break + -- break + elseif unit.character == 'warmonger' and unit.level == 3 then + self.reroll_cost = self.reroll_cost - self.parent.shop_level + if self.reroll_cost < 0 then self.reroll_cost = 0 end end end 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 - self.text = Text({{text = '[bg10]reroll: [yellow]5', font = pixul_font, alignment = 'center'}}, global_text_tags) + self.text = Text({{text = '[bg10]reroll: [yellow]' .. self.reroll_cost, font = pixul_font, alignment = 'center'}}, global_text_tags) end end end @@ -991,14 +1083,23 @@ function RerollButton:update(dt) if (self.selected and input.m1.pressed) or input.r.pressed then if self.parent:is(BuyScreen) then - if gold < 2 then + local is_error = false + local error_text = '' + local all_locked = true + for i = 1, self.parent.num_cards do + all_locked = all_locked and locked_state.locks[i] == true + end + if gold < self.reroll_cost then is_error = true; error_text = '[fg]not enough gold' + elseif all_locked then is_error = true; error_text = '[fg]all shop cards are locked' + end + if is_error then self.spring:pull(0.2, 200, 10) self.selected = true error1:play{pitch = random:float(0.95, 1.05), volume = 0.5} if not self.info_text then self.info_text = InfoText{group = main.current.ui} self.info_text:activate({ - {text = '[fg]not enough gold', font = pixul_font, alignment = 'center'}, + {text = error_text, font = pixul_font, alignment = 'center'}, }, nil, nil, nil, nil, 16, 4, nil, 2) self.info_text.x, self.info_text.y = gw/2, gh/2 + 10 end @@ -1008,12 +1109,12 @@ function RerollButton:update(dt) self.parent:set_cards(self.parent.shop_level, true) self.selected = true self.spring:pull(0.2, 200, 10) - gold = gold - 2 + gold = gold - self.reroll_cost self.parent.shop_text:set_text{{text = '[wavy_mid, fg]shop [fg]- [fg, nudge_down]gold: [yellow, nudge_down]' .. gold, font = pixul_font, alignment = 'center'}} system.save_run(self.parent.level, self.parent.loop, gold, self.parent.units, self.parent.passives, self.parent.shop_level, self.parent.shop_xp, run_passive_pool, locked_state) end elseif self.parent:is(Arena) then - if gold < 5 and not self.free_reroll then + if gold < self.reroll_cost and not self.free_reroll then self.spring:pull(0.2, 200, 10) self.selected = true error1:play{pitch = random:float(0.95, 1.05), volume = 0.5} @@ -1030,10 +1131,10 @@ function RerollButton:update(dt) self.parent:set_passives(true) self.selected = true self.spring:pull(0.2, 200, 10) - if not self.free_reroll then gold = gold - 5 end + if not self.free_reroll then gold = gold - self.reroll_cost end self.parent.shop_text:set_text{{text = '[fg, nudge_down]gold: [yellow, nudge_down]' .. gold, font = pixul_font, alignment = 'center'}} self.free_reroll = false - self.text = Text({{text = '[bg10]reroll: [yellow]5', font = pixul_font, alignment = 'center'}}, global_text_tags) + self.text = Text({{text = '[bg10]reroll: [yellow]' .. self.reroll_cost, font = pixul_font, alignment = 'center'}}, global_text_tags) end end @@ -1059,12 +1160,12 @@ function RerollButton:on_mouse_enter() pop2:play{pitch = random:float(0.95, 1.05), volume = 0.5} self.selected = true if self.parent:is(BuyScreen) then - self.text:set_text{{text = '[fgm5]reroll: 2', font = pixul_font, alignment = 'center'}} + self.text:set_text{{text = '[fgm5]reroll: ' .. self.reroll_cost, font = pixul_font, alignment = 'center'}} elseif self.parent:is(Arena) then if self.free_reroll then self.text:set_text{{text = '[fgm5]reroll: 0', font = pixul_font, alignment = 'center'}} else - self.text:set_text{{text = '[fgm5]reroll: 5', font = pixul_font, alignment = 'center'}} + self.text:set_text{{text = '[fgm5]reroll: ' .. self.reroll_cost, font = pixul_font, alignment = 'center'}} end end self.spring:pull(0.2, 200, 10) @@ -1073,18 +1174,27 @@ end function RerollButton:on_mouse_exit() if self.parent:is(BuyScreen) then - self.text:set_text{{text = '[bg10]reroll: [yellow]2', font = pixul_font, alignment = 'center'}} + self.text:set_text{{text = '[bg10]reroll: [yellow]' .. self.reroll_cost, font = pixul_font, alignment = 'center'}} elseif self.parent:is(Arena) then if self.free_reroll then self.text:set_text{{text = '[fgm5]reroll: [yellow]0', font = pixul_font, alignment = 'center'}} else - self.text:set_text{{text = '[fgm5]reroll: [yellow]5', font = pixul_font, alignment = 'center'}} + self.text:set_text{{text = '[fgm5]reroll: [yellow]' .. self.reroll_cost, font = pixul_font, alignment = 'center'}} end end self.selected = false end +function RerollButton:adjust_reroll_cost(amount) + self.reroll_cost = self.reroll_cost + amount + if self.reroll_cost < 0 then self.reroll_cost = 0 end + if self.parent:is(BuyScreen) then + self.text:set_text{{text = '[bg10]reroll: [yellow]' .. self.reroll_cost, font = pixul_font, alignment = 'center'}} + end +end + + TutorialCharacterPart = Object:extend() @@ -1115,15 +1225,18 @@ function TutorialCharacterPart:on_mouse_enter() self.selected = true self.spring:pull(0.2, 200, 10) self.info_text = InfoText{group = main.current.tutorial} + self.info_text:activate(generate_character_text(self.character, self.level, nil, nil), nil, nil, nil, nil, 16, 4, nil, 2) + --[[ self.info_text:activate({ {text = '[' .. character_color_strings[self.character] .. ']' .. self.character:capitalize() .. '[fg] - [yellow]Lv.' .. self.level, font = pixul_font, alignment = 'center', height_multiplier = 1.25}, {text = '[fg]Classes: ' .. character_class_strings[self.character], font = pixul_font, alignment = 'center', height_multiplier = 1.25}, {text = character_descriptions[self.character](self.level), font = pixul_font, alignment = 'center', height_multiplier = 2}, - {text = '[' .. (self.level == 3 and 'yellow' or 'light_bg') .. ']Lv.3 [' .. (self.level == 3 and 'fg' or 'light_bg') .. ']Effect - ' .. + {text = '[' .. (self.level == 3 and 'yellow' or 'light_bg') .. ']Lv.3 [' .. (self.level == 3 and 'fg' or 'light_bg') .. ']Effect - ' .. (self.level == 3 and character_effect_names[self.character] or character_effect_names_gray[self.character]), font = pixul_font, alignment = 'center', height_multiplier = 1.25}, {text = (self.level == 3 and character_effect_descriptions[self.character]() or character_effect_descriptions_gray[self.character]()), font = pixul_font, alignment = 'center'}, }, nil, nil, nil, nil, 16, 4, nil, 2) + ]]-- self.info_text.x, self.info_text.y = gw/2, gh/2 + gh/4 - 12 end @@ -1217,6 +1330,7 @@ function CharacterPart:update(dt) self.parent:set_party_and_sets() self.parent:refresh_cards() self.parent.party_text:set_text({{text = '[wavy_mid, fg]party ' .. tostring(#self.parent.units) .. '/' .. tostring(max_units), font = pixul_font, alignment = 'center'}}) + if self.character == 'physician' and self.level == 3 then self.parent.reroll_button:adjust_reroll_cost(1) end system.save_run(self.parent.level, self.parent.loop, gold, self.parent.units, self.parent.passives, self.parent.shop_level, self.parent.shop_xp, run_passive_pool, locked_state) else self.parent.parent:gain_gold(self:get_sale_price()) @@ -1259,15 +1373,18 @@ function CharacterPart:on_mouse_enter() self.selected = true self.spring:pull(0.2, 200, 10) self.info_text = InfoText{group = main.current.ui, force_update = self.force_update} + self.info_text:activate(generate_character_text(self.character, self.level, self:get_sale_price(), nil), nil, nil, nil, nil, 16, 4, nil, 2) + --[[ self.info_text:activate({ {text = '[' .. character_color_strings[self.character] .. ']' .. self.character:capitalize() .. '[fg] - [yellow]Lv.' .. self.level .. '[fg], tier [yellow]' .. character_tiers[self.character] .. '[fg] - sells for [yellow]' .. self:get_sale_price(), font = pixul_font, alignment = 'center', height_multiplier = 1.25}, {text = '[fg]Classes: ' .. character_class_strings[self.character], font = pixul_font, alignment = 'center', height_multiplier = 1.25}, {text = character_descriptions[self.character](self.level), font = pixul_font, alignment = 'center', height_multiplier = 2}, - {text = '[' .. (self.level == 3 and 'yellow' or 'light_bg') .. ']Lv.3 [' .. (self.level == 3 and 'fg' or 'light_bg') .. ']Effect - ' .. + {text = '[' .. (self.level == 3 and 'yellow' or 'light_bg') .. ']Lv.3 [' .. (self.level == 3 and 'fg' or 'light_bg') .. ']Effect - ' .. (self.level == 3 and character_effect_names[self.character] or character_effect_names_gray[self.character]), font = pixul_font, alignment = 'center', height_multiplier = 1.25}, {text = (self.level == 3 and character_effect_descriptions[self.character]() or character_effect_descriptions_gray[self.character]()), font = pixul_font, alignment = 'center'}, }, nil, nil, nil, nil, 16, 4, nil, 2) + ]]-- self.info_text.x, self.info_text.y = gw/2, gh/2 + 10 --[[ @@ -1529,7 +1646,7 @@ function ItemCard:create_info_text() else self.info_text = InfoText{group = main.current.ui, force_update = true} self.info_text:activate({ - {text = '[fg]' .. passive_names[self.passive] .. ', [yellow]Lv.' .. self.level .. '[fg], XP: [yellow]' .. self.xp .. '/' .. self.max_xp .. '[fg], +1 XP cost: [yellow]5[fg], sells for: [yellow]' .. + {text = '[fg]' .. passive_names[self.passive] .. ', [yellow]Lv.' .. self.level .. '[fg], XP: [yellow]' .. self.xp .. '/' .. self.max_xp .. '[fg], +1 XP cost: [yellow]5[fg], sells for: [yellow]' .. tostring((self.level == 1 and 10) or (self.level == 2 and 20) or (self.level == 3 and 30)), font = pixul_font, alignment = 'center', height_multiplier = 1.25}, {text = passive_descriptions_level[self.passive](self.level), font = pixul_font, alignment = 'center', height_multiplier = 1.25}, }, nil, nil, nil, nil, 16, 4, nil, 2) @@ -1589,6 +1706,7 @@ function ShopCard:init(args) table.insert(self.class_icons, ClassIcon{group = main.current.effects, x = x + (i-1)*20, y = self.y + 6, class = class, character = self.unit, units = self.parent.units, parent = self}) end self.cost = character_tiers[self.unit] + self.lock_button = LockButton{group = main.current.effects, x = self.x, y = self.y + 55, parent = self} self.spring:pull(0.2, 200, 10) self:refresh() end @@ -1619,10 +1737,12 @@ function ShopCard:update(dt) ui_switch1:play{pitch = random:float(0.95, 1.05), volume = 0.5} _G[random:table{'coins1', 'coins2', 'coins3'}]:play{pitch = random:float(0.95, 1.05), volume = 0.5} self:die() + locked_state.locks[self.i] = false + locked_state.cards[self.i] = nil self.parent.cards[self.i] = nil self.parent:refresh_cards() self.parent.party_text:set_text({{text = '[wavy_mid, fg]party ' .. tostring(#self.parent.units) .. '/' .. tostring(max_units), font = pixul_font, alignment = 'center'}}) - locked_state = {locked = self.parent.locked, cards = {self.parent.cards[1] and self.parent.cards[1].unit, self.parent.cards[2] and self.parent.cards[2].unit, self.parent.cards[3] and self.parent.cards[3].unit}} + -- locked_state = {locked = self.parent.locked, cards = {self.parent.cards[1] and self.parent.cards[1].unit, self.parent.cards[2] and self.parent.cards[2].unit, self.parent.cards[3] and self.parent.cards[3].unit}} system.save_run(self.parent.level, self.parent.loop, gold, self.parent.units, self.parent.passives, self.parent.shop_level, self.parent.shop_xp, run_passive_pool, locked_state) else error1:play{pitch = random:float(0.95, 1.05), volume = 0.5} @@ -1658,7 +1778,11 @@ end function ShopCard:draw() graphics.push(self.x, self.y, 0, self.sx*self.spring.x, self.sy*self.spring.x) - if self.selected then + if locked_state.locks[self.i] and self.selected then + graphics.rectangle(self.x, self.y, self.w - 2, self.h - 2, 6, 6, bg[3]) + elseif locked_state.locks[self.i] then + graphics.rectangle(self.x, self.y, self.w - 2, self.h - 2, 6, 6, bg[7]) + elseif self.selected then graphics.rectangle(self.x, self.y, self.w, self.h, 6, 6, bg[-1]) end if self.owned then @@ -1723,6 +1847,7 @@ function ShopCard:die(dont_spawn_effect) self.info_text.dead = true self.info_text = nil end + self.lock_button.dead = true end @@ -1757,15 +1882,18 @@ function CharacterIcon:on_mouse_enter() ui_hover1:play{pitch = random:float(1.3, 1.5), volume = 0.5} self.spring:pull(0.2, 200, 10) self.info_text = InfoText{group = main.current.ui} + self.info_text:activate(generate_character_text(self.character, nil, nil, self.parent.cost), nil, nil, nil, nil, 16, 4, nil, 2) + --[[ self.info_text:activate({ {text = '[' .. character_color_strings[self.character] .. ']' .. self.character:capitalize() .. '[fg] - cost: [yellow]' .. self.parent.cost, font = pixul_font, alignment = 'center', height_multiplier = 1.25}, {text = '[fg]Classes: ' .. character_class_strings[self.character], font = pixul_font, alignment = 'center', height_multiplier = 1.25}, {text = character_descriptions[self.character](1), font = pixul_font, alignment = 'center', height_multiplier = 2}, - {text = '[' .. (self.level == 3 and 'yellow' or 'light_bg') .. ']Lv.3 [' .. (self.level == 3 and 'fg' or 'light_bg') .. ']Effect - ' .. + {text = '[' .. (self.level == 3 and 'yellow' or 'light_bg') .. ']Lv.3 [' .. (self.level == 3 and 'fg' or 'light_bg') .. ']Effect - ' .. (self.level == 3 and character_effect_names[self.character] or character_effect_names_gray[self.character]), font = pixul_font, alignment = 'center', height_multiplier = 1.25}, {text = (self.level == 3 and character_effect_descriptions[self.character]() or character_effect_descriptions_gray[self.character]()), font = pixul_font, alignment = 'center'}, -- {text = character_stats[self.character](1), font = pixul_font, alignment = 'center'}, }, nil, nil, nil, nil, 16, 4, nil, 2) + ]]-- self.info_text.x, self.info_text.y = gw/2, gh/2 + 10 end @@ -2080,3 +2208,67 @@ function ClassIcon:unhighlight() self.highlighted = false self.spring:pull(0.05, 200, 10) end + +function generate_character_text(character, level, sale, cost) + local t = {} + + local line = '[' .. character_color_strings[character] .. ']' .. character:capitalize() .. '[fg] - ' + if cost then + line = line .. 'cost: [yellow]' .. cost + else + line = line .. '[yellow]Lv.' .. level + if sale then + line = line .. '[fg], tier [yellow]' .. character_tiers[character] .. '[fg] - sells for [yellow]' .. sale + end + end + local element = {text = line, font = pixul_font, alignment = 'center', height_multiplier = 1.25} + table.insert(t, element) + + line = '[fg]Classes: ' .. character_class_strings[character] + element = {text = line, font = pixul_font, alignment = 'center', height_multiplier = 1.25} + table.insert(t, element) + + level = level or 1 + line = character_descriptions[character](level) + local n + if type(line) == 'table' then + n = line['line_count'] or 1 + for i = 1, n - 1 do + element = {text = line[i], font = pixul_font, alignment = 'center', height_multiplier = 1.1} + table.insert(t, element) + end + element = {text = line[n], font = pixul_font, alignment = 'center', height_multiplier = 2} + table.insert(t, element) + else + element = {text = line, font = pixul_font, alignment = 'center', height_multiplier = 2} + table.insert(t, element) + end + + if level == 3 then + line = '[yellow]Lv.3 [fg]Effect - ' .. character_effect_names[character] + else + line = '[light_bg]Lv.3 Effect - ' .. character_effect_names_gray[character] + end + element = {text = line, font = pixul_font, alignment = 'center', height_multiplier = 1.25} + table.insert(t, element) + + if level == 3 then + line = character_effect_descriptions[character]() + else + line = character_effect_descriptions_gray[character]() + end + if type(line) == 'table' then + n = line['line_count'] or 1 + for i = 1, n - 1 do + element = {text = line[i], font = pixul_font, alignment = 'center', height_multiplier = 1.1} + table.insert(t, element) + end + element = {text = line[n], font = pixul_font, alignment = 'center'} + table.insert(t, element) + else + element = {text = line, font = pixul_font, alignment = 'center'} + table.insert(t, element) + end + + return t +end diff --git a/enemies.lua b/enemies.lua index 61f9c81..a6e1d41 100644 --- a/enemies.lua +++ b/enemies.lua @@ -427,7 +427,7 @@ function Seeker:on_collision_enter(other, contact) hit2:play{pitch = random:float(0.95, 1.05), volume = 0.35} if other:is(Seeker) or other:is(Player) then self.headbutting = false end end - + elseif other:is(Turret) then self.headbutting = false _G[random:table{'player_hit1', 'player_hit2'}]:play{pitch = random:float(0.95, 1.05), volume = 0.35} @@ -537,6 +537,7 @@ function Seeker:hit(damage, projectile, dot, from_enemy) 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} + --[[ MOD: always have a chance to drop gold. if main.current.mercenary_level > 0 then if random:bool((main.current.mercenary_level == 2 and 16) or (main.current.mercenary_level == 1 and 8) or 0) then trigger:after(0.01, function() @@ -545,6 +546,13 @@ function Seeker:hit(damage, projectile, dot, from_enemy) end) end end + ]]-- + if random:bool((main.current.mercenary_level == 2 and 20) or (main.current.mercenary_level == 1 and 12) or 4) then + trigger:after(0.01, function() + if not main.current.main.world then return end + Gold{group = main.current.main, x = self.x, y = self.y} + end) + end --[[ if main.current.healer_level > 0 then @@ -695,6 +703,9 @@ function Seeker:hit(damage, projectile, dot, from_enemy) end) end end + + if projectile and projectile.parent and projectile.parent.on_enemy_kill then projectile.parent.on_enemy_kill(self) end + end end @@ -798,27 +809,43 @@ end ExploderMine = Object:extend() ExploderMine:implement(GameObject) +ExploderMine:implement(Physics) function ExploderMine:init(args) self:init_game_object(args) self.hfx:add('hit', 1) self.vr = 0 self.dvr = random:float(-math.pi/4, math.pi/4) self.rs = 0 + self.color = blue[-10] + self.counter = 0 + self.disarmed = false + self.hacked = false + self.detect_radius = (main.current.player.mine_detector == 1 and 30) or (main.current.player.mine_detector == 2 and 55) or (main.current.player.mine_detector == 3 and 80) or 5 + self:set_as_circle(self.detect_radius, 'static', 'ghost') self.t:tween(0.05, self, {rs = args.rs}, math.cubic_in_out, function() self.spring:pull(0.15) - self.t:every(0.8 - current_new_game_plus*0.1, function() - mine1:play{pitch = 1 + self.t:get_every_iteration'mine_count'*0.1, volume = 0.5} + self.t:every(2.5 - current_new_game_plus*0.2, function() + self.counter = self.counter + 1 + self:update_color() + mine1:play{pitch = 1 + self.counter * 0.1, volume = 0.5} self.spring:pull(0.5, 200, 10) self.hfx:use('hit', 0.5, 200, 10, 0.2) - end, 3, function() - shoot1:play{pitch = random:float(0.95, 1.05), volume = 0.4} - cannoneer1:play{pitch = random:float(0.95, 1.05), volume = 0.4} - for i = 1, 4 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} - local n = math.floor(8 + current_new_game_plus*1.5) + end, 4, function() if main.current.main.world then - 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 = 120 + math.min(5*self.parent.level, 300), dmg = 1.3*self.parent.dmg} + shoot1:play{pitch = random:float(0.95, 1.05), volume = 0.4} + cannoneer1:play{pitch = random:float(0.95, 1.05), volume = 0.4} + for i = 1, 4 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} + if not self.disarmed then + local n = math.floor(8 + current_new_game_plus*1.5) + for i = 1, n do + if self.hacked then + Projectile{group = main.current.main, x = self.x, y = self.y, color = red[0], r = (i-1)*math.pi/(n/2), v = 300, dmg = 3*self.parent.dmg, character = self.parent.character, + parent = self.parent, level = self.parent.level, ricochet = self.parent.ricochet} + else + EnemyProjectile{group = main.current.main, x = self.x, y = self.y, color = blue[0], r = (i-1)*math.pi/(n/2), v = 120 + math.min(5*self.parent.level, 300), dmg = 1.3*self.parent.dmg} + end + end end end self.dead = true @@ -840,6 +867,43 @@ function ExploderMine:draw() end +function ExploderMine:on_trigger_enter(other, contact) + -- if self.cant_be_picked_up then return end + if other:is(Player) and not (self.disarmed or self.hacked) then + 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 + disarm1:play{pitch = random:float(0.9, 1.1), volume = 1} + disarm2:play{pitch = random:float(0.9, 1.1), volume = 0,5} + local units = other:get_all_units() + for _, unit in ipairs(units) do + if unit.character == 'technomancer' then + self.hacked = true + self.parent = unit + if unit.level == 3 then + unit.exploder = true + for _, drone in ipairs(unit.drones) do + drone.mine_time = unit.power_up_duration + end + unit.t:after(unit.power_up_duration, function() unit.exploder = false end, 'mine_pickup') + end + break + end + end + self.disarmed = not self.hacked + self:update_color() + end +end + + +function ExploderMine:update_color() + if self.counter < 5 then + if self.hacked then self.color = red[-10 + self.counter * 5] + elseif self.disarmed then self.color = bg[1 + self.counter * 2] + else self.color = blue[-10 + self.counter * 5] end + end +end + + EnemyCritter = Object:extend() diff --git a/main.lua b/main.lua index 65a6cc9..506f782 100644 --- a/main.lua +++ b/main.lua @@ -25,6 +25,8 @@ function init() gambler1 = Sound('Collect 5.ogg', s) usurer1 = Sound('Shadow Punch 2.ogg', s) orb1 = Sound('Collect 2.ogg', s) + disarm1 = Sound('Collect 4.ogg', s) + disarm2 = Sound('Damage 3.ogg', s) gold1 = Sound('Collect 5.ogg', s) gold2 = Sound('Coins - Gears - Slot.ogg', s) psychic1 = Sound('Magical Impact 13.ogg', s) @@ -36,6 +38,7 @@ function init() earth3 = Sound('Earth Bolt 20.ogg', s) illusion1 = Sound('Buff 5.ogg', s) thunder1 = Sound('399656__bajko__sfx-thunder-blast.ogg', s) + thunder2 = Sound('Wind Bolt 8.ogg', s) flagellant1 = Sound('Whipping Horse 3.ogg', s) bard2 = Sound('376532__womb-affliction__flute-trill.ogg', s) arcane2 = Sound('Magical Impact 12.ogg', s) @@ -96,6 +99,7 @@ function init() spawn_mark1 = Sound('Bonus 2.ogg', s) spawn_mark2 = Sound('Bonus.ogg', s) alert1 = Sound('Click.ogg', s) + alert2 = Sound('Alert sounds 3.ogg', s) elementor1 = Sound('Wind Bolt 18.ogg', s) saboteur_hit1 = Sound('Explosion Flesh_01.ogg', s) saboteur_hit2 = Sound('Explosion Flesh_02.ogg', s) @@ -233,6 +237,7 @@ function init() psycholeak = Image('psycholeak') divine_blessing = Image('divine_blessing') hardening = Image('hardening') + mine_detector = Image('mine_detector') class_colors = { ['warrior'] = yellow[0], @@ -330,6 +335,12 @@ function init() ['usurer'] = 'Usurer', ['gambler'] = 'Gambler', ['thief'] = 'Thief', + ['astralist'] = 'Astralist', + ['physician'] = 'Physician', + ['warmonger'] = 'Warmonger', + ['avenger'] = 'Avenger', + ['herald'] = 'Herald', + ['technomancer'] = 'Technomancer', } character_colors = { @@ -390,6 +401,12 @@ function init() ['usurer'] = purple[0], ['gambler'] = yellow2[0], ['thief'] = red[0], + ['astralist'] = purple[0], + ['physician'] = green[0], + ['warmonger'] = yellow2[0], + ['avenger'] = blue2[0], + ['herald'] = fg[0], + ['technomancer'] = red[0], } character_color_strings = { @@ -450,6 +467,12 @@ function init() ['usurer'] = 'purple', ['gambler'] = 'yellow2', ['thief'] = 'red', + ['astralist'] = 'purple', + ['physician'] = 'green', + ['warmonger'] = 'yellow2', + ['avenger'] = 'blue2', + ['herald'] = 'fg', + ['technomancer'] = 'red', } character_classes = { @@ -510,6 +533,12 @@ function init() ['usurer'] = {'curser', 'mercenary', 'voider'}, ['gambler'] = {'mercenary', 'sorcerer'}, ['thief'] = {'rogue', 'mercenary'}, + ['astralist'] = {'voider', 'explorer', 'psyker'}, + ['physician'] = {'healer', 'mercenary'}, + ['warmonger'] = {'nuker', 'mercenary'}, + ['avenger'] = {'healer', 'voider', 'sorcerer'}, + ['herald'] = {'warrior', 'enchanter', 'psyker'}, + ['technomancer'] = {'ranger', 'conjurer', 'rogue'}, } character_class_strings = { @@ -570,6 +599,12 @@ function init() ['usurer'] = '[purple]Curser, [yellow2]Mercenary, [purple]Voider', ['gambler'] = '[yellow2]Mercenary, [blue2]Sorcerer', ['thief'] = '[red]Rogue, [yellow2]Mercenary', + ['astralist'] = '[purple]Voider, [fg]Explorer, Psyker', + ['physician'] = '[green]Healer, [yellow2]Mercenary', + ['warmonger'] = '[red]Nuker, [yellow2]Mercenary', + ['avenger'] = '[green]Healer, [purple]Voider, [blue2]Sorcerer', + ['herald'] = '[yellow]Warrior, [blue]Enchanter, [fg]Psyker', + ['technomancer'] = '[green]Ranger, [orange]Builder, [red]Rogue', } get_character_stat_string = function(character, level) @@ -603,7 +638,7 @@ function init() ['bomber'] = function(lvl) return '[fg]plants a bomb, when it explodes it deals [yellow]' .. 2*get_character_stat('bomber', lvl, 'dmg') .. ' AoE[fg] damage' end, ['stormweaver'] = function(lvl) return '[fg]infuses projectiles with chain lightning that deals [yellow]20%[fg] damage to [yellow]2[fg] enemies' end, ['sage'] = function(lvl) return '[fg]shoots a slow projectile that draws enemies in' end, - ['squire'] = function(lvl) return '[yellow]+20%[fg] damage and defense to all allies' end, + ['squire'] = function(lvl) return '[yellow]+20%[fg] damage and defense to all allies' end, ['cannoneer'] = function(lvl) return '[fg]shoots a projectile that deals [yellow]' .. 2*get_character_stat('cannoneer', lvl, 'dmg') .. ' AoE[fg] damage' end, ['dual_gunner'] = function(lvl) return '[fg]shoots two parallel projectiles, each dealing [yellow]' .. get_character_stat('dual_gunner', lvl, 'dmg') .. '[fg] damage' end, ['hunter'] = function(lvl) return '[fg]shoots an arrow that deals [yellow]' .. get_character_stat('hunter', lvl, 'dmg') .. '[fg] damage and has a [yellow]20%[fg] chance to summon a pet' end, @@ -647,6 +682,20 @@ function init() ['usurer'] = function(lvl) return '[fg]curses [yellow]3[fg] nearby enemies indefinitely with debt, 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, + ['astralist'] = function(lvl) return {line_count = 2, + '[fg]shoots a slow moving projectile that deals [yellow]' .. get_character_stat('astralist', lvl, 'dmg') .. '[fg] damage', + '[fg]projectiles have [yellow]2[fg] pierce, and slow enemies hit by [yellow]80%[fg] for [yellow]2[fg] seconds'} end, + ['physician'] = function(lvl) return '[fg]picking up gold releases [yellow]1[fg] healing orb' end, + ['warmonger'] = function(lvl) return '[fg]deals [yellow]' .. get_character_stat('warmonger', lvl, 'dmg') .. '[fg] damage in [yellow]1[fg] random area for each shop level' end, + ['avenger'] = function(lvl) return {line_count = 3, + '[fg]shoots a projectile that deals [yellow]' .. get_character_stat('avenger', lvl, 'dmg') .. '[fg] damage, has [yellow]2[fg] pierce and [yellow]10[fg] knockback', + '[fg]on enemy hit, has a [yellow]20%[fg] chance to create a void rift', + '[fg]on enemy hit, has a [yellow]20%[fg] chance to create a healing orb'} end, + ['herald'] = function(lvl) return '[fg]deals [yellow]' .. get_character_stat('herald', lvl, 'dmg') .. '[fg] AoE damage, on enemy kill gain [yellow]1%[fg] max hp' end, + ['technomancer'] = function(lvl) return {line_count = 3, + '[fg]shoots a projectile straight forward that deals [yellow]' .. get_character_stat('technomancer', lvl, 'dmg') .. ' [fg] damage', + '[fg]spawns [yellow]2[fg] drones that follow beside and copy the main attack[yellow]', + '[fg]instead of disarming enemy mines, hack them to hurt enemies when they detonate'} end, } character_effect_names = { @@ -707,6 +756,12 @@ function init() ['usurer'] = '[purple]Bankruptcy', ['gambler'] = '[yellow2]Multicast', ['thief'] = '[red]Ultrakill', + ['astralist'] = '[purple]Star Seeker', + ['physician'] = '[green]Waiting Room', + ['warmonger'] = '[yellow2]War Bonds', + ['avenger'] = '[blue2]Double Edged', + ['herald'] = '[fg]Clarion Call', + ['technomancer'] = '[red]Multiple Options', } character_effect_names_gray = { @@ -767,6 +822,12 @@ function init() ['usurer'] = '[light_bg]Bankruptcy', ['gambler'] = '[light_bg]Multicast', ['thief'] = '[light_bg]Ultrakill', + ['astralist'] = '[light_bg]Star Seeker', + ['physician'] = '[light_bg]Waiting Room', + ['warmonger'] = '[light_bg]War Bonds', + ['avenger'] = '[light_bg]Double Edged', + ['herald'] = '[light_bg]Clarion Call', + ['technomancer'] = '[light_bg]Multiple Options', } character_effect_descriptions = { @@ -827,6 +888,26 @@ function init() ['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, + ['astralist'] = function() return {line_count = 2, + '[fg]instead release [yellow]3[fg] projectiles from the middle of the arena', + '[fg]projectiles are now also homing and deal [yellow]' .. get_character_stat('astralist', 3, 'dmg') .. '[fg] damage for [yellow]2[fg] seconds'} end, + ['physician'] = function() return {line_count = 2, + '[fg]character rerolls cost [yellow]1[fg] less gold', + '[fg]picking up gold releases [yellow]2[fg] healing orbs'} end, + ['warmonger'] = function() return '[fg]item rerolls cost [yellow]1[fg] less gold for each shop level' end, + ['avenger'] = function() return {line_count = 3, + '[yellow]x2[fg] damage, [yellow]x2[fg] attack speed, [yellow]x2[fg] pierce, [yellow]x2[fg] knockback', + '[fg]on enemy kill repeat attack and increment sorcerer repeat counter', + '[fg]on healing orb pickup create a void nova and increment repeat counter'} end, + ['herald'] = function() return {line_count = 3, + '[fg]Attack hits all enemies, on enemy kill gain [yellow]5%[fg] max hp instead', + '[fg]deal [yellow]10%[fg] of current hp as damage to self and grant [yellow]+1%[fg] max hp to all allies', + '[fg]also deal [yellow]5%[fg] of current hp as extra damage to enemies hit'} end, + ['technomancer'] = function() return {line_count = 4, + '[yellow]+2[fg] drones, pickups give a special [yellow]4[fg] second bonus to drones', + '[yellow2]coin[fg] - increase attack speed by [yellow]300%', + '[green]healing orb[fg] - projectiles bounce [yellow]3[fg] times', + '[blue]exploder mine[fg] - projectiles explode when they die'} end, } character_effect_descriptions_gray = { @@ -887,32 +968,52 @@ function init() ['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 knife crits it deals ' .. 10*get_character_stat('thief', 3, 'dmg') .. ' damage, chains 10 times and grants 1 gold' end, + ['astralist'] = function() return {line_count = 2, + '[light_bg]instead release 3 projectiles from the middle of the arena', + '[light_bg]projectiles are now also homing and deal ' .. get_character_stat('astralist', 3, 'dmg') .. 'damage for 2 seconds'} end, + ['physician'] = function() return {line_count = 2, + '[light_bg]character rerolls cost 1 less gold', + '[light_bg]picking up gold releases 2 healing orbs'} end, + ['warmonger'] = function() return '[light_bg]item rerolls cost 1 less gold for each shop level' end, + ['avenger'] = function() return {line_count = 3, + '[light_bg]x2 damage, x2 attack speed, x2 pierce, x2 knockback', + '[light_bg]on enemy kill repeat attack and increment sorcerer repeat counter', + '[light_bg]on healing orb pickup create a void nova and increment repeat counter'} end, + ['herald'] = function() return {line_count = 3, + '[light_bg]Attack hits all enemies, on enemy kill gain 5% max hp instead', + '[light_bg]deal 10% of current hp as damage to self and grant +1% max hp to all allies', + '[light_bg]also deal 5% of current hp as extra damage to enemies hit'} end, + ['technomancer'] = function() return {line_count = 4, + '[light_bg]+2 drones, pickups give a special 4 second bonus to drones', + '[light_bg]coin - increase attack speed by 300%', + '[light_bg]healing orb - projectiles bounce 3 times', + '[light_bg]exploder mine - projectiles explode when they die'} end, } character_stats = { ['vagrant'] = function(lvl) return get_character_stat_string('vagrant', lvl) end, - ['swordsman'] = function(lvl) return get_character_stat_string('swordsman', lvl) end, - ['wizard'] = function(lvl) return get_character_stat_string('wizard', lvl) end, - ['magician'] = function(lvl) return get_character_stat_string('magician', lvl) end, - ['archer'] = function(lvl) return get_character_stat_string('archer', lvl) end, - ['scout'] = function(lvl) return get_character_stat_string('scout', lvl) end, - ['cleric'] = function(lvl) return get_character_stat_string('cleric', lvl) end, - ['outlaw'] = function(lvl) return get_character_stat_string('outlaw', lvl) end, - ['blade'] = function(lvl) return get_character_stat_string('blade', lvl) end, - ['elementor'] = function(lvl) return get_character_stat_string('elementor', lvl) end, - ['saboteur'] = function(lvl) return get_character_stat_string('saboteur', lvl) end, - ['bomber'] = function(lvl) return get_character_stat_string('bomber', lvl) end, - ['stormweaver'] = function(lvl) return get_character_stat_string('stormweaver', lvl) end, - ['sage'] = function(lvl) return get_character_stat_string('sage', lvl) end, - ['squire'] = function(lvl) return get_character_stat_string('squire', lvl) end, - ['cannoneer'] = function(lvl) return get_character_stat_string('cannoneer', lvl) end, - ['dual_gunner'] = function(lvl) return get_character_stat_string('dual_gunner', lvl) end, - ['hunter'] = function(lvl) return get_character_stat_string('hunter', lvl) end, - ['sentry'] = function(lvl) return get_character_stat_string('sentry', lvl) end, - ['chronomancer'] = function(lvl) return get_character_stat_string('chronomancer', lvl) end, - ['spellblade'] = function(lvl) return get_character_stat_string('spellblade', lvl) end, - ['psykeeper'] = function(lvl) return get_character_stat_string('psykeeper', lvl) end, - ['engineer'] = function(lvl) return get_character_stat_string('engineer', lvl) end, + ['swordsman'] = function(lvl) return get_character_stat_string('swordsman', lvl) end, + ['wizard'] = function(lvl) return get_character_stat_string('wizard', lvl) end, + ['magician'] = function(lvl) return get_character_stat_string('magician', lvl) end, + ['archer'] = function(lvl) return get_character_stat_string('archer', lvl) end, + ['scout'] = function(lvl) return get_character_stat_string('scout', lvl) end, + ['cleric'] = function(lvl) return get_character_stat_string('cleric', lvl) end, + ['outlaw'] = function(lvl) return get_character_stat_string('outlaw', lvl) end, + ['blade'] = function(lvl) return get_character_stat_string('blade', lvl) end, + ['elementor'] = function(lvl) return get_character_stat_string('elementor', lvl) end, + ['saboteur'] = function(lvl) return get_character_stat_string('saboteur', lvl) end, + ['bomber'] = function(lvl) return get_character_stat_string('bomber', lvl) end, + ['stormweaver'] = function(lvl) return get_character_stat_string('stormweaver', lvl) end, + ['sage'] = function(lvl) return get_character_stat_string('sage', lvl) end, + ['squire'] = function(lvl) return get_character_stat_string('squire', lvl) end, + ['cannoneer'] = function(lvl) return get_character_stat_string('cannoneer', lvl) end, + ['dual_gunner'] = function(lvl) return get_character_stat_string('dual_gunner', lvl) end, + ['hunter'] = function(lvl) return get_character_stat_string('hunter', lvl) end, + ['sentry'] = function(lvl) return get_character_stat_string('sentry', lvl) end, + ['chronomancer'] = function(lvl) return get_character_stat_string('chronomancer', lvl) end, + ['spellblade'] = function(lvl) return get_character_stat_string('spellblade', lvl) end, + ['psykeeper'] = function(lvl) return get_character_stat_string('psykeeper', lvl) end, + ['engineer'] = function(lvl) return get_character_stat_string('engineer', lvl) end, ['plague_doctor'] = function(lvl) return get_character_stat_string('plague_doctor', lvl) end, ['barbarian'] = function(lvl) return get_character_stat_string('barbarian', lvl) end, ['juggernaut'] = function(lvl) return get_character_stat_string('juggernaut', lvl) end, @@ -947,6 +1048,12 @@ function init() ['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, + ['astralist'] = function(lvl) return get_character_stat_string('astralist', lvl) end, + ['physician'] = function(lvl) return get_character_stat_string('physician', lvl) end, + ['warmonger'] = function(lvl) return get_character_stat_string('warmonger', lvl) end, + ['avenger'] = function(lvl) return get_character_stat_string('avenger', lvl) end, + ['herald'] = function(lvl) return get_character_stat_string('herald', lvl) end, + ['technomancer'] = function(lvl) return get_character_stat_string('technomancer', lvl) end, } class_stat_multipliers = { @@ -1001,8 +1108,8 @@ function init() ['forcer'] = function(lvl) return '[' .. ylb1(lvl) .. ']2[light_bg]/[' .. ylb2(lvl) .. ']4 [fg]- [' .. ylb1(lvl) .. ']+25%[light_bg]/[' .. ylb2(lvl) .. ']+50% [fg]knockback force to all allies' end, ['swarmer'] = function(lvl) return '[' .. ylb1(lvl) .. ']2[light_bg]/[' .. ylb2(lvl) .. ']4 [fg]- [' .. ylb1(lvl) .. ']+1[light_bg]/[' .. ylb2(lvl) .. ']+3 [fg]hits to critters' end, ['voider'] = function(lvl) return '[' .. ylb1(lvl) .. ']2[light_bg]/[' .. ylb2(lvl) .. ']4 [fg]- [' .. ylb1(lvl) .. ']+20%[light_bg]/[' .. ylb2(lvl) .. ']+40% [fg]damage over time to allied voiders' end, - ['sorcerer'] = function(lvl) - return '[' .. ylb1(lvl) .. ']2[light_bg]/[' .. ylb2(lvl) .. ']4[light_bg]/[' .. ylb3(lvl) .. ']6 [fg]- sorcerers repeat their attacks once every [' .. + ['sorcerer'] = function(lvl) + return '[' .. ylb1(lvl) .. ']2[light_bg]/[' .. ylb2(lvl) .. ']4[light_bg]/[' .. ylb3(lvl) .. ']6 [fg]- sorcerers repeat their attacks once every [' .. ylb1(lvl) .. ']4[light_bg]/[' .. ylb2(lvl) .. ']3[light_bg]/[' .. ylb3(lvl) .. ']2[fg] attacks' end, ['mercenary'] = function(lvl) return '[' .. ylb1(lvl) .. ']2[light_bg]/[' .. ylb2(lvl) .. ']4 [fg]- [' .. ylb1(lvl) .. ']+8%[light_bg]/[' .. ylb2(lvl) .. ']+16% [fg]chance for enemies to drop gold on death' end, @@ -1016,8 +1123,7 @@ function init() [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', 'merchant', 'miner'} - non_cooldown_characters = {'squire', 'chronomancer', 'psykeeper', 'merchant', 'miner'} + non_attacking_characters = {'cleric', 'stormweaver', 'squire', 'chronomancer', 'sage', 'bane', 'carver', 'fairy', 'priest', 'merchant', 'miner', 'infestor', 'jester', 'silencer', 'warden', 'physician'} character_tiers = { ['vagrant'] = 1, @@ -1077,11 +1183,17 @@ function init() ['usurer'] = 3, ['gambler'] = 3, ['thief'] = 4, + ['astralist'] = 4, + ['physician'] = 1, + ['warmonger'] = 2, + ['avenger'] = 4, + ['herald'] = 4, + ['technomancer'] = 4, } launches_projectiles = function(character) - local classes = {'vagrant', 'archer', 'scout', 'outlaw', 'blade', 'wizard', 'cannoneer', 'dual_gunner', 'hunter', 'spellblade', 'engineer', 'corruptor', 'beastmaster', 'jester', 'assassin', 'barrager', - 'arcanist', 'illusionist', 'artificer', 'miner', 'thief', 'sentry'} + local classes = {'vagrant', 'archer', 'scout', 'outlaw', 'blade', 'wizard', 'cannoneer', 'dual_gunner', 'hunter', 'spellblade', 'engineer', 'corruptor', 'beastmaster', 'jester', 'assassin', 'barrager', + 'arcanist', 'illusionist', 'artificer', 'miner', 'thief', 'sentry', 'astralist', 'avenger', 'technomancer'} return table.any(classes, function(v) return v == character end) end @@ -1279,6 +1391,7 @@ function init() ['psycholeak'] = 'Psycholeak', ['divine_blessing'] = 'Divine Blessing', ['hardening'] = 'Hardening', + ['mine_detector'] = 'Mine Detector', } passive_descriptions = { @@ -1366,6 +1479,7 @@ function init() ['psycholeak'] = '[fg]position [yellow]1[fg] generates [yellow]1[fg] psyker orb every [yellow]10[fg] seconds', ['divine_blessing'] = '[fg]generate [yellow]1[fg] healing orb every [yellow]8[fg] seconds', ['hardening'] = '[yellow]+150%[fg] defense to all allies for [yellow]3[fg] seconds after an ally dies', + ['mine_detector'] = '[yellow]+25/50/75[[fg] range for disarming enemy exploder mines', } local ts = function(lvl, a, b, c) return '[' .. ylb1(lvl) .. ']' .. tostring(a) .. '[light_bg]/[' .. ylb2(lvl) .. ']' .. tostring(b) .. '[light_bg]/[' .. ylb3(lvl) .. ']' .. tostring(c) .. '[fg]' end @@ -1454,6 +1568,7 @@ function init() ['psycholeak'] = function(lvl) return '[fg]position [yellow]1[fg] generates [yellow]1[fg] psyker orb every [yellow]10[fg] seconds' end, ['divine_blessing'] = function(lvl) return '[fg]generate [yellow]1[fg] healing orb every [yellow]8[fg] seconds' end, ['hardening'] = function(lvl) return '[yellow]+150%[fg] defense to all allies for [yellow]3[fg] seconds after an ally dies' end, + ['mine_detector'] = function(lvl) return ts(lvl, '+25', '50', '75') .. ' range for disarming enemy exploder mines' end, } level_to_tier_weights = { @@ -1491,7 +1606,7 @@ function init() [4] = {4, 5}, [5] = {5, 8}, [6] = {8, 10}, - [7] = {8, 10}, + [7] = {8, 10}, [8] = {12, 14}, [9] = {14, 18}, [10] = {10, 13}, @@ -1502,9 +1617,9 @@ function init() [15] = {14, 18}, [16] = {12, 12}, [17] = {12, 12}, - [18] = {20, 24}, + [18] = {20, 24}, [19] = {8, 12}, - [20] = {10, 14}, + [20] = {10, 14}, [21] = {20, 28}, [22] = {32, 32}, [23] = {36, 36}, @@ -1691,7 +1806,7 @@ function init() end unlevellable_items = { - 'speed_3', 'damage_4', 'shoot_5', 'death_6', 'lasting_7', 'kinetic_bomb', 'porcupine_technique', 'last_stand', 'annihilation', + 'speed_3', 'damage_4', 'shoot_5', 'death_6', 'lasting_7', 'kinetic_bomb', 'porcupine_technique', 'last_stand', 'annihilation', 'tremor', 'heavy_impact', 'fracture', 'meat_shield', 'divine_punishment', 'unleash', 'freezing_field', 'burning_field', 'gravity_field', 'magnetism', 'insurance', 'dividends', 'haste', 'rearm', 'ceremonial_dagger', 'burning_strike', 'lucky_strike', 'healing_strike', 'psycholeak', 'divine_blessing', 'hardening', } @@ -1706,15 +1821,15 @@ function init() main_song_instance = _G[random:table{'song1', 'song2', 'song3', 'song4', 'song5'}]:play{volume = 0.5} main = Main() - main:add(MainMenu'mainmenu') - main:go_to('mainmenu') + main:add(MainMenu'main_menu') + main:go_to('main_menu') --[[ main:add(BuyScreen'buy_screen') main:go_to('buy_screen', run.level or 1, run.units or {}, passives, run.shop_level or 1, run.shop_xp or 0) -- main:go_to('buy_screen', 7, run.units or {}, {'unleash'}) ]]-- - + --[[ gold = 10 run_passive_pool = { @@ -1724,7 +1839,7 @@ function init() 'assassination', 'flying_daggers', 'ultimatum', 'magnify', 'echo_barrage', 'unleash', 'reinforce', 'payback', 'enchanted', 'freezing_field', 'burning_field', 'gravity_field', 'magnetism', 'insurance', 'dividends', 'berserking', 'unwavering_stance', 'unrelenting_stance', 'blessing', 'haste', 'divine_barrage', 'orbitism', 'psyker_orbs', 'psychosink', 'rearm', 'taunt', 'construct_instability', 'intimidation', 'vulnerability', 'temporal_chains', 'ceremonial_dagger', 'homing_barrage', 'critical_strike', 'noxious_strike', 'infesting_strike', 'burning_strike', 'lucky_strike', 'healing_strike', 'stunning_strike', - 'silencing_strike', 'culling_strike', 'lightning_strike', 'psycholeak', 'divine_blessing', 'hardening', 'kinetic_strike', + 'silencing_strike', 'culling_strike', 'lightning_strike', 'psycholeak', 'divine_blessing', 'hardening', 'kinetic_strike', 'mine_detector', } main:add(Arena'arena') main:go_to('arena', 21, 0, { @@ -1907,7 +2022,7 @@ function open_options(self) 'assassination', 'flying_daggers', 'ultimatum', 'magnify', 'echo_barrage', 'unleash', 'reinforce', 'payback', 'enchanted', 'freezing_field', 'burning_field', 'gravity_field', 'magnetism', 'insurance', 'dividends', 'berserking', 'unwavering_stance', 'unrelenting_stance', 'blessing', 'haste', 'divine_barrage', 'orbitism', 'psyker_orbs', 'psychosink', 'rearm', 'taunt', 'construct_instability', 'intimidation', 'vulnerability', 'temporal_chains', 'ceremonial_dagger', 'homing_barrage', 'critical_strike', 'noxious_strike', 'infesting_strike', 'burning_strike', 'lucky_strike', 'healing_strike', 'stunning_strike', - 'silencing_strike', 'culling_strike', 'lightning_strike', 'psycholeak', 'divine_blessing', 'hardening', 'kinetic_strike', + 'silencing_strike', 'culling_strike', 'lightning_strike', 'psycholeak', 'divine_blessing', 'hardening', 'kinetic_strike', 'mine_detector', } max_units = math.clamp(7 + current_new_game_plus, 7, 12) main:add(BuyScreen'buy_screen') @@ -2024,14 +2139,14 @@ function open_options(self) love.window.setMode(window_width, window_height) end} - self.screen_shake_button = Button{group = self.ui, x = gw/2 - 57, y = gh - 100, w = 110, force_update = true, button_text = '[bg10]screen shake: ' .. tostring(state.no_screen_shake and 'no' or 'yes'), + self.screen_shake_button = Button{group = self.ui, x = gw/2 - 57, y = gh - 100, w = 110, force_update = true, button_text = '[bg10]screen shake: ' .. tostring(state.no_screen_shake and 'no' or 'yes'), fg_color = 'bg10', bg_color = 'bg', action = function(b) ui_switch1:play{pitch = random:float(0.95, 1.05), volume = 0.5} state.no_screen_shake = not state.no_screen_shake b:set_text('screen shake: ' .. tostring(state.no_screen_shake and 'no' or 'yes')) end} - self.cooldown_snake_button = Button{group = self.ui, x = gw/2 + 75, y = gh - 100, w = 145, force_update = true, button_text = '[bg10]cooldowns on snake: ' .. tostring(state.cooldown_snake and 'yes' or 'no'), + self.cooldown_snake_button = Button{group = self.ui, x = gw/2 + 75, y = gh - 100, w = 145, force_update = true, button_text = '[bg10]cooldowns on snake: ' .. tostring(state.cooldown_snake and 'yes' or 'no'), fg_color = 'bg10', bg_color = 'bg', action = function(b) ui_switch1:play{pitch = random:float(0.95, 1.05), volume = 0.5} state.cooldown_snake = not state.cooldown_snake @@ -2045,7 +2160,7 @@ function open_options(self) b:set_text('arrow on snake: ' .. tostring(state.arrow_snake and 'yes' or 'no')) end} - self.screen_movement_button = Button{group = self.ui, x = gw/2 - 69, y = gh - 75, w = 135, force_update = true, button_text = '[bg10]screen movement: ' .. tostring(state.no_screen_movement and 'no' or 'yes'), + self.screen_movement_button = Button{group = self.ui, x = gw/2 - 69, y = gh - 75, w = 135, force_update = true, button_text = '[bg10]screen movement: ' .. tostring(state.no_screen_movement and 'no' or 'yes'), fg_color = 'bg10', bg_color = 'bg', action = function(b) ui_switch1:play{pitch = random:float(0.95, 1.05), volume = 0.5} state.no_screen_movement = not state.no_screen_movement diff --git a/mainmenu.lua b/mainmenu.lua index 8aee8b2..f542321 100644 --- a/mainmenu.lua +++ b/mainmenu.lua @@ -97,7 +97,7 @@ function MainMenu:on_enter(from) 'assassination', 'flying_daggers', 'ultimatum', 'magnify', 'echo_barrage', 'unleash', 'reinforce', 'payback', 'enchanted', 'freezing_field', 'burning_field', 'gravity_field', 'magnetism', 'insurance', 'dividends', 'berserking', 'unwavering_stance', 'unrelenting_stance', 'blessing', 'haste', 'divine_barrage', 'orbitism', 'psyker_orbs', 'psychosink', 'rearm', 'taunt', 'construct_instability', 'intimidation', 'vulnerability', 'temporal_chains', 'ceremonial_dagger', 'homing_barrage', 'critical_strike', 'noxious_strike', 'infesting_strike', 'burning_strike', 'lucky_strike', 'healing_strike', 'stunning_strike', - 'silencing_strike', 'culling_strike', 'lightning_strike', 'psycholeak', 'divine_blessing', 'hardening', 'kinetic_strike', + 'silencing_strike', 'culling_strike', 'lightning_strike', 'psycholeak', 'divine_blessing', 'hardening', 'kinetic_strike', 'mine_detector', } run_time = run.time or 0 gold = run.gold or 3 diff --git a/player.lua b/player.lua index f42df40..d96dc8a 100644 --- a/player.lua +++ b/player.lua @@ -14,7 +14,13 @@ function Player:init(args) self.classes = character_classes[self.character] self.damage_dealt = 0 + self.has_cooldown = false + self.can_use_aspd = false + if self.character == 'vagrant' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true 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) @@ -24,12 +30,18 @@ function Player:init(args) end, nil, nil, 'shoot') elseif self.character == 'swordsman' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true self.attack_sensor = Circle(self.x, self.y, 48) self.t:cooldown(3, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, function() self:attack(96) end, nil, nil, 'attack') elseif self.character == 'wizard' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true self.attack_sensor = Circle(self.x, self.y, 128) 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) @@ -39,6 +51,9 @@ function Player:init(args) end, nil, nil, 'shoot') elseif self.character == 'magician' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true 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() if self.magician_invulnerable then return end @@ -55,6 +70,9 @@ function Player:init(args) end elseif self.character == 'gambler' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true self.sorcerer_count = 0 local cast = function(pitch_a) local enemy = table.shuffle(main.current.main:get_objects_by_classes(main.current.enemies))[1] @@ -110,6 +128,9 @@ function Player:init(args) end, nil, nil, 'attack') elseif self.character == 'archer' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true self.attack_sensor = Circle(self.x, self.y, 160) self.t:cooldown(2, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, function() local closest_enemy = self:get_closest_object_in_shape(self.attack_sensor, main.current.enemies) @@ -119,6 +140,9 @@ function Player:init(args) end, nil, nil, 'shoot') elseif self.character == 'scout' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true 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) @@ -128,6 +152,9 @@ function Player:init(args) end, nil, nil, 'shoot') elseif self.character == 'thief' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true 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) @@ -137,6 +164,7 @@ function Player:init(args) end, nil, nil, 'shoot') elseif self.character == 'cleric' then + self.has_cooldown = true self.t:every(8, function() if self.level == 3 then for i = 1, 4 do @@ -186,6 +214,9 @@ function Player:init(args) end, nil, nil, 'heal') elseif self.character == 'arcanist' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true self.sorcerer_count = 0 self.attack_sensor = Circle(self.x, self.y, 128) self.t:cooldown(4, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, function() @@ -206,6 +237,8 @@ function Player:init(args) end, nil, nil, 'shoot') elseif self.character == 'artificer' then + self.has_cooldown = true + self.can_do_damage = true self.sorcerer_count = 0 self.attack_sensor = Circle(self.x, self.y, 96) self.t:every(6, function() @@ -233,6 +266,9 @@ function Player:init(args) end, nil, nil, 'spawn') elseif self.character == 'outlaw' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true 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() local closest_enemy = self:get_closest_object_in_shape(self.attack_sensor, main.current.enemies) @@ -242,12 +278,18 @@ function Player:init(args) end, nil, nil, 'shoot') elseif self.character == 'blade' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true self.attack_sensor = Circle(self.x, self.y, 64) self.t:cooldown(4, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, function() self:shoot() end, nil, nil, 'shoot') elseif self.character == 'elementor' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true self.attack_sensor = Circle(self.x, self.y, 128) self.t:cooldown(7, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, function() local enemy = self:get_random_object_in_shape(self.attack_sensor, main.current.enemies) @@ -257,6 +299,9 @@ function Player:init(args) end, nil, nil, 'attack') elseif self.character == 'psychic' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true self.sorcerer_count = 0 self.attack_sensor = Circle(self.x, self.y, self.level == 3 and 512 or 64) 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() @@ -290,6 +335,8 @@ function Player:init(args) end, nil, nil, 'attack') elseif self.character == 'saboteur' then + self.has_cooldown = true + self.can_do_damage = true 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) @@ -299,6 +346,8 @@ function Player:init(args) end, nil, nil, 'spawn') elseif self.character == 'bomber' then + self.has_cooldown = true + self.can_do_damage = true self.t:every(8, function() SpawnEffect{group = main.current.effects, x = self.x, y = self.y, color = self.color, action = function(x, y) Bomb{group = main.current.main, x = x, y = y, parent = self, level = self.level, conjurer_buff_m = self.conjurer_buff_m or 1} @@ -306,6 +355,7 @@ function Player:init(args) end, nil, nil, 'spawn') elseif self.character == 'stormweaver' then + self.has_cooldown = true self.t:every(8, function() stormweaver1:play{pitch = random:float(0.95, 1.05), volume = 0.5} local units = self:get_all_units() @@ -315,6 +365,11 @@ function Player:init(args) end, nil, nil, 'buff') elseif self.character == 'sage' then + self.has_cooldown = true + self.can_use_aspd = true + if self.level == 3 then + self.can_do_damage = true + end self.attack_sensor = Circle(self.x, self.y, 96) self.t:cooldown(9, 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) @@ -324,6 +379,9 @@ function Player:init(args) end, nil, nil, 'shoot') elseif self.character == 'cannoneer' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true self.attack_sensor = Circle(self.x, self.y, 128) self.t:cooldown(6, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, function() local closest_enemy = self:get_closest_object_in_shape(self.attack_sensor, main.current.enemies) @@ -333,6 +391,9 @@ function Player:init(args) end, nil, nil, 'shoot') elseif self.character == 'vulcanist' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true self.sorcerer_count = 0 self.attack_sensor = Circle(self.x, self.y, 128) self.t:every(12, function() @@ -373,6 +434,9 @@ function Player:init(args) end, nil, nil, 'attack') elseif self.character == 'dual_gunner' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true self.dg_counter = 0 self.attack_sensor = Circle(self.x, self.y, 96) self.gun_kata_sensor = Circle(self.x, self.y, 160) @@ -384,6 +448,9 @@ function Player:init(args) end, nil, nil, 'shoot') elseif self.character == 'hunter' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true self.attack_sensor = Circle(self.x, self.y, 160) self.t:cooldown(2, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, function() local closest_enemy = self:get_closest_object_in_shape(self.attack_sensor, main.current.enemies) @@ -398,6 +465,9 @@ function Player:init(args) end elseif self.character == 'spellblade' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true self.t:every(2, function() self:shoot(random:float(0, 2*math.pi)) end, nil, nil, 'shoot') @@ -407,6 +477,8 @@ function Player:init(args) self.last_heal_time = love.timer.getTime() elseif self.character == 'engineer' then + self.has_cooldown = true + self.can_do_damage = true self.t:every(8, function() SpawnEffect{group = main.current.effects, x = self.x, y = self.y, color = orange[0], action = function(x, y) Turret{group = main.current.main, x = x, y = y, parent = self} @@ -431,6 +503,9 @@ function Player:init(args) end elseif self.character == 'plague_doctor' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true self.t:every(5, function() self:dot_attack(24, {duration = 12, plague_doctor_unmovable = true}) end, nil, nil, 'attack') @@ -442,6 +517,9 @@ function Player:init(args) end elseif self.character == 'witch' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true self.sorcerer_count = 0 self.t:every(4, function() self:dot_attack(42, {duration = random:float(12, 16)}) @@ -458,18 +536,27 @@ function Player:init(args) end, nil, nil, 'attack') elseif self.character == 'barbarian' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true self.attack_sensor = Circle(self.x, self.y, 48) self.t:cooldown(8, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, function() self:attack(96, {stun = 4}) end, nil, nil, 'attack') elseif self.character == 'juggernaut' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true self.attack_sensor = Circle(self.x, self.y, 64) self.t:cooldown(8, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, function() self:attack(128, {juggernaut_push = true}) end, nil, nil, 'attack') elseif self.character == 'lich' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true self.attack_sensor = Circle(self.x, self.y, 128) self.t:cooldown(4, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, function() local closest_enemy = self:get_closest_object_in_shape(self.attack_sensor, main.current.enemies) @@ -479,16 +566,21 @@ function Player:init(args) end, nil, nil, 'shoot') elseif self.character == 'cryomancer' then + self.can_do_damage = true self.t:after(0.01, function() self.dot_area = DotArea{group = main.current.effects, x = self.x, y = self.y, rs = self.area_size_m*72, color = self.color, dmg = self.area_dmg_m*self.dmg, character = self.character, level = self.level, parent = self} end) elseif self.character == 'pyromancer' then + self.can_do_damage = true self.t:after(0.01, function() self.dot_area = DotArea{group = main.current.effects, x = self.x, y = self.y, rs = self.area_size_m*48, color = self.color, dmg = self.area_dmg_m*self.dmg, character = self.character, level = self.level, parent = self} end) elseif self.character == 'corruptor' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true self.attack_sensor = Circle(self.x, self.y, 160) self.t:cooldown(2, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, function() local closest_enemy = self:get_closest_object_in_shape(self.attack_sensor, main.current.enemies) @@ -498,6 +590,9 @@ function Player:init(args) end, nil, nil, 'shoot') elseif self.character == 'beastmaster' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true self.attack_sensor = Circle(self.x, self.y, 160) self.t:cooldown(2, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, function() local closest_enemy = self:get_closest_object_in_shape(self.attack_sensor, main.current.enemies) @@ -507,6 +602,9 @@ function Player:init(args) end, nil, nil, 'shoot') elseif self.character == 'launcher' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true self.attack_sensor = Circle(self.x, self.y, 96) 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} @@ -523,6 +621,8 @@ function Player:init(args) end, nil, nil, 'attack') elseif self.character == 'jester' then + self.has_cooldown = true + self.can_use_aspd = true 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() @@ -539,6 +639,9 @@ function Player:init(args) end, nil, nil, 'attack') elseif self.character == 'usurer' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true 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() @@ -554,6 +657,8 @@ function Player:init(args) end, nil, nil, 'attack') elseif self.character == 'silencer' then + self.has_cooldown = true + self.can_use_aspd = true self.sorcerer_count = 0 self.attack_sensor = Circle(self.x, self.y, 96) self.wide_attack_sensor = Circle(self.x, self.y, 128) @@ -565,6 +670,7 @@ function Player:init(args) for _, enemy in ipairs(enemies) do enemy:curse('silencer', 6*(self.hex_duration_m or 1), self.level == 3, self) if self.level == 3 then + self.can_do_damage = true enemy:apply_dot(self.dmg*(self.dot_dmg_m or 1)*(main.current.chronomancer_dot or 1), 6*(self.hex_duration_m or 1)) end HitCircle{group = main.current.effects, x = self.x, y = self.y, rs = 6, color = blue2[0], duration = 0.1} @@ -585,6 +691,9 @@ function Player:init(args) end, nil, nil, 'attack') elseif self.character == 'assassin' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true 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) @@ -594,6 +703,8 @@ function Player:init(args) end, nil, nil, 'shoot') elseif self.character == 'host' then + self.has_cooldown = true + self.can_do_damage = true if self.level == 3 then self.t:every(1, function() critter1:play{pitch = random:float(0.95, 1.05), volume = 0.35} @@ -609,16 +720,21 @@ function Player:init(args) end elseif self.character == 'carver' then + self.has_cooldown = true self.t:every(16, function() Tree{group = main.current.main, x = self.x, y = self.y, color = self.color, parent = self, level = self.level} end, nil, nil, 'spawn') elseif self.character == 'sentry' then + self.has_cooldown = true + self.can_do_damage = true self.t:every(7, function() Sentry{group = main.current.main, x = self.x, y = self.y, color = self.color, parent = self, level = self.level} end, nil, nil, 'spawn') elseif self.character == 'bane' then + self.has_cooldown = true + self.can_use_aspd = true 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() @@ -633,6 +749,11 @@ function Player:init(args) end, nil, nil, 'attack') elseif self.character == 'psykino' then + self.has_cooldown = true + self.can_use_aspd = true + if self.level == 3 then + self.can_do_damage = true + end self.t:every(4, function() local center_enemy = self:get_random_object_in_shape(Circle(self.x, self.y, 160), main.current.enemies) if center_enemy then @@ -641,6 +762,9 @@ function Player:init(args) end, nil, nil, 'attack') elseif self.character == 'barrager' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true self.barrager_counter = 0 self.attack_sensor = Circle(self.x, self.y, 128) self.t:cooldown(4, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, function() @@ -664,6 +788,9 @@ function Player:init(args) end, nil, nil, 'shoot') elseif self.character == 'highlander' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true self.attack_sensor = Circle(self.x, self.y, 36) self.t:cooldown(4, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, function() if self.level == 3 then @@ -676,19 +803,13 @@ function Player:init(args) end, nil, nil, 'attack') elseif self.character == 'fairy' then + self.has_cooldown = true self.t:every(6, function() + local units = self:get_all_units() + units = table.select(units, function(v) return v.can_use_aspd end) if self.level == 3 then - local units = self:get_all_units() - local unit_1 = random:table(units) - local runs = 0 - if unit_1 then - while table.any(non_attacking_characters, function(v) return v == unit_1.character end) and runs < 1000 do unit_1 = random:table(units); runs = runs + 1 end - end + local unit_1 = random:table_remove(units) local unit_2 = random:table(units) - local runs = 0 - if unit_2 then - while table.any(non_attacking_characters, function(v) return v == unit_2.character end) and runs < 1000 do unit_2 = random:table(units); runs = runs + 1 end - end if unit_1 then unit_1.fairy_aspd_m = 3 unit_1.fairyd = true @@ -718,9 +839,7 @@ function Player:init(args) end else - local unit = random:table(self:get_all_units()) - local runs = 0 - while table.any(non_attacking_characters, function(v) return v == unit.character end) and runs < 1000 do unit = random:table(self:get_all_units()); runs = runs + 1 end + local unit = random:table(units) if unit then unit.fairyd = true unit.fairy_aspd_m = 2 @@ -745,6 +864,7 @@ function Player:init(args) end, nil, nil, 'heal') elseif self.character == 'warden' then + self.has_cooldown = true self.sorcerer_count = 0 self.t:every(12, function() local ward = function() @@ -804,6 +924,7 @@ function Player:init(args) end, nil, nil, 'buff') elseif self.character == 'priest' then + self.has_cooldown = true if self.level == 3 then self.t:after(0.01, function() local all_units = self:get_all_units() @@ -833,6 +954,8 @@ function Player:init(args) end, nil, nil, 'heal') elseif self.character == 'infestor' then + self.has_cooldown = true + self.can_use_aspd = true 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() @@ -847,6 +970,7 @@ function Player:init(args) end, nil, nil, 'attack') elseif self.character == 'flagellant' then + self.has_cooldown = true self.t:every(8, function() buff1:play{pitch = random:float(0.95, 1.05), volume = 0.3} flagellant1:play{pitch = random:float(0.95, 1.05), volume = 0.4} @@ -866,6 +990,215 @@ function Player:init(args) end end end, nil, nil, 'buff') + + elseif self.character == 'astralist' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true + self.astral_count = 0 + self.attack_sensor = Circle(self.x, self.y, self.level == 3 and 512 or 128) + 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), {v = 25, pierce = 2, homing = (self.level == 3)}) + end + end, nil, nil, 'shoot') + + elseif self.character == 'warmonger' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true + if main.current.main.world then + self.attack_sensor = Circle(self.x, self.y, 128) + self.strike = AirStrikeIndicator{group = main.current.main, color = self.color} + local air_strikes = main.current.shop_level or 1 + self.t:after(1, function() + if not main.current.main.world then return end + self.t:cooldown(5, function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, function() + alert2:play{pitch = random:float(0.95, 1.05), volume = 0.3} + thunder2:play{pitch = random:float(0.95, 1.05), volume = 0.5} + local x_pos = 0 + local y_pos = self.strike.y + local rot = 0 + local margin = 32 + local inc = gw / air_strikes + local i = 0 + self.t:every(0.2, function() + camera:shake(4, 0.5) + _G[random:table{'cannoneer1', 'cannoneer2', 'cannon_hit_wall1'}]:play{pitch = random:float(0.95, 1.05), volume = 0.25} + _G[random:table{'fire1', 'fire2', 'fire3'}]:play{pitch = random:float(0.95, 1.05), volume = 0.25} + x_pos = i*inc + random:int(margin, inc - 2 * margin) + if self.strike.is_going_left then x_pos = gw - x_pos end + if y_pos < 50 then y_pos = y_pos + 10*random:int(0, 3) + elseif y_pos > gh - 50 then y_pos = y_pos + 10*random:int(-3, 0) + else y_pos = y_pos + 10*random:int(-3, 3) end + rot = random:float(-0.5, 0.5) + Area{group = main.current.effects, x = x_pos, y = y_pos, r = rot, w = self.area_size_m*64, color = self.color, dmg = (self.area_dmg_m or 1)*self.dmg, + character = self.character, level = self.level, parent = self, void_rift = self.void_rift, echo_barrage = self.echo_barrage} + i = i + 1 + end, air_strikes, function() self.t:after(1, function() self.strike:go_next() end) end) + end, nil, nil, 'attack') + end) + end + + elseif self.character == 'avenger' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true + self.void_rift = true + self.sorcerer_count = 0 + self.attack_sensor = Circle(self.x, self.y, 64) + local strike = 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 = 2, knockback = 10}) + end + end + local increment_repeat = function() + 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:sorcerer_repeat() + self.t:after(0.1, function() + strike() + end) + end + end + end + local rift_nova = function(x_pos, y_pos) + self.t:after(0.01, function() + ui_transition1:play{pitch = random:float(0.8, 1.2), volume = 0.5} + if not main.current.main.world then return end + local rift = DotArea{group = main.current.effects, x = x_pos, y = y_pos, rs = self.area_size_m*16, color = self.color, dmg = self.area_dmg_m*self.dmg*(self.dot_dmg_m or 1), + void_rift = true, duration = 3, parent = self} + self.t:tween(2.7, rift.shape, {rs = self.area_size_m*48}, math.cubic_out, function() + self.t:tween(0.3, rift.shape, {rs = 0}, math.cubic_in, function() + arcane2:play{pitch = random:float(0.7, 1.3), volume = 0.25} + local r = random:float(0, math.pi) + for i = 1, 2 do + Projectile{group = main.current.main, x = x_pos, y = y_pos, v = 500, r = r, color = self.color, dmg = self.dmg, character = self.character, parent = self, level = self.level, pierce = 4, knockback = 20} + r = r + math.pi + end + end) + end) + end) + end + self.on_enemy_hit = function(enemy) + -- void rift creation is handled elsewhere + if random:bool(20) then + self.t:after(0.01, function() + if not main.current.main.world then return end + HealingOrb{group = main.current.main, x = enemy.x, y = enemy.y} + end) + end + end + self.on_enemy_kill = function(enemy) + self.t:after(0.1, function() + strike() + increment_repeat() + end) + end + self.on_heal_pickup = function(heal) + rift_nova(heal.x, heal.y) + increment_repeat() + end + self.t:cooldown((self.level == 3 and 0.75 or 1.5), function() local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies); return enemies and #enemies > 0 end, function() + strike() + increment_repeat() + end, nil, nil, 'shoot') + + elseif self.character == 'herald' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true + self.attack_sensor = Circle(self.x, self.y, 32) + + if self.level < 3 then + self.t:cooldown(3, + function() + local enemies = self:get_objects_in_shape(self.attack_sensor, main.current.enemies) + return enemies and #enemies > 0 + end, + function() + self:attack(96) + end, nil, nil, 'attack') + else + self.t:cooldown(3, + function() + local enemies = main.current.main:get_objects_by_classes(main.current.enemies) + return enemies and #enemies > 0 + end, + function() + local units = self:get_all_units() + local enemies = main.current.main:get_objects_by_classes(main.current.enemies) + local wrapper = {r = self.r, parent = self} + local resonance_m = (self.resonance == 1 and 0.03) or (self.resonance == 2 and 0.05) or (self.resonance == 3 and 0.07) or 0 + local attack_d = self.area_dmg_m * self.dmg + 0.05 * self.hp + attack_d = attack_d + attack_d * resonance_m * #enemies + + self:hit(0.1 * self.hp) + for _, unit in ipairs(units) do + unit.herald_hp_m = unit.herald_hp_m and (unit.herald_hp_m + 0.01) or 1.01 + end + + HitCircle{group = main.current.effects, x = self.x, y = self.y, rs = 6, color = fg[0], duration = 0.1} + psychic1:play{pitch = random:float(0.9, 1.1), volume = 0.4} + for _, enemy in ipairs(enemies) do + HitParticle{group = main.current.effects, x = enemy.x, y = enemy.y, color = self.color} + HitParticle{group = main.current.effects, x = enemy.x, y = enemy.y, color = enemy.color} + enemy:hit(attack_d, wrapper) + end + + if self.echo_barrage then + local x_pos = self.x + local y_pos = self.y + local rot = self.r + if random:bool((self.echo_barrage == 1 and 10) or (self.echo_barrage == 2 and 20) or (self.echo_barrage == 3 and 30)) then + self.t:every(0.3, function() + _G[random:table{'cannoneer1', 'cannoneer2'}]:play{pitch = random:float(0.95, 1.05), volume = 0.5} + Area{group = main.current.effects, x = x_pos + random:float(-32, 32), y = y_pos + random:float(-32, 32), r = rot + random:float(0, 2*math.pi), w = self.area_size_m*48, color = self.color, + dmg = 0.5*attack_d, character = self.character, level = self.level, parent = self, echo_barrage_area = true} + end, self.echo_barrage) + end + end + end, nil, nil, 'attack') + end + self.on_enemy_kill = function(enemy) + if (self.herald_hp_m) then + self.herald_hp_m = self.herald_hp_m + ((self.level == 3 and 0.05) or 0.01) + else + self.herald_hp_m = (self.level == 3 and 1.05) or 1.01 + end + end + + elseif self.character == 'technomancer' then + self.has_cooldown = true + self.can_use_aspd = true + self.can_do_damage = true + self.drones = {} + self.power_up_duration = 4 + self.t:after(0.1, function() + if not main.current.main.world then return end + self.drones[1] = AttackDrone{group = main.current.main, parent = self, i = 1} + self.drones[2] = AttackDrone{group = main.current.main, parent = self, i = 2} + if self.level == 3 then + self.drones[3] = AttackDrone{group = main.current.main, parent = self, i = 3} + self.drones[4] = AttackDrone{group = main.current.main, parent = self, i = 4} + end + end) + self.t:every(1.73, function() + _G[random:table{'turret_hit_wall1', 'turret_hit_wall2'}]:play{pitch = random:float(0.9, 1.1), volume = 0.2} + disarm1:play{pitch = random:float(0.7, 1.3), volume = 0.15} + HitCircle{group = main.current.effects, x = self.x + 0.8*self.shape.w*math.cos(self.r), y = self.y + 0.8*self.shape.w*math.sin(self.r), rs = 6} + Projectile{group = main.current.main, x = self.x + 1.6*self.shape.w*math.cos(self.r), y = self.y + 1.6*self.shape.w*math.sin(self.r), v = 250, r = self.r, color = self.color, dmg = self.dmg, character = self.character, + parent = self, level = self.level, ricochet = self.ricochet, exploder = self.exploder} + for _, drone in ipairs(self.drones) do + drone:shoot() + end + end, nil, nil, 'shoot') + + -- end of character setup end self:calculate_stats(true) @@ -921,16 +1254,9 @@ function Player:init(args) if self.leader and self.awakening then main.current.t:after(0.1, function() local units = self:get_all_units() - local mages = {} - for _, unit in ipairs(units) do - if table.any(unit.classes, function(v) return v == 'mage' end) then - table.insert(mages, unit) - end - end - local mage = random:table(mages) + units = table.select(units, function(v) return self:is_class(v, 'mage') and v.can_use_aspd end) + local mage = random:table(units) if mage then - local runs = 0 - while table.any(non_attacking_characters, function(v) return v == mage.character end) and runs < 1000 do mage = random:table(mages); runs = runs + 1 end mage.awakening_aspd_m = (self.awakening == 1 and 1.5) or (self.awakening == 2 and 1.75) or (self.awakening == 3 and 2) mage.awakening_dmg_m = (self.awakening == 1 and 1.5) or (self.awakening == 2 and 1.75) or (self.awakening == 3 and 2) end @@ -1002,21 +1328,24 @@ function Player:init(args) end) end - if self.enchanted then + if self.leader and self.enchanted then main.current.t:after(0.1, function() local units = self:get_all_units() local enchanter_amount = 0 - for _, unit in ipairs(units) do - if table.any(unit.classes, function(v) return v == 'enchanter' end) then + + -- Count sorceror units and filter out non attacking units at the same time. + for i = #units, 1, -1 do + if self:is_class(units[i], 'enchanter') then enchanter_amount = enchanter_amount + 1 end + if not units[i].can_use_aspd then + table.remove(units, i) + end end - + if enchanter_amount >= 2 then local unit = random:table(units) - local runs = 0 if unit then - while table.any(non_attacking_characters, function(v) return v == unit.character end) and runs < 1000 do unit = random:table(units); runs = runs + 1 end unit.enchanted_aspd_m = (self.enchanted == 1 and 1.33) or (self.enchanted == 2 and 1.66) or (self.enchanted == 3 and 1.99) end end @@ -1333,13 +1662,13 @@ function Player:update(dt) end self.buff_def_a = (self.warrior_def_a or 0) - self.buff_aspd_m = (self.chronomancer_aspd_m or 1)*(self.vagrant_aspd_m or 1)*(self.outlaw_aspd_m or 1)*(self.fairy_aspd_m or 1)*(self.psyker_aspd_m or 1)*(self.chronomancy_aspd_m or 1)*(self.awakening_aspd_m or 1)*(self.berserking_aspd_m or 1)*(self.reinforce_aspd_m or 1)*(self.squire_aspd_m or 1)*(self.speed_3_aspd_m or 1)*(self.last_stand_aspd_m or 1)*(self.enchanted_aspd_m or 1)*(self.explorer_aspd_m or 1)*(self.magician_aspd_m or 1) + self.buff_aspd_m = (self.chronomancer_aspd_m or 1)*(self.vagrant_aspd_m or 1)*(self.outlaw_aspd_m or 1)*(self.fairy_aspd_m or 1)*(self.psyker_aspd_m or 1)*(self.chronomancy_aspd_m or 1)*(self.awakening_aspd_m or 1)*(self.berserking_aspd_m or 1)*(self.reinforce_aspd_m or 1)*(self.squire_aspd_m or 1)*(self.speed_3_aspd_m or 1)*(self.last_stand_aspd_m or 1)*(self.enchanted_aspd_m or 1)*(self.explorer_aspd_m or 1)*(self.magician_aspd_m or 1)*(self.technomancer_aspd_m or 1) self.buff_dmg_m = (self.squire_dmg_m or 1)*(self.vagrant_dmg_m or 1)*(self.enchanter_dmg_m or 1)*(self.swordsman_dmg_m or 1)*(self.flagellant_dmg_m or 1)*(self.psyker_dmg_m or 1)*(self.ballista_dmg_m or 1)*(self.awakening_dmg_m or 1)*(self.reinforce_dmg_m or 1)*(self.payback_dmg_m or 1)*(self.immolation_dmg_m or 1)*(self.damage_4_dmg_m or 1)*(self.offensive_stance_dmg_m or 1)*(self.last_stand_dmg_m or 1)*(self.dividends_dmg_m or 1)*(self.explorer_dmg_m or 1) self.buff_def_m = (self.squire_def_m or 1)*(self.ouroboros_def_m or 1)*(self.unwavering_stance_def_m or 1)*(self.reinforce_def_m or 1)*(self.defensive_stance_def_m or 1)*(self.last_stand_def_m or 1)*(self.unrelenting_stance_def_m or 1)*(self.hardening_def_m or 1) self.buff_area_size_m = (self.nuker_area_size_m or 1)*(self.magnify_area_size_m or 1)*(self.unleash_area_size_m or 1)*(self.last_stand_area_size_m or 1) self.buff_area_dmg_m = (self.nuker_area_dmg_m or 1)*(self.amplify_area_dmg_m or 1)*(self.unleash_area_dmg_m or 1)*(self.last_stand_area_dmg_m or 1) self.buff_mvspd_m = (self.wall_rider_mvspd_m or 1)*(self.centipede_mvspd_m or 1)*(self.squire_mvspd_m or 1)*(self.last_stand_mvspd_m or 1)*(self.haste_mvspd_m or 1) - self.buff_hp_m = (self.flagellant_hp_m or 1) + self.buff_hp_m = (self.flagellant_hp_m or 1)*(self.herald_hp_m or 1) self:calculate_stats() if self.attack_sensor then self.attack_sensor:move_to(self.x, self.y) end @@ -1677,6 +2006,15 @@ function Player:hit(damage, from_undead) end if self.dot_area then self.dot_area.dead = true; self.dot_area = nil end + + if self.character == 'warmonger' then + self.strike.dead = true + elseif self.character == 'technomancer' then + for _, drone in ipairs(self.drones) do + drone.dead = true + end + end + end end end @@ -1730,6 +2068,11 @@ function Player:chain_infuse(duration) end +function Player:is_class(unit, class) + return table.any(unit.classes, function(v) return v == class end) +end + + function Player:get_all_units() local followers local leader = (self.leader and self) or self.parent @@ -1811,6 +2154,10 @@ function Player:shoot(r, mods) end end + if self.character == 'avenger' then + dmg_m = self.level == 3 and 4 or 2 + 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() @@ -1874,6 +2221,17 @@ function Player:shoot(r, mods) end end, 20) end + + elseif self.character == 'astralist' and self.level == 3 then + r = (6 + self.astral_count) * math.pi / 12 + self.astral_count = (self.astral_count + 1) % 8 + HitCircle{group = main.current.effects, x = gw/2, y = gh/2, rs = 6} + for i = 1, 3 do + local t = {group = main.current.main, x = gw/2 + 11*math.cos(r), y = gh/2 + 11*math.sin(r), nil, r = r, color = self.color, dmg = self.dmg*dmg_m, crit = crit, character = self.character, parent = self, level = self.level} + Projectile(table.merge(t, mods or {})) + r = r + 2*math.pi/3 + end + else HitCircle{group = main.current.effects, x = self.x + 0.8*self.shape.w*math.cos(r), y = self.y + 0.8*self.shape.w*math.sin(r), rs = 6} local t = {group = main.current.main, x = self.x + 1.6*self.shape.w*math.cos(r), y = self.y + 1.6*self.shape.w*math.sin(r), v = 250, r = r, color = self.color, dmg = self.dmg*dmg_m, crit = crit, character = self.character, @@ -1881,14 +2239,14 @@ function Player:shoot(r, mods) Projectile(table.merge(t, mods or {})) end - if self.character == 'vagrant' or self.character == 'artificer' then + if self.character == 'vagrant' or self.character == 'artificer' or self.character == 'avenger' then shoot1:play{pitch = random:float(0.95, 1.05), volume = 0.2} elseif self.character == 'dual_gunner' then dual_gunner1:play{pitch = random:float(0.95, 1.05), volume = 0.3} 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' or self.character == 'arcanist' then + elseif self.character == 'wizard' or self.character == 'lich' or self.character == 'arcanist' or self.character == 'astralist' 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' or self.character == 'thief' then @@ -1908,6 +2266,10 @@ function Player:shoot(r, mods) arcane1:play{pitch = random:float(0.95, 1.05), volume = 0.3} end + if self.character == 'astralist' 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 @@ -1922,7 +2284,7 @@ function Player:attack(area, mods) character = self.character, level = self.level, parent = self} Area(table.merge(t, mods)) - if self.character == 'swordsman' or self.character == 'barbarian' or self.character == 'juggernaut' or self.character == 'highlander' then + if self.character == 'swordsman' or self.character == 'barbarian' or self.character == 'juggernaut' or self.character == 'highlander' or self.character == 'herald' then _G[random:table{'swordsman1', 'swordsman2'}]:play{pitch = random:float(0.9, 1.1), volume = 0.75} elseif self.character == 'elementor' then elementor1:play{pitch = random:float(0.9, 1.1), volume = 0.5} @@ -2211,10 +2573,20 @@ function Projectile:die(x, y, r, n) if self.level == 3 then self.parent.t:every(0.3, function() _G[random:table{'cannoneer1', 'cannoneer2'}]:play{pitch = random:float(0.95, 1.05), volume = 0.5} - Area{group = main.current.effects, x = self.x + random:float(-32, 32), y = self.y + random:float(-32, 32), r = self.r + random:float(0, 2*math.pi), w = self.parent.area_size_m*48, color = self.color, + Area{group = main.current.effects, x = self.x + random:float(-32, 32), y = self.y + random:float(-32, 32), r = self.r + random:float(0, 2*math.pi), w = self.parent.area_size_m*48, color = self.color, dmg = 0.5*self.parent.area_dmg_m*self.dmg, character = self.character, level = self.level, parent = self, void_rift = self.parent.void_rift, echo_barrage = self.parent.echo_barrage} end, 7) end + elseif self.character == 'technomancer' and self.exploder then + trigger:after(0.01, function() + shoot1:play{pitch = random:float(0.95, 1.05), volume = 0.4} + cannoneer1:play{pitch = random:float(0.95, 1.05), volume = 0.4} + local n = math.floor(8 + current_new_game_plus*1.5) + for i = 1, n do + Projectile{group = main.current.main, x = self.x, y = self.y, color = self.color, r = (i-1)*math.pi/(n/2), v = 300, dmg = self.parent.dmg, + character = self.character, parent = self.parent, level = self.level} + end + end) end end @@ -2229,7 +2601,7 @@ function Projectile:on_collision_enter(other, contact) else r = 0 end if other:is(Wall) then - if self.character == 'archer' or self.character == 'hunter' or self.character == 'barrage' or self.character == 'barrager' or self.character == 'sentry' then + if self.character == 'archer' or self.character == 'hunter' or self.character == 'barrage' or self.character == 'barrager' or self.character == 'sentry' or self.character == 'technomancer' then if self.ricochet <= 0 then self:die(x, y, r, 0) WallArrow{group = main.current.main, x = x, y = y, r = self.r, color = self.color} @@ -2259,7 +2631,7 @@ function Projectile:on_collision_enter(other, contact) 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 == 'witch' then + elseif self.character == 'wizard' or self.character == 'lich' or self.character == 'arcanist' or self.character == 'arcanist_projectile' or self.character == 'witch' or self.character == 'astralist' or self.character == 'avenger' 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 @@ -2315,13 +2687,13 @@ 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' or self.character == 'miner' or self.character == 'thief' or - self.character == 'psyker' or self.character == 'sentry' then + self.character == 'jester' or self.character == 'assassin' or self.character == 'barrager' or self.character == 'beastmaster' or self.character == 'witch' or self.character == 'miner' or self.character == 'thief' or + self.character == 'psyker' or self.character == 'sentry' or self.character == 'technomancer' then hit2:play{pitch = random:float(0.95, 1.05), volume = 0.35} if self.character == 'spellblade' or self.character == 'psyker' then magic_area1:play{pitch = random:float(0.95, 1.05), volume = 0.15} end - elseif self.character == 'wizard' or self.character == 'lich' or self.character == 'arcanist' then + elseif self.character == 'wizard' or self.character == 'lich' or self.character == 'arcanist' or self.character == 'astralist' or self.character == 'avenger' 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} @@ -2357,6 +2729,13 @@ function Projectile:on_trigger_enter(other, contact) other:apply_dot((self.crit and 4*self.dmg or self.dmg/2)*(self.dot_dmg_m or 1)*(main.current.chronomancer_dot or 1), 3) end + if self.character == 'astralist' then + other:slow(0.2, 5) + if self.level == 3 then + other:apply_dot(self.dmg*(self.dot_dmg_m or 1)*(main.current.chronomancer_dot or 1), 2) + end + end + if self.parent and self.parent.chain_infused then local units = self.parent:get_all_units() local stormweaver_level = 0 @@ -2374,7 +2753,7 @@ function Projectile:on_trigger_enter(other, contact) if dst then dst:hit(0.2*self.dmg*(self.distance_dmg_m or 1)) LightningLine{group = main.current.effects, src = src, dst = dst} - src = dst + src = dst end end end @@ -2392,7 +2771,7 @@ function Projectile:on_trigger_enter(other, contact) if dst then dst:hit(0.33*((self.parent.lightning_strike == 1 and 0.6) or (self.parent.lightning_strike == 2 and 0.8) or (self.parent.lightning_strike == 3 and 1))*self.dmg*(self.distance_dmg_m or 1)) LightningLine{group = main.current.effects, src = src, dst = dst} - src = dst + src = dst end end end) @@ -2416,12 +2795,16 @@ function Projectile:on_trigger_enter(other, contact) if self.parent and self.parent.explosive_arrow and table.any(self.parent.classes, function(v) return v == 'ranger' end) then if random:bool((self.parent.explosive_arrow == 1 and 10) or (self.parent.explosive_arrow == 2 and 20) or (self.parent.explosive_arrow == 3 and 30)) then _G[random:table{'cannoneer1', 'cannoneer2'}]:play{pitch = random:float(0.95, 1.05), volume = 0.5} - Area{group = main.current.effects, x = self.x, y = self.y, r = self.r + random:float(0, 2*math.pi), w = self.parent.area_size_m*32, color = self.color, + Area{group = main.current.effects, x = self.x, y = self.y, r = self.r + random:float(0, 2*math.pi), w = self.parent.area_size_m*32, color = self.color, dmg = ((self.parent.explosive_arrow == 1 and 0.1) or (self.parent.explosive_arrow == 2 and 0.2) or (self.parent.explosive_arrow == 3 and 0.3))*self.parent.area_dmg_m*self.dmg, character = self.character, level = self.level, parent = self, void_rift = self.parent.void_rift, echo_barrage = self.parent.echo_barrage} end end + if self.character == 'avenger' then + self.parent.on_enemy_hit(other) + end + if self.parent and self.parent.void_rift and table.any(self.parent.classes, function(v) return v == 'mage' or v == 'nuker' or v == 'voider' end) then if random:bool(20) then DotArea{group = main.current.effects, x = self.x, y = self.y, rs = self.parent.area_size_m*24, color = self.color, dmg = self.parent.area_dmg_m*self.dmg*(self.parent.dot_dmg_m or 1), void_rift = true, duration = 1} @@ -2471,7 +2854,7 @@ function Area:init(args) for i = 1, 1 do HitParticle{group = main.current.effects, x = enemy.x, y = enemy.y, color = enemy.color} end if self.character == 'wizard' or self.character == 'magician' or self.character == 'elementor' or self.character == 'psychic' then magic_hit1:play{pitch = random:float(0.95, 1.05), volume = 0.5} - elseif self.character == 'swordsman' or self.character == 'barbarian' or self.character == 'juggernaut' or self.character == 'highlander' then + elseif self.character == 'swordsman' or self.character == 'barbarian' or self.character == 'juggernaut' or self.character == 'highlander' or self.character == 'herald' then hit2:play{pitch = random:float(0.95, 1.05), volume = 0.35} elseif self.character == 'blade' then blade_hit1:play{pitch = random:float(0.9, 1.1), volume = 0.35} @@ -2508,7 +2891,7 @@ function Area:init(args) if random:bool((p.echo_barrage == 1 and 10) or (p.echo_barrage == 2 and 20) or (p.echo_barrage == 3 and 30)) then p.t:every(0.3, function() _G[random:table{'cannoneer1', 'cannoneer2'}]:play{pitch = random:float(0.95, 1.05), volume = 0.5} - Area{group = main.current.effects, x = self.x + random:float(-32, 32), y = self.y + random:float(-32, 32), r = self.r + random:float(0, 2*math.pi), w = p.area_size_m*48, color = p.color, + Area{group = main.current.effects, x = self.x + random:float(-32, 32), y = self.y + random:float(-32, 32), r = self.r + random:float(0, 2*math.pi), w = p.area_size_m*48, color = p.color, dmg = 0.5*p.area_dmg_m*self.dmg, character = self.character, level = p.level, parent = p, echo_barrage_area = true} end, p.echo_barrage) end @@ -2516,7 +2899,7 @@ function Area:init(args) else if self.parent.void_rift and table.any(self.parent.classes, function(v) return v == 'mage' or v == 'nuker' or v == 'voider' end) then if random:bool(20) then - DotArea{group = main.current.effects, x = self.x, y = self.y, rs = self.parent.area_size_m*24, color = self.color, dmg = self.parent.area_dmg_m*self.dmg*(self.parent.dot_dmg_m or 1), + DotArea{group = main.current.effects, x = self.x, y = self.y, rs = self.parent.area_size_m*24, color = self.color, dmg = self.parent.area_dmg_m*(self.dmg or self.parent.dmg)*(self.parent.dot_dmg_m or 1), void_rift = true, duration = 1, parent = self.parent} end end @@ -2524,7 +2907,7 @@ function Area:init(args) if random:bool((self.parent.echo_barrage == 1 and 10) or (self.parent.echo_barrage == 2 and 20) or (self.parent.echo_barrage == 3 and 30)) then self.parent.t:every(0.3, function() _G[random:table{'cannoneer1', 'cannoneer2'}]:play{pitch = random:float(0.95, 1.05), volume = 0.5} - Area{group = main.current.effects, x = self.x + random:float(-32, 32), y = self.y + random:float(-32, 32), r = self.r + random:float(0, 2*math.pi), w = self.parent.area_size_m*48, color = self.parent.color, + Area{group = main.current.effects, x = self.x + random:float(-32, 32), y = self.y + random:float(-32, 32), r = self.r + random:float(0, 2*math.pi), w = self.parent.area_size_m*48, color = self.parent.color, dmg = 0.5*self.parent.area_dmg_m*(self.dmg or self.parent.dmg), character = self.character, level = self.parent.level, parent = self.parent, echo_barrage_area = true} end, self.parent.echo_barrage) end @@ -2738,7 +3121,7 @@ ForceArea:implement(Physics) function ForceArea:init(args) self:init_game_object(args) self.shape = Circle(self.x, self.y, self.rs) - + self.color = fg[0] self.color_transparent = Color(args.color.r, args.color.g, args.color.b, 0.08) self.rs = 0 @@ -2984,7 +3367,7 @@ function ForceField:init(args) self:init_game_object(args) self:set_as_circle((self.parent and self.parent.magnify and (self.parent.magnify == 1 and 14) or (self.parent.magnify == 2 and 17) or (self.parent.magnify == 3 and 20)) or 12, 'static', 'force_field') self.hfx:add('hit', 1) - + self.color = fg[0] self.color_transparent = Color(yellow[0].r, yellow[0].g, yellow[0].b, 0.08) self.rs = 0 @@ -3204,7 +3587,7 @@ function Turret:init(args) self.color = orange[0] self.attack_sensor = Circle(self.x, self.y, 256) turret_deploy:play{pitch = 1.2, volume = 0.2} - + self.t:every({2.75, 3.5}, function() self.t:every({0.1, 0.2}, function() self.hfx:use('hit', 0.25, 200, 10) @@ -3264,7 +3647,7 @@ function Turret:init(args) _G[random:table{'cannoneer1', 'cannoneer2'}]:play{pitch = random:float(0.95, 1.05), volume = 0.5} end end) - + self.upgrade_dmg_m = 1 self.upgrade_aspd_m = 1 end @@ -3400,7 +3783,7 @@ function Bomb:init(args) self:set_as_rectangle(8, 8, 'static', 'player') self:set_restitution(0.5) self.hfx:add('hit', 1) - + mine1:play{pitch = random:float(0.95, 1.05), volume = 0.5} self.color = orange[0] self.dmg = 2*get_character_stat('bomber', self.level, 'dmg') @@ -3422,7 +3805,7 @@ end function Bomb:explode() camera:shake(4, 0.5) - local t = {group = main.current.effects, x = self.x, y = self.y, r = self.r, w = self.parent.area_size_m*64*(self.level == 3 and 2 or 1), color = self.color, + local t = {group = main.current.effects, x = self.x, y = self.y, r = self.r, w = self.parent.area_size_m*64*(self.level == 3 and 2 or 1), color = self.color, dmg = self.parent.area_dmg_m*self.dmg*(self.parent.conjurer_buff_m or 1)*(self.level == 3 and 2 or 1), character = self.character, parent = self.parent} Area(table.merge(t, mods or {})) if not self.parent.construct_instability and not self.parent.rearm then self.dead = true end @@ -3434,7 +3817,7 @@ function Bomb:explode() if self.parent.construct_instability then camera:shake(2, 0.5) local n = (self.parent.construct_instability == 1 and 1) or (self.parent.construct_instability == 2 and 1.5) or (self.parent.construct_instability == 3 and 2) or 1 - Area{group = main.current.effects, x = self.x, y = self.y, r = self.r + random:float(-math.pi/16, math.pi/16), w = self.parent.area_size_m*48*(self.level == 3 and 2 or 1), color = self.color, + Area{group = main.current.effects, x = self.x, y = self.y, r = self.r + random:float(-math.pi/16, math.pi/16), w = self.parent.area_size_m*48*(self.level == 3 and 2 or 1), color = self.color, dmg = n*self.parent.dmg*self.parent.area_dmg_m*(self.level == 3 and 2 or 1), parent = self.parent} _G[random:table{'cannoneer1', 'cannoneer2'}]:play{pitch = random:float(0.95, 1.05), volume = 0.5} self.dead = true @@ -3470,7 +3853,7 @@ function Saboteur:init(args) self:init_unit() self:set_as_rectangle(8, 8, 'dynamic', 'player') self:set_restitution(0.5) - + self.color = character_colors.saboteur self.character = 'saboteur' self.classes = character_classes.saboteur @@ -3515,7 +3898,7 @@ end function Saboteur:on_collision_enter(other, contact) if table.any(main.current.enemies, function(v) return other:is(v) end) then camera:shake(4, 0.5) - local t = {group = main.current.effects, x = self.x, y = self.y, r = self.r, w = (self.crit and 1.5 or 1)*self.area_size_m*64, color = self.color, + local t = {group = main.current.effects, x = self.x, y = self.y, r = self.r, w = (self.crit and 1.5 or 1)*self.area_size_m*64, color = self.color, dmg = (self.crit and 2 or 1)*self.area_dmg_m*self.actual_dmg*(self.conjurer_buff_m or 1), character = self.character, parent = self.parent} Area(table.merge(t, mods or {})) @@ -3544,7 +3927,7 @@ function Automaton:init(args) self:init_unit() self:set_as_rectangle(8, 8, 'dynamic', 'player') self:set_restitution(0.5) - + self.color = character_colors.artificer self.character = 'artificer' self.classes = {'sorcerer', 'conjurer'} @@ -3720,9 +4103,47 @@ function Gold:on_trigger_enter(other, contact) local th for _, unit in ipairs(units) do if unit.character == 'miner' then - th = unit + th = unit.level == 3 and 2 or 1 + trigger:after(0.01, function() + if not main.current.main.world then return end + _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 * th) 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 = unit.dmg, character = unit.character, parent = unit, level = unit.level} + Projectile(table.merge(t, mods or {})) + r = r + 2*math.pi/4 / th + end + end) + elseif unit.character == 'physician' then + th = unit.level == 3 and 2 or 1 + trigger:after(0.01, function() + if not main.current.main.world then return end + for i = 1, th do + local check_circle = Circle(random:float(main.current.x1 + 16, main.current.x2 - 16), random:float(main.current.y1 + 16, main.current.y2 - 16), 2) + local objects = main.current.main:get_objects_in_shape(check_circle, {Seeker, EnemyCritter, Critter, Sentry, Automaton, Bomb, Volcano, Saboteur, Pet, Turret}) + while #objects > 0 do + check_circle:move_to(random:float(main.current.x1 + 16, main.current.x2 - 16), random:float(main.current.y1 + 16, main.current.y2 - 16)) + objects = main.current.main:get_objects_in_shape(check_circle, {Seeker, EnemyCritter, Critter, Sentry, Automaton, Bomb, Volcano, Saboteur, Pet, Turret}) + end + SpawnEffect{group = main.current.effects, x = check_circle.x, y = check_circle.y, color = green[0], action = function(x, y) + local check_circle = Circle(x, y, 2) + local objects = main.current.main:get_objects_in_shape(check_circle, {Seeker, EnemyCritter, Critter, Sentry, Automaton, Bomb, Volcano, Saboteur, Pet, Turret}) + if #objects == 0 then + HealingOrb{group = main.current.main, x = x, y = y} + end + end} + end + end) + elseif unit.character == 'technomancer' and unit.level == 3 then + unit.technomancer_aspd_m = 3 + for _, drone in ipairs(unit.drones) do + drone.gold_time = unit.power_up_duration + end + unit.t:after(unit.power_up_duration, function() unit.technomancer_aspd_m = 1 end, 'gold_pickup') end end + --[[ if th then if th.level == 3 then trigger:after(0.01, function() @@ -3750,6 +4171,7 @@ function Gold:on_trigger_enter(other, contact) end) end end + ]]-- end end @@ -3861,6 +4283,19 @@ function HealingOrb:on_trigger_enter(other, contact) main.current.player:barrage(main.current.player.r, 5, nil, 3) end) end + + local units = other:get_all_units() + for _, unit in ipairs(units) do + if unit.character == 'avenger' and unit.level == 3 then + unit.on_heal_pickup(self) + elseif unit.character == 'technomancer' and unit.level == 3 then + unit.ricochet = 3 + for _, drone in ipairs(unit.drones) do + drone.heal_time = unit.power_up_duration + end + unit.t:after(unit.power_up_duration, function() unit.ricochet = 0 end, 'heal_pickup') + end + end end end @@ -3998,3 +4433,105 @@ function Critter:on_trigger_enter(other, contact) other:hit(self.dmg, self) end end + + + + +AirStrikeIndicator = Object:extend() +AirStrikeIndicator:implement(GameObject) +function AirStrikeIndicator:init(args) + self:init_game_object(args) + self.is_going_left = true + self:go_next() +end + + +function AirStrikeIndicator:update(dt) + self:update_game_object(dt) +end + + +function AirStrikeIndicator:draw() + graphics.push(self.x, self.y, math.pi/4) + graphics.rectangle(self.x, self.y, 15, 15, 0, 0, self.color, 2) + graphics.rectangle(self.x, self.y, 5, 5, 0, 0, self.color) + -- graphics.circle(self.x, self.y, 4, self.color) + graphics.pop() +end + + +function AirStrikeIndicator:go_next() + if (self.is_going_left) then + self.is_going_left = false + self.x = 49 + else + self.is_going_left = true + self.x = gw - 49 + end + self.y = random:int(30, gh - 30) +end + + + + +AttackDrone = Object:extend() +AttackDrone:implement(GameObject) +function AttackDrone:init(args) + self:init_game_object(args) + self.i = self.i or 0 + self.x_offset = 22 * (self.i % 2 == 1 and 1 or -1) + self.y_offset = 5 * (((self.i - 1) / 2) < 1 and 1 or -1) + self.r = 0 + self.t_offset = self.i < 3 and 0 or math.pi/2 + self.w = 8 + self.h = 8 + self.heal_time = 0 + self.gold_time = 0 + self.mine_time = 0 +end + + +function AttackDrone:update(dt) + self:update_game_object(dt) + if dt and dt > 0 then + self.t_offset = self.t_offset + 5 * dt -- * self.parent.buff_aspd_m + local oscillate = 16 * math.cos(self.t_offset) * (self.i % 2 == 1 and 1 or -1) + self.r = self.parent.r + self.x = self.parent.x + (self.x_offset + oscillate) * math.cos(self.r - math.pi/2) + self.y_offset * math.cos(self.r) + self.y = self.parent.y + (self.x_offset + oscillate) * math.sin(self.r - math.pi/2) + self.y_offset * math.sin(self.r) + + if self.heal_time > 0 then self.heal_time = self.heal_time - dt end + if self.gold_time > 0 then self.gold_time = self.gold_time - dt end + if self.mine_time > 0 then self.mine_time = self.mine_time - dt end + end +end + + +function AttackDrone:draw() + graphics.push(self.x, self.y, self.r) + graphics.rectangle(self.x, self.y, self.w, self.h, 3, 3, self.parent.color) + if self.heal_time > 0 then + graphics.rectangle(self.x, self.y + 8 * (self.i % 2 == 0 and 1 or -1), 8 * self.heal_time / self.parent.power_up_duration, 4, 0, 0, green[0]) + end + if self.heal_time > 0 and self.gold_time > 0 then + graphics.rectangle(self.x, self.y + 14 * (self.i % 2 == 0 and 1 or -1), 8 * self.gold_time / self.parent.power_up_duration, 4, 0, 0, yellow2[0]) + elseif self.gold_time > 0 then + graphics.rectangle(self.x , self.y + 8 * (self.i % 2 == 0 and 1 or -1), 8 * self.gold_time / self.parent.power_up_duration, 4, 0, 0, yellow2[0]) + end + if self.heal_time > 0 and self.gold_time > 0 and self.mine_time > 0 then + graphics.rectangle(self.x, self.y + 20 * (self.i % 2 == 0 and 1 or -1), 8 * self.mine_time / self.parent.power_up_duration, 4, 0, 0, blue[0]) + elseif (self.heal_time > 0 or self.gold_time > 0) and self.mine_time > 0 then + graphics.rectangle(self.x , self.y + 14 * (self.i % 2 == 0 and 1 or -1), 8 * self.mine_time / self.parent.power_up_duration, 4, 0, 0, blue[0]) + elseif self.mine_time > 0 then + graphics.rectangle(self.x , self.y + 8 * (self.i % 2 == 0 and 1 or -1), 8 * self.mine_time / self.parent.power_up_duration, 4, 0, 0, blue[0]) + end + graphics.pop() +end + +function AttackDrone:shoot() + _G[random:table{'turret_hit_wall1', 'turret_hit_wall2'}]:play{pitch = random:float(0.9, 1.1), volume = 0.2} + disarm1:play{pitch = random:float(0.7, 1.3), volume = 0.15} + HitCircle{group = main.current.effects, x = self.x + 0.8*self.w*math.cos(self.r), y = self.y + 0.8*self.w*math.sin(self.r), rs = 6} + Projectile{group = main.current.main, x = self.x + 1.6*self.w*math.cos(self.r), y = self.y + 1.6*self.w*math.sin(self.r), v = 250, r = self.r, color = self.parent.color, dmg = self.parent.dmg, character = self.parent.character, + parent = self.parent, level = self.parent.level, ricochet = self.parent.ricochet, exploder = self.parent.exploder} +end diff --git a/shared.lua b/shared.lua index 68441ee..853801e 100644 --- a/shared.lua +++ b/shared.lua @@ -659,6 +659,19 @@ function InfoText:deactivate() end +function InfoText:popup(text, ox, oy, sx, sy, ow, oh, tox, toy, delay) + self.ox, self.oy = ox or 0, oy or 0 + self.sx, self.sy = 0, 0 + self.ow, self.oh = ow or 0, oh or 0 + self.tox, self.toy = tox or 0, toy or 0 + self.text:set_text(text) + self.t:tween(0.1, self, {sx = sx or 1, sy = sy or 1}, math.cubic_in_out) + self.spring:pull(0.5) + delay = delay or 1 + self.t:after(delay, function() self.t:tween(0.05, self, {sy = 0}, math.linear, function() self.dead = true end) end) +end + + ColorRamp = Object:extend()