You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1260 lines
51 KiB
1260 lines
51 KiB
#!/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', ' ', '.', '-', '_'], |
|
['<DEL', 'SPACE', 'DONE', 'CANCEL'] |
|
] |
|
|
|
# SDL2 Setup |
|
self.window = None |
|
self.renderer = None |
|
self.font_manager = None |
|
self.running = True |
|
|
|
# Gamepad state |
|
self.gamepad = None |
|
self.button_states = {} |
|
self.last_button_time = {} |
|
|
|
# Colors |
|
self.colors = { |
|
'white': sdl2.ext.Color(255, 255, 255), |
|
'black': sdl2.ext.Color(0, 0, 0), |
|
'gray': sdl2.ext.Color(128, 128, 128), |
|
'light_gray': sdl2.ext.Color(200, 200, 200), |
|
'dark_gray': sdl2.ext.Color(64, 64, 64), |
|
'blue': sdl2.ext.Color(70, 130, 200), |
|
'light_blue': sdl2.ext.Color(120, 180, 255), |
|
'green': sdl2.ext.Color(50, 200, 50), |
|
'light_green': sdl2.ext.Color(100, 255, 100), |
|
'red': sdl2.ext.Color(200, 50, 50), |
|
'yellow': sdl2.ext.Color(255, 220, 0), |
|
'orange': sdl2.ext.Color(255, 165, 0), |
|
'purple': sdl2.ext.Color(150, 50, 200) |
|
} |
|
|
|
self.load_profiles() |
|
|
|
def init_sdl(self): |
|
"""Initialize SDL2 components""" |
|
sdl2.ext.init(joystick=True) |
|
|
|
# Create window |
|
self.window = sdl2.ext.Window( |
|
title="Profile Manager", |
|
size=(640, 480) |
|
) |
|
self.window.show() |
|
|
|
# Create renderer |
|
self.renderer = sdl2.ext.Renderer( |
|
self.window, |
|
flags=sdl2.SDL_RENDERER_ACCELERATED | sdl2.SDL_RENDERER_PRESENTVSYNC |
|
) |
|
|
|
# Initialize font system - create multiple sizes |
|
font_path = "assets/decterm.ttf" if os.path.exists("assets/decterm.ttf") else None |
|
self.fonts = { |
|
'title': sdl2.ext.FontManager(font_path=font_path, size=36), |
|
'large': sdl2.ext.FontManager(font_path=font_path, size=28), |
|
'medium': sdl2.ext.FontManager(font_path=font_path, size=22), |
|
'small': sdl2.ext.FontManager(font_path=font_path, size=18), |
|
'tiny': sdl2.ext.FontManager(font_path=font_path, size=14) |
|
} |
|
|
|
# Initialize gamepad |
|
self.init_gamepad() |
|
|
|
def init_gamepad(self): |
|
"""Initialize gamepad support""" |
|
sdl2.SDL_Init(sdl2.SDL_INIT_JOYSTICK | sdl2.SDL_INIT_GAMECONTROLLER) |
|
|
|
num_joysticks = sdl2.SDL_NumJoysticks() |
|
if num_joysticks > 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 == '<DEL': |
|
# Delete last character |
|
if self.input_text: |
|
self.input_text = self.input_text[:-1] |
|
elif selected_char == 'SPACE': |
|
# Add space |
|
if len(self.input_text) < 20: |
|
self.input_text += ' ' |
|
elif selected_char == 'DONE': |
|
# Finish input |
|
if self.input_text.strip(): |
|
# Check if profile already exists |
|
if self.input_text.strip() in self.profiles: |
|
self.show_error_dialog(f"Profile '{self.input_text.strip()}' already exists!") |
|
else: |
|
if self.create_profile(self.input_text): |
|
self.current_screen = "main_menu" |
|
self.selected_index = 0 |
|
self.input_active = False |
|
else: |
|
self.show_error_dialog("Failed to create profile!") |
|
else: |
|
self.show_error_dialog("Profile name cannot be empty!") |
|
elif selected_char == 'CANCEL': |
|
# Cancel input |
|
self.input_text = "" |
|
self.input_active = False |
|
else: |
|
# Add character |
|
if len(self.input_text) < 20: |
|
self.input_text += selected_char |
|
|
|
def adjust_setting(self, direction: int): |
|
"""Adjust setting value left/right""" |
|
if self.current_screen == "edit_profile" and self.active_profile: |
|
profile = self.profiles[self.active_profile] |
|
|
|
if self.selected_index == 0: # Difficulty |
|
difficulties = ["easy", "normal", "hard", "expert"] |
|
current = difficulties.index(profile.settings["difficulty"]) |
|
new_index = (current + direction) % len(difficulties) |
|
profile.settings["difficulty"] = difficulties[new_index] |
|
|
|
elif self.selected_index == 1: # Sound Volume |
|
profile.settings["sound_volume"] = max(0, min(100, |
|
profile.settings["sound_volume"] + direction * 5)) |
|
|
|
elif self.selected_index == 2: # Music Volume |
|
profile.settings["music_volume"] = max(0, min(100, |
|
profile.settings["music_volume"] + direction * 5)) |
|
|
|
elif self.selected_index == 3: # Screen Shake |
|
profile.settings["screen_shake"] = not profile.settings["screen_shake"] |
|
|
|
def load_leaderboard_data(self): |
|
"""Load leaderboard data based on current type""" |
|
if not self.api_enabled: |
|
self.leaderboard_data = [] |
|
return |
|
|
|
try: |
|
if self.leaderboard_type == "device": |
|
self.leaderboard_data = self.integration.get_device_leaderboard(10) |
|
else: # global |
|
self.leaderboard_data = self.integration.get_global_leaderboard(10) |
|
except Exception as e: |
|
print(f"Error loading leaderboard: {e}") |
|
self.leaderboard_data = [] |
|
|
|
def show_error_dialog(self, message: str): |
|
"""Show an error dialog with the given message""" |
|
self.show_error = True |
|
self.error_message = message |
|
self.error_timer = time.time() + 3.0 # Show error for 3 seconds |
|
|
|
def render(self): |
|
"""Main rendering method""" |
|
# Clear with dark background |
|
self.renderer.clear(self.colors['black']) |
|
|
|
# Draw subtle background pattern/gradient |
|
for y in range(0, 480, 20): |
|
alpha = int(20 * (1 - y / 480)) # Fade effect |
|
if alpha > 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 == '<DEL': |
|
display_char = 'DEL' |
|
elif char == 'SPACE': |
|
display_char = 'SPC' |
|
|
|
text_x = current_x + key_widths[col_idx] // 2 |
|
text_y = row_y + 8 |
|
self.draw_text(display_char, text_x, text_y, text_color, 'tiny', center=True) |
|
|
|
current_x += key_widths[col_idx] + 5 |
|
|
|
# Instructions for virtual keyboard |
|
self.draw_panel(50, 420, 540, 40, self.colors['black'], self.colors['dark_gray']) |
|
self.draw_text("Arrows: Navigate • Enter: Select • Escape: Cancel", |
|
320, 435, self.colors['light_gray'], 'tiny', center=True) |
|
|
|
def render_edit_profile(self): |
|
"""Render profile editing screen with improved layout""" |
|
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"Edit: {profile.name}" |
|
self.draw_text(title, 320, 15, self.colors['light_blue'], 'title', center=True) |
|
|
|
# Profile stats |
|
stats_text = f"Games: {profile.games_played} • Score: {profile.best_score}" |
|
self.draw_text(stats_text, 320, 50, self.colors['light_green'], 'small', center=True) |
|
|
|
# Settings panel |
|
panel_height = 280 |
|
self.draw_panel(60, 90, 520, panel_height, self.colors['dark_gray'], self.colors['gray']) |
|
|
|
# Settings title |
|
self.draw_text("Settings", 320, 105, self.colors['white'], 'large', center=True) |
|
|
|
settings_items = [ |
|
("Difficulty", f"{profile.settings['difficulty'].title()}"), |
|
("Sound Vol", f"{profile.settings['sound_volume']}%"), |
|
("Music Vol", f"{profile.settings['music_volume']}%"), |
|
("Screen Shake", "On" if profile.settings['screen_shake'] else "Off"), |
|
] |
|
|
|
start_y = 130 |
|
item_height = 35 |
|
|
|
for i, (label, value) in enumerate(settings_items): |
|
item_y = start_y + i * item_height |
|
selected = (i == self.selected_index) |
|
|
|
# Setting item background |
|
if selected: |
|
self.draw_panel(80, item_y, 480, 25, self.colors['blue'], self.colors['light_blue']) |
|
text_color = self.colors['white'] |
|
value_color = self.colors['light_green'] |
|
# Navigation arrows for selected item |
|
self.draw_text("◄", 70, item_y + 5, self.colors['white'], 'small') |
|
self.draw_text("►", 570, item_y + 5, self.colors['white'], 'small') |
|
else: |
|
text_color = self.colors['light_gray'] |
|
value_color = self.colors['yellow'] |
|
|
|
# Setting label and value |
|
self.draw_text(label, 90, item_y + 5, text_color, 'medium') |
|
self.draw_text(value, 500, item_y + 5, value_color, 'medium') |
|
|
|
# Action buttons |
|
button_y = 310 |
|
save_selected = (self.selected_index == len(settings_items)) |
|
back_selected = (self.selected_index == len(settings_items) + 1) |
|
|
|
self.draw_button("Save", 200, button_y, 80, 30, save_selected) |
|
self.draw_button("← Back", 320, button_y, 80, 30, back_selected) |
|
|
|
# Instructions |
|
self.draw_panel(10, 420, 620, 40, self.colors['black'], self.colors['dark_gray']) |
|
self.draw_text("Left/Right: Adjust • Enter: Save/Back • Escape: Cancel", |
|
320, 435, self.colors['light_gray'], 'tiny', center=True) |
|
|
|
def render_leaderboard(self): |
|
"""Render leaderboard screen""" |
|
# Header |
|
self.renderer.fill((0, 0, 640, 80), self.colors['dark_gray']) |
|
|
|
title = f"{self.leaderboard_type.title()} Leaderboard" |
|
self.draw_text(title, 320, 15, self.colors['light_blue'], 'title', center=True) |
|
|
|
if not self.api_enabled: |
|
self.draw_text("Server offline - No leaderboard available", 320, 50, |
|
self.colors['red'], 'medium', center=True) |
|
self.draw_button("← Back", 270, 200, 100, 30, True) |
|
return |
|
|
|
# Toggle button |
|
toggle_text = f"Type: {self.leaderboard_type.title()} (Left/Right to toggle)" |
|
toggle_selected = (self.selected_index == 0) |
|
self.draw_button(toggle_text, 120, 90, 400, 25, toggle_selected) |
|
|
|
# Refresh button |
|
refresh_selected = (self.selected_index == 1) |
|
self.draw_button("Refresh", 150, 125, 80, 25, refresh_selected) |
|
|
|
# Back button |
|
back_selected = (self.selected_index == 2) |
|
self.draw_button("← Back", 250, 125, 80, 25, back_selected) |
|
|
|
# Leaderboard data |
|
if not self.leaderboard_data: |
|
self.draw_text("No leaderboard data available", 320, 200, |
|
self.colors['yellow'], 'medium', center=True) |
|
else: |
|
# Leaderboard panel |
|
panel_height = min(250, len(self.leaderboard_data) * 25 + 40) |
|
self.draw_panel(50, 160, 540, panel_height, |
|
self.colors['black'], self.colors['gray']) |
|
|
|
# Headers |
|
self.draw_text("Rank", 60, 175, self.colors['light_blue'], 'small') |
|
self.draw_text("Player", 120, 175, self.colors['light_blue'], 'small') |
|
self.draw_text("Score", 350, 175, self.colors['light_blue'], 'small') |
|
self.draw_text("Games", 450, 175, self.colors['light_blue'], 'small') |
|
|
|
if self.leaderboard_type == "global": |
|
self.draw_text("Device", 520, 175, self.colors['light_blue'], 'small') |
|
|
|
# Leaderboard entries |
|
for i, entry in enumerate(self.leaderboard_data): |
|
entry_y = 195 + i * 22 |
|
|
|
# Highlight current user |
|
is_current_user = (self.active_profile and |
|
entry.get('user_id') == self.active_profile) |
|
text_color = self.colors['light_green'] if is_current_user else self.colors['light_gray'] |
|
|
|
# Rank |
|
self.draw_text(str(entry.get('rank', i+1)), 60, entry_y, text_color, 'tiny') |
|
|
|
# Player name |
|
player_name = entry.get('user_id', 'Unknown') |
|
if len(player_name) > 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()
|
|
|