#!/usr/bin/env python3 """ User Profile Manager for Games A PySDL2-based profile management system with gamepad-only controls Features: - Create new user profiles - Edit existing profiles - Delete profiles - Select active profile - JSON-based storage - Gamepad navigation only """ import os import json import time from typing import Dict, List, Optional, Any from dataclasses import dataclass, asdict from datetime import datetime import sdl2 import sdl2.ext from sdl2.ext.compat import byteify # Import the user profile integration system from engine.user_profile_integration import UserProfileIntegration @dataclass class UserProfile: """User profile data structure""" name: str created_date: str last_played: str games_played: int = 0 total_score: int = 0 best_score: int = 0 settings: Dict[str, Any] = None achievements: List[str] = None def __post_init__(self): if self.settings is None: self.settings = { "difficulty": "normal", "sound_volume": 50, "music_volume": 50, "screen_shake": True, "auto_save": True } if self.achievements is None: self.achievements = [] class ProfileManager: """Main profile management system""" def __init__(self, profiles_file: str = "user_profiles.json"): self.profiles_file = profiles_file self.profiles: Dict[str, UserProfile] = {} self.active_profile: Optional[str] = None # Initialize user profile integration system self.integration = UserProfileIntegration(profiles_file) self.device_id = self.integration.get_device_id() self.api_enabled = self.integration.api_enabled # UI State self.current_screen = "main_menu" # main_menu, profile_list, create_profile, edit_profile, leaderboard, profile_stats self.selected_index = 0 self.input_text = "" self.input_active = False self.editing_field = None # Error dialog state self.show_error = False self.error_message = "" self.error_timer = 0 # Leaderboard data self.leaderboard_data = [] self.leaderboard_type = "device" # "device" or "global" # Virtual keyboard state self.vk_cursor_x = 0 # Virtual keyboard cursor X self.vk_cursor_y = 0 # Virtual keyboard cursor Y self.virtual_keyboard = [ ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'], ['K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T'], ['U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3'], ['4', '5', '6', '7', '8', '9', ' ', '.', '-', '_'], [' 0: self.gamepad = sdl2.SDL_JoystickOpen(0) print(f"Gamepad detected: {sdl2.SDL_JoystickName(self.gamepad).decode()}") else: print("No gamepad detected - using keyboard fallback") def load_profiles(self): """Load profiles from JSON file""" if os.path.exists(self.profiles_file): try: with open(self.profiles_file, 'r') as f: data = json.load(f) self.profiles = { name: UserProfile(**profile_data) for name, profile_data in data.get('profiles', {}).items() } self.active_profile = data.get('active_profile') except (json.JSONDecodeError, KeyError) as e: print(f"Error loading profiles: {e}") self.profiles = {} def save_profiles(self): """Save profiles to JSON file""" data = { 'profiles': { name: asdict(profile) for name, profile in self.profiles.items() }, 'active_profile': self.active_profile } try: with open(self.profiles_file, 'w') as f: json.dump(data, f, indent=2) except IOError as e: print(f"Error saving profiles: {e}") def create_profile(self, name: str) -> bool: """Create a new profile""" if name in self.profiles or not name.strip(): return False now = datetime.now().isoformat() profile = UserProfile( name=name.strip(), created_date=now, last_played=now ) self.profiles[name.strip()] = profile self.save_profiles() # Register with API server if available if self.api_enabled: result = self.integration.register_new_user(name.strip()) if result: print(f"Profile {name.strip()} registered with server") else: print(f"Warning: Profile {name.strip()} created locally but not registered with server") return True def delete_profile(self, name: str) -> bool: """Delete a profile""" if name in self.profiles: del self.profiles[name] if self.active_profile == name: self.active_profile = None self.save_profiles() return True return False def set_active_profile(self, name: str) -> bool: """Set the active profile""" if name in self.profiles: self.active_profile = name self.profiles[name].last_played = datetime.now().isoformat() self.save_profiles() # Update integration system to load the new profile self.integration.reload_profile() return True return False def handle_gamepad_input(self): """Handle gamepad input with debouncing""" if not self.gamepad: return current_time = time.time() # D-pad navigation hat_state = sdl2.SDL_JoystickGetHat(self.gamepad, 0) # Up if hat_state & sdl2.SDL_HAT_UP: if self.can_process_input('up', current_time): self.navigate_up() # Down if hat_state & sdl2.SDL_HAT_DOWN: if self.can_process_input('down', current_time): self.navigate_down() # Left if hat_state & sdl2.SDL_HAT_LEFT: if self.can_process_input('left', current_time): self.navigate_left() # Right if hat_state & sdl2.SDL_HAT_RIGHT: if self.can_process_input('right', current_time): self.navigate_right() # Buttons button_count = sdl2.SDL_JoystickNumButtons(self.gamepad) for i in range(min(button_count, 16)): # Limit to reasonable number if sdl2.SDL_JoystickGetButton(self.gamepad, i): if self.can_process_input(f'button_{i}', current_time): self.handle_button_press(i) def can_process_input(self, input_key: str, current_time: float) -> bool: """Check if enough time has passed to process input (debouncing)""" delay = 0.15 # 150ms delay if input_key not in self.last_button_time: self.last_button_time[input_key] = current_time return True if current_time - self.last_button_time[input_key] > delay: self.last_button_time[input_key] = current_time return True return False def handle_button_press(self, button: int): """Handle gamepad button presses""" # If error dialog is shown, dismiss it on any button press if self.show_error: self.show_error = False return # Button mappings (common gamepad layout) # 0: A/Cross - Confirm # 1: B/Circle - Back # 2: X/Square - Delete/Special # 3: Y/Triangle - Menu if button == 3: # A/Confirm self.handle_confirm() elif button == 4: # B/Back self.handle_back() elif button == 9: # X/Delete self.handle_delete() elif button == 10: # Y/Menu self.handle_menu() def navigate_up(self): """Navigate up in current screen""" if self.show_error: return if self.current_screen == "main_menu": self.selected_index = max(0, self.selected_index - 1) elif self.current_screen == "profile_list": self.selected_index = max(0, self.selected_index - 1) elif self.current_screen == "edit_profile": self.selected_index = max(0, self.selected_index - 1) elif self.current_screen == "create_profile" and self.input_active: # Virtual keyboard navigation self.vk_cursor_y = max(0, self.vk_cursor_y - 1) # Adjust x cursor if current row is shorter max_x = len(self.virtual_keyboard[self.vk_cursor_y]) - 1 self.vk_cursor_x = min(self.vk_cursor_x, max_x) def navigate_down(self): """Navigate down in current screen""" if self.show_error: return if self.current_screen == "main_menu": max_index = 5 # Create, Select, Settings, Leaderboard, Stats, Exit (0-5) self.selected_index = min(max_index, self.selected_index + 1) elif self.current_screen == "profile_list": max_index = len(self.profiles) # Profiles + Back self.selected_index = min(max_index, self.selected_index + 1) elif self.current_screen == "edit_profile": max_index = 5 # 4 settings + save + back (0-5) self.selected_index = min(max_index, self.selected_index + 1) elif self.current_screen == "leaderboard": max_index = 2 # Toggle type, refresh, back (0-2) self.selected_index = min(max_index, self.selected_index + 1) elif self.current_screen == "profile_stats": max_index = 0 # Just back button self.selected_index = min(max_index, self.selected_index + 1) elif self.current_screen == "create_profile" and self.input_active: # Virtual keyboard navigation max_y = len(self.virtual_keyboard) - 1 self.vk_cursor_y = min(max_y, self.vk_cursor_y + 1) # Adjust x cursor if current row is shorter max_x = len(self.virtual_keyboard[self.vk_cursor_y]) - 1 self.vk_cursor_x = min(self.vk_cursor_x, max_x) def navigate_left(self): """Navigate left (for adjusting values)""" if self.show_error: return if self.current_screen == "edit_profile": self.adjust_setting(-1) elif self.current_screen == "leaderboard": # Toggle between device and global leaderboard if self.selected_index == 0: self.leaderboard_type = "global" if self.leaderboard_type == "device" else "device" self.load_leaderboard_data() elif self.current_screen == "create_profile" and self.input_active: # Virtual keyboard navigation self.vk_cursor_x = max(0, self.vk_cursor_x - 1) def navigate_right(self): """Navigate right (for adjusting values)""" if self.show_error: return if self.current_screen == "edit_profile": self.adjust_setting(1) elif self.current_screen == "leaderboard": # Toggle between device and global leaderboard if self.selected_index == 0: self.leaderboard_type = "global" if self.leaderboard_type == "device" else "device" self.load_leaderboard_data() elif self.current_screen == "create_profile" and self.input_active: # Virtual keyboard navigation max_x = len(self.virtual_keyboard[self.vk_cursor_y]) - 1 self.vk_cursor_x = min(max_x, self.vk_cursor_x + 1) def handle_confirm(self): """Handle confirm action (A button)""" if self.show_error: return if self.current_screen == "main_menu": if self.selected_index == 0: # Create Profile self.current_screen = "create_profile" self.input_text = "" self.input_active = True elif self.selected_index == 1: # Select Profile self.current_screen = "profile_list" self.selected_index = 0 elif self.selected_index == 2: # Settings if self.active_profile: self.current_screen = "edit_profile" self.selected_index = 0 elif self.selected_index == 3: # Leaderboard if self.api_enabled: self.current_screen = "leaderboard" self.selected_index = 0 self.load_leaderboard_data() elif self.selected_index == 4: # Profile Stats if self.active_profile: self.current_screen = "profile_stats" self.selected_index = 0 elif self.selected_index == 5: # Exit self.running = False elif self.current_screen == "profile_list": profile_names = list(self.profiles.keys()) if self.selected_index < len(profile_names): # Select profile profile_name = profile_names[self.selected_index] self.set_active_profile(profile_name) self.current_screen = "main_menu" self.selected_index = 0 else: # Back option self.current_screen = "main_menu" self.selected_index = 1 elif self.current_screen == "create_profile": if self.input_active: # Handle virtual keyboard selection self.handle_virtual_keyboard_input() else: # Start text input mode self.input_active = True self.vk_cursor_x = 0 self.vk_cursor_y = 0 elif self.current_screen == "edit_profile": if self.selected_index == 4: # Save (index 4) self.save_profiles() self.current_screen = "main_menu" self.selected_index = 0 elif self.selected_index == 5: # Back (index 5) self.current_screen = "main_menu" self.selected_index = 0 elif self.current_screen == "leaderboard": if self.selected_index == 1: # Refresh self.load_leaderboard_data() elif self.selected_index == 2: # Back self.current_screen = "main_menu" self.selected_index = 3 elif self.current_screen == "profile_stats": if self.selected_index == 0: # Back self.current_screen = "main_menu" self.selected_index = 4 def handle_back(self): """Handle back action (B button)""" if self.show_error: return if self.current_screen == "main_menu": self.running = False elif self.current_screen in ["profile_list", "create_profile", "edit_profile", "leaderboard", "profile_stats"]: self.current_screen = "main_menu" self.selected_index = 0 self.input_active = False def handle_delete(self): """Handle delete action (X button)""" if self.show_error: return if self.current_screen == "profile_list": profile_names = list(self.profiles.keys()) if self.selected_index < len(profile_names): profile_name = profile_names[self.selected_index] self.delete_profile(profile_name) self.selected_index = min(self.selected_index, len(self.profiles) - 1) elif self.current_screen == "create_profile": # Delete character from input if self.input_text: self.input_text = self.input_text[:-1] def handle_menu(self): """Handle menu action (Y button)""" pass # Reserved for future use def handle_virtual_keyboard_input(self): """Handle virtual keyboard character selection""" if self.vk_cursor_y >= len(self.virtual_keyboard): return row = self.virtual_keyboard[self.vk_cursor_y] if self.vk_cursor_x >= len(row): return selected_char = row[self.vk_cursor_x] if selected_char == ' 5: color = sdl2.ext.Color(alpha, alpha, alpha * 2) self.renderer.draw_line((0, y, 640, y), color) if self.current_screen == "main_menu": self.render_main_menu() elif self.current_screen == "profile_list": self.render_profile_list() elif self.current_screen == "create_profile": self.render_create_profile() elif self.current_screen == "edit_profile": self.render_edit_profile() elif self.current_screen == "leaderboard": self.render_leaderboard() elif self.current_screen == "profile_stats": self.render_profile_stats() # Render error dialog on top if active if self.show_error: self.render_error_dialog() # Auto-dismiss after timer expires if time.time() > self.error_timer: self.show_error = False self.renderer.present() def render_main_menu(self): """Render main menu screen with improved layout""" # Draw background gradient effect self.renderer.fill((0, 0, 640, 120), self.colors['dark_gray']) # Title title = "Profile Manager" self.draw_text(title, 320, 20, self.colors['light_blue'], 'title', center=True) # Subtitle with active profile and API status if self.active_profile: active_text = f"Active: {self.active_profile}" self.draw_text(active_text, 320, 60, self.colors['light_green'], 'medium', center=True) else: self.draw_text("No active profile", 320, 60, self.colors['yellow'], 'medium', center=True) # API connection status api_status = "API: Connected" if self.api_enabled else "API: Offline" api_color = self.colors['light_green'] if self.api_enabled else self.colors['red'] self.draw_text(api_status, 320, 80, api_color, 'tiny', center=True) # Device ID display device_text = f"Device: {self.device_id}" self.draw_text(device_text, 320, 95, self.colors['light_gray'], 'tiny', center=True) # Menu panel panel_x, panel_y = 120, 120 panel_width, panel_height = 400, 240 self.draw_panel(panel_x, panel_y, panel_width, panel_height, self.colors['dark_gray'], self.colors['gray']) # Menu items with proper spacing menu_items = [ "Create Profile", "Select Profile", "Edit Settings", "Leaderboard", "Profile Stats", "Exit" ] button_width = 280 button_height = 30 button_x = panel_x + 60 start_y = panel_y + 15 for i, item in enumerate(menu_items): button_y = start_y + i * (button_height + 8) selected = (i == self.selected_index) # Gray out unavailable options if (item == "Edit Settings" or item == "Profile Stats") and not self.active_profile: self.draw_button(f"{item} (No Profile)", button_x, button_y, button_width, button_height, False, disabled=True) elif item == "Leaderboard" and not self.api_enabled: self.draw_button(f"{item} (Offline)", button_x, button_y, button_width, button_height, False, disabled=True) else: self.draw_button(item, button_x, button_y, button_width, button_height, selected) # Controls help panel help_panel_y = 380 self.draw_panel(10, help_panel_y, 620, 80, self.colors['black'], self.colors['dark_gray']) self.draw_text("Controls:", 20, help_panel_y + 8, self.colors['light_blue'], 'small') self.draw_text("↑↓ Navigate Enter Confirm Escape Back", 20, help_panel_y + 30, self.colors['light_gray'], 'tiny') def render_profile_list(self): """Render profile selection screen with improved layout""" # Header self.renderer.fill((0, 0, 640, 70), self.colors['dark_gray']) self.draw_text("Select Profile", 320, 15, self.colors['light_blue'], 'title', center=True) profile_names = list(self.profiles.keys()) if not profile_names: # No profiles message self.draw_panel(120, 100, 400, 150, self.colors['dark_gray'], self.colors['gray']) self.draw_text("No profiles found", 320, 150, self.colors['yellow'], 'large', center=True) self.draw_text("Create one first", 320, 180, self.colors['light_gray'], 'medium', center=True) # Back button self.draw_button("← Back", 270, 300, 100, 30, True) else: # Profile list panel panel_height = min(280, len(profile_names) * 55 + 60) self.draw_panel(30, 80, 580, panel_height, self.colors['black'], self.colors['gray']) # Profile entries for i, name in enumerate(profile_names): profile = self.profiles[name] entry_y = 95 + i * 50 entry_selected = (i == self.selected_index) # Profile entry background entry_color = self.colors['blue'] if entry_selected else self.colors['dark_gray'] border_color = self.colors['light_blue'] if entry_selected else self.colors['gray'] self.draw_panel(40, entry_y, 560, 40, entry_color, border_color) # Profile name name_color = self.colors['white'] if entry_selected else self.colors['light_gray'] self.draw_text(name, 50, entry_y + 5, name_color, 'medium') # Profile stats (compact) stats_text = f"Games: {profile.games_played} • Score: {profile.best_score}" stats_color = self.colors['light_gray'] if entry_selected else self.colors['gray'] self.draw_text(stats_text, 50, entry_y + 22, stats_color, 'tiny') # Active indicator if name == self.active_profile: self.draw_text("★", 580, entry_y + 12, self.colors['light_green'], 'small') # Back button back_y = 95 + len(profile_names) * 50 + 10 back_selected = (self.selected_index == len(profile_names)) self.draw_button("← Back", 270, back_y, 100, 30, back_selected) # Instructions self.draw_panel(10, 420, 620, 40, self.colors['black'], self.colors['dark_gray']) self.draw_text("Enter: Select • Escape: Back • Delete: Remove", 320, 435, self.colors['light_gray'], 'tiny', center=True) def render_create_profile(self): """Render profile creation screen with virtual keyboard""" # Header self.renderer.fill((0, 0, 640, 80), self.colors['dark_gray']) self.draw_text("Create Profile", 320, 15, self.colors['light_blue'], 'title', center=True) # Input field input_x, input_y = 120, 90 input_width, input_height = 400, 30 input_bg = self.colors['white'] if self.input_active else self.colors['light_gray'] input_border = self.colors['blue'] if self.input_active else self.colors['gray'] self.draw_panel(input_x, input_y, input_width, input_height, input_bg, input_border, 2) # Input text display_text = self.input_text if self.input_text else "Profile Name" text_color = self.colors['black'] if self.input_text else self.colors['gray'] self.draw_text(display_text, input_x + 10, input_y + 8, text_color, 'medium') if self.input_active: # Virtual keyboard self.render_virtual_keyboard() else: # Start input instruction self.draw_text("Press Enter to start typing", 320, 150, self.colors['yellow'], 'medium', center=True) # Back button self.draw_button("← Back", 270, 200, 100, 30, True) # Instructions if not self.input_active: self.draw_panel(50, 400, 540, 60, self.colors['black'], self.colors['dark_gray']) self.draw_text("Enter: Start Input • Escape: Back", 320, 420, self.colors['light_gray'], 'small', center=True) def render_virtual_keyboard(self): """Render the virtual keyboard""" kb_start_x = 50 kb_start_y = 150 key_width = 45 key_height = 30 for row_idx, row in enumerate(self.virtual_keyboard): row_y = kb_start_y + row_idx * (key_height + 5) # Special handling for bottom row (commands) if row_idx == len(self.virtual_keyboard) - 1: # Bottom row with command keys key_widths = [80, 80, 80, 80] # Wider keys for commands x_offset = 140 # Center the bottom row else: key_widths = [key_width] * len(row) x_offset = kb_start_x current_x = x_offset for col_idx, char in enumerate(row): selected = (row_idx == self.vk_cursor_y and col_idx == self.vk_cursor_x) # Key background if selected: self.draw_panel(current_x, row_y, key_widths[col_idx], key_height, self.colors['blue'], self.colors['light_blue']) text_color = self.colors['white'] else: self.draw_panel(current_x, row_y, key_widths[col_idx], key_height, self.colors['dark_gray'], self.colors['gray']) text_color = self.colors['light_gray'] # Key text display_char = char if char == ' 20: player_name = player_name[:17] + "..." self.draw_text(player_name, 120, entry_y, text_color, 'tiny') # Score score = entry.get('best_score', 0) self.draw_text(str(score), 350, entry_y, text_color, 'tiny') # Games games = entry.get('total_games', 0) self.draw_text(str(games), 450, entry_y, text_color, 'tiny') # Device (global leaderboard only) if self.leaderboard_type == "global": device = entry.get('device_id', '')[:8] self.draw_text(device, 520, entry_y, text_color, 'tiny') # Instructions self.draw_panel(10, 420, 620, 40, self.colors['black'], self.colors['dark_gray']) self.draw_text("Left/Right: Toggle Type • Enter: Select • Escape: Back", 320, 435, self.colors['light_gray'], 'tiny', center=True) def render_profile_stats(self): """Render detailed profile statistics""" if not self.active_profile: self.current_screen = "main_menu" return profile = self.profiles[self.active_profile] # Header self.renderer.fill((0, 0, 640, 80), self.colors['dark_gray']) title = f"Stats: {profile.name}" self.draw_text(title, 320, 15, self.colors['light_blue'], 'title', center=True) # Get additional info from integration system integration_info = self.integration.get_profile_info() # Main stats panel self.draw_panel(50, 90, 540, 260, self.colors['dark_gray'], self.colors['gray']) # Basic stats stats_y = 105 line_height = 20 self.draw_text("Profile Statistics", 320, stats_y, self.colors['white'], 'large', center=True) stats_y += 30 # Local stats self.draw_text("Local Statistics:", 60, stats_y, self.colors['light_blue'], 'medium') stats_y += line_height self.draw_text(f"Games Played: {profile.games_played}", 70, stats_y, self.colors['light_gray'], 'small') stats_y += line_height self.draw_text(f"Best Score: {profile.best_score}", 70, stats_y, self.colors['light_gray'], 'small') stats_y += line_height self.draw_text(f"Total Score: {profile.total_score}", 70, stats_y, self.colors['light_gray'], 'small') stats_y += line_height self.draw_text(f"Achievements: {len(profile.achievements)}", 70, stats_y, self.colors['light_gray'], 'small') stats_y += line_height # Creation and last played dates try: created = datetime.fromisoformat(profile.created_date).strftime("%Y-%m-%d") last_played = datetime.fromisoformat(profile.last_played).strftime("%Y-%m-%d") except: created = "Unknown" last_played = "Unknown" self.draw_text(f"Created: {created}", 70, stats_y, self.colors['light_gray'], 'small') stats_y += line_height self.draw_text(f"Last Played: {last_played}", 70, stats_y, self.colors['light_gray'], 'small') stats_y += line_height # Server stats (if available) if self.api_enabled and integration_info: stats_y += 10 self.draw_text("Server Status:", 60, stats_y, self.colors['light_green'], 'medium') stats_y += line_height self.draw_text(f"Device ID: {integration_info.get('device_id', 'Unknown')}", 70, stats_y, self.colors['light_gray'], 'small') stats_y += line_height self.draw_text("✓ Profile synced with server", 70, stats_y, self.colors['light_green'], 'small') else: stats_y += 10 self.draw_text("Server Status:", 60, stats_y, self.colors['red'], 'medium') stats_y += line_height self.draw_text("✗ Server offline", 70, stats_y, self.colors['red'], 'small') # Settings summary stats_y = 105 settings_x = 350 self.draw_text("Current Settings:", settings_x, stats_y + 30, self.colors['light_blue'], 'medium') stats_y += 50 difficulty = profile.settings.get('difficulty', 'normal').title() self.draw_text(f"Difficulty: {difficulty}", settings_x + 10, stats_y, self.colors['light_gray'], 'small') stats_y += line_height sound_vol = profile.settings.get('sound_volume', 50) self.draw_text(f"Sound Volume: {sound_vol}%", settings_x + 10, stats_y, self.colors['light_gray'], 'small') stats_y += line_height music_vol = profile.settings.get('music_volume', 50) self.draw_text(f"Music Volume: {music_vol}%", settings_x + 10, stats_y, self.colors['light_gray'], 'small') stats_y += line_height screen_shake = "On" if profile.settings.get('screen_shake', True) else "Off" self.draw_text(f"Screen Shake: {screen_shake}", settings_x + 10, stats_y, self.colors['light_gray'], 'small') # Back button back_selected = (self.selected_index == 0) self.draw_button("← Back", 270, 370, 100, 30, back_selected) # Instructions self.draw_panel(10, 420, 620, 40, self.colors['black'], self.colors['dark_gray']) self.draw_text("Enter: Back • Escape: Back to Main Menu", 320, 435, self.colors['light_gray'], 'tiny', center=True) def render_error_dialog(self): """Render error dialog overlay""" # Semi-transparent overlay overlay_color = sdl2.ext.Color(0, 0, 0, 128) # Black with alpha self.renderer.fill((0, 0, 640, 480), overlay_color) # Error dialog box dialog_width = 400 dialog_height = 120 dialog_x = (640 - dialog_width) // 2 dialog_y = (480 - dialog_height) // 2 # Dialog background with red border self.draw_panel(dialog_x, dialog_y, dialog_width, dialog_height, self.colors['black'], self.colors['red'], border_width=4) # Error title self.draw_text("ERROR", dialog_x + dialog_width // 2, dialog_y + 20, self.colors['red'], 'large', center=True) # Error message self.draw_text(self.error_message, dialog_x + dialog_width // 2, dialog_y + 50, self.colors['white'], 'medium', center=True) # Dismiss instruction self.draw_text("Press any key to continue...", dialog_x + dialog_width // 2, dialog_y + 80, self.colors['light_gray'], 'small', center=True) def draw_text(self, text: str, x: int, y: int, color, font_type='medium', center: bool = False): """Draw text on screen with improved styling""" if not text: return factory = sdl2.ext.SpriteFactory(renderer=self.renderer) font = self.fonts.get(font_type, self.fonts['medium']) text_sprite = factory.from_text(text, color=color, fontmanager=font) if center: x = x - text_sprite.size[0] // 2 text_sprite.position = (x, y) self.renderer.copy(text_sprite, dstrect=text_sprite.position) return text_sprite.size def draw_panel(self, x: int, y: int, width: int, height: int, bg_color=None, border_color=None, border_width=2): """Draw a styled panel/box""" if bg_color: self.renderer.fill((x, y, width, height), bg_color) if border_color: # Draw border for i in range(border_width): self.renderer.draw_rect((x + i, y + i, width - 2*i, height - 2*i), border_color) def draw_button(self, text: str, x: int, y: int, width: int, height: int, selected: bool = False, disabled: bool = False): """Draw a styled button""" # Button colors based on state if disabled: bg_color = self.colors['black'] border_color = self.colors['dark_gray'] text_color = self.colors['dark_gray'] elif selected: bg_color = self.colors['blue'] border_color = self.colors['light_blue'] text_color = self.colors['white'] else: bg_color = self.colors['dark_gray'] border_color = self.colors['gray'] text_color = self.colors['light_gray'] # Draw button background and border self.draw_panel(x, y, width, height, bg_color, border_color) # Draw button text centered text_x = x + width // 2 text_y = y + height // 2 - 12 # Approximate text height offset self.draw_text(text, text_x, text_y, text_color, 'medium', center=True) def handle_keyboard_input(self, event): """Handle keyboard input for navigation only (no text input)""" key = event.key.keysym.sym # If error dialog is shown, dismiss it on any key press if self.show_error: self.show_error = False return # Navigation mode only if key == sdl2.SDLK_UP: self.navigate_up() elif key == sdl2.SDLK_DOWN: self.navigate_down() elif key == sdl2.SDLK_LEFT: self.navigate_left() elif key == sdl2.SDLK_RIGHT: self.navigate_right() elif key == sdl2.SDLK_RETURN or key == sdl2.SDLK_SPACE: self.handle_confirm() elif key == sdl2.SDLK_ESCAPE: self.handle_back() elif key == sdl2.SDLK_DELETE or key == sdl2.SDLK_BACKSPACE: self.handle_delete() elif key == sdl2.SDLK_TAB: self.handle_menu() def run(self): """Main application loop""" self.init_sdl() # Use SDL2's built-in timing instead of Clock target_fps = 30 # Lower FPS for better performance on smaller screens frame_time = 1000 // target_fps # milliseconds per frame while self.running: events = sdl2.ext.get_events() for event in events: if event.type == sdl2.SDL_QUIT: self.running = False elif event.type == sdl2.SDL_KEYDOWN: self.handle_keyboard_input(event) # Handle gamepad input (if available) self.handle_gamepad_input() # Render frame frame_start = sdl2.SDL_GetTicks() self.render() # Cap framerate frame_duration = sdl2.SDL_GetTicks() - frame_start if frame_duration < frame_time: sdl2.SDL_Delay(frame_time - frame_duration) # Cleanup if self.gamepad: sdl2.SDL_JoystickClose(self.gamepad) sdl2.ext.quit() def main(): """Entry point""" print("Starting Profile Manager with User Profile Integration...") print("Features:") print(" • Profile creation and management") print(" • API server integration for leaderboards") print(" • Device-specific and global leaderboards") print(" • Detailed profile statistics") print(" • Settings management") print("") print("Controls:") print(" Arrow Keys: Navigate menus") print(" Enter/Space: Confirm/Select") print(" Escape: Back/Cancel") print(" Delete/Backspace: Delete") print(" Tab: Menu (reserved)") print(" Left/Right: Adjust settings and toggle options") print("") print("Gamepad Controls (if connected):") print(" D-Pad: Navigate menus") print(" A Button: Confirm/Select") print(" B Button: Back/Cancel") print(" X Button: Delete") print("") manager = ProfileManager() print(f"Device ID: {manager.device_id}") print(f"API Server: {'Connected' if manager.api_enabled else 'Offline'}") print("") manager.run() if __name__ == "__main__": main()