From 30b14294cf9927549d1e43ae3d6808e49ee543e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rihards=20=C5=A0ev=C4=8Duks?= Date: Wed, 25 Feb 2026 06:25:21 +0000 Subject: [PATCH] Add menu_system.py --- menu_system.py | 675 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 675 insertions(+) create mode 100644 menu_system.py diff --git a/menu_system.py b/menu_system.py new file mode 100644 index 0000000..c44155b --- /dev/null +++ b/menu_system.py @@ -0,0 +1,675 @@ +import pygame +from settings import * + + +class Button: + """Reusable button class for UI""" + + def __init__(self, x, y, width, height, text, color, text_color, hover_color): + self.rect = pygame.Rect(x, y, width, height) + self.text = text + self.color = color + self.hover_color = hover_color + self.text_color = text_color + self.is_hovered = False + + def draw(self, screen, font): + current_color = self.hover_color if self.is_hovered else self.color + pygame.draw.rect(screen, current_color, self.rect) + pygame.draw.rect(screen, (255, 255, 255), self.rect, 2) + + text_surface = font.render(self.text, True, self.text_color) + text_rect = text_surface.get_rect(center=self.rect.center) + screen.blit(text_surface, text_rect) + + def check_hover(self, mouse_pos): + self.is_hovered = self.rect.collidepoint(mouse_pos) + + def is_clicked(self, mouse_pos): + return self.rect.collidepoint(mouse_pos) + + +class InputBox: + """Text input field for naming saves""" + + def __init__(self, x, y, width, height, text=''): + self.rect = pygame.Rect(x, y, width, height) + self.text = text + self.active = False + self.cursor_visible = True + self.cursor_timer = 0 + + def handle_event(self, event): + if event.type == pygame.MOUSEBUTTONDOWN: + if self.rect.collidepoint(event.pos): + self.active = True + else: + self.active = False + + if event.type == pygame.KEYDOWN: + if self.active: + if event.key == pygame.K_BACKSPACE: + self.text = self.text[:-1] + elif event.key == pygame.K_RETURN: + return "submit" + elif len(self.text) < 30: + if event.unicode.isprintable(): + self.text += event.unicode + return None + + def update(self, dt): + self.cursor_timer += dt + if self.cursor_timer > 0.5: + self.cursor_visible = not self.cursor_visible + self.cursor_timer = 0 + + def draw(self, screen, font): + pygame.draw.rect(screen, (50, 50, 50), self.rect) + color = (100, 200, 100) if self.active else (100, 100, 100) + pygame.draw.rect(screen, color, self.rect, 2) + + text_surface = font.render(self.text, True, (255, 255, 255)) + screen.blit(text_surface, (self.rect.x + 10, self.rect.y + 8)) + + if self.active and self.cursor_visible: + cursor_x = self.rect.x + 10 + text_surface.get_width() + pygame.draw.line(screen, (255, 255, 255), + (cursor_x, self.rect.y + 5), + (cursor_x, self.rect.y + self.rect.height - 5)) + + +class MainMenu: + """Main menu screen""" + + def __init__(self, game): + self.game = game + self.button_width = 300 + self.button_height = 60 + self.button_x = (SCREEN_WIDTH - self.button_width) // 2 + + self.new_game_btn = Button( + self.button_x, + SCREEN_HEIGHT // 2 - 100, + self.button_width, + self.button_height, + "New Game", + (50, 100, 150), + (255, 255, 255), + (100, 150, 200) + ) + + self.load_game_btn = Button( + self.button_x, + SCREEN_HEIGHT // 2, + self.button_width, + self.button_height, + "Load Game", + (50, 100, 150), + (255, 255, 255), + (100, 150, 200) + ) + + self.settings_btn = Button( + self.button_x, + SCREEN_HEIGHT // 2 + 70, + self.button_width, + self.button_height, + "Settings", + (100, 100, 100), + (255, 255, 255), + (150, 150, 150) + ) + + self.quit_btn = Button( + self.button_x, + SCREEN_HEIGHT // 2 + 140, + self.button_width, + self.button_height, + "Quit", + (150, 50, 50), + (255, 255, 255), + (200, 100, 100) + ) + + self.font_title = pygame.font.SysFont("consolas", 72, bold=True) + self.font_button = pygame.font.SysFont("consolas", 24) + + def handle_events(self): + for event in pygame.event.get(): + if event.type == pygame.QUIT: + return "quit" + + if event.type == pygame.MOUSEBUTTONDOWN: + if event.button == 1: + if self.new_game_btn.is_clicked(event.pos): + return "new_game" + elif self.load_game_btn.is_clicked(event.pos): + return "load_game" + elif self.settings_btn.is_clicked(event.pos): + return "settings" + elif self.quit_btn.is_clicked(event.pos): + return "quit" + + return None + + def update(self): + mouse_pos = pygame.mouse.get_pos() + self.new_game_btn.check_hover(mouse_pos) + self.load_game_btn.check_hover(mouse_pos) + self.settings_btn.check_hover(mouse_pos) + self.quit_btn.check_hover(mouse_pos) + + def draw(self, screen): + screen.fill(BACKGROUND_COLOR) + + title = self.font_title.render("PROJECTDD", True, (255, 255, 255)) + title_rect = title.get_rect(center=(SCREEN_WIDTH // 2, 100)) + screen.blit(title, title_rect) + + self.new_game_btn.draw(screen, self.font_button) + self.load_game_btn.draw(screen, self.font_button) + self.settings_btn.draw(screen, self.font_button) + self.quit_btn.draw(screen, self.font_button) + + pygame.display.flip() + + +class SettingsMenu: + """Settings menu for fullscreen toggle""" + + def __init__(self, game): + self.game = game + self.font_title = pygame.font.SysFont("consolas", 36, bold=True) + self.font_label = pygame.font.SysFont("consolas", 20) + self.font_button = pygame.font.SysFont("consolas", 18) + + self.button_width = 200 + self.button_height = 50 + + # Fullscreen toggle + self.fullscreen_btn = Button( + SCREEN_WIDTH // 2 - 100, + SCREEN_HEIGHT // 2 - 50, + 200, + 50, + f"Fullscreen: {'ON' if FULLSCREEN else 'OFF'}", + (100, 100, 100), + (255, 255, 255), + (150, 150, 150) + ) + + # Back button + self.back_btn = Button( + SCREEN_WIDTH // 2 - 100, + SCREEN_HEIGHT // 2 + 50, + 200, + 50, + "Back", + (100, 100, 150), + (255, 255, 255), + (150, 150, 200) + ) + + def handle_events(self): + for event in pygame.event.get(): + if event.type == pygame.QUIT: + return "back" + + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + return "back" + + if event.type == pygame.MOUSEBUTTONDOWN: + if event.button == 1: + # Check fullscreen button + if self.fullscreen_btn.is_clicked(event.pos): + self.game.toggle_fullscreen() + self.fullscreen_btn.text = f"Fullscreen: {'ON' if FULLSCREEN else 'OFF'}" + + # Check back button + if self.back_btn.is_clicked(event.pos): + return "back" + + return None + + def update(self, dt): + mouse_pos = pygame.mouse.get_pos() + self.back_btn.check_hover(mouse_pos) + self.fullscreen_btn.check_hover(mouse_pos) + + def draw(self, screen): + screen.fill(BACKGROUND_COLOR) + + title = self.font_title.render("Settings", True, (255, 255, 255)) + title_rect = title.get_rect(center=(SCREEN_WIDTH // 2, 80)) + screen.blit(title, title_rect) + + # Draw fullscreen button + self.fullscreen_btn.draw(screen, self.font_button) + + # Draw back button + self.back_btn.draw(screen, self.font_button) + + pygame.display.flip() + + +class NewGameDialog: + """Dialog for naming a new game""" + + def __init__(self, game): + self.game = game + self.font_title = pygame.font.SysFont("consolas", 36, bold=True) + self.font_label = pygame.font.SysFont("consolas", 20) + + self.name_input = InputBox( + SCREEN_WIDTH // 2 - 150, + SCREEN_HEIGHT // 2 - 20, + 300, + 50, + "My World" + ) + + self.button_width = 120 + self.button_height = 50 + + self.create_btn = Button( + SCREEN_WIDTH // 2 - 260, + SCREEN_HEIGHT // 2 + 80, + self.button_width, + self.button_height, + "Create", + (50, 150, 50), + (255, 255, 255), + (100, 200, 100) + ) + + self.cancel_btn = Button( + SCREEN_WIDTH // 2 + 140, + SCREEN_HEIGHT // 2 + 80, + self.button_width, + self.button_height, + "Cancel", + (150, 50, 50), + (255, 255, 255), + (200, 100, 100) + ) + + def handle_events(self): + for event in pygame.event.get(): + if event.type == pygame.QUIT: + return "cancel" + + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + return "cancel" + + self.name_input.handle_event(event) + + if event.type == pygame.MOUSEBUTTONDOWN: + if event.button == 1: + if self.create_btn.is_clicked(event.pos): + if self.name_input.text.strip(): + return f"create:{self.name_input.text}" + elif self.cancel_btn.is_clicked(event.pos): + return "cancel" + + return None + + def update(self, dt): + self.name_input.update(dt) + mouse_pos = pygame.mouse.get_pos() + self.create_btn.check_hover(mouse_pos) + self.cancel_btn.check_hover(mouse_pos) + + def draw(self, screen): + screen.fill(BACKGROUND_COLOR) + + title = self.font_title.render("Create New World", True, (255, 255, 255)) + title_rect = title.get_rect(center=(SCREEN_WIDTH // 2, 80)) + screen.blit(title, title_rect) + + label = self.font_label.render("World Name:", True, (255, 255, 255)) + screen.blit(label, (SCREEN_WIDTH // 2 - 150, SCREEN_HEIGHT // 2 - 60)) + + self.name_input.draw(screen, self.font_label) + + self.create_btn.draw(screen, self.font_label) + self.cancel_btn.draw(screen, self.font_label) + + pygame.display.flip() + + +class LoadGameDialog: + """Dialog for selecting a save to load""" + + def __init__(self, game, save_system): + self.game = game + self.save_system = save_system + self.font_title = pygame.font.SysFont("consolas", 36, bold=True) + self.font_label = pygame.font.SysFont("consolas", 18) + self.font_small = pygame.font.SysFont("consolas", 14) + + self.saves = self.save_system.get_save_files() + self.selected_index = 0 + + self.scroll_offset = 0 + self.visible_saves = 5 + + self.button_width = 120 + self.button_height = 50 + + self.load_btn = Button( + SCREEN_WIDTH // 2 - 260, + SCREEN_HEIGHT - 100, + self.button_width, + self.button_height, + "Load", + (50, 150, 50), + (255, 255, 255), + (100, 200, 100) + ) + + self.delete_btn = Button( + SCREEN_WIDTH // 2 - 65, + SCREEN_HEIGHT - 100, + self.button_width, + self.button_height, + "Delete", + (150, 50, 50), + (255, 255, 255), + (200, 100, 100) + ) + + self.cancel_btn = Button( + SCREEN_WIDTH // 2 + 140, + SCREEN_HEIGHT - 100, + self.button_width, + self.button_height, + "Cancel", + (100, 100, 150), + (255, 255, 255), + (150, 150, 200) + ) + + def handle_events(self): + for event in pygame.event.get(): + if event.type == pygame.QUIT: + return "cancel" + + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + return "cancel" + elif event.key == pygame.K_UP: + self.selected_index = max(0, self.selected_index - 1) + elif event.key == pygame.K_DOWN: + self.selected_index = min(len(self.saves) - 1, self.selected_index + 1) + elif event.key == pygame.K_RETURN: + if self.saves: + return f"load:{self.selected_index}" + + if event.type == pygame.MOUSEBUTTONDOWN: + if event.button == 1: + if self.load_btn.is_clicked(event.pos): + if self.saves: + return f"load:{self.selected_index}" + elif self.delete_btn.is_clicked(event.pos): + if self.saves: + return f"delete:{self.selected_index}" + elif self.cancel_btn.is_clicked(event.pos): + return "cancel" + + for i in range(self.visible_saves): + save_idx = self.scroll_offset + i + if save_idx < len(self.saves): + y = 150 + i * 80 + if pygame.Rect(50, y, SCREEN_WIDTH - 100, 70).collidepoint(event.pos): + self.selected_index = save_idx + + elif event.button == 4: + self.scroll_offset = max(0, self.scroll_offset - 1) + elif event.button == 5: + self.scroll_offset = min(max(0, len(self.saves) - self.visible_saves), + self.scroll_offset + 1) + + return None + + def update(self): + mouse_pos = pygame.mouse.get_pos() + self.load_btn.check_hover(mouse_pos) + self.delete_btn.check_hover(mouse_pos) + self.cancel_btn.check_hover(mouse_pos) + + if self.selected_index < self.scroll_offset: + self.scroll_offset = self.selected_index + elif self.selected_index >= self.scroll_offset + self.visible_saves: + self.scroll_offset = self.selected_index - self.visible_saves + 1 + + def draw(self, screen): + screen.fill(BACKGROUND_COLOR) + + title = self.font_title.render("Load Game", True, (255, 255, 255)) + title_rect = title.get_rect(center=(SCREEN_WIDTH // 2, 50)) + screen.blit(title, title_rect) + + if not self.saves: + no_saves = self.font_label.render("No saves found!", True, (200, 100, 100)) + screen.blit(no_saves, (SCREEN_WIDTH // 2 - no_saves.get_width() // 2, SCREEN_HEIGHT // 2)) + else: + for i in range(self.visible_saves): + save_idx = self.scroll_offset + i + if save_idx < len(self.saves): + save = self.saves[save_idx] + y = 150 + i * 80 + + if save_idx == self.selected_index: + pygame.draw.rect(screen, (60, 100, 140), pygame.Rect(50, y, SCREEN_WIDTH - 100, 70)) + else: + pygame.draw.rect(screen, (40, 40, 40), pygame.Rect(50, y, SCREEN_WIDTH - 100, 70)) + + pygame.draw.rect(screen, (150, 150, 150), pygame.Rect(50, y, SCREEN_WIDTH - 100, 70), 2) + + filename = save["name"] + parts = filename.replace("save_", "").replace(".json", "").split("_") + world_name = parts[0] if parts else "Unknown" + + name_text = self.font_label.render(f"World: {world_name}", True, (255, 255, 255)) + screen.blit(name_text, (70, y + 10)) + + from datetime import datetime + mod_time = datetime.fromtimestamp(save["modified"]).strftime("%Y-%m-%d %H:%M:%S") + time_text = self.font_small.render(f"Saved: {mod_time}", True, (200, 200, 200)) + screen.blit(time_text, (70, y + 40)) + + self.load_btn.draw(screen, self.font_label) + self.delete_btn.draw(screen, self.font_label) + self.cancel_btn.draw(screen, self.font_label) + + pygame.display.flip() + + +class PauseMenu: + """Pause menu overlay""" + + def __init__(self, game): + self.game = game + self.button_width = 250 + self.button_height = 50 + self.button_x = (SCREEN_WIDTH - self.button_width) // 2 + + self.resume_btn = Button( + self.button_x, + SCREEN_HEIGHT // 2 - 100, + self.button_width, + self.button_height, + "Resume", + (50, 150, 50), + (255, 255, 255), + (100, 200, 100) + ) + + self.save_btn = Button( + self.button_x, + SCREEN_HEIGHT // 2, + self.button_width, + self.button_height, + "Save Game", + (50, 100, 150), + (255, 255, 255), + (100, 150, 200) + ) + + self.backup_btn = Button( + self.button_x, + SCREEN_HEIGHT // 2 + 60, + self.button_width, + self.button_height, + "Backup", + (150, 150, 50), + (255, 255, 255), + (200, 200, 100) + ) + + self.quit_btn = Button( + self.button_x, + SCREEN_HEIGHT // 2 + 120, + self.button_width, + self.button_height, + "Quit to Menu", + (150, 50, 50), + (255, 255, 255), + (200, 100, 100) + ) + + self.font_title = pygame.font.SysFont("consolas", 48, bold=True) + self.font_button = pygame.font.SysFont("consolas", 20) + self.message = "" + self.message_timer = 0 + + def handle_events(self): + for event in pygame.event.get(): + if event.type == pygame.QUIT: + return "quit" + + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + return "resume" + + if event.type == pygame.MOUSEBUTTONDOWN: + if event.button == 1: + if self.resume_btn.is_clicked(event.pos): + return "resume" + elif self.save_btn.is_clicked(event.pos): + self.show_message("Game saved!") + return "save" + elif self.backup_btn.is_clicked(event.pos): + self.show_message("Backup created!") + return "backup" + elif self.quit_btn.is_clicked(event.pos): + return "quit_to_menu" + + return None + + def show_message(self, text): + self.message = text + self.message_timer = 2.0 + + def update(self, dt): + mouse_pos = pygame.mouse.get_pos() + self.resume_btn.check_hover(mouse_pos) + self.save_btn.check_hover(mouse_pos) + self.backup_btn.check_hover(mouse_pos) + self.quit_btn.check_hover(mouse_pos) + + if self.message_timer > 0: + self.message_timer -= dt + + def draw(self, screen): + overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)) + overlay.set_alpha(128) + overlay.fill((0, 0, 0)) + screen.blit(overlay, (0, 0)) + + title = self.font_title.render("PAUSED", True, (255, 255, 255)) + title_rect = title.get_rect(center=(SCREEN_WIDTH // 2, 80)) + screen.blit(title, title_rect) + + self.resume_btn.draw(screen, self.font_button) + self.save_btn.draw(screen, self.font_button) + self.backup_btn.draw(screen, self.font_button) + self.quit_btn.draw(screen, self.font_button) + + if self.message_timer > 0: + message = self.font_button.render(self.message, True, (100, 255, 100)) + message_rect = message.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT - 50)) + screen.blit(message, message_rect) + + pygame.display.flip() + + +class DeathScreen: + """Death screen with respawn and quit options""" + + def __init__(self, game): + self.game = game + self.button_width = 250 + self.button_height = 60 + self.button_x = (SCREEN_WIDTH - self.button_width) // 2 + + self.respawn_btn = Button( + self.button_x, + SCREEN_HEIGHT // 2 - 50, + self.button_width, + self.button_height, + "Respawn", + (50, 150, 50), + (255, 255, 255), + (100, 200, 100) + ) + + self.quit_btn = Button( + self.button_x, + SCREEN_HEIGHT // 2 + 50, + self.button_width, + self.button_height, + "Quit to Menu", + (150, 50, 50), + (255, 255, 255), + (200, 100, 100) + ) + + self.font_title = pygame.font.SysFont("consolas", 72, bold=True) + self.font_button = pygame.font.SysFont("consolas", 24) + + def handle_events(self): + for event in pygame.event.get(): + if event.type == pygame.QUIT: + return "quit" + + if event.type == pygame.MOUSEBUTTONDOWN: + if event.button == 1: + if self.respawn_btn.is_clicked(event.pos): + return "respawn" + elif self.quit_btn.is_clicked(event.pos): + return "quit_to_menu" + + return None + + def update(self, dt): + mouse_pos = pygame.mouse.get_pos() + self.respawn_btn.check_hover(mouse_pos) + self.quit_btn.check_hover(mouse_pos) + + def draw(self, screen): + overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)) + overlay.set_alpha(200) + overlay.fill((0, 0, 0)) + screen.blit(overlay, (0, 0)) + + title = self.font_title.render("YOU DIED", True, (255, 0, 0)) + title_rect = title.get_rect(center=(SCREEN_WIDTH // 2, 100)) + screen.blit(title, title_rect) + + self.respawn_btn.draw(screen, self.font_button) + self.quit_btn.draw(screen, self.font_button) + + pygame.display.flip() \ No newline at end of file