diff --git a/arena.lua b/arena.lua index d0f80f1..245f248 100644 --- a/arena.lua +++ b/arena.lua @@ -14,6 +14,8 @@ function Arena:on_enter(from, level, units, passives) self.units = units self.passives = passives + trigger:tween(2, main_song_instance, {volume = 0.5, pitch = 1}, math.linear) + steam.friends.setRichPresence('steam_display', '#StatusFull') steam.friends.setRichPresence('text', 'Arena - Level ' .. self.level) @@ -22,6 +24,7 @@ function Arena:on_enter(from, level, units, passives) self.post_main = Group() self.effects = Group() self.ui = Group() + self.credits = Group() self.main:disable_collision_between('player', 'player') self.main:disable_collision_between('player', 'projectile') self.main:disable_collision_between('player', 'enemy_projectile') @@ -40,9 +43,6 @@ function Arena:on_enter(from, level, units, passives) self.enemies = {Seeker, EnemyCritter} self.color = self.color or fg[0] - self.level = 25 - self.can_quit = true - -- Spawn solids and player self.x1, self.y1 = gw/2 - 0.8*gw/2, gh/2 - 0.8*gh/2 self.x2, self.y2 = gw/2 + 0.8*gw/2, gh/2 + 0.8*gh/2 @@ -99,12 +99,23 @@ function Arena:on_enter(from, level, units, passives) SpawnEffect{group = self.effects, x = gw/2, y = gh/2 - 48} SpawnEffect{group = self.effects, x = gw/2, y = gh/2, action = function(x, y) spawn1:play{pitch = random:float(0.8, 1.2), volume = 0.15} - self.boss = Seeker{group = self.main, x = x, y = y, character = 'seeker', level = self.level, boss = level_to_boss[self.level]} + SpawnMarker{group = self.effects, x = x, y = y} + self.t:after(0.75, function() + self.boss = Seeker{group = self.main, x = x, y = y, character = 'seeker', level = self.level, boss = level_to_boss[self.level]} + end) end} - self.t:every(function() return #self.main:get_objects_by_classes(self.enemies) <= 0 end, function() + self.t:every(function() + if self.boss and not self.boss.dead then + return (#self.main:get_objects_by_classes(self.enemies) <= 1) and not self.spawning_enemies + elseif self.boss and self.boss.dead then + return (#self.main:get_objects_by_classes(self.enemies) <= 0) and not self.spawning_enemies + end + end, function() self.hfx:use('condition1', 0.25, 200, 10) self.hfx:pull('condition2', 0.0625) self.t:after(0.5, function() + self.spawning_enemies = true + self.t:after((8 + math.floor(self.level/2))*0.1 + 0.5 + 0.75, function() self.spawning_enemies = false end, 'spawning_enemies') local spawn_type = random:table{'left', 'middle', 'right'} local spawn_points = {left = {x = self.x1 + 32, y = gh/2}, middle = {x = gw/2, y = gh/2}, right = {x = self.x2 - 32, y = gh/2}} local p = spawn_points[spawn_type] @@ -114,7 +125,7 @@ function Arena:on_enter(from, level, units, passives) end) end) self.t:every(function() return self.start_time <= 0 and (self.boss and self.boss.dead) and #self.main:get_objects_by_classes(self.enemies) <= 0 and not self.spawning_enemies and not self.quitting end, function() - self.can_quit = true + self:quit() if self.level == 6 then state.achievement_speed_booster = true system.save_state() @@ -172,7 +183,7 @@ function Arena:on_enter(from, level, units, passives) alert1:play{pitch = 1.2, volume = 0.5} camera:shake(4, 0.25) SpawnEffect{group = self.effects, x = gw/2, y = gh/2 - 48} - self.t:every(function() return #self.main:get_objects_by_classes(self.enemies) <= 0 end, function() + self.t:every(function() return #self.main:get_objects_by_classes(self.enemies) <= 0 and not self.spawning_enemies end, function() self.wave = self.wave + 1 if self.wave > self.max_waves then return end self.hfx:use('condition1', 0.25, 200, 10) @@ -186,6 +197,8 @@ function Arena:on_enter(from, level, units, passives) end) end else + self.spawning_enemies = true + self.t:after((8 + (self.wave-1)*2)*0.1 + 0.5 + 0.75, function() self.spawning_enemies = false end, 'spawning_enemies') local spawn_type = random:table{'left', 'middle', 'right'} local spawn_points = {left = {x = self.x1 + 32, y = gh/2}, middle = {x = gw/2, y = gh/2}, right = {x = self.x2 - 32, y = gh/2}} local p = spawn_points[spawn_type] @@ -195,12 +208,12 @@ function Arena:on_enter(from, level, units, passives) end) end, self.max_waves+1) end) - self.t:every(function() return #self.main:get_objects_by_classes(self.enemies) <= 0 and self.wave > self.max_waves end, function() self.can_quit = true end) + self.t:every(function() return #self.main:get_objects_by_classes(self.enemies) <= 0 and self.wave > self.max_waves and not self.quitting and not self.spawning_enemies end, function() self:quit() end) end) - if self.level == 18 and self.trailer then + if self.level == 20 and self.trailer then Text2{group = self.ui, x = gw/2, y = gh/2 - 24, lines = {{text = '[fg, wavy]SNKRX', font = fat_font, alignment = 'center'}}} - Text2{group = self.ui, x = gw/2, y = gh/2, sx = 0.5, sy = 0.5, lines = {{text = '[fg, wavy_mid]wishlist now!', font = fat_font, alignment = 'center'}}} + Text2{group = self.ui, x = gw/2, y = gh/2, sx = 0.5, sy = 0.5, lines = {{text = '[fg, wavy_mid]play now!', font = fat_font, alignment = 'center'}}} Text2{group = self.ui, x = gw/2, y = gh/2 + 24, sx = 0.5, sy = 0.5, lines = {{text = '[light_bg, wavy_mid]music: kubbi - ember', font = fat_font, alignment = 'center'}}} end end @@ -236,28 +249,143 @@ function Arena:on_enter(from, level, units, passives) self.psyker_level = class_levels.psyker self.conjurer_level = class_levels.conjurer + self.t:every(0.375, function() + local p = random:table(star_positions) + Star{group = star_group, x = p.x, y = p.y} + end) +end + + +function Arena:on_exit() + self.floor:destroy() + self.main:destroy() + self.post_main:destroy() + self.effects:destroy() + self.ui:destroy() + self.credits:destroy() + self.t:destroy() + self.floor = nil + self.main = nil + self.post_main = nil + self.effects = nil + self.ui = nil + self.credits = nil + self.units = nil + self.passives = nil + self.player = nil + self.t = nil + self.springs = nil + self.flashes = nil + self.hfx = nil end function Arena:update(dt) - if input.k.pressed then - SpawnEffect{group = self.effects, x = gw/2, y = gh/2, action = function(x, y) - spawn1:play{pitch = random:float(0.8, 1.2), volume = 0.15} - Seeker{group = self.main, x = x, y = y, character = 'seeker', level = self.level, boss = 'randomizer'} - end} + if main_song_instance:isStopped() then + main_song_instance = _G[random:table{'song1', 'song2', 'song3', 'song4', 'song5'}]:play{volume = 0.5} end - if input.escape.pressed and not self.transitioning then + if input.escape.pressed and not self.transitioning and not self.in_credits then if not self.paused then trigger:tween(0.25, _G, {slow_amount = 0}, math.linear, function() slow_amount = 0 self.paused = true - self.paused_t1 = Text2{group = self.ui, x = gw/2, y = gh/2 - 68, sx = 0.6, sy = 0.6, lines = {{text = '[fg]<- or a -> or d', font = fat_font, alignment = 'center'}}} - self.paused_t2 = Text2{group = self.ui, x = gw/2, y = gh/2 - 52, lines = {{text = '[fg]turn left turn right', font = pixul_font, alignment = 'center'}}} - self.paused_t3 = Text2{group = self.ui, x = gw/2, y = gh/2 - 22, sx = 0.6, sy = 0.6, lines = {{text = '[fg]n - mute sfx', font = fat_font, alignment = 'center'}}} - self.paused_t4 = Text2{group = self.ui, x = gw/2, y = gh/2 + 0, sx = 0.6, sy = 0.6, lines = {{text = '[fg]m - mute music', font = fat_font, alignment = 'center'}}} - self.paused_t5 = Text2{group = self.ui, x = gw/2, y = gh/2 + 22, sx = 0.6, sy = 0.6, lines = {{text = '[fg]esc - resume game', font = fat_font, alignment = 'center'}}} - self.paused_t6 = Text2{group = self.ui, x = gw/2, y = gh/2 + 44, sx = 0.6, sy = 0.6, lines = {{text = '[fg]r - restart run', font = fat_font, alignment = 'center'}}} + self.paused_t1 = Text2{group = self.ui, x = gw/2, y = gh/2 - 68, sx = 0.6, sy = 0.6, lines = {{text = '[bg10]<-, a or m1 ->, d or m2', font = fat_font, alignment = 'center'}}} + self.paused_t2 = Text2{group = self.ui, x = gw/2, y = gh/2 - 52, lines = {{text = '[bg10]turn left turn right', font = pixul_font, alignment = 'center'}}} + + self.resume_button = Button{group = self.ui, x = gw/2, y = gh - 160, force_update = true, button_text = 'resume game (esc)', fg_color = 'bg10', bg_color = 'bg', action = function(b) + trigger:tween(0.25, _G, {slow_amount = 1}, math.linear, function() + slow_amount = 1 + self.paused = false + self.paused_t1.dead = true + self.paused_t2.dead = true + self.paused_t1 = nil + self.paused_t2 = nil + if self.resume_button then self.resume_button.dead = true; self.resume_button = nil end + if self.restart_button then self.restart_button.dead = true; self.restart_button = nil end + if self.sfx_button then self.sfx_button.dead = true; self.sfx_button = nil end + if self.music_button then self.music_button.dead = true; self.music_button = nil end + if self.video_button_1 then self.video_button_1.dead = true; self.video_button_1 = nil end + if self.video_button_2 then self.video_button_2.dead = true; self.video_button_2 = nil end + if self.video_button_3 then self.video_button_3.dead = true; self.video_button_3 = nil end + if self.quit_button then self.quit_button.dead = true; self.quit_button = nil end + end, 'pause') + end} + + self.restart_button = Button{group = self.ui, x = gw/2, y = gh - 135, force_update = true, button_text = 'restart run (r)', fg_color = 'bg10', bg_color = 'bg', action = function(b) + self.transitioning = true + 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} + TransitionEffect{group = main.transitions, x = gw/2, y = gh/2, color = fg[0], transition_action = function() + slow_amount = 1 + gold = 2 + passives = {} + main_song_instance:stop() + run_passive_pool_by_tiers = { + [1] = { 'wall_echo', 'wall_rider', 'centipede', 'temporal_chains', 'amplify', 'amplify_x', 'ballista', 'ballista_x', 'blunt_arrow', 'berserking', 'unwavering_stance', 'assassination', 'unleash', 'blessing', + 'hex_master', 'force_push', 'spawning_pool'}, + [2] = {'ouroboros_technique_r', 'ouroboros_technique_l', 'intimidation', 'vulnerability', 'resonance', 'point_blank', 'longshot', 'explosive_arrow', 'chronomancy', 'awakening', 'ultimatum', 'concentrated_fire', + 'reinforce', 'payback', 'whispers_of_doom', 'heavy_impact', 'immolation', 'call_of_the_void'}, + [3] = {'divine_machine_arrow', 'divine_punishment', 'flying_daggers', 'crucio', 'hive', 'void_rift'}, + } + main:add(BuyScreen'buy_screen') + main:go_to('buy_screen', 0, {}, passives) + end, text = Text({{text = '[wavy, bg]restarting...', font = pixul_font, alignment = 'center'}}, global_text_tags)} + end} + + self.sfx_button = Button{group = self.ui, x = gw/2, y = gh - 110, force_update = true, button_text = 'toggle sfx (n)', fg_color = 'bg10', bg_color = 'bg', action = function(b) + ui_switch2:play{pitch = random:float(0.95, 1.05), volume = 0.5} + b.spring:pull(0.2, 200, 10) + b.selected = true + ui_switch1:play{pitch = random:float(0.95, 1.05), volume = 0.5} + if sfx.volume == 0.5 then + sfx.volume = 0 + elseif sfx.volume == 0 then + sfx.volume = 0.5 + end + end} + + self.music_button = Button{group = self.ui, x = gw/2, y = gh - 85, force_update = true, button_text = 'toggle music (m)', fg_color = 'bg10', bg_color = 'bg', action = function(b) + ui_switch2:play{pitch = random:float(0.95, 1.05), volume = 0.5} + b.spring:pull(0.2, 200, 10) + b.selected = true + ui_switch1:play{pitch = random:float(0.95, 1.05), volume = 0.5} + if music.volume == 0.5 then + music.volume = 0 + elseif music.volume == 0 then + music.volume = 0.5 + end + end} + + self.video_button_1 = Button{group = self.ui, x = gw/2 - 86, y = gh - 60, force_update = true, button_text = 'window size-', fg_color = 'bg10', bg_color = 'bg', action = function() + sx, sy = sx - 1, sy - 1 + love.window.setMode(480*sx, 270*sy) + state.sx, state.sy = sx, sy + state.fullscreen = false + end} + + self.video_button_2 = Button{group = self.ui, x = gw/2, y = gh - 60, force_update = true, button_text = 'window size+', fg_color = 'bg10', bg_color = 'bg', action = function() + sx, sy = sx + 1, sy + 1 + love.window.setMode(480*sx, 270*sy) + state.sx, state.sy = sx, sy + state.fullscreen = false + end} + + self.video_button_3 = Button{group = self.ui, x = gw/2 + 79, y = gh - 60, force_update = true, button_text = 'fullscreen', fg_color = 'bg10', bg_color = 'bg', action = function() + local _, _, flags = love.window.getMode() + local window_width, window_height = love.window.getDesktopDimensions(flags.display) + sx, sy = window_width/480, window_height/270 + ww, wh = window_width, window_height + love.window.setMode(window_width, window_height, {fullscreen = true}) + state.fullscreen = true + end} + + self.quit_button = Button{group = self.ui, x = gw/2, y = gh - 35, force_update = true, button_text = 'quit', fg_color = 'bg10', bg_color = 'bg', action = function() + system.save_state() + steam.shutdown() + love.event.quit() + end} end, 'pause') else trigger:tween(0.25, _G, {slow_amount = 1}, math.linear, function() @@ -265,21 +393,21 @@ function Arena:update(dt) self.paused = false self.paused_t1.dead = true self.paused_t2.dead = true - self.paused_t3.dead = true - self.paused_t4.dead = true - self.paused_t5.dead = true - self.paused_t6.dead = true self.paused_t1 = nil self.paused_t2 = nil - self.paused_t3 = nil - self.paused_t4 = nil - self.paused_t5 = nil - self.paused_t6 = nil + if self.resume_button then self.resume_button.dead = true; self.resume_button = nil end + if self.restart_button then self.restart_button.dead = true; self.restart_button = nil end + if self.sfx_button then self.sfx_button.dead = true; self.sfx_button = nil end + if self.music_button then self.music_button.dead = true; self.music_button = nil end + if self.video_button_1 then self.video_button_1.dead = true; self.video_button_1 = nil end + if self.video_button_2 then self.video_button_2.dead = true; self.video_button_2 = nil end + if self.video_button_3 then self.video_button_3.dead = true; self.video_button_3 = nil end + if self.quit_button then self.quit_button.dead = true; self.quit_button = nil end end, 'pause') end end - if self.paused or self.died and not self.transitioning then + if self.paused or self.died or self.won and not self.transitioning then if input.r.pressed then self.transitioning = true ui_transition2:play{pitch = random:float(0.95, 1.05), volume = 0.5} @@ -289,7 +417,7 @@ function Arena:update(dt) slow_amount = 1 gold = 2 passives = {} - cascade_instance:stop() + main_song_instance:stop() run_passive_pool_by_tiers = { [1] = { 'wall_echo', 'wall_rider', 'centipede', 'temporal_chains', 'amplify', 'amplify_x', 'ballista', 'ballista_x', 'blunt_arrow', 'berserking', 'unwavering_stance', 'assassination', 'unleash', 'blessing', 'hex_master', 'force_push', 'spawning_pool'}, @@ -301,189 +429,214 @@ function Arena:update(dt) main:go_to('buy_screen', 0, {}, passives) end, text = Text({{text = '[wavy, bg]restarting...', font = pixul_font, alignment = 'center'}}, global_text_tags)} end + + if input.escape.pressed then + self.in_credits = false + if self.credits_button then self.credits_button:on_mouse_exit() end + for _, object in ipairs(self.credits.objects) do + object.dead = true + end + self.credits:update(0) + end end self:update_game_object(dt*slow_amount) - cascade_instance.pitch = math.clamp(slow_amount*self.main_slow_amount, 0.05, 1) + main_song_instance.pitch = math.clamp(slow_amount*self.main_slow_amount, 0.05, 1) + star_group:update(dt*slow_amount) self.floor:update(dt*slow_amount) self.main:update(dt*slow_amount*self.main_slow_amount) self.post_main:update(dt*slow_amount) self.effects:update(dt*slow_amount) self.ui:update(dt*slow_amount) + self.credits:update(dt) +end - if self.can_quit and #self.main:get_objects_by_classes(self.enemies) <= 0 and not self.transitioning then - self.can_quit = false - self.quitting = true - - if self.level == 25 then - if not self.win_text and not self.win_text2 then - self.won = true - trigger:tween(1, _G, {slow_amount = 0}, math.linear, function() slow_amount = 0 end) - 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.win_text = Text2{group = self.ui, x = gw/2, y = gh/2 - 66, force_update = true, lines = {{text = '[wavy_mid, cbyc2]congratulations!', font = fat_font, alignment = 'center'}}} - trigger:after(2.5, function() - if new_game_plus == 10 then - - else - self.win_text2 = Text2{group = self.ui, x = gw/2, y = gh/2 + 20, force_update = true, lines = { - {text = "[fg]you've beaten the game!", font = pixul_font, alignment = 'center', height_multiplier = 1.24}, - {text = "[fg]i made this game in 3 months as a dev challenge", font = pixul_font, alignment = 'center', height_multiplier = 1.24}, - {text = "[fg]and i'm happy with how it turned out!", font = pixul_font, alignment = 'center', height_multiplier = 1.24}, - {text = "[fg]if you liked it too and want to play more games like this:", font = pixul_font, alignment = 'center', height_multiplier = 4}, - {text = "[fg]i will release more games this year, so stay tuned!", font = pixul_font, alignment = 'center', height_multiplier = 1.4}, - {text = "[wavy_mid, yellow]thanks for playing!", font = pixul_font, alignment = 'center'}, - }} - SteamFollowButton{group = self.ui, x = gw/2, y = gh/2 + 34, force_update = true} - RestartButton{group = self.ui, x = gw - 40, y = gh - 20, force_update = true} - trigger:after(12, function() - self.try_ng_text = Text2{group = self.ui, x = gw - 140, y = gh - 20, force_update = true, lines = { - {text = '[cbyc3, wavy_mid]try a harder difficulty:', font = pixul_font}, - }} - end) - end - end) - - if new_game_plus == 1 then - state.achievement_new_game_1 = true - system.save_state() - steam.userStats.setAchievement('NEW_GAME_1') - steam.userStats.storeStats() - end +function Arena:quit() + self.quitting = true + if self.level == 25 then + if not self.win_text and not self.win_text2 then + self.won = true + trigger:tween(1, _G, {slow_amount = 0}, math.linear, function() slow_amount = 0 end) + 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.win_text = Text2{group = self.ui, x = gw/2, y = gh/2 - 66, force_update = true, lines = {{text = '[wavy_mid, cbyc2]congratulations!', font = fat_font, alignment = 'center'}}} + trigger:after(2.5, function() if new_game_plus == 5 then - state.achievement_new_game_5 = true - system.save_state() - steam.userStats.setAchievement('NEW_GAME_5') - steam.userStats.storeStats() + self.win_text2 = Text2{group = self.ui, x = gw/2, y = gh/2 + 30, force_update = true, lines = { + {text = "[fg]now you've really beaten the game!", font = pixul_font, alignment = 'center', height_multiplier = 1.24}, + {text = "[fg]thanks a lot for playing it and completing it entirely!", font = pixul_font, alignment = 'center', height_multiplier = 1.24}, + {text = "[fg]this game was inspired by:", font = pixul_font, alignment = 'center', height_multiplier = 4}, + {text = "[fg]so check those games out, they're fun!", font = pixul_font, alignment = 'center', height_multiplier = 1.24}, + {text = "[fg]and to get more games like this in the future:", font = pixul_font, alignment = 'center', height_multiplier = 4}, + {text = "[wavy_mid, yellow]thanks for playing!", font = pixul_font, alignment = 'center'}, + }} + SteamFollowButton{group = self.ui, x = gw/2, y = gh/2 + 78, 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 - 32, y = gh - 20, force_update = true, button_text = 'quit', fg_color = 'bg10', bg_color = 'bg', action = function() love.event.quit() end} + local open_url = function(b, url) + ui_switch2:play{pitch = random:float(0.95, 1.05), volume = 0.5} + b.spring:pull(0.2, 200, 10) + b.selected = true + ui_switch1:play{pitch = random:float(0.95, 1.05), volume = 0.5} + system.open_url(url) + end + Button{group = self.ui, x = gw/2 - 50, 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, 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} + else + self.win_text2 = Text2{group = self.ui, x = gw/2, y = gh/2 + 20, force_update = true, lines = { + {text = "[fg]you've beaten the game!", font = pixul_font, alignment = 'center', height_multiplier = 1.24}, + {text = "[fg]i made this game in 3 months as a dev challenge", font = pixul_font, alignment = 'center', height_multiplier = 1.24}, + {text = "[fg]and i'm happy with how it turned out!", font = pixul_font, alignment = 'center', height_multiplier = 1.24}, + {text = "[fg]if you liked it too and want to play more games like this:", font = pixul_font, alignment = 'center', height_multiplier = 4}, + {text = "[fg]i will release more games this year, so stay tuned!", font = pixul_font, alignment = 'center', height_multiplier = 1.4}, + {text = "[wavy_mid, yellow]thanks for playing!", font = pixul_font, alignment = 'center'}, + }} + SteamFollowButton{group = self.ui, x = gw/2, y = gh/2 + 34, force_update = true} + RestartButton{group = self.ui, x = gw - 40, y = gh - 20, force_update = true} + trigger:after(12, function() + self.try_ng_text = Text2{group = self.ui, x = gw - 220, y = gh - 20, force_update = true, lines = { + {text = '[cbyc3]try a harder difficulty with +1 max snake size:', font = pixul_font}, + }} + end) + self.credits_button = 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} end + end) - if new_game_plus == 10 then - state.achievement_game_complete = 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() - steam.userStats.setAchievement('RANGERS_WIN') - steam.userStats.storeStats() - end - - if self.warrior_level >= 2 then - state.achievement_warriors_win = true - system.save_state() - steam.userStats.setAchievement('WARRIORS_WIN') - steam.userStats.storeStats() - end - - if self.mage_level >= 2 then - state.achievement_mages_win = true - system.save_state() - steam.userStats.setAchievement('MAGES_WIN') - steam.userStats.storeStats() - end - - if self.rogue_level >= 2 then - state.achievement_rogues_win = true - system.save_state() - steam.userStats.setAchievement('ROGUES_WIN') - steam.userStats.storeStats() - end - - if self.healer_level >= 2 then - state.achievement_healers_win = true - system.save_state() - steam.userStats.setAchievement('HEALERS_WIN') - steam.userStats.storeStats() - end - - if self.enchanter_level >= 2 then - state.achievement_enchanters_win = true - system.save_state() - steam.userStats.setAchievement('ENCHANTERS_WIN') - steam.userStats.storeStats() - end - - if self.nuker_level >= 2 then - state.achievement_nukers_win = true - system.save_state() - steam.userStats.setAchievement('NUKERS_WIN') - steam.userStats.storeStats() - end - - if self.conjurer_level >= 2 then - state.achievement_conjurers_win = true - system.save_state() - steam.userStats.setAchievement('CONJURERS_WIN') - steam.userStats.storeStats() - end - - if self.psyker_level >= 2 then - state.achievement_psykers_win = true - system.save_state() - steam.userStats.setAchievement('PSYKERS_WIN') - steam.userStats.storeStats() - end - - if self.curser_level >= 2 then - state.achievement_cursers_win = true - system.save_state() - steam.userStats.setAchievement('CURSERS_WIN') - steam.userStats.storeStats() - end - - if self.forcer_level >= 2 then - state.achievement_forcers_win = true - system.save_state() - steam.userStats.setAchievement('FORCERS_WIN') - steam.userStats.storeStats() - end - - if self.swarmer_level >= 2 then - state.achievement_swarmers_win = true - system.save_state() - steam.userStats.setAchievement('SWARMERS_WIN') - steam.userStats.storeStats() - end - - if self.voider_level >= 2 then - state.achievement_voiders_win = true - system.save_state() - steam.userStats.setAchievement('VOIDERS_WIN') - steam.userStats.storeStats() - end + if new_game_plus == 1 then + state.achievement_new_game_1 = true + system.save_state() + steam.userStats.setAchievement('NEW_GAME_1') + steam.userStats.storeStats() end - else - if not self.arena_clear_text then self.arena_clear_text = Text2{group = self.ui, x = gw/2, y = gh/2 - 48, lines = {{text = '[wavy_mid, cbyc]arena clear!', font = fat_font, alignment = 'center'}}} end - self.t:after(3, function() - if self.level % 3 == 0 then - self.arena_clear_text.dead = true - trigger:tween(1, _G, {slow_amount = 0}, math.linear, function() slow_amount = 0 end) - 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) - local card_w, card_h = 100, 100 - local w = 3*card_w + 2*20 - self.choosing_passives = true - self.cards = {} - local tier_1 = random:weighted_pick(unpack(level_to_passive_tier_weights[level or self.level])) - local tier_2 = random:weighted_pick(unpack(level_to_passive_tier_weights[level or self.level])) - local tier_3 = random:weighted_pick(unpack(level_to_passive_tier_weights[level or self.level])) - local passive_pool_copy = table.copy(run_passive_pool_by_tiers) - local passive_1 = random:table_remove(passive_pool_copy[tier_1]) - local passive_2 = random:table_remove(passive_pool_copy[tier_2]) - local passive_3 = random:table_remove(passive_pool_copy[tier_3]) - table.insert(self.cards, PassiveCard{group = main.current.ui, x = gw/2 - w/2 + 0*(card_w + 20) + card_w/2, y = gh/2 - 6, w = card_w, h = card_h, arena = self, passive = passive_1, force_update = true}) - table.insert(self.cards, PassiveCard{group = main.current.ui, x = gw/2 - w/2 + 1*(card_w + 20) + card_w/2, y = gh/2 - 6, w = card_w, h = card_h, arena = self, passive = passive_2, force_update = true}) - table.insert(self.cards, PassiveCard{group = main.current.ui, x = gw/2 - w/2 + 2*(card_w + 20) + card_w/2, y = gh/2 - 6, w = card_w, h = card_h, arena = self, passive = passive_3, force_update = true}) - self.passive_text = Text2{group = self.ui, x = gw/2, y = gh/2 - 65, lines = {{text = '[fg, wavy]choose one', font = fat_font, alignment = 'center'}}} - else - self:transition() - end - end, 'transition') + if new_game_plus == 5 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() + steam.userStats.setAchievement('RANGERS_WIN') + steam.userStats.storeStats() + end + + if self.warrior_level >= 2 then + state.achievement_warriors_win = true + system.save_state() + steam.userStats.setAchievement('WARRIORS_WIN') + steam.userStats.storeStats() + end + + if self.mage_level >= 2 then + state.achievement_mages_win = true + system.save_state() + steam.userStats.setAchievement('MAGES_WIN') + steam.userStats.storeStats() + end + + if self.rogue_level >= 2 then + state.achievement_rogues_win = true + system.save_state() + steam.userStats.setAchievement('ROGUES_WIN') + steam.userStats.storeStats() + end + + if self.healer_level >= 2 then + state.achievement_healers_win = true + system.save_state() + steam.userStats.setAchievement('HEALERS_WIN') + steam.userStats.storeStats() + end + + if self.enchanter_level >= 2 then + state.achievement_enchanters_win = true + system.save_state() + steam.userStats.setAchievement('ENCHANTERS_WIN') + steam.userStats.storeStats() + end + + if self.nuker_level >= 2 then + state.achievement_nukers_win = true + system.save_state() + steam.userStats.setAchievement('NUKERS_WIN') + steam.userStats.storeStats() + end + + if self.conjurer_level >= 2 then + state.achievement_conjurers_win = true + system.save_state() + steam.userStats.setAchievement('CONJURERS_WIN') + steam.userStats.storeStats() + end + + if self.psyker_level >= 2 then + state.achievement_psykers_win = true + system.save_state() + steam.userStats.setAchievement('PSYKERS_WIN') + steam.userStats.storeStats() + end + + if self.curser_level >= 2 then + state.achievement_cursers_win = true + system.save_state() + steam.userStats.setAchievement('CURSERS_WIN') + steam.userStats.storeStats() + end + + if self.forcer_level >= 2 then + state.achievement_forcers_win = true + system.save_state() + steam.userStats.setAchievement('FORCERS_WIN') + steam.userStats.storeStats() + end + + if self.swarmer_level >= 2 then + state.achievement_swarmers_win = true + system.save_state() + steam.userStats.setAchievement('SWARMERS_WIN') + steam.userStats.storeStats() + end + + if self.voider_level >= 2 then + state.achievement_voiders_win = true + system.save_state() + steam.userStats.setAchievement('VOIDERS_WIN') + steam.userStats.storeStats() + end end + + else + if not self.arena_clear_text then self.arena_clear_text = Text2{group = self.ui, x = gw/2, y = gh/2 - 48, lines = {{text = '[wavy_mid, cbyc]arena clear!', font = fat_font, alignment = 'center'}}} end + self.t:after(3, function() + if self.level % 3 == 0 then + self.arena_clear_text.dead = true + trigger:tween(1, _G, {slow_amount = 0}, math.linear, function() slow_amount = 0 end) + 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) + local card_w, card_h = 100, 100 + local w = 3*card_w + 2*20 + self.choosing_passives = true + self.cards = {} + local tier_1 = random:weighted_pick(unpack(level_to_passive_tier_weights[level or self.level])) + local tier_2 = random:weighted_pick(unpack(level_to_passive_tier_weights[level or self.level])) + local tier_3 = random:weighted_pick(unpack(level_to_passive_tier_weights[level or self.level])) + local passive_pool_copy = table.copy(run_passive_pool_by_tiers) + local passive_1 = random:table_remove(passive_pool_copy[tier_1]) + local passive_2 = random:table_remove(passive_pool_copy[tier_2]) + local passive_3 = random:table_remove(passive_pool_copy[tier_3]) + table.insert(self.cards, PassiveCard{group = main.current.ui, x = gw/2 - w/2 + 0*(card_w + 20) + card_w/2, y = gh/2 - 6, w = card_w, h = card_h, arena = self, passive = passive_1, force_update = true}) + table.insert(self.cards, PassiveCard{group = main.current.ui, x = gw/2 - w/2 + 1*(card_w + 20) + card_w/2, y = gh/2 - 6, w = card_w, h = card_h, arena = self, passive = passive_2, force_update = true}) + table.insert(self.cards, PassiveCard{group = main.current.ui, x = gw/2 - w/2 + 2*(card_w + 20) + card_w/2, y = gh/2 - 6, w = card_w, h = card_h, arena = self, passive = passive_3, force_update = true}) + self.passive_text = Text2{group = self.ui, x = gw/2, y = gh/2 - 65, lines = {{text = '[fg, wavy]choose one', font = fat_font, alignment = 'center'}}} + else + self:transition() + end + end, 'transition') end end @@ -533,10 +686,12 @@ function Arena:draw() end camera:detach() - if self.level == 18 and self.trailer then graphics.rectangle(gw/2, gh/2, 2*gw, 2*gh, nil, nil, modal_transparent) end + if self.level == 20 and self.trailer then graphics.rectangle(gw/2, gh/2, 2*gw, 2*gh, nil, nil, modal_transparent) end if self.choosing_passives or self.won or self.paused or self.died then graphics.rectangle(gw/2, gh/2, 2*gw, 2*gh, nil, nil, modal_transparent) end - self.ui:draw() + + if self.in_credits then graphics.rectangle(gw/2, gh/2, 2*gw, 2*gh, nil, nil, modal_transparent_2) end + self.credits:draw() end @@ -548,15 +703,93 @@ function Arena:die() {text = '[wavy_mid, cbyc]you died...', font = fat_font, alignment = 'center', height_multiplier = 1.25}, }} self.t:after(2, function() - self.death_info_text = Text2{group = self.ui, x = gw/2, y = gh/2 + 24, sx = 0.7, sy = 0.7, lines = { + self.death_info_text = Text2{group = self.ui, x = gw/2, y = gh/2, sx = 0.7, sy = 0.7, lines = { {text = '[wavy_mid, fg]level reached: [wavy_mid, yellow]' .. self.level, font = fat_font, alignment = 'center'}, - {text = '[wavy_mid, fg]r - start new run', font = fat_font, alignment = 'center'}, }} + self.restart_button = Button{group = self.ui, x = gw/2, y = gh/2 + 24, force_update = true, button_text = 'restart run (r)', fg_color = 'bg10', bg_color = 'bg', action = function(b) + self.transitioning = true + 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} + TransitionEffect{group = main.transitions, x = gw/2, y = gh/2, color = fg[0], transition_action = function() + slow_amount = 1 + gold = 2 + passives = {} + main_song_instance:stop() + run_passive_pool_by_tiers = { + [1] = { 'wall_echo', 'wall_rider', 'centipede', 'temporal_chains', 'amplify', 'amplify_x', 'ballista', 'ballista_x', 'blunt_arrow', 'berserking', 'unwavering_stance', 'assassination', 'unleash', 'blessing', + 'hex_master', 'force_push', 'spawning_pool'}, + [2] = {'ouroboros_technique_r', 'ouroboros_technique_l', 'intimidation', 'vulnerability', 'resonance', 'point_blank', 'longshot', 'explosive_arrow', 'chronomancy', 'awakening', 'ultimatum', 'concentrated_fire', + 'reinforce', 'payback', 'whispers_of_doom', 'heavy_impact', 'immolation', 'call_of_the_void'}, + [3] = {'divine_machine_arrow', 'divine_punishment', 'flying_daggers', 'crucio', 'hive', 'void_rift'}, + } + main:add(BuyScreen'buy_screen') + main:go_to('buy_screen', 0, {}, passives) + end, text = Text({{text = '[wavy, bg]restarting...', font = pixul_font, alignment = 'center'}}, global_text_tags)} + end} end) end end +function Arena:create_credits() + local open_url = function(b, url) + ui_switch2:play{pitch = random:float(0.95, 1.05), volume = 0.5} + b.spring:pull(0.2, 200, 10) + b.selected = true + ui_switch1:play{pitch = random:float(0.95, 1.05), volume = 0.5} + system.open_url(url) + end + + self.in_credits = true + Text2{group = self.credits, x = 60, y = 20, lines = {{text = '[bg10]main dev: ', font = pixul_font}}} + Button{group = self.credits, x = 117, y = 20, button_text = 'a327ex', fg_color = 'bg10', bg_color = 'bg', credits_button = true, action = function(b) open_url(b, 'https://store.steampowered.com/dev/a327ex/') end} + Text2{group = self.credits, x = 60, y = 50, lines = {{text = '[blue]code: ', font = pixul_font}}} + Button{group = self.credits, x = 102, y = 50, button_text = 'love2d', fg_color = 'bluem5', bg_color = 'blue', credits_button = true, action = function(b) open_url(b, 'https://love2d.org') end} + Button{group = self.credits, x = 159, y = 50, button_text = 'bakpakin', fg_color = 'bluem5', bg_color = 'blue', credits_button = true, action = function(b) open_url(b, 'https://github.com/bakpakin/binser') end} + Button{group = self.credits, x = 226, y = 50, button_text = 'davisdude', fg_color = 'bluem5', bg_color = 'blue', credits_button = true, action = function(b) open_url(b, 'https://github.com/davisdude/mlib') end} + Button{group = self.credits, x = 295, y = 50, button_text = 'tesselode', fg_color = 'bluem5', bg_color = 'blue', credits_button = true, action = function(b) open_url(b, 'https://github.com/tesselode/ripple') end} + Text2{group = self.credits, x = 60, y = 80, lines = {{text = '[green]music: ', font = pixul_font}}} + Button{group = self.credits, x = 100, y = 80, button_text = 'kubbi', fg_color = 'greenm5', bg_color = 'green', credits_button = true, action = function(b) open_url(b, 'https://kubbimusic.com/album/ember') end} + Text2{group = self.credits, x = 60, y = 110, lines = {{text = '[yellow]sounds: ', font = pixul_font}}} + Button{group = self.credits, x = 135, y = 110, button_text = 'sidearm studios', fg_color = 'yellowm5', bg_color = 'yellow', credits_button = true, action = function(b) + open_url(b, 'https://sidearm-studios.itch.io/ultimate-sound-fx-bundle') end} + Button{group = self.credits, x = 217, y = 110, button_text = 'justinbw', fg_color = 'yellowm5', bg_color = 'yellow', credits_button = true, action = function(b) + open_url(b, 'https://freesound.org/people/JustinBW/sounds/80921/') end} + Button{group = self.credits, x = 279, y = 110, button_text = 'jcallison', fg_color = 'yellowm5', bg_color = 'yellow', credits_button = true, action = function(b) + open_url(b, 'https://freesound.org/people/jcallison/sounds/258269/') end} + Button{group = self.credits, x = 342, y = 110, button_text = 'hybrid_v', fg_color = 'yellowm5', bg_color = 'yellow', credits_button = true, action = function(b) + open_url(b, 'https://freesound.org/people/Hybrid_V/sounds/321215/') end} + Button{group = self.credits, x = 427, y = 110, button_text = 'womb_affliction', fg_color = 'yellowm5', bg_color = 'yellow', credits_button = true, action = function(b) + open_url(b, 'https://freesound.org/people/womb_affliction/sounds/376532/') end} + Button{group = self.credits, x = 106, y = 130, button_text = 'bajko', fg_color = 'yellowm5', bg_color = 'yellow', credits_button = true, action = function(b) + open_url(b, 'https://freesound.org/people/bajko/sounds/399656/') end} + Button{group = self.credits, x = 157, y = 130, button_text = 'benzix2', fg_color = 'yellowm5', bg_color = 'yellow', credits_button = true, action = function(b) + open_url(b, 'https://freesound.org/people/benzix2/sounds/467951/') end} + Button{group = self.credits, x = 204, y = 130, button_text = 'lord', fg_color = 'yellowm5', bg_color = 'yellow', credits_button = true, action = function(b) + open_url(b, 'https://store.steampowered.com/developer/T_TGames') end} + Text2{group = self.credits, x = 70, y = 160, lines = {{text = '[red]playtesters: ', font = pixul_font}}} + Button{group = self.credits, x = 130, y = 160, 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 = 160, 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 = 160, 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 = 160, 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 = 160, 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 = 160, 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 = 180, 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 = 180, 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 = 223, y = 180, button_text = 'F', fg_color = 'redm5', bg_color = 'red', credits_button = true, action = function(b) + open_url(b, 'https://twitter.com/notyps') end} +end + + function Arena:transition() self.transitioning = true local gold_gained = random:int(level_to_gold_gained[self.level][1], level_to_gold_gained[self.level][2]) @@ -689,6 +922,9 @@ end function Arena:spawn_n_enemies(p, j, n) if self.died then return end + if self.arena_clear_text then return end + if self.quitting then return end + j = j or 1 n = n or 4 self.last_spawn_enemy_time = love.timer.getTime() diff --git a/assets/images/arrow.png b/assets/images/arrow.png new file mode 100644 index 0000000..cc08d40 Binary files /dev/null and b/assets/images/arrow.png differ diff --git a/assets/images/icon.png b/assets/images/icon.png new file mode 100644 index 0000000..ad41b21 Binary files /dev/null and b/assets/images/icon.png differ diff --git a/assets/media/header_capsule_2.png b/assets/media/header_capsule_2.png new file mode 100644 index 0000000..b02a164 Binary files /dev/null and b/assets/media/header_capsule_2.png differ diff --git a/assets/media/hero_capsule_2.png b/assets/media/hero_capsule_2.png new file mode 100644 index 0000000..88de7d3 Binary files /dev/null and b/assets/media/hero_capsule_2.png differ diff --git a/assets/media/library_capsule_2.png b/assets/media/library_capsule_2.png new file mode 100644 index 0000000..13f5513 Binary files /dev/null and b/assets/media/library_capsule_2.png differ diff --git a/assets/media/library_hero_2.png b/assets/media/library_hero_2.png new file mode 100644 index 0000000..efe5962 Binary files /dev/null and b/assets/media/library_hero_2.png differ diff --git a/assets/media/main_capsule_2.png b/assets/media/main_capsule_2.png new file mode 100644 index 0000000..daee916 Binary files /dev/null and b/assets/media/main_capsule_2.png differ diff --git a/assets/media/screenshot10.png b/assets/media/screenshot10.png new file mode 100644 index 0000000..13079bc Binary files /dev/null and b/assets/media/screenshot10.png differ diff --git a/assets/media/screenshot11.png b/assets/media/screenshot11.png new file mode 100644 index 0000000..3e48544 Binary files /dev/null and b/assets/media/screenshot11.png differ diff --git a/assets/media/screenshot11_text.png b/assets/media/screenshot11_text.png new file mode 100644 index 0000000..3b2da36 Binary files /dev/null and b/assets/media/screenshot11_text.png differ diff --git a/assets/media/screenshot12.png b/assets/media/screenshot12.png new file mode 100644 index 0000000..d7c9c84 Binary files /dev/null and b/assets/media/screenshot12.png differ diff --git a/assets/media/screenshot12_text.png b/assets/media/screenshot12_text.png new file mode 100644 index 0000000..665611b Binary files /dev/null and b/assets/media/screenshot12_text.png differ diff --git a/assets/media/screenshot13.png b/assets/media/screenshot13.png new file mode 100644 index 0000000..5bdcb27 Binary files /dev/null and b/assets/media/screenshot13.png differ diff --git a/assets/media/screenshot13_text.png b/assets/media/screenshot13_text.png new file mode 100644 index 0000000..a188828 Binary files /dev/null and b/assets/media/screenshot13_text.png differ diff --git a/assets/media/screenshot14.png b/assets/media/screenshot14.png new file mode 100644 index 0000000..11b92d0 Binary files /dev/null and b/assets/media/screenshot14.png differ diff --git a/assets/media/screenshot15.png b/assets/media/screenshot15.png new file mode 100644 index 0000000..436c32d Binary files /dev/null and b/assets/media/screenshot15.png differ diff --git a/assets/media/screenshot16.png b/assets/media/screenshot16.png new file mode 100644 index 0000000..acc4a23 Binary files /dev/null and b/assets/media/screenshot16.png differ diff --git a/assets/media/screenshot16_text.png b/assets/media/screenshot16_text.png new file mode 100644 index 0000000..e12d101 Binary files /dev/null and b/assets/media/screenshot16_text.png differ diff --git a/assets/media/screenshot17.png b/assets/media/screenshot17.png new file mode 100644 index 0000000..d8831ac Binary files /dev/null and b/assets/media/screenshot17.png differ diff --git a/assets/media/small_capsule_2.png b/assets/media/small_capsule_2.png new file mode 100644 index 0000000..f4143a7 Binary files /dev/null and b/assets/media/small_capsule_2.png differ diff --git a/assets/media/youtube_thumbnail.png b/assets/media/youtube_thumbnail.png new file mode 100644 index 0000000..d4c891f Binary files /dev/null and b/assets/media/youtube_thumbnail.png differ diff --git a/assets/sounds/Kubbi - Ember - 01 Pathfinder.ogg b/assets/sounds/Kubbi - Ember - 01 Pathfinder.ogg new file mode 100644 index 0000000..f4a4fde Binary files /dev/null and b/assets/sounds/Kubbi - Ember - 01 Pathfinder.ogg differ diff --git a/assets/sounds/Kubbi - Ember - 02 Ember.ogg b/assets/sounds/Kubbi - Ember - 02 Ember.ogg new file mode 100644 index 0000000..006a211 Binary files /dev/null and b/assets/sounds/Kubbi - Ember - 02 Ember.ogg differ diff --git a/assets/sounds/Kubbi - Ember - 03 Firelight.ogg b/assets/sounds/Kubbi - Ember - 03 Firelight.ogg new file mode 100644 index 0000000..407a1d4 Binary files /dev/null and b/assets/sounds/Kubbi - Ember - 03 Firelight.ogg differ diff --git a/assets/sounds/Kubbi - Ember - 04 Cascade.ogg b/assets/sounds/Kubbi - Ember - 04 Cascade.ogg index 7257704..ee49bb3 100644 Binary files a/assets/sounds/Kubbi - Ember - 04 Cascade.ogg and b/assets/sounds/Kubbi - Ember - 04 Cascade.ogg differ diff --git a/assets/sounds/Kubbi - Ember - 05 Compass.ogg b/assets/sounds/Kubbi - Ember - 05 Compass.ogg new file mode 100644 index 0000000..110afa8 Binary files /dev/null and b/assets/sounds/Kubbi - Ember - 05 Compass.ogg differ diff --git a/assets/sounds/Kubbi - Ember - 09 Formed by Glaciers.ogg b/assets/sounds/Kubbi - Ember - 09 Formed by Glaciers.ogg new file mode 100644 index 0000000..88ae4bc Binary files /dev/null and b/assets/sounds/Kubbi - Ember - 09 Formed by Glaciers.ogg differ diff --git a/buy_screen.lua b/buy_screen.lua index fd602f2..0e50676 100644 --- a/buy_screen.lua +++ b/buy_screen.lua @@ -11,6 +11,7 @@ function BuyScreen:on_exit() self.main:destroy() self.effects:destroy() self.ui:destroy() + self.t:destroy() self.main = nil self.effects = nil self.ui = nil @@ -18,10 +19,18 @@ function BuyScreen:on_exit() self.party_text = nil self.sets_text = nil self.items_text = nil + self.ng_text = nil self.characters = nil self.sets = nil self.cards = nil self.info_text = nil + self.units = nil + self.passives = nil + self.player = nil + self.t = nil + self.springs = nil + self.flashes = nil + self.hfx = nil end @@ -32,7 +41,7 @@ function BuyScreen:on_enter(from, level, units, passives) camera.x, camera.y = gw/2, gh/2 if self.level == 0 then - cascade_instance = cascade:play{volume = 0.5, loop = true} + main_song_instance = _G[random:table{'song1', 'song2', 'song3', 'song4', 'song5'}]:play{volume = 0.5} self.level = 1 self.first_screen = true end @@ -43,6 +52,7 @@ function BuyScreen:on_enter(from, level, units, passives) self.main = Group() self.effects = Group() self.ui = Group() + self.tutorial = Group() self:set_cards() self:set_party_and_sets() @@ -55,24 +65,85 @@ function BuyScreen:on_enter(from, level, units, passives) self.ng_text = Text({{text = '[fg]NG+' .. new_game_plus, font = pixul_font, alignment = 'center'}}, global_text_tags) if not self.first_screen then RerollButton{group = self.main, x = 150, y = 18, parent = self} end - GoButton{group = self.main, x = gw - 100, y = gh - 20, parent = self} - -- WishlistButton{group = self.main, x = gw - 147, y = gh - 20, parent = self} + GoButton{group = self.main, x = gw - 90, y = gh - 20, parent = self} + self.tutorial_button = Button{group = self.main, x = gw/2 + 142, y = 18, button_text = '?', fg_color = 'yellowm5', bg_color = 'yellow', action = function() + self.in_tutorial = true + self.title_text = Text2{group = self.tutorial, x = gw/2, y = 35, lines = {{text = '[fg]WELCOME TO SNKRX!', font = fat_font, alignment = 'center'}}} + self.tutorial_text = Text2{group = self.tutorial, x = 228, y = 160, lines = { + {text = '[fg]You control a snake of multiple heroes that auto-attack nearby enemies.', font = pixul_font, height_multiplier = 1.2}, + {text = '[fg]You can steer the snake left or right by pressing [yellow]A/D[fg] or [yellow]left/right arrows[fg].', font = pixul_font, height_multiplier = 2.2}, + {text = '[fg]Combine the same heroes to level them up:', font = pixul_font, height_multiplier = 1.2}, + {text = '[fg]At [yellow]Lv.3[fg] heroes unlock special effects.', font = pixul_font, height_multiplier = 2.2}, + {text = '[fg]Hire heroes of the same classes to unlock class passives:', font = pixul_font, height_multiplier = 1.2}, + {text = '[fg]Each hero can have between [yellow]1 to 3[fg] classes.', font = pixul_font, height_multiplier = 2.2}, + {text = '[fg]You gain [yellow]1 interest per 5 gold[fg], up to a maximum of 5.', font = pixul_font, height_multiplier = 1.2}, + {text = "[fg]This means that saving above [yellow]25 gold[fg] doesn't yield more interest.", font = pixul_font, height_multiplier = 2.2}, + {text = "[yellow, wavy_mid]Good luck!", font = pixul_font, height_multiplier = 2.2, alignment = 'center'}, + }} + + self.tutorial_cards = {} + table.insert(self.tutorial_cards, TutorialCharacterPart{group = self.tutorial, x = gw/2 + 34, y = gh/2 - 30, character = 'swordsman', level = 1}) + table.insert(self.tutorial_cards, TutorialCharacterPart{group = self.tutorial, x = gw/2 + 54, y = gh/2 - 30, character = 'swordsman', level = 1}) + table.insert(self.tutorial_cards, TutorialCharacterPart{group = self.tutorial, x = gw/2 + 74, y = gh/2 - 30, character = 'swordsman', level = 1}) + table.insert(self.tutorial_cards, TutorialCharacterPart{group = self.tutorial, x = gw/2 + 34, y = gh/2 - 10, character = 'swordsman', level = 2}) + table.insert(self.tutorial_cards, TutorialCharacterPart{group = self.tutorial, x = gw/2 + 54, y = gh/2 - 10, character = 'swordsman', level = 2}) + table.insert(self.tutorial_cards, TutorialCharacterPart{group = self.tutorial, x = gw/2 + 74, y = gh/2 - 10, character = 'swordsman', level = 2}) + table.insert(self.tutorial_cards, TutorialCharacterPart{group = self.tutorial, x = gw/2 + 114, y = gh/2 - 30, character = 'swordsman', level = 2}) + table.insert(self.tutorial_cards, TutorialCharacterPart{group = self.tutorial, x = gw/2 + 114, y = gh/2 - 10, character = 'swordsman', level = 3}) + table.insert(self.tutorial_cards, TutorialClassIcon{group = self.tutorial, x = gw/2 + 114, y = gh/2 + 18, class = 'warrior', units = {}}) + table.insert(self.tutorial_cards, TutorialClassIcon{group = self.tutorial, x = gw/2 + 134, y = gh/2 + 18, class = 'warrior', units = {{character = 'swordsman'}, {character = 'barbarian'}, {character = 'juggernaut'}}}) + table.insert(self.tutorial_cards, TutorialClassIcon{group = self.tutorial, x = gw/2 + 154, y = gh/2 + 18, class = 'warrior', units = {{character = 'swordsman'}, {character = 'barbarian'}, {character = 'juggernaut'}, + {character = 'vagrant'}, {character = 'outlaw'}, {character = 'blade'}} + }) + + self.close_button = Button{group = self.tutorial, x = gw - 20, y = 20, button_text = 'x', bg_color = 'bg', fg_color = 'bg10', action = function() + trigger:after(0.01, function() + self:quit_tutorial() + end) + end} + end} + + trigger:tween(1, main_song_instance, {volume = 0.2}, math.linear) end function BuyScreen:update(dt) + if main_song_instance:isStopped() then + main_song_instance = _G[random:table{'song1', 'song2', 'song3', 'song4', 'song5'}]:play{volume = 0.2} + end + self:update_game_object(dt*slow_amount) - cascade_instance.pitch = 1 - self.main:update(dt*slow_amount) - self.effects:update(dt*slow_amount) - self.ui:update(dt*slow_amount) + if not self.in_tutorial then + self.main:update(dt*slow_amount) + self.effects:update(dt*slow_amount) + self.ui:update(dt*slow_amount) + if self.shop_text then self.shop_text:update(dt) end + if self.sets_text then self.sets_text:update(dt) end + if self.party_text then self.party_text:update(dt) end + if self.items_text then self.items_text:update(dt) end + if self.ng_text then self.ng_text:update(dt) end + else + self.tutorial:update(dt*slow_amount) + end - if self.shop_text then self.shop_text:update(dt) end - if self.sets_text then self.sets_text:update(dt) end - if self.party_text then self.party_text:update(dt) end - if self.items_text then self.items_text:update(dt) end - if self.ng_text then self.ng_text:update(dt) end + if self.in_tutorial and input.escape.pressed then + self:quit_tutorial() + end +end + + +function BuyScreen:quit_tutorial() + self.in_tutorial = false + self.tutorial_text.dead = true + self.tutorial_text = nil + self.title_text.dead = true + self.title_text = nil + for _, t in ipairs(self.tutorial_cards) do t.dead = true end + self.close_button.dead = true + self.close_button = nil + self.tutorial_cards = {} + self.tutorial:update(0) end @@ -88,40 +159,58 @@ function BuyScreen:draw() if new_game_plus > 0 then self.ng_text:draw(240, 20) end + + if self.in_tutorial then + graphics.rectangle(gw/2, gh/2, 2*gw, 2*gh, nil, nil, modal_transparent_2) + arrow:draw(gw/2 + 93, gh/2 - 30, 0, 0.4, 0.35) + arrow:draw(gw/2 + 93, gh/2 - 10, 0, 0.4, 0.35) + end + self.tutorial:draw() end function BuyScreen:buy(character, i) local bought if table.any(self.units, function(v) return v.character == character end) and gold >= character_tiers[character] then - gold = gold - character_tiers[character] - self.shop_text:set_text{{text = '[wavy_mid, fg]shop [fg]- [fg, nudge_down]gold: [yellow, nudge_down]' .. gold, font = pixul_font, alignment = 'center'}} - for _, unit in ipairs(self.units) do - if unit.character == character then - if unit.level == 1 then - unit.reserve[1] = unit.reserve[1] + 1 - if unit.reserve[1] > 1 then - unit.reserve[1] = 0 - unit.level = 2 - unit.spawn_effect = true - end - elseif unit.level == 2 then - unit.reserve[1] = unit.reserve[1] + 1 - if unit.reserve[1] > 2 then - if unit.reserve[2] == 1 then - unit.reserve[2] = 0 + if table.any(self.units, function(v) return v.character == character and v.level == 3 end) then + if not self.info_text then + self.info_text = InfoText{group = main.current.ui} + self.info_text:activate({ + {text = "[fg]this unit has already reached max level", font = pixul_font, alignment = 'center'}, + }, nil, nil, nil, nil, 16, 4, nil, 2) + self.info_text.x, self.info_text.y = gw - 140, gh - 20 + end + self.t:after(2, function() self.info_text:deactivate(); self.info_text.dead = true; self.info_text = nil end, 'info_text') + else + gold = gold - character_tiers[character] + self.shop_text:set_text{{text = '[wavy_mid, fg]shop [fg]- [fg, nudge_down]gold: [yellow, nudge_down]' .. gold, font = pixul_font, alignment = 'center'}} + for _, unit in ipairs(self.units) do + if unit.character == character then + if unit.level == 1 then + unit.reserve[1] = unit.reserve[1] + 1 + if unit.reserve[1] > 1 then unit.reserve[1] = 0 - unit.level = 3 + unit.level = 2 unit.spawn_effect = true - else - unit.reserve[2] = unit.reserve[2] + 1 - unit.reserve[1] = 0 + end + elseif unit.level == 2 then + unit.reserve[1] = unit.reserve[1] + 1 + if unit.reserve[1] > 2 then + if unit.reserve[2] == 1 then + unit.reserve[2] = 0 + unit.reserve[1] = 0 + unit.level = 3 + unit.spawn_effect = true + else + unit.reserve[2] = unit.reserve[2] + 1 + unit.reserve[1] = 0 + end end end end end + bought = true end - bought = true else if #self.units >= max_units then if not self.info_text then @@ -217,6 +306,7 @@ end function SteamFollowButton:update(dt) self:update_game_object(dt) + if main.current.in_credits then return end if self.selected and input.m1.pressed then ui_switch2:play{pitch = random:float(0.95, 1.05), volume = 0.5} @@ -237,6 +327,7 @@ end function SteamFollowButton:on_mouse_enter() + if main.current.in_credits then return end love.mouse.setCursor(love.mouse.getSystemCursor'hand') 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} @@ -247,6 +338,7 @@ end function SteamFollowButton:on_mouse_exit() + if main.current.in_credits then return end love.mouse.setCursor() self.text:set_text{{text = '[greenm5]follow me on steam!', font = pixul_font, alignment = 'center'}} self.selected = false @@ -327,6 +419,7 @@ end function RestartButton:update(dt) + if main.current.in_credits then return end self:update_game_object(dt) if self.selected and input.m1.pressed then @@ -338,7 +431,7 @@ function RestartButton:update(dt) slow_amount = 1 gold = 2 passives = {} - cascade_instance:stop() + main_song_instance:stop() new_game_plus = new_game_plus + 1 state.new_game_plus = new_game_plus system.save_state() @@ -358,6 +451,7 @@ end function RestartButton:on_mouse_enter() + if main.current.in_credits then return end 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 @@ -367,6 +461,7 @@ end function RestartButton:on_mouse_exit() + if main.current.in_credits then return end self.text:set_text{{text = '[bg10]NG+' .. tostring(new_game_plus+1), font = pixul_font, alignment = 'center'}} self.selected = false end @@ -374,6 +469,53 @@ end +Button = Object:extend() +Button:implement(GameObject) +function Button:init(args) + self:init_game_object(args) + self.shape = Rectangle(self.x, self.y, pixul_font:get_text_width(self.button_text) + 8, pixul_font.h + 4) + self.interact_with_mouse = true + self.text = Text({{text = '[' .. self.fg_color .. ']' .. self.button_text, font = pixul_font, alignment = 'center'}}, global_text_tags) +end + + +function Button:update(dt) + self:update_game_object(dt) + if main.current.in_credits and not self.credits_button then return end + + if self.selected and input.m1.pressed then + self:action() + end +end + + +function Button:draw() + graphics.push(self.x, self.y, 0, self.spring.x, self.spring.y) + graphics.rectangle(self.x, self.y, self.shape.w, self.shape.h, 4, 4, self.selected and fg[0] or _G[self.bg_color][0]) + self.text:draw(self.x, self.y + 1, 0, 1, 1) + graphics.pop() +end + + +function Button:on_mouse_enter() + if main.current.in_credits and not self.credits_button then return end + 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]' .. self.button_text, font = pixul_font, alignment = 'center'}} + self.spring:pull(0.2, 200, 10) +end + + +function Button:on_mouse_exit() + if main.current.in_credits and not self.credits_button then return end + self.text:set_text{{text = '[' .. self.fg_color .. ']' .. self.button_text, font = pixul_font, alignment = 'center'}} + self.selected = false +end + + + + GoButton = Object:extend() GoButton:implement(GameObject) function GoButton:init(args) @@ -409,7 +551,7 @@ function GoButton:update(dt) TransitionEffect{group = main.transitions, x = self.x, y = self.y, color = character_colors[random:table(self.parent.units).character], transition_action = function() main:add(Arena'arena') main:go_to('arena', ((self.parent.first_screen and 1) or (self.parent.level + 1)), self.parent.units, self.parent.passives) - end, text = Text({{text = '[wavy, bg]level ' .. ((self.parent.first_screen and 1) or (self.parent.level + 1)), font = pixul_font, alignment = 'center'}}, global_text_tags)} + end, text = Text({{text = '[wavy, bg]level ' .. ((self.parent.first_screen and 1) or (self.parent.level + 1)) .. '/25', font = pixul_font, alignment = 'center'}}, global_text_tags)} end end end @@ -502,6 +644,58 @@ end +TutorialCharacterPart = Object:extend() +TutorialCharacterPart:implement(GameObject) +function TutorialCharacterPart:init(args) + self:init_game_object(args) + self.shape = Rectangle(self.x, self.y, self.sx*20, self.sy*20) + self.interact_with_mouse = true + self.spring:pull(0.2, 200, 10) +end + + +function TutorialCharacterPart:update(dt) + self:update_game_object(dt) +end + + +function TutorialCharacterPart:draw() + graphics.push(self.x, self.y, 0, self.sx*self.spring.x, self.sy*self.spring.x) + graphics.rectangle(self.x, self.y, 14, 14, 3, 3, self.highlighted and fg[0] or character_colors[self.character]) + graphics.print_centered(self.level, pixul_font, self.x + 0.5, self.y + 2, 0, 1, 1, 0, 0, self.highlighted and fg[-5] or _G[character_color_strings[self.character]][-5]) + graphics.pop() +end + + +function TutorialCharacterPart:on_mouse_enter() + ui_hover1:play{pitch = random:float(1.3, 1.5), volume = 0.5} + self.selected = true + self.spring:pull(0.2, 200, 10) + self.info_text = InfoText{group = main.current.tutorial} + 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 - ' .. + (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 + + +function TutorialCharacterPart:on_mouse_exit() + self.selected = false + if self.info_text then + self.info_text:deactivate() + self.info_text.dead = true + end + self.info_text = nil +end + + + CharacterPart = Object:extend() CharacterPart:implement(GameObject) function CharacterPart:init(args) @@ -571,6 +765,7 @@ function CharacterPart:on_mouse_enter() }, nil, nil, nil, nil, 16, 4, nil, 2) self.info_text.x, self.info_text.y = gw/2, gh/2 + 10 + --[[ if self.parent:is(BuyScreen) then for _, set in ipairs(self.parent.sets) do if table.any(character_classes[self.character], function(v) return v == set.class end) then @@ -578,6 +773,7 @@ function CharacterPart:on_mouse_enter() end end end + ]]-- end @@ -600,6 +796,7 @@ function CharacterPart:on_mouse_exit() end self.info_text = nil + --[[ if self.parent:is(BuyScreen) then for _, set in ipairs(self.parent.sets) do if table.any(character_classes[self.character], function(v) return v == set.class end) then @@ -607,6 +804,7 @@ function CharacterPart:on_mouse_exit() end end end + ]]-- end @@ -619,6 +817,7 @@ function CharacterPart:die() self.info_text = nil end + --[[ if self.selected and self.parent:is(BuyScreen) then for _, set in ipairs(self.parent.sets) do if table.any(character_classes[self.character], function(v) return v == set.class end) then @@ -626,6 +825,7 @@ function CharacterPart:die() end end end + ]]-- end @@ -876,7 +1076,7 @@ function CharacterIcon:init(args) self:init_game_object(args) self.shape = Rectangle(self.x, self.y, 40, 20) self.interact_with_mouse = true - self.character_text = Text({{text = '[' .. character_color_strings[self.character] .. ']' .. self.character, font = pixul_font, alignment = 'center'}}, global_text_tags) + self.character_text = Text({{text = '[' .. character_color_strings[self.character] .. ']' .. string.lower(character_names[self.character]), font = pixul_font, alignment = 'center'}}, global_text_tags) end @@ -930,6 +1130,84 @@ end +TutorialClassIcon = Object:extend() +TutorialClassIcon:implement(GameObject) +function TutorialClassIcon:init(args) + self:init_game_object(args) + self.shape = Rectangle(self.x, self.y + 11, 20, 40) + self.interact_with_mouse = true + self.spring:pull(0.2, 200, 10) +end + + +function TutorialClassIcon:update(dt) + self:update_game_object(dt) +end + + +function TutorialClassIcon:draw() + graphics.push(self.x, self.y, 0, self.sx*self.spring.x, self.sy*self.spring.x) + local i, j, n = class_set_numbers[self.class](self.units) + + graphics.rectangle(self.x, self.y, 16, 24, 4, 4, self.highlighted and fg[0] or ((n >= i) and class_colors[self.class] or bg[3])) + _G[self.class]:draw(self.x, self.y, 0, 0.3, 0.3, 0, 0, self.highlighted and fg[-5] or ((n >= i) and _G[class_color_strings[self.class]][-5] or bg[10])) + graphics.rectangle(self.x, self.y + 26, 16, 16, 3, 3, self.highlighted and fg[0] or bg[3]) + if i == 2 then + if self.highlighted then + graphics.line(self.x - 3, self.y + 20, self.x - 3, self.y + 25, (n >= 1) and fg[-5] or fg[-10], 3) + graphics.line(self.x - 3, self.y + 27, self.x - 3, self.y + 32, (n >= 2) and fg[-5] or fg[-10], 3) + graphics.line(self.x + 4, self.y + 20, self.x + 4, self.y + 25, (n >= 3) and fg[-5] or fg[-10], 3) + graphics.line(self.x + 4, self.y + 27, self.x + 4, self.y + 32, (n >= 4) and fg[-5] or fg[-10], 3) + else + graphics.line(self.x - 3, self.y + 20, self.x - 3, self.y + 25, (n >= 1) and class_colors[self.class] or bg[10], 3) + graphics.line(self.x - 3, self.y + 27, self.x - 3, self.y + 32, (n >= 2) and class_colors[self.class] or bg[10], 3) + graphics.line(self.x + 4, self.y + 20, self.x + 4, self.y + 25, (n >= 3) and class_colors[self.class] or bg[10], 3) + graphics.line(self.x + 4, self.y + 27, self.x + 4, self.y + 32, (n >= 4) and class_colors[self.class] or bg[10], 3) + end + elseif i == 3 then + if self.highlighted then + graphics.line(self.x - 3, self.y + 19, self.x - 3, self.y + 22, (n >= 1) and fg[-5] or fg[-10], 3) + graphics.line(self.x - 3, self.y + 24, self.x - 3, self.y + 27, (n >= 2) and fg[-5] or fg[-10], 3) + graphics.line(self.x - 3, self.y + 29, self.x - 3, self.y + 32, (n >= 3) and fg[-5] or fg[-10], 3) + graphics.line(self.x + 4, self.y + 19, self.x + 4, self.y + 22, (n >= 4) and fg[-5] or fg[-10], 3) + graphics.line(self.x + 4, self.y + 24, self.x + 4, self.y + 27, (n >= 5) and fg[-5] or fg[-10], 3) + graphics.line(self.x + 4, self.y + 29, self.x + 4, self.y + 32, (n >= 6) and fg[-5] or fg[-10], 3) + else + graphics.line(self.x - 3, self.y + 19, self.x - 3, self.y + 22, (n >= 1) and class_colors[self.class] or bg[10], 3) + graphics.line(self.x - 3, self.y + 24, self.x - 3, self.y + 27, (n >= 2) and class_colors[self.class] or bg[10], 3) + graphics.line(self.x - 3, self.y + 29, self.x - 3, self.y + 32, (n >= 3) and class_colors[self.class] or bg[10], 3) + graphics.line(self.x + 4, self.y + 19, self.x + 4, self.y + 22, (n >= 4) and class_colors[self.class] or bg[10], 3) + graphics.line(self.x + 4, self.y + 24, self.x + 4, self.y + 27, (n >= 5) and class_colors[self.class] or bg[10], 3) + graphics.line(self.x + 4, self.y + 29, self.x + 4, self.y + 32, (n >= 6) and class_colors[self.class] or bg[10], 3) + end + end + graphics.pop() +end + + +function TutorialClassIcon:on_mouse_enter() + ui_hover1:play{pitch = random:float(1.3, 1.5), volume = 0.5} + self.spring:pull(0.2, 200, 10) + local i, j, owned = class_set_numbers[self.class](self.units) + self.info_text = InfoText{group = main.current.tutorial} + self.info_text:activate({ + {text = '[' .. class_color_strings[self.class] .. ']' .. self.class:capitalize() .. '[fg] - owned: [yellow]' .. owned, font = pixul_font, alignment = 'center', height_multiplier = 1.25}, + {text = class_descriptions[self.class]((owned >= j and 2) or (owned >= i and 1) or 0), font = pixul_font, alignment = 'center'}, + }, nil, nil, nil, nil, 16, 4, nil, 2) + self.info_text.x, self.info_text.y = gw/2 - 25, gh/2 + 25 +end + + +function TutorialClassIcon:on_mouse_exit() + if self.info_text then + self.info_text:deactivate() + self.info_text.dead = true + end + self.info_text = nil +end + + + ClassIcon = Object:extend() ClassIcon:implement(GameObject) diff --git a/devlog.md b/devlog.md index 5f19e01..0ba9ffc 100644 --- a/devlog.md +++ b/devlog.md @@ -1082,3 +1082,8 @@ Once that's done I'll do credits screen, NG+10 (game complete) screen, and find Spending a lot of time balancing the game. The end game is pretty hectic and I need to make sure that it's reachable at NG+~7+. Basically just playing the game a lot, changing things that are too strong weak, then playing more and seeing if it's better. + +# Week 12-13 - 03-17/05/21 + +These 2 last weeks have been a lot of playtesting and just taking care of details. But now it's finally over. +I finally finished this game and I'm fairly happy with how it turned out. I'll write more about it in a separate blog post, I don't really have the energy to write much now. diff --git a/enemies.lua b/enemies.lua index 3139782..798f840 100644 --- a/enemies.lua +++ b/enemies.lua @@ -22,7 +22,7 @@ function Seeker:init(args) HitCircle{group = main.current.effects, x = self.x, y = self.y, rs = 6, color = green[0], duration = 0.1} for _, enemy in ipairs(enemies) do LightningLine{group = main.current.effects, src = self, dst = enemy, color = green[0]} - enemy:speed_boost(3 + self.level*0.1 + new_game_plus*0.1) + enemy:speed_boost(3 + self.level*0.05 + new_game_plus*0.2) end end end) @@ -96,14 +96,14 @@ function Seeker:init(args) LightningLine{group = main.current.effects, src = self, dst = enemy, color = blue[0]} enemy:hit(10000) shoot1:play{pitch = random:float(0.95, 1.05), volume = 0.4} - local n = 8 + new_game_plus - for i = 1, n do EnemyProjectile{group = main.current.main, x = enemy.x, y = enemy.y, color = blue[0], r = (i-1)*math.pi/(n/2), v = 150 + 5*enemy.level, dmg = 2*enemy.dmg} end + local n = 8 + new_game_plus*2 + for i = 1, n do EnemyProjectile{group = main.current.main, x = enemy.x, y = enemy.y, color = blue[0], r = (i-1)*math.pi/(n/2), v = 150 + 5*enemy.level, dmg = (1 + 0.2*new_game_plus)*enemy.dmg} end end end) elseif self.boss == 'randomizer' then self.t:every_immediate(0.07, function() self.color = _G[random:table{'green', 'purple', 'yellow', 'blue'}][0]:clone() end) - self.t:every(2, function() + self.t:every(6, function() local attack = random:table{'explode', 'swarm', 'force', 'speed_boost'} if attack == 'explode' then local enemies = self:get_objects_in_shape(Circle(self.x, self.y, 128), {Seeker}) @@ -113,8 +113,8 @@ function Seeker:init(args) LightningLine{group = main.current.effects, src = self, dst = enemy, color = blue[0]} enemy:hit(10000) shoot1:play{pitch = random:float(0.95, 1.05), volume = 0.4} - local n = 8 + new_game_plus - for i = 1, n do EnemyProjectile{group = main.current.main, x = enemy.x, y = enemy.y, color = blue[0], r = (i-1)*math.pi/(n/2), v = 150 + 5*enemy.level, dmg = 2*enemy.dmg} end + local n = 8 + new_game_plus*2 + for i = 1, n do EnemyProjectile{group = main.current.main, x = enemy.x, y = enemy.y, color = blue[0], r = (i-1)*math.pi/(n/2), v = 150 + 5*enemy.level, dmg = (1 + 0.2*new_game_plus)*enemy.dmg} end end elseif attack == 'swarm' then local enemies = self:get_objects_in_shape(Circle(self.x, self.y, 128), {Seeker}) @@ -145,7 +145,7 @@ function Seeker:init(args) HitCircle{group = main.current.effects, x = self.x, y = self.y, rs = 6, color = green[0], duration = 0.1} for _, enemy in ipairs(enemies) do LightningLine{group = main.current.effects, src = self, dst = enemy, color = green[0]} - enemy:speed_boost(3 + self.level*0.1 + new_game_plus*0.1) + enemy:speed_boost(3 + self.level*0.05 + new_game_plus*0.2) end end end @@ -198,7 +198,7 @@ function Seeker:init(args) end) elseif self.tank then self.color = yellow[0]:clone() - self.buff_hp_m = 1.5 + (0.05*self.level) + self.buff_hp_m = 1.25 + (0.025*self.level) self:calculate_stats() self.hp = self.max_hp elseif self.shooter then @@ -212,12 +212,12 @@ function Seeker:init(args) self.hfx:use('hit', 0.25, 200, 10, 0.1) local r = self.r 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} - EnemyProjectile{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), color = fg[0], r = r, v = 150 + 5*self.level + 5*new_game_plus, - dmg = (new_game_plus*0.1 + 1.5)*self.dmg} + EnemyProjectile{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), color = fg[0], r = r, v = 150 + 5*self.level + 10*new_game_plus, + dmg = (new_game_plus*0.2 + 1)*self.dmg} end) end end, nil, nil, 'shooter') - end) + end) elseif self.spawner then self.color = purple[0]:clone() end @@ -248,8 +248,8 @@ function Seeker:update(dt) if self.headbutt_charging or self.headbutting then self.buff_def_m = 3 end if self.speed_boosting then - local n = math.remap(love.timer.getTime() - self.speed_boosting, 0, (3 + 0.1*self.level + new_game_plus*0.1), 1, 0.5) - self.speed_boosting_mvspd_m = (3 + 0.1*self.level + 0.1*new_game_plus)*n + local n = math.remap(love.timer.getTime() - self.speed_boosting, 0, (3 + 0.05*self.level + new_game_plus*0.2), 1, 0.5) + self.speed_boosting_mvspd_m = (3 + 0.05*self.level + 0.2*new_game_plus)*n if not self.speed_booster and not self.exploder and not self.headbutter and not self.tank and not self.shooter and not self.spawner then self.color.r = math.remap(n, 1, 0.5, green[0].r, red[0].r) self.color.g = math.remap(n, 1, 0.5, green[0].g, red[0].g) @@ -432,7 +432,7 @@ function Seeker:hit(damage, projectile) if self.exploder then shoot1:play{pitch = random:float(0.95, 1.05), volume = 0.4} trigger:after(0.01, function() - local n = 8 + new_game_plus + local n = 8 + new_game_plus*2 for i = 1, n do EnemyProjectile{group = main.current.main, x = self.x, y = self.y, color = blue[0], r = (i-1)*math.pi/(n/2), v = 150 + 5*self.level, dmg = 2*self.dmg} end @@ -468,7 +468,7 @@ function Seeker:hit(damage, projectile) critter1:play{pitch = random:float(0.95, 1.05), volume = 0.5} trigger:after(0.01, function() for i = 1, self.infested do - Critter{group = main.current.main, x = self.x, y = self.y, color = orange[0], r = random:float(0, 2*math.pi), v = 10, dmg = self.infested_dmg, parent = projectile.parent} + Critter{group = main.current.main, x = self.x, y = self.y, color = orange[0], r = random:float(0, 2*math.pi), v = 10, dmg = self.infested_dmg, parent = self.infested_ref} end end) end @@ -503,7 +503,7 @@ function Seeker:slow(amount, duration) end -function Seeker:curse(curse, duration, arg1, arg2) +function Seeker:curse(curse, duration, arg1, arg2, arg3) local curse_m = 1 if main.current.curser_level == 2 then curse_m = 1.5 elseif main.current.curser_level == 1 then curse_m = 1.25 @@ -520,6 +520,7 @@ function Seeker:curse(curse, duration, arg1, arg2) end end + buff1:play{pitch = random:float(0.65, 0.75), volume = 0.25} if curse == 'launcher' then self.t:after(duration*curse_m, function() self.launcher_push = arg1 @@ -534,6 +535,7 @@ function Seeker:curse(curse, duration, arg1, arg2) elseif curse == 'infestor' then self.infested = arg1 self.infested_dmg = arg2 + self.infested_ref = arg3 self.t:after(duration*curse_m, function() self.infested = false end, 'infestor_curse') end end @@ -558,6 +560,7 @@ EnemyCritter:implement(Physics) EnemyCritter:implement(Unit) function EnemyCritter:init(args) self:init_game_object(args) + if tostring(self.x) == tostring(0/0) or tostring(self.y) == tostring(0/0) then self.dead = true; return end self:init_unit() self:set_as_rectangle(7, 4, 'dynamic', 'enemy_projectile') self:set_restitution(0.5) @@ -574,6 +577,11 @@ end function EnemyCritter:update(dt) self:update_game_object(dt) + if self.slowed then self.slow_mvspd_m = self.slowed + else self.slow_mvspd_m = 1 end + self.buff_mvspd_m = (self.speed_boosting_mvspd_m or 1)*(self.slow_mvspd_m or 1)*(self.temporal_chains_mvspd_m or 1) + self:calculate_stats() + if self.being_pushed then local v = math.length(self:get_velocity()) if v < 50 then @@ -665,6 +673,61 @@ function EnemyCritter:speed_boost(duration) end +function EnemyCritter:slow(amount, duration) + self.slowed = amount + self.t:after(duration, function() self.slowed = false end, 'slow') +end + + +function EnemyCritter:curse(curse, duration, arg1, arg2, arg3) + local curse_m = 1 + if main.current.curser_level == 2 then curse_m = 1.5 + elseif main.current.curser_level == 1 then curse_m = 1.25 + else curse_m = 1 end + + if main.current.player.whispers_of_doom then + if not self.doom then self.doom = 0 end + self.doom = self.doom + 1 + if self.doom == 4 then + self.doom = 0 + self:hit(200) + buff1:play{pitch = random:float(0.95, 1.05), volume = 0.5} + ui_switch1:play{pitch = random:float(0.95, 1.05), volume = 0.5} + end + end + + if curse == 'launcher' then + self.t:after(duration*curse_m, function() + self.launcher_push = arg1 + self.launcher = arg2 + self:push(random:float(50, 75)*self.launcher.knockback_m, random:table{0, math.pi, math.pi/2, -math.pi/2}) + end, 'launcher_curse') + elseif curse == 'bard' then + self.bard_cursed = true + elseif curse == 'bane' then + self.baned = true + self.t:after(duration*curse_m, function() self.baned = false end, 'bane_curse') + elseif curse == 'infestor' then + self.infested = arg1 + self.infested_dmg = arg2 + self.infested_ref = arg3 + self.t:after(duration*curse_m, function() self.infested = false end, 'infestor_curse') + end +end + + +function EnemyCritter:apply_dot(dmg, duration) + self.t:every(0.25, function() + hit2:play{pitch = random:float(0.8, 1.2), volume = 0.2} + self:hit(dmg/4) + HitCircle{group = main.current.effects, x = self.x, y = self.y, rs = 6, color = fg[0], duration = 0.1} + for i = 1, 1 do HitParticle{group = main.current.effects, x = self.x, y = self.y, color = self.color} end + for i = 1, 1 do HitParticle{group = main.current.effects, x = self.x, y = self.y, color = purple[0]} end + end, math.floor(duration/0.2)) +end + + + EnemyProjectile = Object:extend() @@ -672,6 +735,7 @@ EnemyProjectile:implement(GameObject) EnemyProjectile:implement(Physics) function EnemyProjectile:init(args) self:init_game_object(args) + if tostring(self.x) == tostring(0/0) or tostring(self.y) == tostring(0/0) then self.dead = true; return end self:set_as_rectangle(10, 4, 'dynamic', 'enemy_projectile') end diff --git a/engine/game/trigger.lua b/engine/game/trigger.lua index d3b9b21..5f743a9 100644 --- a/engine/game/trigger.lua +++ b/engine/game/trigger.lua @@ -180,6 +180,11 @@ function Trigger:resolve_delay(delay) end +function Trigger:destroy() + self.triggers = nil +end + + function Trigger:update(dt) for tag, trigger in pairs(self.triggers) do if trigger.timer then diff --git a/engine/init.lua b/engine/init.lua index 6b8e93e..a1fb606 100644 --- a/engine/init.lua +++ b/engine/init.lua @@ -47,6 +47,7 @@ function engine_run(config) if not web then love.filesystem.setIdentity(config.game_name) steam.init() + system.load_state() local _, _, flags = love.window.getMode() local window_width, window_height = love.window.getDesktopDimensions(flags.display) @@ -63,7 +64,12 @@ function engine_run(config) sx, sy = window_width/(config.game_width or 480), window_height/(config.game_height or 270) ww, wh = window_width, window_height - love.window.setMode(window_width, window_height, {fullscreen = config.fullscreen, vsync = config.vsync, msaa = msaa or 0, display = config.display}) + if state.sx and state.sy then + sx, sy = state.sx, state.sy + love.window.setMode(state.sx*gw, state.sy*gh, {fullscreen = state.fullscreen, vsync = config.vsync, msaa = msaa or 0, display = config.display}) + else + love.window.setMode(window_width, window_height, {fullscreen = config.fullscreen, vsync = config.vsync, msaa = msaa or 0, display = config.display}) + end love.window.setTitle(config.game_name) else @@ -72,6 +78,7 @@ function engine_run(config) ww, wh = 960, 540 end + love.window.setIcon(love.image.newImageData('assets/images/icon.png')) love.graphics.setBackgroundColor(0, 0, 0, 1) love.graphics.setColor(1, 1, 1, 1) love.joystick.loadGamepadMappings("engine/gamecontrollerdb.txt") diff --git a/engine/system.lua b/engine/system.lua index 531bbc2..778e40c 100644 --- a/engine/system.lua +++ b/engine/system.lua @@ -35,7 +35,8 @@ function system.count_all(f) seen[t] = true for k, v in pairs(t) do if type(v) == "table" then count_table(v) - elseif type(v) == "userdata" then f(v) end + elseif type(v) == "userdata" then f(v) + end end end count_table(_G) @@ -45,8 +46,13 @@ end function system.type_count() local counts = {} local enumerate = function(o) - local t = system.type_name(o) - counts[t] = (counts[t] or 0) + 1 + if type(o) == 'function' then + local upvalues = {} + + else + local t = system.type_name(o) + counts[t] = (counts[t] or 0) + 1 + end end system.count_all(enumerate) return counts diff --git a/main.lua b/main.lua index 6417b2c..7974e68 100644 --- a/main.lua +++ b/main.lua @@ -11,8 +11,8 @@ require 'media' function init() shared_init() - input:bind('move_left', {'a', 'left', 'dpleft'}) - input:bind('move_right', {'d', 'right', 'dpright'}) + input:bind('move_left', {'a', 'left', 'dpleft', 'm1'}) + input:bind('move_right', {'d', 'right', 'dpright', 'm2'}) input:bind('move_up', {'w', 'up', 'dpup'}) input:bind('move_down', {'s', 'down', 'dpdown'}) input:bind('enter', {'space', 'return', 'fleft', 'fdown', 'fright'}) @@ -99,7 +99,13 @@ function init() turret_deploy = Sound('321215__hybrid-v__sci-fi-weapons-deploy.ogg', s) rogue_crit1 = Sound('Dagger Stab (Flesh) 4.ogg', s) rogue_crit2 = Sound('Sword hits another sword 6.ogg', s) - cascade = Sound('Kubbi - Ember - 04 Cascade.ogg', {tags = {music}}) + + song1 = Sound('Kubbi - Ember - 01 Pathfinder.ogg', {tags = {music}}) + song2 = Sound('Kubbi - Ember - 02 Ember.ogg', {tags = {music}}) + song3 = Sound('Kubbi - Ember - 03 Firelight.ogg', {tags = {music}}) + song4 = Sound('Kubbi - Ember - 04 Cascade.ogg', {tags = {music}}) + song5 = Sound('Kubbi - Ember - 05 Compass.ogg', {tags = {music}}) + death_song = Sound('Kubbi - Ember - 09 Formed by Glaciers.ogg', {tags = {music}}) speed_booster_elite = Image('speed_booster_elite') exploder_elite = Image('exploder_elite') @@ -162,6 +168,7 @@ function init() hive = Image('hive') void_rift = Image('void_rift') star = Image('star') + arrow = Image('arrow') class_colors = { ['warrior'] = yellow[0], @@ -444,7 +451,7 @@ function init() ['saboteur'] = function(lvl) return '[fg]calls [yellow]2[fg] saboteurs to seek targets and deal [yellow]' .. get_character_stat('saboteur', 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]+15%[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, @@ -462,7 +469,7 @@ function init() ['beastmaster'] = function(lvl) return '[fg]spawn [yellow]2[fg] small critters if the beastmaster crits' end, ['launcher'] = function(lvl) return '[fg]nearby enemies are pushed after [yellow]4[fg] seconds, taking [yellow]' .. 2*get_character_stat('launcher', lvl, 'dmg') .. '[fg] damage on wall hit' end, ['bard'] = function(lvl) return "[fg]throws a knife that deals [yellow]" .. get_character_stat('bard', lvl, 'dmg') .. "[fg] damage and inflicts enemies hit with the bard's curse" end, - ['assassin'] = function(lvl) return '[fg]throws a piercing knife that deals [yellow]' .. get_character_stat('assassin', lvl, 'dmg') .. '[fg] damage and inflicts poison that deals [yellow]' .. + ['assassin'] = function(lvl) return '[fg]throws a piercing knife that deals [yellow]' .. get_character_stat('assassin', lvl, 'dmg') .. '[fg] damage + [yellow]' .. get_character_stat('assassin', lvl, 'dmg')/2 .. '[fg] damage per second for [yellow]3[fg] seconds' end, ['host'] = function(lvl) return '[fg]periodically spawn [yellow]1[fg] small critter' end, ['carver'] = function(lvl) return '[fg]carves a statue that periodically heals [yellow]1[fg] unit for [yellow]20%[fg] max HP if in range' end, @@ -489,7 +496,7 @@ function init() ['saboteur'] = '[orange]Demoman', ['stormweaver'] = '[blue]Wide Lightning', ['sage'] = '[purple]Dimension Compression', - ['squire'] = '[yellow]Repair', + ['squire'] = '[yellow]Shiny Gear', ['cannoneer'] = '[orange]Cannon Barrage', ['dual_gunner'] = '[green]Gun Kata', ['hunter'] = '[green]Feral Pack', @@ -533,7 +540,7 @@ function init() ['saboteur'] = '[light_bg]Demoman', ['stormweaver'] = '[light_bg]Wide Lightning', ['sage'] = '[light_bg]Dimension Compression', - ['squire'] = '[light_bg]Repair', + ['squire'] = '[light_bg]Shiny Gear', ['cannoneer'] = '[light_bg]Cannon Barrage', ['dual_gunner'] = '[light_bg]Gun Kata', ['hunter'] = '[light_bg]Feral Pack', @@ -577,7 +584,7 @@ function init() ['saboteur'] = function() return '[fg]the explosion has [yellow]50%[fg] chance to crit, increasing in size and dealing [yellow]2x[fg] damage' end, ['stormweaver'] = function() return "[fg]chain lightning's trigger area of effect and number of units hit is [yellow]doubled" end, ['sage'] = function() return '[fg]when the projectile expires deal [yellow]' .. 3*get_character_stat('sage', 3, 'dmg') .. '[fg] damage to all enemies under its influence' end, - ['squire'] = function() return '[fg]you can reroll your item choices once, these opportunities stack if unused' end, + ['squire'] = function() return '[yellow]+30%[fg] damage, attack speed, movement speed and defense to all allies' end, ['cannoneer'] = function() return '[fg]showers the hit area in [yellow]5[fg] additional cannon shots that deal [yellow]' .. get_character_stat('cannoneer', 3, 'dmg')/2 .. '[fg] AoE damage' end, ['dual_gunner'] = function() return '[fg]every 5th attack shoot in rapid succession for [yellow]2[fg] seconds' end, ['hunter'] = function() return '[fg]summons [yellow]3[fg] pets and the pets ricochet off walls once' end, @@ -621,7 +628,7 @@ function init() ['saboteur'] = function() return '[light_bg]the explosion has 50% chance to crit, increasing in size and dealing 2x damage' end, ['stormweaver'] = function() return "[light_bg]chain lightning's trigger area of effect and number of units hit is doubled" end, ['sage'] = function() return '[light_bg]when the projectile expires deal ' .. 3*get_character_stat('sage', 3, 'dmg') .. ' damage to all enemies under its influence' end, - ['squire'] = function() return '[light_bg]you can reroll your item choices once, these opportunities stack if unused' end, + ['squire'] = function() return '[light_bg]+30% damage, attack speed, movement speed and defense to all allies' end, ['cannoneer'] = function() return '[light_bg]showers the hit area in 5 additional cannon shots that deal ' .. get_character_stat('cannoneer', 3, 'dmg')/2 .. ' AoE damage' end, ['dual_gunner'] = function() return '[light_bg]every 5th attack shoot in rapid succession for 2 seconds' end, ['hunter'] = function() return '[light_bg]summons 3 pets and the pets ricochet off walls once' end, @@ -648,7 +655,7 @@ function init() ['highlander'] = function() return '[light_bg]quickly repeats the attack 3 times' end, ['fairy'] = function() return '[light_bg]heals 2 units instead and grants them an additional 100% attack speed' end, ['priest'] = function() return '[light_bg]at the start of the round pick 3 units at random and grants them a buff that prevents death once' end, - ['infestor'] = function() return '[light_bg][yellow]triples the number of critters released' end, + ['infestor'] = function() return '[light_bg]triples the number of critters released' end, ['flagellant'] = function() return '[light_bg]deals ' .. 2*get_character_stat('flagellant', 3, 'dmg') .. ' damage to all allies and grants +12% damage to all allies per cast' end, } @@ -951,7 +958,7 @@ function init() ['magnify'] = '[yellow]+25%[fg] area size', ['concentrated_fire'] = '[yellow]-50%[fg] area size and [yellow]+100%[fg] area damage', ['unleash'] = '[yellow]+2%[fg] area size and damage per second', - ['reinforce'] = '[yellow]+10%[fg] damage, defense and attack speed to all allies if you have at least one enchanter', + ['reinforce'] = '[yellow]+10%[fg] damage, defense and attack speed to all allies with at least one enchanter', ['payback'] = '[yellow]+5%[fg] damage to all allies whenever an enchanter is hit', ['blessing'] = '[yellow]+20%[fg] healing effectiveness', ['hex_master'] = '[yellow]+25%[fg] curse duration', @@ -1031,20 +1038,20 @@ function init() [9] = {50, 30, 15, 5}, [10] = {50, 30, 15, 5}, [11] = {45, 30, 20, 5}, - [12] = {45, 30, 20, 5}, - [13] = {40, 30, 20, 10}, - [14] = {40, 30, 20, 10}, - [15] = {35, 35, 20, 10}, - [16] = {30, 40, 20, 10}, - [17] = {20, 40, 25, 15}, - [18] = {20, 40, 25, 15}, - [19] = {15, 40, 30, 15}, - [20] = {10, 40, 30, 20}, - [21] = {5, 40, 35, 20}, - [22] = {5, 35, 35, 25}, - [23] = {5, 35, 35, 25}, - [24] = {0, 30, 40, 30}, - [25] = {0, 25, 40, 35}, + [12] = {40, 30, 20, 10}, + [13] = {35, 30, 25, 10}, + [14] = {30, 30, 25, 15}, + [15] = {25, 30, 30, 15}, + [16] = {25, 25, 30, 20}, + [17] = {20, 25, 35, 20}, + [18] = {15, 25, 35, 25}, + [19] = {10, 25, 40, 25}, + [20] = {5, 25, 40, 30}, + [21] = {0, 25, 40, 35}, + [22] = {0, 20, 40, 40}, + [23] = {0, 20, 35, 45}, + [24] = {0, 10, 30, 60}, + [25] = {0, 0, 0, 100}, } level_to_gold_gained = { @@ -1159,48 +1166,81 @@ function init() } gold = 2 passives = {} + steam.userStats.requestCurrentStats() system.load_state() new_game_plus = state.new_game_plus or 0 - steam.userStats.requestCurrentStats() - max_units = 7 + math.floor(new_game_plus/2) + max_units = 7 + new_game_plus main = Main() + + -- main_song_instance = _G[random:table{'song1', 'song2', 'song3', 'song4', 'song5'}]:play{volume = 0.5} + main:add(BuyScreen'buy_screen') main:go_to('buy_screen', 0, {}, passives) - + --[[ + main:add(Arena'arena') + main:go_to('arena', 20, { + {character = 'vagrant', level = 3}, + {character = 'spellblade', level = 3}, + {character = 'assassin', level = 3}, + {character = 'scout', level = 3}, + {character = 'engineer', level = 3}, + {character = 'swordsman', level = 3}, + {character = 'archer', level = 3}, + }, passives) + main:add(Media'media') main:go_to('media') ]]-- + + trigger:every(2, function() + if debugging_memory then + for k, v in pairs(system.type_count()) do + print(k, v) + end + print("-- " .. math.round(tonumber(collectgarbage("count"))/1024, 3) .. "MB --") + print() + end + end) end function update(dt) main:update(dt) - star_group:update(dt) + + --[[ + if input.b.pressed then + -- debugging_memory = not debugging_memory + for k, v in pairs(system.type_count()) do + print(k, v) + end + print("-- " .. math.round(tonumber(collectgarbage("count"))/1024, 3) .. "MB --") + print() + end + ]]-- if input.n.pressed then if sfx.volume == 0.5 then sfx.volume = 0 + state.volume_muted = true elseif sfx.volume == 0 then sfx.volume = 0.5 + state.volume_muted = false end end if input.m.pressed then if music.volume == 0.5 then + state.music_muted = true music.volume = 0 elseif music.volume == 0 then music.volume = 0.5 + state.music_muted = false end end - if input.k.pressed then - steam.userStats.setAchievement('ASCENSION_1') - steam.userStats.storeStats() - end - - if input.l.pressed then + if input.f12.pressed then steam.userStats.resetAllStats(true) steam.userStats.storeStats() end diff --git a/media.lua b/media.lua index 4c5fb81..9767cb2 100644 --- a/media.lua +++ b/media.lua @@ -11,7 +11,6 @@ function Media:on_enter(from) self.effects = Group() self.ui = Group() - self.mode = 'achievements' graphics.set_background_color(fg[0]) end @@ -27,8 +26,11 @@ function Media:draw() self.main:draw() self.effects:draw() self.ui:draw() - - if self.mode == 'achievements' then - graphics.print_centered('GG', fat_font, 32, 32, 0, 1, 1, 0, 0, fg[-5]) - end end + +--[[ +build your party: hire heroes, rank them up and defeat endless waves of enemies +make synergies: combine heroes of the same class to unlock unique class passives +find passive items: further enhance your party with powerful passive items +create your build: explore the possibilities and combinations to create your own unique build +]]-- diff --git a/objects.lua b/objects.lua index 497420b..63c6a45 100644 --- a/objects.lua +++ b/objects.lua @@ -97,8 +97,10 @@ function LightningLine:generate() end end local line = table.remove(self.lines, min_i) - table.insert(self.points, line.x1) - table.insert(self.points, line.y1) + if line then + table.insert(self.points, line.x1) + table.insert(self.points, line.y1) + end end end @@ -230,14 +232,14 @@ function Unit:calculate_stats(first_run) if self.boss then local x = self.level local y = {0, 0, 3, 0, 0, 6, 0, 0, 9, 0, 0, 12, 0, 0, 15, 0, 0, 18, 0, 0, 21, 0, 0, 24, 25} - self.base_hp = 100 + (new_game_plus*5) + (100 + new_game_plus*5)*y[x] - self.base_dmg = 50 + 15*y[x] + self.base_hp = 100 + (new_game_plus*10) + (90 + new_game_plus*14)*y[x] + self.base_dmg = (25 + new_game_plus*10) + (7 + new_game_plus*4)*y[x] self.base_mvspd = 35 + 1.5*y[x] else local x = self.level local y = {0, 1, 3, 3, 4, 6, 5, 6, 9, 7, 8, 12, 10, 11, 15, 12, 13, 18, 16, 17, 21, 17, 20, 24, 25} - self.base_hp = 25 + (new_game_plus*2) + (25 + new_game_plus*2)*y[x] - self.base_dmg = 10 + 3*y[x] + self.base_hp = 22 + (new_game_plus*3) + (17 + new_game_plus*2.6)*y[x] + self.base_dmg = (4 + new_game_plus*1.5) + (2.2 + new_game_plus)*y[x] self.base_mvspd = 70 + 3*y[x] end elseif self:is(Saboteur) then diff --git a/player.lua b/player.lua index 59d5bd4..3c2a10f 100644 --- a/player.lua +++ b/player.lua @@ -204,7 +204,7 @@ function Player:init(args) elseif self.character == 'juggernaut' then self.t:every(8, function() - self:attack(64, {juggernaut_push = true}) + self:attack(96, {juggernaut_push = true}) end, nil, nil, 'attack') elseif self.character == 'lich' then @@ -326,7 +326,7 @@ function Player:init(args) end, nil, nil, 'shoot') elseif self.character == 'highlander' then - self.attack_sensor = Circle(self.x, self.y, 64) + self.attack_sensor = Circle(self.x, self.y, 32) 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 self.t:every(0.25, function() @@ -385,7 +385,7 @@ function Player:init(args) local enemies = main.current.main:get_objects_by_classes(main.current.enemies) for _, enemy in ipairs(enemies) do if self:distance_to_object(enemy) < 128 then - enemy:curse('infestor', 6*(self.hex_duration_m or 1), (self.level == 3 and 6 or 2), self.dmg) + enemy:curse('infestor', 6*(self.hex_duration_m or 1), (self.level == 3 and 6 or 2), self.dmg, self) HitCircle{group = main.current.effects, x = self.x, y = self.y, rs = 6, color = orange[0], duration = 0.1} LightningLine{group = main.current.effects, src = self, dst = enemy, color = orange[0]} end @@ -495,8 +495,10 @@ function Player:init(args) end end local mage = random:table(mages) - mage.awakening_aspd_m = 2 - mage.awakening_dmg_m = 2 + if mage then + mage.awakening_aspd_m = 2 + mage.awakening_dmg_m = 2 + end end) end @@ -552,19 +554,21 @@ function Player:init(args) end if self.reinforce then - local units = self:get_all_units() - local any_enchanter = false - for _, unit in ipairs(units) do - if table.any(unit.classes, function(v) return v == 'enchanter' end) then - any_enchanter = true - break + main.current.t:after(0.1, function() + local units = self:get_all_units() + local any_enchanter = false + for _, unit in ipairs(units) do + if table.any(unit.classes, function(v) return v == 'enchanter' end) then + any_enchanter = true + break + end end - end - if any_enchanter then - self.reinforce_dmg_m = 1.1 - self.reinforce_def_m = 1.1 - self.reinforce_aspd_m = 1.1 - end + if any_enchanter then + self.reinforce_dmg_m = 1.1 + self.reinforce_def_m = 1.1 + self.reinforce_aspd_m = 1.1 + end + end) end if self.payback then @@ -602,8 +606,14 @@ function Player:update(dt) if self.character == 'squire' then local all_units = self:get_all_units() for _, unit in ipairs(all_units) do - unit.squire_dmg_m = 1.15 - unit.squire_def_m = 1.15 + unit.squire_dmg_m = 1.2 + unit.squire_def_m = 1.2 + if self.level == 3 then + unit.squire_dmg_m = 1.5 + unit.squire_def_m = 1.5 + unit.squire_aspd_m = 1.3 + unit.squire_mvspd_m = 1.3 + end end elseif self.character == 'chronomancer' then local all_units = self:get_all_units() @@ -733,12 +743,12 @@ 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.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.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.ballista_x_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.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.buff_area_size_m = (self.nuker_area_size_m or 1)*(self.magnify_area_size_m or 1)*(self.concentrated_fire_area_size_m or 1)*(self.unleash_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.amplify_x_area_dmg_m or 1)*(self.concentrated_fire_area_dmg_m or 1)*(self.unleash_area_dmg_m or 1) - self.buff_mvspd_m = (self.wall_rider_mvspd_m or 1)*(self.centipede_mvspd_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:calculate_stats() if self.attack_sensor then self.attack_sensor:move_to(self.x, self.y) end @@ -1424,7 +1434,7 @@ function Projectile:on_collision_enter(other, contact) self.ricochet = self.ricochet - 1 end _G[random:table{'arrow_hit_wall1', 'arrow_hit_wall2'}]:play{pitch = random:float(0.9, 1.1), volume = 0.2} - elseif self.character == 'scout' or self.character == 'outlaw' or self.character == 'blade' or self.character == 'spellblade' or self.character == 'bard' then + elseif self.character == 'scout' or self.character == 'outlaw' or self.character == 'blade' or self.character == 'spellblade' or self.character == 'bard' or self.character == 'beastmaster' then self:die(x, y, r, 0) knife_hit_wall1:play{pitch = random:float(0.9, 1.1), volume = 0.2} local r = Unit.bounce(self, nx, ny) @@ -1489,7 +1499,7 @@ function Projectile:on_trigger_enter(other, contact) end if self.character == 'archer' or self.character == 'scout' or self.character == 'outlaw' or self.character == 'blade' or self.character == 'hunter' or self.character == 'spellblade' or self.character == 'engineer' or - self.character == 'bard' or self.character == 'assassin' or self.character == 'barrager' then + self.character == 'bard' or self.character == 'assassin' or self.character == 'barrager' or self.character == 'beastmaster' then hit2:play{pitch = random:float(0.95, 1.05), volume = 0.35} if self.character == 'spellblade' then magic_area1:play{pitch = random:float(0.95, 1.05), volume = 0.15} @@ -1595,25 +1605,25 @@ function Area:init(args) local resonance_dmg = 0 if self.character == 'elementor' then if self.parent.resonance then resonance_dmg = 2*self.dmg*0.05*#enemies end - enemy:hit(2*self.dmg + resonance_dmg) + enemy:hit(2*self.dmg + resonance_dmg, self) if self.level == 3 then enemy:slow(0.4, 6) end elseif self.character == 'swordsman' then if self.parent.resonance then resonance_dmg = (self.dmg + self.dmg*0.15*#enemies)*0.05*#enemies end - enemy:hit(self.dmg + self.dmg*0.15*#enemies + resonance_dmg) + enemy:hit(self.dmg + self.dmg*0.15*#enemies + resonance_dmg, self) elseif self.character == 'blade' and self.level == 3 then if self.parent.resonance then resonance_dmg = (self.dmg + self.dmg*0.33*#enemies)*0.05*#enemies end - enemy:hit(self.dmg + self.dmg*0.33*#enemies + resonance_dmg) + enemy:hit(self.dmg + self.dmg*0.33*#enemies + resonance_dmg, self) elseif self.character == 'highlander' then if self.parent.resonance then resonance_dmg = 6*self.dmg*0.05*#enemies end - enemy:hit(6*self.dmg + resonance_dmg) + enemy:hit(6*self.dmg + resonance_dmg, self) elseif self.character == 'launcher' then if self.parent.resonance then resonance_dmg = (self.level == 3 and 6*self.dmg*0.05*#enemies or 2*self.dmg*0.05*#enemies) end enemy:curse('launcher', 4*(self.hex_duration_m or 1), (self.level == 3 and 6*self.dmg or 2*self.dmg) + resonance_dmg, self.parent) else if self.parent.resonance then resonance_dmg = self.dmg*0.05*#enemies end - enemy:hit(self.dmg + resonance_dmg) + enemy:hit(self.dmg + resonance_dmg, self) end HitCircle{group = main.current.effects, x = enemy.x, y = enemy.y, rs = 6, color = fg[0], duration = 0.1} for i = 1, 1 do HitParticle{group = main.current.effects, x = enemy.x, y = enemy.y, color = self.color} end @@ -1647,7 +1657,7 @@ function Area:init(args) 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), void_rift = true, duration = 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*(self.parent.dot_dmg_m or 1), void_rift = true, duration = 1, parent = self.parent} end end @@ -1705,7 +1715,7 @@ function DotArea:init(args) enemy.pyrod = self end end - enemy:hit((self.dot_dmg_m or 1)*self.dmg/5) + enemy:hit((self.dot_dmg_m or 1)*self.dmg/5, self) HitCircle{group = main.current.effects, x = enemy.x, y = enemy.y, rs = 6, color = fg[0], duration = 0.1} for i = 1, 1 do HitParticle{group = main.current.effects, x = enemy.x, y = enemy.y, color = self.color} end for i = 1, 1 do HitParticle{group = main.current.effects, x = enemy.x, y = enemy.y, color = enemy.color} end @@ -1723,7 +1733,7 @@ function DotArea:init(args) if self.level == 3 then enemy:slow(0.4, 4) end - enemy:hit((self.dot_dmg_m or 1)*2*self.dmg) + enemy:hit((self.dot_dmg_m or 1)*2*self.dmg, self) HitCircle{group = main.current.effects, x = enemy.x, y = enemy.y, rs = 6, color = fg[0], duration = 0.1} for i = 1, 1 do HitParticle{group = main.current.effects, x = enemy.x, y = enemy.y, color = self.color} end for i = 1, 1 do HitParticle{group = main.current.effects, x = enemy.x, y = enemy.y, color = enemy.color} end @@ -1757,7 +1767,7 @@ function DotArea:init(args) if #enemies > 0 then self.spring:pull(0.05, 200, 10) end for _, enemy in ipairs(enemies) do hit2:play{pitch = random:float(0.8, 1.2), volume = 0.2} - enemy:hit((self.dot_dmg_m or 1)*self.dmg/5) + enemy:hit((self.dot_dmg_m or 1)*self.dmg/5, self) HitCircle{group = main.current.effects, x = enemy.x, y = enemy.y, rs = 6, color = fg[0], duration = 0.1} for i = 1, 1 do HitParticle{group = main.current.effects, x = enemy.x, y = enemy.y, color = self.color} end for i = 1, 1 do HitParticle{group = main.current.effects, x = enemy.x, y = enemy.y, color = enemy.color} end @@ -2281,6 +2291,7 @@ end function Critter:on_trigger_enter(other, contact) if other:is(Seeker) then + critter2:play{pitch = random:float(0.65, 0.85), volume = 0.1} self:hit(1) other:hit(self.dmg, self) end diff --git a/shared.lua b/shared.lua index b33d150..2428409 100644 --- a/shared.lua +++ b/shared.lua @@ -19,6 +19,7 @@ function shared_init() _G[name .. '_transparent_weak'] = Color(color[0].r, color[0].g, color[0].b, 0.25) end modal_transparent = Color(0.1, 0.1, 0.1, 0.6) + modal_transparent_2 = Color(0.1, 0.1, 0.1, 0.9) bg_off = Color(46, 46, 46) bg_gradient = GradientImage('vertical', Color(128, 128, 128, 0), Color(0, 0, 0, 0.3)) @@ -30,7 +31,10 @@ function shared_init() sfx = SoundTag() sfx.volume = 0.5 music = SoundTag() - music.volume = 0 + music.volume = 0.5 + + if state.volume_muted then sfx.volume = 0 end + if state.music_muted then music.volume = 0 end fat_font = Font('FatPixelFont', 8) pixul_font = Font('PixulBrush', 8) @@ -40,13 +44,9 @@ function shared_init() shadow_shader = Shader(nil, 'shadow.frag') star_canvas = Canvas(gw, gh, {stencil = true}) star_group = Group() - local star_positions = {} + star_positions = {} for i = -30, gh + 30, 15 do table.insert(star_positions, {x = -40, y = i}) end for i = -30, gw, 15 do table.insert(star_positions, {x = i, y = gh + 40}) end - trigger:every(0.375, function() - local p = random:table(star_positions) - Star{group = star_group, x = p.x, y = p.y} - end) end @@ -87,7 +87,7 @@ function shared_draw(draw_action) end) background_canvas:draw(0, 0, 0, sx, sy) - shadow_canvas:draw(6, 6, 0, sx, sy) + shadow_canvas:draw(1.5*sx, 1.5*sy, 0, sx, sy) main_canvas:draw(0, 0, 0, sx, sy) end @@ -111,6 +111,7 @@ function Star:update(dt) self.x = self.x + self.v*math.cos(-math.pi/4) self.y = self.y + self.v*math.sin(-math.pi/4) self.vr = self.vr + self.dvr*dt + if self.x > gw + 64 then self.dead = true end end @@ -505,6 +506,10 @@ global_text_tags = { green5 = TextTag{draw = function(c, i, text) graphics.set_color(green[5]) end}, blue5 = TextTag{draw = function(c, i, text) graphics.set_color(blue[5]) end}, bluem5 = TextTag{draw = function(c, i, text) graphics.set_color(blue[-5]) end}, + redm5 = TextTag{draw = function(c, i, text) graphics.set_color(red[-5]) end}, + orangem5 = TextTag{draw = function(c, i, text) graphics.set_color(orange[-5]) end}, + purplem5 = TextTag{draw = function(c, i, text) graphics.set_color(purple[-5]) end}, + yellowm5 = TextTag{draw = function(c, i, text) graphics.set_color(yellow[-5]) end}, wavy = TextTag{update = function(c, dt, i, text) c.oy = 2*math.sin(4*time + i) end}, wavy_mid = TextTag{update = function(c, dt, i, text) c.oy = 0.75*math.sin(3*time + i) end}, wavy_mid2 = TextTag{update = function(c, dt, i, text) c.oy = 0.5*math.sin(3*time + i) end}, diff --git a/todo b/todo index 201cf7d..4e3cbc4 100644 --- a/todo +++ b/todo @@ -1,24 +1,4 @@ -General balance -Trailers - 3-4 pure gameplay playthroughs showcasing different builds - 1 normal 30-40s trailer -Misc - Squire's Lv.3 effect - NG+1-10 (difficulty ramps up faster and goes higher than normal at the end, the player also gains more gold per round, and on NG+1 and NG+5 the maximum number of units is increased by 1 (10->11->12) - NG+10 end screen - New music - Credits - Level X/25 text - - -Engine improvements for after SNKRX release - on_hit: - on_collision_enter/exit are automatically called and automatically call on_hit/on_leave for each object - This enables the definition of on_hit on each object and the question of where the logic should stay is solved/dodged - Spurred by Wall needing to have its own on_hit function to do something when the player hits it, without having to change player code for all Walls, - Defining on_hit on the Wall creation call for that specific Wall, and thus that specific Wall will have this behavior while other walls won't - https://i.imgur.com/asPdpnQ.png ? - Not sure if I should go all the way with event systems like this or only have it work for specific cases, need to think about it more - -Rewrite - DPS list - needs every damage event to be logged with source, destination and damage values being detached from intermediary objects (areas, projectiles, etc) +Lich buff +Highlander buff +Concentrated Fire change to "chance to create secondary AoEs on AoE hit" (like Cannoneer's Lv.3) +Change Cannoneer's Lv.3 to 7 repeats, and each repeat should be slightly slower