diff --git a/NS.py b/NS.py index deb268b..01c3d2c 100644 --- a/NS.py +++ b/NS.py @@ -8,7 +8,7 @@ # general, visit https://scrape.nugget.fun/ # -import argparse, pathlib +import argparse, pathlib, operator from random import randint, choice, random from math import pi from copy import copy @@ -36,7 +36,7 @@ from lib.pgfw.pgfw.Sprite import Sprite, RainbowSprite from lib.pgfw.pgfw.Animation import Animation from lib.pgfw.pgfw.extension import ( get_step, get_step_relative, get_delta, reflect_angle, get_distance, render_box, get_hsla_color, get_hue_shifted_surface, - get_color_swapped_surface, load_frames, fill_colorkey + get_color_swapped_surface, load_frames, fill_colorkey, get_segments ) from lib.pgfw.pgfw.gfx_extension import aa_filled_polygon @@ -64,7 +64,80 @@ class NS(Game, Animation): CHANNEL_COUNT = 8 NO_RESET_TIMEOUT = 3000 + class Score: + + def __init__(self, milliseconds=None, level_index=None): + self.milliseconds = milliseconds + self.level_index = level_index + + @classmethod + def from_string(cls, line: str): + milliseconds, level_index = (int(field) for field in line.strip().split()) + if level_index == -1: + level_index = None + return cls(milliseconds, level_index) + + @classmethod + def level(cls, milliseconds: int, level_index: int): + return cls(milliseconds, level_index) + + @classmethod + def full(cls, milliseconds: int): + return cls(milliseconds) + + @classmethod + def blank_level(cls, level_index: int): + return cls(level_index=level_index) + + @classmethod + def blank_full(cls): + return cls() + + def is_full(self): + return self.level_index is None + + def formatted(self): + if self.milliseconds is None: + return "--:--.-" + else: + minutes, remainder = divmod(int(self.milliseconds), 60000) + seconds, fraction = divmod(remainder, 1000) + return f"{int(minutes)}:{int(seconds):02}.{fraction // 100}" + + def blank(self): + return self.milliseconds is None + + def serialize(self): + if self.level_index is None: + serialized_level_index = -1 + else: + serialized_level_index = self.level_index + return f"{self.milliseconds} {serialized_level_index}" + + def __str__(self): + return self.formatted() + + def __repr__(self): + return f"" + + def __lt__(self, other): + if self.level_index == other.level_index: + if self.milliseconds == other.milliseconds: + return False + elif self.blank() or other.blank(): + return other.blank() + else: + return self.milliseconds < other.milliseconds + else: + if self.is_full() or other.is_full(): + return other.is_full() + else: + return self.level_index < other.level_index + def __init__(self): + """ + Parse the command line, set config types, initialize the serial reader, subscribe to events, and initialize child objects + """ # Specify possible arguments and parse the command line. If the -h flag is passed, the argparse library will # print a help message and end the program. parser = argparse.ArgumentParser() @@ -100,7 +173,8 @@ class NS(Game, Animation): }, "system": { - "bool": "minimize-load-time" + "bool": ["minimize-load-time", "enable-level-select"], + "int": ["lives-boss-rush-mode", "lives-level-select-mode"] }, "pads": { @@ -147,14 +221,13 @@ class NS(Game, Animation): self.serial_thread = Thread(target=self.read_serial) self.serial_thread.start() Animation.__init__(self, self) + # All events will pass through self.respond self.subscribe(self.respond, KEYDOWN) self.subscribe(self.respond, KEYUP) self.subscribe(self.respond) ds = self.get_display_surface() - self.background = Surface(ds.get_size()) - self.background.fill((0, 0, 0)) - self.level_select = LevelSelect(self) - self.platform = Platform(self) + # Child objects for managing more specific parts of the game + self.platform = Platform(self, self.get_configuration("pads", "center")) self.tony = Tony(self) self.logo = Logo(self) self.title = Title(self) @@ -163,11 +236,28 @@ class NS(Game, Animation): self.dialogue = Dialogue(self) self.chemtrails = Chemtrails(self) self.boss = Boss(self) + self.level_select = LevelSelect(self) self.last_press = get_ticks() self.register(self.blink_score, interval=500) + self.register(self.close_pop_up) self.play(self.blink_score) self.reset() - self.most_recent_time = None + self.most_recent_score = None + self.pop_up_font = pygame.font.Font(self.get_resource(Dialogue.FONT_PATH), 12) + self.pop_up_text = "" + # Start the score list with all blank scores + self.scores = [] + blank_count = 14 + for level_index in range(3): + for _ in range(blank_count): + self.scores.append(NS.Score.blank_level(level_index)) + for _ in range(blank_count): + self.scores.append(NS.Score.blank_full()) + # Add existing scores to the list from file + with open(self.get_resource("scores"), "rt") as score_file: + for line in score_file: + if line.strip(): + self.scores.append(NS.Score.from_string(line)) clear() def serial_enabled(self): @@ -238,9 +328,6 @@ class NS(Game, Animation): self.no_reset_elapsed = 0 self.title.activate() - def set_most_recent_time(self, score): - self.most_recent_time = score - def blink_score(self): self.score_hidden = not self.score_hidden @@ -286,6 +373,43 @@ class NS(Game, Animation): if self.get_delegate().compare(event, "reset-game"): self.reset() + def pop_up(self, text): + """ + Trigger a pop up message that displays for a certain amount of time before being closed automatically. Adds a line of + text to a variable that contains all pop up messages in case there is a previously sent message that needs to continue + being displayed. + + @param text message to display + """ + self.pop_up_text += f"{text}\n" + self.halt(self.close_pop_up) + self.play(self.close_pop_up, play_once=True, delay=3000) + + def close_pop_up(self): + """ + Close the pop up message by removing all text from the pop up text variable. This will cause the pop up to stop being + drawn each frame. + """ + self.pop_up_text = "" + + def add_time_to_scores(self, milliseconds: int, level_index=None): + """ + Add a time to the list of scores. This method will build a score object, add it to the list, and write to the scores file. + + @param milliseconds player's time in milliseconds + @param level_index the level this time corresponds to or None for a full game + """ + if level_index is None: + score = NS.Score.full(milliseconds) + else: + score = NS.Score.level(milliseconds, level_index) + self.scores.append(score) + self.most_recent_score = score + with open(self.get_resource("scores"), "wt") as score_file: + for score in sorted(self.scores): + if not score.blank(): + score_file.write(f"{score.serialize()}\n") + def update(self): Animation.update(self) last_frame_duration = self.time_filter.get_last_frame_duration() @@ -293,7 +417,7 @@ class NS(Game, Animation): self.apply_serial() if self.title.active or self.ending.active or self.dialogue.active: self.no_reset_elapsed += last_frame_duration - # if we received good input, reset the auto reset timer + # If we received good input, reset the auto reset timer if 0b11 <= self.serial_data <= 0b1100: self.no_reset_elapsed = 0 if self.no_reset_elapsed >= self.NO_RESET_TIMEOUT: @@ -309,6 +433,13 @@ class NS(Game, Animation): self.chemtrails.update() self.boss.update_dialogue() self.wipe.update() + # Draw the pop up text line by line if there is any + pop_up_y = 0 + for line in self.pop_up_text.split("\n"): + if line: + surface = self.pop_up_font.render(line, False, (0, 0, 0), (255, 255, 255)) + self.get_display_surface().blit(surface, (0, pop_up_y)) + pop_up_y += surface.get_height() self.idle_elapsed += self.time_filter.get_last_frame_duration() if self.idle_elapsed >= self.IDLE_TIMEOUT: self.reset() @@ -324,32 +455,119 @@ class LevelSelect(GameChild): def __init__(self, parent): GameChild.__init__(self, parent) self.subscribe(self.respond, KEYDOWN) + y = 250 + indent = 10 + dsr = self.get_display_surface().get_rect() + self.platforms = [Platform(self, (dsr.centerx, y)), Platform(self, (0, y)), Platform(self, (0, y))] + self.platforms[1].view.location.left = dsr.left + indent + self.platforms[2].view.location.right = dsr.right - indent + self.platforms[0].set_glowing((NS.LNW, NS.LSE)) + self.platforms[1].set_glowing((NS.LNW, NS.LSW)) + self.platforms[2].set_glowing((NS.LNW, NS.LNE)) + preview_rect = pygame.Rect(0, 0, dsr.w / 3 - 40, 160) + self.previews = [] + font = pygame.font.Font(self.get_resource(Dialogue.FONT_PATH), 18) + padding = 4 + for level_index, text in enumerate(("NORMAL", "ADVANCED", "EXPERT")): + self.previews.append(Sprite(self, 100)) + text = font.render(text, True, (255, 255, 255)) + text = pygame.transform.rotate(text, 90) + text_rect = text.get_rect() + text_rect.midleft = preview_rect.midleft + environment = pygame.transform.smoothscale( + self.get_game().boss.backgrounds[level_index].frames[0], + (preview_rect.w - text_rect.w - padding, preview_rect.h - padding * 2)) + environment_rect = environment.get_rect() + environment_rect.midright = preview_rect.right - padding, preview_rect.centery + boss = pygame.transform.smoothscale(self.get_game().boss.level_sprite(level_index).frames[0], + environment_rect.inflate(-64, -28).size) + boss_rect = boss.get_rect() + boss_rect.center = environment_rect.center + for hue in range(0, 360, 8): + frame = pygame.Surface(preview_rect.size) + color = Color(0, 0, 0) + color.hsla = hue, 100, 50, 100 + frame.fill(color) + frame.blit(text, text_rect) + frame.blit(environment, environment_rect) + frame.blit(boss, boss_rect) + self.previews[-1].add_frame(frame) + self.previews[-1].location.midbottom = self.platforms[level_index].view.location.centerx, \ + self.platforms[level_index].view.location.top - 12 def activate(self): self.active = True + for platform in self.platforms: + platform.activate() def deactivate(self): self.active = False + for platform in self.platforms: + platform.deactivate() def reset(self): self.deactivate() + self.level_index_selected = None + self.zoom = 1.0 + for level_index in range(3): + self.platforms[level_index].view.unhide() + self.previews[level_index].unhide() def respond(self, event): - if self.active: - launch_level = None - if event.key == K_1: - launch_level = 0 - elif event.key == K_2: - launch_level = 1 - elif event.key == K_3: - launch_level = 2 - if launch_level is not None: - self.deactivate() - self.get_game().boss.start_level(launch_level) + """ + Respond to CTRL + key presses to launch a level or toggle level select mode + """ + level_index = None + if pygame.key.get_mods() & pygame.KMOD_CTRL: + if event.key in (pygame.K_1, pygame.K_2, pygame.K_3): + self.launch(event.key - pygame.K_1) + elif event.key == pygame.K_l: + level_select_enabled = not self.get_configuration("system", "enable-level-select") + self.get_configuration().set("system", "enable-level-select", level_select_enabled) + self.get_game().pop_up(f"Level select mode set to {level_select_enabled}") + + def launch(self, index): + """ + Start a level through the boss object + """ + self.get_game().boss.start_level(index) + self.deactivate() + + def launch_selected_index(self): + """ + Launch level index stored in the member variable + """ + self.launch(self.level_index_selected) def update(self): if self.active: - self.get_display_surface().fill((255, 255, 0)) + self.get_game().logo.update() + if self.level_index_selected is None: + for level_index, platform in enumerate(self.platforms): + if platform.get_glowing_edge() == self.get_game().platform.get_edge_pressed(): + self.level_index_selected = level_index + break + if self.level_index_selected is not None: + for level_index in range(3): + if level_index != self.level_index_selected: + self.platforms[level_index].view.play(self.platforms[level_index].view.wipe_out) + self.previews[level_index].play(self.previews[level_index].wipe_out) + self.get_audio().play_sfx("complete_pattern_3") + elif not self.get_game().wipe.is_playing() and any(preview.is_hidden() for preview in self.previews): + self.get_game().wipe.start(self.launch_selected_index) + for platform in self.platforms: + platform.update() + for ii, preview in enumerate(self.previews): + if ii == self.level_index_selected: + self.zoom += 0.1 + frame = pygame.transform.scale( + preview.get_current_frame(), (int(preview.location.w * self.zoom), int(preview.location.h * self.zoom))) + rect = frame.get_rect() + rect.center = preview.location.center + preview.update() + self.get_display_surface().blit(frame, rect) + else: + preview.update() class Button(Sprite): @@ -600,15 +818,12 @@ class Title(Animation): Handles displaying and drawing the title screen. """ - UNLOCK_MOVES = NS.NW, NS.N, NS.NE, NS.NW + UNLOCK_MOVES = NS.NW, NS.N, NS.NE, NS.S def __init__(self, parent): Animation.__init__(self, parent) - self.plank = Sprite(self) - self.plank.load_from_path(self.get_resource("Title_plank.png"), True) ds = self.get_display_surface() dsr = ds.get_rect() - self.plank.location.center = dsr.center self.angle = pi / 8 self.video = Video(self, 320) self.video.location.center = 329, 182 @@ -636,47 +851,45 @@ class Title(Animation): def start_game(self): """ - Turn off the title screen and display the level select. Set the most recent time to None so the most - recent high score stops blinking. + Turn off the title screen and either display the level select or start level one if level select is disabled. Set the + most recent time to None so the most recent high score stops blinking. """ self.deactivate() - self.get_game().set_most_recent_time(None) - self.get_game().level_select.activate() + self.get_game().most_recent_score = None + if self.get_configuration("system", "enable-level-select"): + self.get_game().level_select.activate() + else: + self.get_game().level_select.launch(0) def draw_scores(self): - step = 75 + step = 56 ds = self.get_display_surface() - lines = map(int, open(self.get_resource("scores")).readlines()) - entries = ["BEST"] + sorted(lines)[:9] + if not self.get_configuration("system", "enable-level-select"): + entries = ["BEST"] + sorted([score for score in self.get_game().scores if score.is_full()])[:15] + else: + entries = ["NORMAL"] + sorted([score for score in self.get_game().scores if score.level_index == 0])[:3] + \ + ["ADVANCED"] + sorted([score for score in self.get_game().scores if score.level_index == 1])[:3] + \ + ["EXPERT"] + sorted([score for score in self.get_game().scores if score.level_index == 2])[:7] for ii, entry in enumerate(entries): - if ii == 0 or ii == 5: - y = 30 + if ii == 0 or ii == 8: + y = 20 font = Font(self.get_resource(Dialogue.FONT_PATH), 18) - if ii > 0: - text = self.get_formatted_time(entry) + if isinstance(entry, NS.Score): + text = entry.formatted() else: text = entry - message = render_box(font, text, True, Color(255, 255, 255), - Color(128, 128, 128), Color(0, 0, 0), padding=2) + message = render_box(font, text, True, Color(255, 255, 255), Color(128, 128, 128), Color(0, 0, 0), padding=2) message.set_alpha(230) rect = message.get_rect() rect.top = y - if ii < 5: + if ii < 8: rect.left = -1 else: rect.right = ds.get_width() + 1 - if not entry == self.get_game().most_recent_time or not self.get_game().score_hidden: + if not entry == self.get_game().most_recent_score or not self.get_game().score_hidden: ds.blit(message, rect) y += step - def get_formatted_time(self, entry): - if int(entry) == 5999999: - return "--:--.-" - else: - minutes, milliseconds = divmod(int(entry), 60000) - seconds, fraction = divmod(milliseconds, 1000) - return "%i:%02i.%i" % (minutes, seconds, fraction / 100) - def show_video(self): self.video.unhide() self.play(self.hide_video, delay=self.get_configuration("time", "attract-gif-length"), play_once=True) @@ -1051,10 +1264,13 @@ class Platform(GameChild): on-screen representation. """ - def __init__(self, parent): + def __init__(self, parent, center): """ Initialize four lights, one for each pad on the platform. Initialize a Sprite for the pad graphics with one frameset per six possible combinations of lights. Initialize masks for creating a glow effect on the pads. + + @param parent PGFW game object that initialized this object + @param center tuple that gives the (x, y) screen coordinates of this platform """ GameChild.__init__(self, parent) self.lights = [ @@ -1072,7 +1288,7 @@ class Platform(GameChild): self.view.add_frameset([4], name=str(NS.NE)) self.view.add_frameset([5], name=str(NS.W)) self.view.add_frameset([6], name=str(NS.S)) - self.view.location.center = self.get_configuration("pads", "center") + self.view.location.center = center self.glow_masks = [] base_images = load_frames(self.get_resource("pad_mask"), True) for image in base_images: @@ -1299,9 +1515,11 @@ class Platform(GameChild): else: self.view.set_frameset(str(glowing)) self.view.update() - for light in self.lights: - if light.glowing(): - self.get_display_surface().blit(self.glow_masks[light.position][light.glow_index], self.view.location, None, BLEND_RGBA_ADD) + if not self.view.is_hidden() and not self.view.is_playing(self.view.wipe_out): + for light in self.lights: + if light.glowing(): + self.get_display_surface().blit( + self.glow_masks[light.position][light.glow_index], self.view.location, None, BLEND_RGBA_ADD) class Light(Animation): @@ -1370,12 +1588,11 @@ class Light(Animation): Checks the attack state to determine whether to start or stop glowing """ Animation.update(self) - if not self.get_game().title.active: + if not self.get_game().title.active and not self.get_game().level_select.active: boss = self.get_game().boss chemtrails = self.get_game().chemtrails # checks the boss attack queue and chameleon queue index to see if the glow should be started now - if boss.queue and not self.is_playing(self.glow) \ - and self.in_orientation(boss.queue[chemtrails.queue_index]): + if boss.queue and not self.is_playing(self.glow) and self.in_orientation(boss.queue[chemtrails.queue_index]): self.play(self.glow) # turns off the glow elif self.is_playing(self.glow) and (not boss.queue or not self.in_orientation(boss.queue[chemtrails.queue_index])): @@ -1403,13 +1620,7 @@ class Light(Animation): lightness = 0 else: lightness = 40 - lines( - self.get_display_surface(), - get_hsla_color( - int(self.color.hsla[0]), saturation, lightness - ), - True, shifted, 3 - ) + lines(self.get_display_surface(), get_hsla_color(int(self.color.hsla[0]), saturation, lightness), True, shifted, 3) def in_orientation(self, orientation): """ @@ -1483,7 +1694,7 @@ class Chemtrails(Sprite): if self.active: self.orient() Sprite.update(self) - if not self.get_game().title.active: + if not self.get_game().title.active and not self.get_game().level_select.active: boss = self.get_game().boss if boss.queue: self.timer.tick() @@ -1621,7 +1832,12 @@ class Boys(Meter): background = load(self.get_resource("HUD_lives.png")).convert() rect = background.get_rect() rect.bottomleft = 6, dsr.bottom - 4 - self.setup(background, rect, 60, (0, 255, 0), 3, "scrapeIcons/scrapeIcons_01.png") + # The amount of lives depends on whether it's boss rush or level select mode. + if self.get_configuration("system", "enable-level-select"): + lives = self.get_configuration("system", "lives-level-select-mode") + else: + lives = self.get_configuration("system", "lives-boss-rush-mode") + self.setup(background, rect, 60, (0, 255, 0), lives, "scrapeIcons/scrapeIcons_01.png") class BossSprite(Sprite): @@ -1661,12 +1877,12 @@ class Boss(Animation): self.spoopy.load_from_path("Spoopy.png", True) # Set up alien sprite with boil and hurt animations self.visitor = BossSprite(self, 42) + self.visitor.add_frameset(name="normal", switch=True) + self.visitor.load_from_path("alienAnimations/alienBoil", True) self.visitor.add_frameset(name="hurt", switch=True) self.visitor.load_from_path("alienAnimations/alienHit", True) self.visitor.add_frameset(name="entrance", switch=True) self.visitor.load_from_path("alienAnimations/alienIntro", True) - self.visitor.add_frameset(name="normal", switch=True) - self.visitor.load_from_path("alienAnimations/alienBoil", True) for sprite in self.kool_man, self.visitor, self.spoopy: sprite.location.topleft = 207, 10 self.health = Health(self) @@ -2042,13 +2258,16 @@ class Boss(Animation): self.queue = [] self.brandish_complete = True if win: + if self.get_configuration("system", "enable-level-select"): + self.get_game().add_time_to_scores(self.time_elapsed, self.level_index) if self.level_index == 0: self.kool_man.set_frameset(0) elif self.level_index == 1: self.visitor.set_frameset("hurt") elif self.level_index == 2: self.spoopy.set_frameset(0) - self.add_score() + if not self.get_configuration("system", "enable-level-select"): + self.get_game().add_time_to_scores(self.time_elapsed) self.backgrounds[self.level_index].set_frameset("shining") else: self.play(self.flash_player_damage) @@ -2056,10 +2275,6 @@ class Boss(Animation): self.kills += not win self.play(self.show_end_dialogue, delay=3000, play_once=True) - def add_score(self): - self.get_game().set_most_recent_time(self.time_elapsed) - open(self.get_resource("scores"), "a").write(str(self.time_elapsed) + "\n") - def show_end_dialogue(self): dialogue = self.get_game().dialogue dialogue.activate() @@ -2073,30 +2288,53 @@ class Boss(Animation): if self.player_defeated: dialogue.show_text("Wiped out!") else: - dialogue.show_text("Well done! But it's not over yet!") + if self.get_configuration("system", "enable-level-select"): + dialogue.show_text("Ouch! Oof!") + else: + dialogue.show_text("Well done! But it's not over yet!") elif self.level_index == 2: if self.player_defeated: dialogue.show_text("Just like I thought!") else: - dialogue.show_text("H-how? But you're only a lizard! How could you" + - " manage to defeat all of us?") + if self.get_configuration("system", "enable-level-select"): + dialogue.show_text("H-how? But you're only a lizard!") + else: + dialogue.show_text("H-how? But you're only a lizard! How could you" + + " manage to defeat all of us?") if self.player_defeated: self.countdown.activate() else: self.play(self.end_dialogue, delay=5000, play_once=True) - def transition_to_battle(self): - index = self.level_index + (not self.player_defeated) - if self.kills >= 3: - self.get_game().reset(True) - elif index < 3: - self.start_level(index) + def end_dialogue(self): + self.get_game().dialogue.deactivate() + if not self.battle_finished: + self.combo(delay=1300) else: + self.get_game().wipe.start(self.transition_after_battle) + + def transition_after_battle(self): + """ + Determine whether to reset to title screen, relaunch the current level, launch the next level, or activate the ending object and + call the appropriate method. + """ + # If the player is out of lives, reset the game. + if self.get_game().chemtrails.boys.amount <= 0: + self.get_game().reset(True) + # Check if the ending screen should be activated. + elif not self.player_defeated and self.get_configuration("system", "enable-level-select") or self.level_index == 2: + defeated_level_index = self.level_index game = self.get_game() game.boss.reset() game.chemtrails.reset() game.platform.reset() - game.ending.activate() + game.ending.activate(defeated_level_index) + else: + # Level index to launch is the current level if the player was defeated, otherwise it's the next level. If the player wasn't + # defeated, level select mode would have launched the game ending, so the next level will only be launched when boss rush + # mode is active. + index = self.level_index + (not self.player_defeated) + self.start_level(index) def transition_to_title(self): self.get_game().reset(True) @@ -2109,13 +2347,6 @@ class Boss(Animation): elif self.level_index == 2: self.spoopy.set_frameset(0) - def end_dialogue(self): - self.get_game().dialogue.deactivate() - if not self.battle_finished: - self.combo(delay=1300) - else: - self.get_game().wipe.start(self.transition_to_battle) - def start_player_damage(self): """ Launch the flash player damage effect and queue it to end after a certain amount of time @@ -2172,6 +2403,19 @@ class Boss(Animation): self.visitor.get_current_frameset().reset() self.visitor.set_frameset("entrance") + def level_sprite(self, level_index): + """ + Return the boss sprite associated with this the given level index. + + @param level_index index of the level of the requested sprite + """ + if level_index == 0: + return self.kool_man + elif level_index == 1: + return self.visitor + else: + return self.spoopy + def update(self): """ Update graphics @@ -2192,7 +2436,7 @@ class Boss(Animation): if not self.battle_finished: self.combo() else: - self.get_game().wipe.start(self.transition_to_battle) + self.get_game().wipe.start(self.transition_after_battle) self.advance_prompt.cancel_first_press() else: self.time_elapsed += self.get_game().time_filter.get_last_frame_duration() @@ -2230,7 +2474,7 @@ class Boss(Animation): dialogue = self.get_game().dialogue if dialogue.active: self.get_game().dialogue.update() - if self.countdown.active: + if self.countdown.active and self.get_game().chemtrails.boys.amount > 0: self.advance_prompt.update() @@ -2277,8 +2521,6 @@ class Countdown(GameChild): if self.get_game().chemtrails.boys.amount > 0: self.heading.update() self.glyphs[int(self.remaining / 1000)].update() - else: - self.game_over.update() if not self.get_game().wipe.is_playing(): if self.remaining <= 0: self.get_game().wipe.start(self.end_game) @@ -2558,9 +2800,13 @@ class Health(Meter): class Ending(Animation): + """ + Scene for the end of a successful play session. The Tony and slime bag sprites will be displayed, and Tony will say something + to the player. The player's time will be displayed as a sprite that bounces around the screen. + """ - TEXT = "Wow! You vanquished all the goons and skated like a pro, slime bag." + \ - " You made your\nfather proud today. I love you, child.", + BOSS_RUSH_TEXT = "Wow! You vanquished all the goons and skated like a pro, slime bag." + \ + " You made your\nfather proud today. I love you, child." def __init__(self, parent): Animation.__init__(self, parent) @@ -2568,7 +2814,7 @@ class Ending(Animation): self.slime_bag.load_from_path(self.get_resource("Introduction_slime_bag.png"), True) self.slime_bag.location.center = self.get_display_surface().get_rect().centerx, 300 self.tony_avatar = load(self.get_resource("Introduction_tony_avatar.png")).convert() - self.advance_prompt = AdvancePrompt(self) + self.font = Font(self.get_resource("rounded-mplus-1m-bold.ttf"), 64) self.register(self.start, self.start_wipe) def reset(self): @@ -2576,18 +2822,16 @@ class Ending(Animation): self.slime_bag.unhide() self.halt() self.text_index = 0 - self.advance_prompt.reset() self.angle = choice((pi / 4, 3 * pi / 4, 5 * pi / 4, 7 * pi / 4)) def deactivate(self): self.active = False - def activate(self): + def activate(self, level_index): + self.defeated_level_index = level_index self.active = True self.play(self.start, delay=3000, play_once=True) - font = Font(self.get_resource("rounded-mplus-1m-bold.ttf"), 64) - time = self.get_game().title.get_formatted_time(self.get_game().most_recent_time) - foreground = font.render(time, False, (180, 150, 20), (255, 255, 255)).convert_alpha() + foreground = self.font.render(str(self.get_game().most_recent_score), False, (180, 150, 20), (255, 255, 255)).convert_alpha() dsr = self.get_display_surface().get_rect() self.text = RainbowSprite(self, foreground, 180, 200) self.text.location.midtop = dsr.centerx, 80 @@ -2601,10 +2845,25 @@ class Ending(Animation): self.get_audio().play_bgm("end") def start(self): - self.advance_prompt.cancel_first_press() dialogue = self.get_game().dialogue - dialogue.set_name("Tony") - dialogue.show_text(self.TEXT[0]) + if self.get_configuration("system", "enable-level-select"): + rank = 0 + for score in sorted([score for score in self.get_game().scores if score.level_index == self.defeated_level_index]): + rank += 1 + if score == self.get_game().most_recent_score: + break + text = f"You vanquished my goon and got the #{rank} rank! Well done, slime bag.\n" + if self.defeated_level_index == 2: + dialogue.set_name("Tony") + text += "You made your father proud today. I love you child." + elif self.defeated_level_index == 1: + text += "Give expert mode a try next." + else: + text += "Give advanced mode a try next." + else: + text = self.BOSS_RUSH_TEXT + dialogue.set_name("Tony") + dialogue.show_text(text) self.text_index = 0 def end_game(self): @@ -2623,21 +2882,19 @@ class Ending(Animation): self.get_game().tony.update() self.slime_bag.update() dsr = self.get_display_surface().get_rect() + # Bounce the time sprite around the screen if self.text.location.right > dsr.right or self.text.location.left < dsr.left: self.angle = reflect_angle(self.angle, 0) if self.text.location.right > dsr.right: self.text.move(dsr.right - self.text.location.right) else: self.text.move(dsr.left - self.text.location.left) - if self.text.location.bottom > self.get_game().dialogue.avatar_box.location.top \ - or self.text.location.top < dsr.top: + if self.text.location.bottom > self.get_game().dialogue.avatar_box.location.top or self.text.location.top < dsr.top: self.angle = reflect_angle(self.angle, pi) if self.text.location.top < dsr.top: self.text.move(dy=dsr.top - self.text.location.top) else: - self.text.move( - dy=self.get_game().dialogue.avatar_box.location.top - \ - self.text.location.bottom) + self.text.move(dy=self.get_game().dialogue.avatar_box.location.top - self.text.location.bottom) dx, dy = get_delta(self.angle, 5, False) self.text.move(dx, dy) self.text.update() diff --git a/config b/config index ff9edac..61d29e2 100644 --- a/config +++ b/config @@ -30,7 +30,10 @@ effects = yes [system] # will force set display->effects to off -minimize-load-time = no +minimize-load-time = yes +enable-level-select = yes +lives-boss-rush-mode = 3 +lives-level-select-mode = 1 [mouse] visible = no diff --git a/resource/scores b/resource/scores index 2628058..168988a 100644 --- a/resource/scores +++ b/resource/scores @@ -1,16 +1,10 @@ -5999999 -5999999 -5999999 -5999999 -5999999 -5999999 -5999999 -5999999 -5999999 -5999999 -52586 -54614 -52434 -168209 -141407 -139786 +51849 0 +35340 1 +37954 1 +39171 1 +42320 1 +50057 1 +64604 1 +38638 2 +100000 -1 +1000000 -1