import os import random import time from engine import maze, config from engine.collision_system import CollisionLayer from runtime_paths import bundle_path class Graphics: def __init__(self, game): self.game = game self.loaded_theme_index = None def load_assets(self): theme_index = self.get_theme_index() print(f"[gfx] load_assets requested: level={self.game.current_level + 1} theme={theme_index}") if getattr(self.game, "startup_loading_active", False): self.game._update_startup_loading( "Loading graphics", detail=f"Preparing theme {theme_index}", progress=0.3, ) if not hasattr(self, "theme_assets_cache"): self.theme_assets_cache = {} if not hasattr(self, "blood_layer_sprites"): self.blood_layer_sprites = [] if not hasattr(self, "cave_foreground_tiles"): self.cave_foreground_tiles = [] if not hasattr(self, "tunnel_cover_texture"): self.tunnel_cover_texture = None if not getattr(self, "common_assets_loaded", False): print("Loading graphics assets...") if getattr(self.game, "startup_loading_active", False): self.game._update_startup_loading( "Loading graphics", detail="Decoding sprites and tiles", progress=0.4, ) self.rat_assets = {} self.rat_assets_textures = {} self.rat_image_sizes = {} self.bomb_assets = {} self.assets = {} for sex in ["MALE", "FEMALE", "BABY"]: self.rat_assets[sex] = {} self.rat_assets_textures[sex] = {} self.rat_image_sizes[sex] = {} for direction in ["UP", "DOWN", "LEFT", "RIGHT"]: self.rat_assets[sex][direction] = self.game.render_engine.load_image( f"Rat/BMP_{sex}_{direction}.png", transparent_color=((125, 125, 125), (128, 128, 128)), ) texture = self.game.render_engine.load_image( f"Rat/BMP_{sex}_{direction}.png", transparent_color=((125, 125, 125), (128, 128, 128)), surface=False, ) self.rat_assets_textures[sex][direction] = texture self.rat_image_sizes[sex][direction] = texture.size for n in range(5): self.bomb_assets[n] = self.game.render_engine.load_image( f"Rat/BMP_BOMB{n}.png", transparent_color=((125, 125, 125), (128, 128, 128)), ) rat_asset_dir = bundle_path("assets", "Rat") for file in sorted(os.listdir(rat_asset_dir)): if file.endswith(".png") and not file.startswith("."): # Check if it's one of our expected BMP files or other known assets # to avoid loading temporary or irrelevant PNGs file_key = file[:-4] try: self.assets[file_key] = self.game.render_engine.load_image( f"Rat/{file}", transparent_color=((125, 125, 125), (128, 128, 128)), ) except (FileNotFoundError, IOError) as e: print(f"Warning: Could not load asset {file}: {e}") print("Pre-generating blood stain pool...") if getattr(self.game, "startup_loading_active", False): self.game._update_startup_loading( "Loading graphics", detail="Generating blood pool", progress=0.58, ) self.blood_stain_textures = [] for _ in range(10): blood_surface = self.game.render_engine.generate_blood_surface() blood_texture = self.game.render_engine.draw_blood_surface(blood_surface, (0, 0)) if blood_texture: self.blood_stain_textures.append(blood_texture) self.common_assets_loaded = True print("[gfx] common assets loaded") else: print("[gfx] common assets cache hit") if theme_index not in self.theme_assets_cache: print(f"Loading theme assets {theme_index}...") if getattr(self.game, "startup_loading_active", False): self.game._update_startup_loading( "Loading graphics", detail=f"Loading theme {theme_index} art", progress=0.74, ) self.theme_assets_cache[theme_index] = { "floor_tile": self.game.render_engine.create_color_surface((128, 128, 128)), "tunnel": self.game.render_engine.load_image("Rat/BMP_TUNNEL.png"), "grasses": [ self.game.render_engine.load_image(f"Rat/BMP_{theme_index}_GRASS_{i+1}.png", surface=True) for i in range(4) ], "grass_textures": [ self.game.render_engine.load_image(f"Rat/BMP_{theme_index}_GRASS_{i+1}.png") for i in range(4) ], "flowers": [ self.game.render_engine.load_image(f"Rat/BMP_{theme_index}_FLOWER_{i+1}.png", surface=True) for i in range(4) ], "flower_textures": [ self.game.render_engine.load_image(f"Rat/BMP_{theme_index}_FLOWER_{i+1}.png") for i in range(4) ], "caves": { direction: self.game.render_engine.load_image( f"Rat/BMP_{theme_index}_CAVE_{direction}.png", transparent_color=((125, 125, 125), (128, 128, 128)), surface=False, ) for direction in ["UP", "DOWN", "LEFT", "RIGHT"] }, "explosions": { direction: self.game.render_engine.load_image( f"Rat/BMP_{theme_index}_EXPLOSION_{direction}.png", transparent_color=((125, 125, 125), (128, 128, 128)), surface=False, ) for direction in ["UP", "DOWN", "LEFT", "RIGHT"] }, "edges": { direction: self.game.render_engine.load_image(f"Rat/BMP_{theme_index}_{direction}.png", surface=True) for direction in ["N", "S", "E", "W"] }, "corners": { direction: self.game.render_engine.load_image(f"Rat/BMP_{theme_index}_{direction}.png", surface=True) for direction in ["NE", "NW", "SE", "SW"] }, "inner_corners": { direction: self.game.render_engine.load_image(f"Rat/BMP_{theme_index}_{direction}.png", surface=True) for direction in ["EN", "ES", "WN", "WS"] }, } print(f"[gfx] theme cache miss -> loaded theme {theme_index}") else: print(f"[gfx] theme cache hit -> reusing theme {theme_index}") if getattr(self.game, "startup_loading_active", False): self.game._update_startup_loading( "Loading graphics", detail="Finishing render setup", progress=0.84, ) self.loaded_theme_index = theme_index theme_assets = self.theme_assets_cache[theme_index] self.floor_tile = theme_assets["floor_tile"] self.tunnel = theme_assets["tunnel"] self.grasses = theme_assets["grasses"] self.grass_textures = theme_assets["grass_textures"] self.flowers = theme_assets["flowers"] self.flower_textures = theme_assets["flower_textures"] self.caves = theme_assets["caves"] self.explosions = theme_assets["explosions"] self.edges = theme_assets["edges"] self.corners = theme_assets["corners"] self.inner_corners = theme_assets["inner_corners"] def get_theme_index(self): return self.game.current_level % 32 // 8 + 1 # ==================== RENDERING ==================== def draw_maze(self): if self.game.background_texture is None: print(f"[gfx] generating background texture for level={self.game.current_level + 1} theme={self.loaded_theme_index}") self.regenerate_background() self.game.render_engine.draw_background(self.game.background_texture) # Draw blood layer as sprites (optimized - no background regeneration) self.draw_blood_layer() def regenerate_tunnel_cover(self): """Generate an overlay texture that covers internal tunnel passages with grass sub-tiles. A tunnel cell is considered an internal passage only when it is surrounded by occupied cells (walls or tunnels) on all four sides. In that case there is no cave entrance drawn by cave_foreground, so we cover it here. """ tunnel_tiles = [] half_cell = self.game.cell_size // 2 def occupied(x, y): return self.game.map.in_bounds(x, y) and self.game.map.get_cell(x, y) != maze.MAP_EMPTY for y, row in enumerate(self.game.map.tiles): for x, cell in enumerate(row): if cell != maze.MAP_TUNNEL: continue above = occupied(x, y - 1) below = occupied(x, y + 1) left = occupied(x - 1, y) right = occupied(x + 1, y) if not (above and below and left and right): continue px = x * self.game.cell_size py = y * self.game.cell_size for qx, qy, sx, sy in [ (0, 0, 0, 0), (half_cell, 0, half_cell, 0), (0, half_cell, 0, half_cell), (half_cell, half_cell, half_cell, half_cell), ]: grass = random.choice(self.grasses) tunnel_tiles.append((grass, sx, sy, half_cell, half_cell, px + qx, py + qy)) self.tunnel_cover_texture = self.game.render_engine.create_overlay_texture(tunnel_tiles) def draw_cave_foreground(self): active_cave_explosions = {} for unit in self.game.units.values(): if unit.collision_layer != CollisionLayer.EXPLOSION: continue if not self.game.map.is_tunnel(*unit.position): continue active_cave_explosions[unit.position] = getattr(unit, "cave_direction", None) for cell_x, cell_y, direction, surface, x, y in self.cave_foreground_tiles: if (cell_x, cell_y) in active_cave_explosions: explosion_direction = active_cave_explosions[(cell_x, cell_y)] or direction surface = self.explosions.get(explosion_direction, surface) self.game.render_engine.draw_image(x, y, surface, anchor="nw", tag="cave") def draw_blood_layer(self): """Draw all blood stains as sprites overlay (optimized)""" for blood_texture, x, y in self.blood_layer_sprites: self.game.render_engine.draw_image(x, y, blood_texture, tag="blood") def regenerate_background(self): """Generate or regenerate the background texture (static - no blood stains)""" texture_tiles = [] self.cave_foreground_tiles = [] self.regenerate_tunnel_cover() half_cell = self.game.cell_size // 2 def draw(surface, x, y): texture_tiles.append((surface, x, y)) return True # allow callers to count successful draws def draw_cave(surface, x, y, direction): self.cave_foreground_tiles.append((x // self.game.cell_size, y // self.game.cell_size, direction, surface, x, y)) def occupied(x, y): return self.game.map.in_bounds(x, y) and self.game.map.get_cell(x, y) != maze.MAP_EMPTY def is_wall(x, y): return self.game.map.in_bounds(x, y) and self.game.map.get_cell(x, y) == maze.MAP_WALL def is_tunnel(x, y): return self.game.map.in_bounds(x, y) and self.game.map.get_cell(x, y) == maze.MAP_TUNNEL def random_wall(): return random.choice(self.grasses) def random_wall_texture(): return random.choice(self.grass_textures) def random_flower(): return random.choice(self.flowers) def random_flower_texture(): return random.choice(self.flower_textures) for y, row in enumerate(self.game.map.tiles): for x, cell in enumerate(row): px = x * self.game.cell_size py = y * self.game.cell_size if cell == maze.MAP_EMPTY: continue if cell == maze.MAP_WALL: wall_tiles_drawn = 0 def draw_wall(surface, x, y): nonlocal wall_tiles_drawn draw(surface, x, y) wall_tiles_drawn += 1 if x == 0 or y == 0 or x == self.game.map.width - 1 or y == self.game.map.height - 1: draw_wall(random_wall(), px, py) if x > 0 and y > 0 and (not is_wall(x - 1, y - 1) or not is_wall(x, y - 1) or not is_wall(x - 1, y)): north = is_wall(x, y - 1) west = is_wall(x - 1, y) if north or west: if north and west: draw_wall(self.inner_corners["WN"], px, py) elif north and not west: draw_wall(self.edges["W"], px, py) else: draw_wall(self.edges["N"], px, py) else: draw_wall(self.corners["NW"], px, py) if y < self.game.map.height - 1 and x < self.game.map.width - 1: south = is_wall(x, y + 1) east = is_wall(x + 1, y) southeast = is_wall(x + 1, y + 1) if southeast and south and east: if ( random.randrange(10) != 0 or x == 0 or y == 0 or x == self.game.map.width - 2 or y == self.game.map.height - 2 or is_tunnel(x + 1, y) or is_tunnel(x, y + 1) or is_tunnel(x + 1, y + 1) ): draw_wall(random_wall(), px + half_cell, py + half_cell) else: draw_wall(random_flower(), px + half_cell, py + half_cell) elif south or east: if south and east: draw_wall(self.inner_corners["ES"], px + half_cell, py + half_cell) elif south and not east: draw_wall(self.edges["E"], px + half_cell, py + half_cell) else: draw_wall(self.edges["S"], px + half_cell, py + half_cell) else: draw_wall(self.corners["SE"], px + half_cell, py + half_cell) if y > 0 and x < self.game.map.width - 1 and (not is_wall(x + 1, y - 1) or not is_wall(x, y - 1) or not is_wall(x + 1, y)): north = is_wall(x, y - 1) east = is_wall(x + 1, y) if north or east: if north and east: draw_wall(self.inner_corners["EN"], px + half_cell, py) elif north and not east: draw_wall(self.edges["E"], px + half_cell, py) else: draw_wall(self.edges["N"], px + half_cell, py) else: draw_wall(self.corners["NE"], px + half_cell, py) if y < self.game.map.height - 1 and x > 0 and (not is_wall(x - 1, y + 1) or not is_wall(x, y + 1) or not is_wall(x - 1, y)): south = is_wall(x, y + 1) west = is_wall(x - 1, y) if south or west: if south and west: draw_wall(self.inner_corners["WS"], px, py + half_cell) elif south and not west: draw_wall(self.edges["W"], px, py + half_cell) else: draw_wall(self.edges["S"], px, py + half_cell) else: draw_wall(self.corners["SW"], px, py + half_cell) # Fallback: isolated/surrounded wall cells must not show background color if wall_tiles_drawn == 0: draw(random_wall(), px, py) elif cell == maze.MAP_TUNNEL: above = occupied(x, y - 1) below = occupied(x, y + 1) left = occupied(x - 1, y) right = occupied(x + 1, y) if above: if below: if left: if right: # Internal tunnel passage: leave it empty so it uses # the background fill color and stays visually open. pass else: draw_cave(self.caves["RIGHT"], px, py, "RIGHT") else: draw_cave(self.caves["LEFT"], px, py, "LEFT") else: draw_cave(self.caves["DOWN"], px, py, "DOWN") else: draw_cave(self.caves["UP"], px, py, "UP") # Blood stains now handled separately as overlay layer self.game.background_texture = self.game.render_engine.create_texture(texture_tiles, fill_color=(128, 128, 128)) def add_blood_stain(self, position): """Add a blood stain as sprite overlay (optimized - no background regeneration)""" # Pick random blood texture from pre-generated pool if not self.blood_stain_textures: return blood_texture = random.choice(self.blood_stain_textures) x = position[0] * self.game.cell_size y = position[1] * self.game.cell_size # Add to blood layer sprites instead of regenerating background self.blood_layer_sprites.append((blood_texture, x, y)) def scroll_cursor(self, x=0, y=0): if self.game.pointer[0] + x > self.game.map.width or self.game.pointer[1] + y > self.game.map.height: return self.game.pointer = ( max(1, min(self.game.map.width-2, self.game.pointer[0] + x)), max(1, min(self.game.map.height-2, self.game.pointer[1] + y)) ) self.game.render_engine.scroll_view(self.game.pointer) # ==================== MENU RENDERING ==================== def _menu_font(self, size): clamped = max(10, min(69, int(size))) return self.game.render_engine.fonts[clamped] def _draw_start_menu_difficulty_selector(self, x, y, width, height): colors = config.START_MENU_COLORS difficulty_config = self.game._difficulty_config() accent = difficulty_config["accent"] render_engine = self.game.render_engine center_x = x + width // 2 compact_selector = height <= 72 title_font = self._menu_font(render_engine.target_size[1] // 38) value_font = self._menu_font(render_engine.target_size[1] // 23) arrow_font = value_font section_gap = 4 if compact_selector else 6 title_line_height = max(14, render_engine.target_size[1] // 32) value_line_height = max(18, render_engine.target_size[1] // 24) current_y = y + 2 render_engine.draw_text("Difficulty", title_font, ("center", current_y), colors["muted"]) current_y += title_line_height + section_gap arrow_offset = max(34, min(52, width // 7)) arrow_y = current_y - (1 if compact_selector else 0) render_engine.draw_text("<", arrow_font, (center_x - arrow_offset, arrow_y), colors["muted"]) render_engine.draw_text(">", arrow_font, (center_x + arrow_offset, arrow_y), colors["muted"]) render_engine.draw_text( difficulty_config["label"], value_font, ("center", current_y), accent, ) current_y += value_line_height def _draw_start_menu_slider(self, x, y, width, height, setting_name, label, value, selected): colors = config.START_MENU_COLORS style = config.START_MENU_AUDIO_STYLES[setting_name] accent = style["accent"] fill_color = style["fill"] if selected else colors["card_fill"] border_color = accent if selected else (156, 156, 156) text_color = colors["text"] render_engine = self.game.render_engine render_engine.draw_rectangle(x, y, width, height, "start_menu_slider", filling=fill_color) render_engine.draw_rectangle(x, y, width, height, "start_menu_slider", outline=border_color) if selected: render_engine.draw_rectangle(x + 12, y + 10, 8, height - 20, "start_menu_slider", filling=accent) title_y = y + 10 render_engine.draw_text(label, self._menu_font(render_engine.target_size[1] // 32), (x + 34, title_y), text_color) render_engine.draw_text(f"{value}%", self._menu_font(render_engine.target_size[1] // 34), (x + width - 84, title_y + 2), text_color) track_x = x + 34 track_y = y + height - 26 track_width = width - 68 track_height = 14 filled_width = int(track_width * value / 100) knob_width = 14 knob_x = track_x + int((track_width - knob_width) * value / 100) render_engine.draw_rectangle(track_x, track_y, track_width, track_height, "start_menu_slider", filling=colors["track_fill"]) render_engine.draw_rectangle(track_x, track_y, track_width, track_height, "start_menu_slider", outline=(128, 128, 128)) if filled_width > 0: render_engine.draw_rectangle(track_x, track_y, filled_width, track_height, "start_menu_slider", filling=accent) render_engine.draw_rectangle(knob_x, track_y - 4, knob_width, track_height + 8, "start_menu_slider", filling=(255, 255, 255)) render_engine.draw_rectangle(knob_x, track_y - 4, knob_width, track_height + 8, "start_menu_slider", outline=accent) def _current_start_menu_animation_frame(self): animation = getattr(self.game, "start_menu_animation", None) if not animation or not animation["frames"]: return None, (0, 0) if len(animation["frames"]) == 1 or animation["total_duration"] <= 0: return animation["frames"][0], animation["size"] elapsed_ms = int((time.monotonic() - self.game.start_menu_animation_started_at) * 1000) current_offset = elapsed_ms % animation["total_duration"] accumulated = 0 for index, duration in enumerate(animation["durations"]): accumulated += duration if current_offset < accumulated: return animation["frames"][index], animation["size"] return animation["frames"][-1], animation["size"] def _render_audio_menu(self, title, subtitle_lines, primary_action_text, hint_text, image_name="BMP_WEWIN"): colors = config.START_MENU_COLORS render_engine = self.game.render_engine target_width, target_height = render_engine.target_size compact_menu = target_height <= 540 panel_x = max(48, target_width // 12) panel_y = max(34, target_height // 18) panel_width = target_width - panel_x * 2 panel_height = target_height - panel_y * 2 header_height = max(44, target_height // 13) render_engine.draw_rectangle(panel_x, panel_y, panel_width, panel_height, "start_menu", filling=colors["panel_fill"]) render_engine.draw_rectangle(panel_x, panel_y, panel_width, panel_height, "start_menu", outline=colors["panel_border"]) render_engine.draw_rectangle(panel_x, panel_y, panel_width, header_height, "start_menu", filling=colors["header_fill"]) render_engine.draw_text( title, self._menu_font(target_height // 20), ("center", panel_y + 16), colors["text"], ) image = self.assets[image_name] image_width, image_height = render_engine.get_image_size(image) image_y = panel_y + header_height + (14 if compact_menu else 18) render_engine.draw_image( target_width // 2 - image_width // 2 - render_engine.w_offset, image_y - render_engine.h_offset, image, "start_menu", ) info_y = image_y + image_height + 18 line_gap = 18 if compact_menu else max(20, target_height // 34) for index, line in enumerate(subtitle_lines): render_engine.draw_text( line, self._menu_font(target_height // (34 if index == 0 else 36)), ("center", info_y + line_gap * index), colors["text"] if index == 0 else colors["muted"], ) cta_y = info_y + line_gap * len(subtitle_lines) + 8 render_engine.draw_text( primary_action_text, self._menu_font(target_height // 31), ("center", cta_y), colors["text"], ) card_width = max(320, min(panel_width - 160, 720)) card_height = 60 if compact_menu else max(68, target_height // 10) card_x = target_width // 2 - card_width // 2 cards_y = cta_y + (16 if compact_menu else 26) card_gap = 12 if compact_menu else 16 for index, (setting_name, label) in enumerate(config.START_MENU_AUDIO_OPTIONS): card_y = cards_y + index * (card_height + card_gap) self._draw_start_menu_slider( card_x, card_y, card_width, card_height, setting_name, label, getattr(self.game, setting_name), index == self.game.start_menu_selection, ) cards_bottom = cards_y + len(config.START_MENU_AUDIO_OPTIONS) * card_height + (len(config.START_MENU_AUDIO_OPTIONS) - 1) * card_gap if compact_menu: hint_y = min(cards_bottom + 8, panel_y + panel_height - 28) render_engine.draw_text( hint_text, self._menu_font(target_height // 42), ("center", hint_y), colors["muted"], ) else: hint_y = cards_bottom + 10 hint_height = max(34, target_height // 18) hint_width = card_width hint_x = card_x render_engine.draw_rectangle(hint_x, hint_y, hint_width, hint_height, "start_menu", filling=colors["hint_fill"]) render_engine.draw_rectangle(hint_x, hint_y, hint_width, hint_height, "start_menu", outline=(170, 170, 170)) render_engine.draw_text( hint_text, self._menu_font(target_height // 40), ("center", hint_y + 10), colors["muted"], ) def render_start_menu(self): colors = config.START_MENU_COLORS render_engine = self.game.render_engine target_width, target_height = render_engine.target_size compact_menu = target_height <= 540 panel_x = max(48, target_width // 12) panel_y = max(34, target_height // 18) panel_width = target_width - panel_x * 2 panel_height = target_height - panel_y * 2 header_height = max(58, target_height // 10) title_font = self._menu_font(target_height // 15) body_font = self._menu_font(target_height // 28) meta_font = self._menu_font(target_height // 30) cta_font = self._menu_font(target_height // 22) hint_font = self._menu_font(target_height // 32) line_gap = 22 if compact_menu else max(26, target_height // 26) render_engine.draw_rectangle(panel_x, panel_y, panel_width, panel_height, "start_menu", filling=colors["panel_fill"]) render_engine.draw_rectangle(panel_x, panel_y, panel_width, panel_height, "start_menu", outline=colors["panel_border"]) render_engine.draw_rectangle(panel_x, panel_y, panel_width, header_height, "start_menu", filling=colors["header_fill"]) render_engine.draw_text( f"Welcome to Mice, {self.game.profile_integration.get_profile_name()}!", title_font, ("center", panel_y + max(12, header_height // 5)), colors["text"], ) animation_frame, animation_size = self._current_start_menu_animation_frame() animation_bottom = panel_y + header_height + 18 if animation_frame is not None: max_animation_width = panel_width - (70 if compact_menu else 110) display_width = min(animation_size[0], max_animation_width) scale_factor = display_width / animation_size[0] display_height = max(1, int(animation_size[1] * scale_factor)) animation_x = target_width // 2 - display_width // 2 animation_y = panel_y + header_height + (18 if compact_menu else 26) render_engine.draw_image( animation_x - render_engine.w_offset, animation_y - render_engine.h_offset, animation_frame, "start_menu", source_rect=(0, 0, animation_size[0], animation_size[1]), dest_size=(display_width, display_height), ) animation_bottom = animation_y + display_height player_profile = self.game.profile_integration.current_profile device_id = self.game.profile_integration.get_device_id() subtitle_lines = ["A game by Matteo, because he was bored."] if player_profile: if compact_menu: subtitle_lines.append( f"Best: {player_profile['best_score']} | Games: {player_profile['games_played']}" ) else: subtitle_lines.append(f"Device: {device_id}") subtitle_lines.append( f"Best Score: {player_profile['best_score']} | Games: {player_profile['games_played']}" ) elif compact_menu: subtitle_lines.append(f"Guest profile | {device_id}") else: subtitle_lines.append(f"Device: {device_id}") subtitle_lines.append("No profile loaded - playing as guest") info_y = animation_bottom + (18 if compact_menu else 22) for index, line in enumerate(subtitle_lines): render_engine.draw_text( line, body_font if index == 0 else meta_font, ("center", info_y + line_gap * index), colors["text"] if index == 0 else colors["muted"], ) difficulty_width = max(360, min(panel_width - 160, 760)) difficulty_height = 54 if compact_menu else max(72, target_height // 9) difficulty_x = target_width // 2 - difficulty_width // 2 difficulty_y = info_y + line_gap * len(subtitle_lines) + (12 if compact_menu else 18) self._draw_start_menu_difficulty_selector( difficulty_x, difficulty_y, difficulty_width, difficulty_height, ) cta_y = difficulty_y + difficulty_height + (12 if compact_menu else 24) render_engine.draw_text( "Press Return to start", cta_font, ("center", cta_y), colors["text"], ) if compact_menu: render_engine.draw_text( "Arrows change difficulty", hint_font, ("center", cta_y + line_gap), colors["muted"], ) render_engine.draw_text( "Esc quits M toggles audio", hint_font, ("center", cta_y + line_gap + 18), colors["muted"], ) else: render_engine.draw_text( "Arrows change difficulty Esc quits M toggles audio", hint_font, ("center", cta_y + line_gap + 8), colors["muted"], ) def render_pause_menu(self): subtitle_lines = [ f"Level {self.game.current_level + 1} | Points: {self.game.points}", f"Rats in maze: {self.game.unit_manager.count_rats()}", ] self._render_audio_menu( title="Pause", subtitle_lines=subtitle_lines, primary_action_text="Press Return to resume", hint_text="Up/Down select Left/Right adjust Esc quits", image_name="BMP_PAUSE" if "BMP_PAUSE" in self.assets else "BMP_WEWIN", )