"""The main menu screen for language selection, instructions, and starting the game."""
import pygame
import os
import math
from ..utils.constants import SCREEN_WIDTH, SCREEN_HEIGHT, PACKAGE_DIR
from ..utils.localization import localization_manager
# ── Unified colour palette ──────────────────────────────────────────────────
[docs]
C_PANEL = (20, 15, 10, 200) # semi-transparent dark panel
[docs]
C_TITLE = (255, 220, 100) # warm gold
[docs]
C_TEXT = (240, 230, 210) # warm off-white
[docs]
C_MUTED = (180, 165, 140) # secondary text
[docs]
C_BTN_NORMAL = ( 80, 55, 25) # dark brown button fill
[docs]
C_BTN_HOVER = (120, 85, 35) # lighter brown on hover
[docs]
C_BTN_BORDER = (220, 160, 60) # gold border
[docs]
C_DIFF_EASY = ( 30, 120, 50)
[docs]
C_DIFF_MEDIUM = (170, 120, 10)
[docs]
C_DIFF_HARD = (160, 30, 30)
[docs]
C_SELECTED = (255, 255, 255) # white highlight for selected button
[docs]
C_EXIT_NORMAL = (110, 35, 35)
[docs]
C_EXIT_HOVER = (160, 55, 55)
[docs]
RADIUS = 12 # universal border-radius for all buttons / panels
[docs]
class MainMenu:
"""Handles rendering and input for the main menu."""
def __init__(self, background_image=None):
[docs]
self.menu_state = 'language_select'
[docs]
self.username = ""
[docs]
self.input_box_active = True
[docs]
self.difficulty = 'medium'
[docs]
self.background_image = background_image
# ── Single font, clear size hierarchy ──────────────────────────────
def _font(size):
for name in ('segoeui', 'calibri', 'arial', 'freesansbold'):
try:
f = pygame.font.SysFont(name, size)
if f:
return f
except Exception:
pass
return pygame.font.Font(None, size)
[docs]
self.font_title = _font(74)
[docs]
self.font_heading = _font(36)
[docs]
self.font_button = _font(30)
[docs]
self.font_body = _font(24)
[docs]
self.font_small = _font(20)
# ── Title logo ──────────────────────────────────────────────────────
[docs]
self.title_logo = None
try:
logo_path = PACKAGE_DIR / "assets" / "images" / "title_logo.png"
raw = pygame.image.load(str(logo_path)).convert_alpha()
max_w = int(SCREEN_WIDTH * 0.75)
scale = min(max_w / raw.get_width(), 1.0)
logo_w = int(raw.get_width() * scale)
logo_h = int(raw.get_height() * scale)
self.title_logo = pygame.transform.smoothscale(raw, (logo_w, logo_h))
except Exception:
pass
# ── Grass strip ─────────────────────────────────────────────────────
[docs]
self.bg_grass = None
[docs]
self.bg_grass_h = 0
try:
raw = pygame.image.load(
str(PACKAGE_DIR / "assets" / "images" / "bg_grass.png")).convert_alpha()
gh = int(raw.get_height() * SCREEN_WIDTH / raw.get_width())
self.bg_grass = pygame.transform.smoothscale(raw, (SCREEN_WIDTH, gh))
self.bg_grass_h = gh
except Exception:
pass
# ── Beach pig animations ─────────────────────────────────────────────
_PS = 96 # display size per frame (32 px × 3)
_PSW = 128
# Sitting / wiggling pig: 3 frames, 32×32, single column
[docs]
self.wiggle_frames = []
try:
sheet = pygame.image.load(
str(PACKAGE_DIR / "assets" / "images" / "sitting pig wiggle.png")
).convert_alpha()
for r in range(3):
f = sheet.subsurface(pygame.Rect(0, r * 32, 32, 32))
self.wiggle_frames.append(pygame.transform.scale(f.copy(), (_PSW, _PSW)))
except Exception:
self.wiggle_frames = []
# Snoring pig: 5 frames of 32×32, 2-column sheet (last slot empty)
[docs]
self.snore_frames = []
try:
sheet = pygame.image.load(
str(PACKAGE_DIR / "assets" / "images" / "snoring pig.png")
).convert_alpha()
for r in range(3):
for c in range(2):
if r == 2 and c == 1:
continue # empty slot
f = sheet.subsurface(pygame.Rect(c * 32, r * 32, 32, 32))
self.snore_frames.append(pygame.transform.scale(f.copy(), (_PS, _PS)))
except Exception:
self.snore_frames = []
# ── Layout ──────────────────────────────────────────────────────────
cx = SCREEN_WIDTH // 2
cy = SCREEN_HEIGHT // 2
# Compact language button panel, right-aligned
_btn_w = 200
_btn_h = 56
_btn_gap = 18
_pad = 16
_title_h = 44 # approximate font_heading line height
_lp_w = _btn_w + _pad * 2
_lp_h = _pad + _title_h + _btn_h + _btn_gap + _btn_h + _pad
_lp_x = SCREEN_WIDTH - _lp_w - 20
_lp_y = cy - _lp_h // 2
[docs]
self.lang_panel = pygame.Rect(_lp_x, _lp_y, _lp_w, _lp_h)
_bx = _lp_x + _pad
_by = _lp_y + _pad + _title_h
[docs]
self.buttons = {
'en': pygame.Rect(_bx, _by, _btn_w, _btn_h),
'de': pygame.Rect(_bx, _by + _btn_h + _btn_gap, _btn_w, _btn_h),
'exit': pygame.Rect(SCREEN_WIDTH - 150, SCREEN_HEIGHT - 64, 130, 48),
'easy': pygame.Rect(cx - 225, cy + 135, 140, 50),
'medium': pygame.Rect(cx - 70, cy + 135, 140, 50),
'hard': pygame.Rect(cx + 85, cy + 135, 140, 50),
}
[docs]
self.input_box = pygame.Rect(cx - 180, cy + 20, 360, 48)
# ── Event handling ───────────────────────────────────────────────────────
[docs]
def handle_event(self, event, game_manager):
if event.type == pygame.MOUSEBUTTONDOWN:
if self.buttons['exit'].collidepoint(event.pos):
game_manager.quit_game()
return
if self.menu_state == 'language_select':
if event.type == pygame.MOUSEBUTTONDOWN:
if self.buttons['en'].collidepoint(event.pos):
localization_manager.set_language('en')
self.menu_state = 'username_input'
elif self.buttons['de'].collidepoint(event.pos):
localization_manager.set_language('de')
self.menu_state = 'username_input'
elif self.menu_state == 'username_input':
if event.type == pygame.MOUSEBUTTONDOWN:
for d in ('easy', 'medium', 'hard'):
if self.buttons[d].collidepoint(event.pos):
self.difficulty = d
if event.type == pygame.KEYDOWN and self.input_box_active:
if event.key == pygame.K_RETURN:
game_manager.username = self.username
game_manager.start_game(self.difficulty)
elif event.key == pygame.K_BACKSPACE:
self.username = self.username[:-1]
else:
self.username += event.unicode
# ── Top-level render ─────────────────────────────────────────────────────
[docs]
def render(self, screen):
self._render_background(screen)
self._render_grass_strip(screen)
self._render_beach_pigs(screen)
self._render_title(screen)
if self.menu_state == 'language_select':
self._render_language_screen(screen)
elif self.menu_state == 'username_input':
self._render_username_screen(screen)
self._draw_button(screen, 'exit', 'exit_button',
C_EXIT_NORMAL, C_EXIT_HOVER, self.font_button)
# ── Shared helpers ───────────────────────────────────────────────────────
def _render_background(self, screen):
if self.background_image:
screen.blit(self.background_image, (0, 0))
else:
top, bot = (30, 20, 10), (10, 60, 20)
for y in range(SCREEN_HEIGHT):
r = y / SCREEN_HEIGHT
color = tuple(int(top[i] * (1 - r) + bot[i] * r) for i in range(3))
pygame.draw.line(screen, color, (0, y), (SCREEN_WIDTH, y))
def _render_title(self, screen):
cx = SCREEN_WIDTH // 2
cy_title = SCREEN_HEIGHT // 4
if self.title_logo:
r = self.title_logo.get_rect(center=(cx, cy_title))
screen.blit(self.title_logo, r)
else:
text = localization_manager.get('game_title')
shadow = self.font_title.render(text, True, (40, 20, 0))
title = self.font_title.render(text, True, C_TITLE)
r = title.get_rect(center=(cx, cy_title))
screen.blit(shadow, (r.x + 3, r.y + 3))
screen.blit(title, r)
def _draw_panel(self, screen, rect, padding=20):
"""Draw a semi-transparent rounded panel."""
r = rect.inflate(padding * 2, padding * 2)
surf = pygame.Surface(r.size, pygame.SRCALPHA)
pygame.draw.rect(surf, C_PANEL, surf.get_rect(), border_radius=RADIUS)
screen.blit(surf, r.topleft)
def _draw_button(self, screen, key, text_key, fill, hover, font, border=C_BTN_BORDER):
rect = self.buttons[key]
mouse = pygame.mouse.get_pos()
color = hover if rect.collidepoint(mouse) else fill
pygame.draw.rect(screen, color, rect, border_radius=RADIUS)
pygame.draw.rect(screen, border, rect, 2, border_radius=RADIUS)
surf = font.render(localization_manager.get(text_key), True, C_TEXT)
screen.blit(surf, surf.get_rect(center=rect.center))
def _render_grass_strip(self, screen):
if self.bg_grass:
screen.blit(self.bg_grass, (0, SCREEN_HEIGHT - self.bg_grass_h))
def _render_beach_pigs(self, screen):
t = pygame.time.get_ticks()
base_y = SCREEN_HEIGHT - self.bg_grass_h # top of grass strip
# Wiggling pig — left quarter, cycles fast, small bob
if self.wiggle_frames:
frame = self.wiggle_frames[(t // 180) % len(self.wiggle_frames)]
bob = int(math.sin(t / 350.0) * 2)
pw, ph = frame.get_size()
screen.blit(frame, (2 * SCREEN_WIDTH // 5 - pw // 2,
base_y - ph + 28 + bob))
# Snoring pig — right quarter, cycles slowly, gentle breathing bob
if self.snore_frames:
frame = self.snore_frames[(t // 450) % len(self.snore_frames)]
bob = int(math.sin(t / 1400.0) * 1)
pw, ph = frame.get_size()
screen.blit(frame, (3 * SCREEN_WIDTH // 4 - pw // 2,
base_y - ph - 64 + bob))
def _render_label(self, screen, text, center, font, color=C_TEXT):
surf = font.render(text, True, color)
screen.blit(surf, surf.get_rect(center=center))
# ── Language select screen ───────────────────────────────────────────────
def _render_language_screen(self, screen):
lp = self.lang_panel
# ── Compact button panel (always visible) ───────────────────────────
surf = pygame.Surface(lp.size, pygame.SRCALPHA)
pygame.draw.rect(surf, C_PANEL, surf.get_rect(), border_radius=RADIUS)
screen.blit(surf, lp.topleft)
pygame.draw.rect(screen, C_BTN_BORDER, lp, 1, border_radius=RADIUS)
label = self.font_heading.render('Language', True, C_TITLE)
screen.blit(label, label.get_rect(centerx=lp.centerx, y=lp.y + 16))
self._draw_button(screen, 'en', 'english_button', C_BTN_NORMAL, C_BTN_HOVER, self.font_button)
self._draw_button(screen, 'de', 'german_button', C_BTN_NORMAL, C_BTN_HOVER, self.font_button)
# ── Instructions panel (hover only) ─────────────────────────────────
mouse = pygame.mouse.get_pos()
for key in ('en', 'de'):
if self.buttons[key].collidepoint(mouse):
inst_w = lp.x - 40 # from x=20 to just left of button panel
inst_x = 20
inst_panel = self._measure_instructions_panel(key, inst_x, inst_w)
self._render_instructions_panel(screen, key, inst_panel)
break
def _measure_instructions_panel(self, lang, x, width):
"""Return the Rect for the instructions panel sized to its content."""
orig = localization_manager.language
localization_manager.set_language(lang)
body = localization_manager.get('instructions_body')
localization_manager.set_language(orig)
max_w = width - 40
line_h = self.font_body.get_linesize() + 2
title_h = self.font_heading.get_linesize() + 8
body_h = 0
for paragraph in body.splitlines():
if not paragraph:
body_h += line_h // 2
continue
words = paragraph.split()
current = ''
for word in words:
test = (current + ' ' + word).strip()
if self.font_body.size(test)[0] <= max_w:
current = test
else:
if current:
body_h += line_h
current = word
if current:
body_h += line_h
pad = 16
h = pad + title_h + 10 + body_h + pad
cy = SCREEN_HEIGHT // 2
return pygame.Rect(x, cy - h // 2, width, h)
def _render_instructions_panel(self, screen, lang, panel):
orig = localization_manager.language
localization_manager.set_language(lang)
surf = pygame.Surface(panel.size, pygame.SRCALPHA)
pygame.draw.rect(surf, C_PANEL, surf.get_rect(), border_radius=RADIUS)
screen.blit(surf, panel.topleft)
pygame.draw.rect(screen, C_BTN_BORDER, panel, 1, border_radius=RADIUS)
title_h = self.font_heading.get_linesize() + 8
title = self.font_heading.render(localization_manager.get('instructions_title'), True, C_TITLE)
screen.blit(title, title.get_rect(centerx=panel.centerx, y=panel.y + 16))
body = localization_manager.get('instructions_body')
self._render_text_block(screen, body,
(panel.x + 20, panel.y + 16 + title_h + 10),
self.font_body, C_TEXT,
max_width=panel.width - 40)
localization_manager.set_language(orig)
# ── Username / difficulty screen ─────────────────────────────────────────
def _render_username_screen(self, screen):
cx = SCREEN_WIDTH // 2
ib = self.input_box
# One unified panel for the whole form
form_rect = pygame.Rect(cx - 260, ib.y - 70, 520, 290)
surf = pygame.Surface(form_rect.size, pygame.SRCALPHA)
pygame.draw.rect(surf, C_PANEL, surf.get_rect(), border_radius=RADIUS)
screen.blit(surf, form_rect.topleft)
pygame.draw.rect(screen, C_BTN_BORDER, form_rect, 1, border_radius=RADIUS)
# Name prompt
self._render_label(screen,
localization_manager.get('enter_name_title'),
(cx, ib.y - 36), self.font_heading, C_TITLE)
# Input box
border_col = C_SELECTED if self.input_box_active else C_MUTED
pygame.draw.rect(screen, (40, 28, 12), ib, border_radius=8)
pygame.draw.rect(screen, border_col, ib, 2, border_radius=8)
name_surf = self.font_button.render(self.username + '|', True, C_TEXT)
screen.blit(name_surf, (ib.x + 10, ib.y + 9))
# Difficulty
self._render_difficulty_selection(screen)
# Start hint (inside panel)
self._render_label(screen,
localization_manager.get('start_instruction'),
(cx, form_rect.bottom - 24), self.font_small, C_MUTED)
def _render_difficulty_selection(self, screen):
cx = SCREEN_WIDTH // 2
cy = SCREEN_HEIGHT // 2
self._render_label(screen,
localization_manager.get('difficulty_title'),
(cx, cy + 100), self.font_heading, C_TEXT)
fills = {'easy': C_DIFF_EASY, 'medium': C_DIFF_MEDIUM, 'hard': C_DIFF_HARD}
for d in ('easy', 'medium', 'hard'):
rect = self.buttons[d]
color = fills[d]
pygame.draw.rect(screen, color, rect, border_radius=RADIUS)
border = C_SELECTED if self.difficulty == d else C_BTN_BORDER
width = 3 if self.difficulty == d else 1
pygame.draw.rect(screen, border, rect, width, border_radius=RADIUS)
surf = self.font_button.render(
localization_manager.get(f'difficulty_{d}'), True, C_TEXT)
screen.blit(surf, surf.get_rect(center=rect.center))
# ── Text block helper ────────────────────────────────────────────────────
def _render_text_block(self, screen, text, pos, font, color, max_width=None):
x, y = pos
line_h = font.get_linesize() + 2
for paragraph in text.splitlines():
if not paragraph:
y += line_h // 2
continue
if max_width is None:
screen.blit(font.render(paragraph, True, color), (x, y))
y += line_h
else:
# Word-wrap within max_width
words = paragraph.split(' ')
current = ''
for word in words:
test = (current + ' ' + word).strip()
if font.size(test)[0] <= max_width:
current = test
else:
if current:
screen.blit(font.render(current, True, color), (x, y))
y += line_h
current = word
if current:
screen.blit(font.render(current, True, color), (x, y))
y += line_h