Files
mice/engine/graphics.py
T
enne2 5afdf3705b Render rats inside internal tunnel passages
Previously Rat.draw() hid any rat whose center fell inside a tunnel.
The clipping helper only handled single-entrance cells and returned None
for internal passages/crossroads, causing rats to vanish entirely.

Now internal tunnel cells (those with 0 or 2+ open sides) draw the rat
normally so it remains visible while walking through the tunnel.
Single-entrance cells keep the partial clip effect.
Also fix the visible-ratio math for DOWN/RIGHT clipping: the old
formulas subtracted cell_size from a local coordinate, producing
zero or negative visibility.
2026-06-16 20:37:46 +02:00

692 lines
32 KiB
Python

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 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 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 = []
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",
)