diff --git a/NS.py b/NS.py index 28175fc..03e546f 100644 --- a/NS.py +++ b/NS.py @@ -102,7 +102,7 @@ class NS(Game, Animation): def formatted(self): if self.milliseconds is None: - return "--:--.-" + return "-:--.-" else: minutes, remainder = divmod(int(self.milliseconds), 60000) seconds, fraction = divmod(remainder, 1000) @@ -194,7 +194,8 @@ class NS(Game, Animation): "display": { "float": "attract-gif-alpha", - "bool": ["effects", "alpha-effect-title"] + "bool": ["effects", "alpha-effect-title"], + "path": "scores-font" }, "system": { @@ -305,13 +306,12 @@ class NS(Game, Animation): self.register(self.close_pop_up) self.reset() self.most_recent_score = None - self.pop_up_font = pygame.font.Font( - self.get_resource(Dialogue.FONT_PATH), 12) + 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 + blank_count = 25 for level_index in range(3): for _ in range(blank_count): self.scores.append(NS.Score.blank_level(level_index)) @@ -1066,13 +1066,18 @@ class Title(Animation): @param parent GameChild object that will connect this GameChild object to the overall tree and root Game object """ Animation.__init__(self, parent) - ds = self.get_display_surface() - dsr = ds.get_rect() + + # Set up attract mode pop-up self.angle = pi / 8 self.video = Video(self, 320) self.video.location.center = 329, 182 self.register(self.show_video, self.hide_video) self.show_video() + + # Set up scores + font_path = self.get_resource(self.get_configuration("display", "scores-font")) + self.heading_font = pygame.font.Font(font_path, 22) + self.score_font = pygame.font.Font(font_path, 16) self.score_sprites = [] def reset(self): @@ -1112,57 +1117,117 @@ class Title(Animation): else: self.get_game().level_select.launch(0) + def draw_score_to_column(self, score, column, screen_pos, rank): + """ + Blit `score` onto `column`, taking positioning and rank into account + + @param score Score to display in top scores + @param column Surface for displaying score + @param screen_pos absolute screen (x, y) of score topleft + @param rank rank of score + @return height of the drawn score + """ + # Parse both strings and score objects + if isinstance(score, NS.Score): + text = score.formatted() + else: + text = score + + # The background is based on rank + if rank == 0: + bg = 255, 215, 0 + elif rank == 1: + bg = 192, 192, 192 + elif rank == 2: + bg = 205, 127, 50 + else: + bg = 255, 255, 255 + + # Draw the score + score_surface = render_box(self.score_font, text, width=column.get_width(), background=bg) + column.blit(score_surface, (0, screen_pos[1])) + + # Create a blink effect for the most recent score + if score == self.get_game().most_recent_score: + self.score_blanker = BlinkingSprite(self, 500) + blank = pygame.surface.Surface(score_surface.get_size()) + blank.fill(bg) + self.score_blanker.add_frame(blank) + self.score_blanker.location = screen_pos + + # The height is used to move the draw location + return score_surface.get_height() + + def draw_heading_to_column(self, text, column, y): + """ + Blit `text` on `column` in the heading style + + @param text heading text to blit + @param column Surface where heading will be blit + @param y top position of text + @return height of drawn heading + """ + heading = render_box(self.heading_font, text, color=(255, 255, 255), background=(0, 0, 0), width=column.get_width()) + column.blit(heading, (0, y)) + return heading.get_height() + def draw_scores(self): """ - Draw frames for a sprite object for each score and store the sprite in a list to be drawn each frame. + Create two columns, one for each side of the screen. Draw as many scores as can fit along each column, in order from best to worst, separating + them evenly into categories: normal, advanced, and expert. Draw the two columns to the display surface, with the expectation that they will be + removed from the clip and will not be drawn over. Note that this doesn't support non-level select mode anymore. """ - # Create a list of strings in order of which to draw - 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] - - # Create a sprite object for each score and place on the screen in two columns on the left and right edges the screen - step = 56 ds = self.get_display_surface() - self.score_sprites = [] - for ii, entry in enumerate(entries): - - # Reset y counter - if ii == 0 or ii == 8: - y = 20 - - font = pygame.font.Font(self.get_resource(Dialogue.FONT_PATH), 18) - - # Parse both strings and score objects - if isinstance(entry, NS.Score): - text = entry.formatted() + self.score_blanker = None + heading_width, heading_height = self.heading_font.size("ADVANCED") + heading_width += 10 + score_height = self.score_font.size("0")[1] + column_width, column_height = heading_width, ds.get_height() + left_score_count = (column_height - heading_height * 2) // score_height + right_score_count = (column_height - heading_height) // score_height + total_score_count = left_score_count + right_score_count + per_category_count, remainder = divmod(total_score_count, 3) + left_column_sprite = Sprite(self) + left_column = pygame.surface.Surface((column_width, column_height)) + left_column.fill((255, 255, 255)) + x, y = 0, 0 + y += self.draw_heading_to_column("NORMAL", left_column, y) + count = per_category_count + if remainder: + count += 1 + remainder -= 1 + for rank, score in enumerate(sorted([score for score in self.get_game().scores if score.level_index == 0])[:count]): + y += self.draw_score_to_column(score, left_column, (x, y), rank) + left_score_count -= 1 + y += self.draw_heading_to_column("ADVANCED", left_column, y) + count = per_category_count + if remainder: + count += 1 + remainder -= 1 + right_column_sprite = Sprite(self) + right_column = pygame.surface.Surface((column_width, column_height)) + right_column.fill((255, 255, 255)) + column = left_column + for rank, score in enumerate(sorted([score for score in self.get_game().scores if score.level_index == 1])[:count]): + y += self.draw_score_to_column(score, column, (x, y), rank) + if left_score_count == 1: + y = 0 + x = ds.get_width() - column_width + column = right_column + left_column_sprite.add_frame(left_column) + if left_score_count == 0: + right_score_count -= 1 else: - text = entry - - # Create a surface as a box around the score - message = render_box(font, text, True, Color(255, 255, 255), Color(128, 128, 128), Color(0, 0, 0), padding=2) - message.set_alpha(230) - - # Store it in a sprite, use a blinking sprite for the most recent score - if not entry == self.get_game().most_recent_score: - sprite = Sprite(self) - else: - sprite = BlinkingSprite(self, 500) - sprite.add_frame(message) - self.score_sprites.append(sprite) - - # Place the sprite along the column - sprite.location.top = y - if ii < 8: - sprite.location.left = -1 - else: - sprite.location.right = ds.get_width() + 1 - - # Move to the next sprite location - y += step + left_score_count -= 1 + y += self.draw_heading_to_column("EXPERT", right_column, y) + count = per_category_count + for rank, score in enumerate(sorted([score for score in self.get_game().scores if score.level_index == 2])[:count]): + y += self.draw_score_to_column(score, right_column, (x, y), rank) + right_column_sprite.add_frame(right_column) + right_column_sprite.location.topleft = x, 0 + self.score_sprites = [left_column_sprite, right_column_sprite] + for sprite in self.score_sprites: + sprite.update() def show_video(self): self.video.unhide() @@ -1185,7 +1250,13 @@ class Title(Animation): if self.active: ds = self.get_display_surface() dsr = ds.get_rect() + + # Optimize by setting a clip that excludes the area where the scores are drawn + ds.set_clip((self.score_sprites[0].location.right, 0, self.score_sprites[1].location.left - self.score_sprites[0].location.right, dsr.height)) + + # Draw the background self.get_game().logo.update() + # Advance through the unlock pattern platform = self.get_game().platform if not self.get_game().wipe.is_playing() and platform.get_edge_pressed() == self.UNLOCK_MOVES[self.unlock_index]: @@ -1198,6 +1269,7 @@ class Title(Animation): platform.set_glowing(platform.get_buttons_from_edges([self.UNLOCK_MOVES[self.unlock_index]])) self.get_audio().play_sfx("land_0") self.get_game().tony.update() + # Bounce the GIF around the screen if self.video.location.right > dsr.right or self.video.location.left < dsr.left: self.angle = reflect_angle(self.angle, 0) @@ -1213,6 +1285,7 @@ class Title(Animation): self.video.move(dy=dsr.top - self.video.location.top) dx, dy = get_delta(self.angle, 5, False) self.video.move(dx, dy) + # Hide GIFs/attract mode (or keep them hidden) if input is detected. Set a countdown that will turn # attract mode back on if no input is detected before the countdown expires. As long as input keeps # being detected, this block will keep running and restarting the countdown. @@ -1221,10 +1294,11 @@ class Title(Animation): self.get_game().tony.set_frameset("static") self.halt() self.play(self.show_video, delay=self.get_configuration("time", "attract-reset-countdown"), play_once=True) - # self.video.update() - # self.draw_scores() - for score in self.score_sprites: - score.update() + + # Disable clip and draw blanker which creates a blinking affect for a single score + ds.set_clip(None) + if self.score_blanker: + self.score_blanker.update() class Dialogue(Animation): diff --git a/config b/config index 4129450..ca01c69 100644 --- a/config +++ b/config @@ -28,6 +28,7 @@ fullscreen = no attract-gif-alpha = 1.0 effects = True alpha-effect-title = False +scores-font = BPmono.ttf [system] # will force set display->effects to off diff --git a/lib/pgfw b/lib/pgfw index 0954cd7..314b722 160000 --- a/lib/pgfw +++ b/lib/pgfw @@ -1 +1 @@ -Subproject commit 0954cd768f6fed46bf77436317271865a888c3a1 +Subproject commit 314b722528a65e6f0053f8be1844c2d8818319f4 diff --git a/resource/BPmono.ttf b/resource/BPmono.ttf new file mode 100755 index 0000000..e6bb690 Binary files /dev/null and b/resource/BPmono.ttf differ diff --git a/resource/scores b/resource/scores index e69de29..e3897e9 100644 --- a/resource/scores +++ b/resource/scores @@ -0,0 +1,94 @@ +48935 0 +50940 0 +51245 0 +51372 0 +51754 0 +52110 0 +52736 0 +52863 0 +53242 0 +53715 0 +54546 0 +54603 0 +55707 0 +56045 0 +56636 0 +56732 0 +57247 0 +57299 0 +57326 0 +57436 0 +57670 0 +57885 0 +59459 0 +60290 0 +60440 0 +60510 0 +60751 0 +60824 0 +60957 0 +61433 0 +61677 0 +61838 0 +62269 0 +62562 0 +63416 0 +64223 0 +64412 0 +64534 0 +64577 0 +65496 0 +66051 0 +66635 0 +67401 0 +67523 0 +68302 0 +68541 0 +68948 0 +69290 0 +69510 0 +70404 0 +70528 0 +71138 0 +71335 0 +71495 0 +71627 0 +73511 0 +73848 0 +74455 0 +75015 0 +75387 0 +75646 0 +76002 0 +76945 0 +77434 0 +77526 0 +77701 0 +79705 0 +82020 0 +82777 0 +87726 0 +90718 0 +91883 0 +80500 1 +89673 1 +93372 1 +93427 1 +97111 1 +105880 1 +107336 1 +108024 1 +109913 1 +111785 1 +112993 1 +113387 1 +114742 1 +116102 1 +117369 1 +117614 1 +117744 1 +119812 1 +120058 1 +120493 1 +85743 2 +103819 2