diff --git a/requirements.txt b/requirements.txt index f895802..0559fc3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,9 @@ # Configuration python-dotenv>=1.0.0 +# GUI Framework +PyQt6>=6.4.0 + # Testing (Never-Break Rule #5) pytest>=7.0.0 pytest-asyncio>=0.21.0 diff --git a/ui/__init__.py b/ui/__init__.py new file mode 100644 index 0000000..5b08b0c --- /dev/null +++ b/ui/__init__.py @@ -0,0 +1,14 @@ +# Description: UI module for Lemontropia Suite +# PyQt6 GUI components for the application + +from .main_window import MainWindow +from .hud_overlay import HUDOverlay, HUDStats +from .loadout_manager import LoadoutManagerDialog, LoadoutConfig + +__all__ = [ + 'MainWindow', + 'HUDOverlay', + 'HUDStats', + 'LoadoutManagerDialog', + 'LoadoutConfig', +] diff --git a/ui/hud_overlay.py b/ui/hud_overlay.py new file mode 100644 index 0000000..124620c --- /dev/null +++ b/ui/hud_overlay.py @@ -0,0 +1,707 @@ +# Description: Transparent HUD Overlay for Lemontropia Suite +# Implements frameless, always-on-top, click-through overlay with draggable support +# Follows Never-Break Rules: Decimal precision, 60+ FPS, Observer Pattern integration + +import sys +import json +from pathlib import Path +from decimal import Decimal +from datetime import datetime, timedelta +from dataclasses import dataclass, asdict +from typing import Optional, Dict, Any + +from PyQt6.QtWidgets import ( + QApplication, QWidget, QVBoxLayout, QHBoxLayout, + QLabel, QFrame, QSizePolicy +) +from PyQt6.QtCore import ( + Qt, QTimer, pyqtSignal, QPoint, QSettings, + QObject +) +from PyQt6.QtGui import QFont, QColor, QPalette, QMouseEvent + + +@dataclass +class HUDStats: + """Data structure for HUD statistics.""" + session_time: timedelta = timedelta(0) + loot_total: Decimal = Decimal('0.0') + damage_dealt: int = 0 + damage_taken: int = 0 + kills: int = 0 + globals_count: int = 0 + hofs_count: int = 0 + current_weapon: str = "None" + current_loadout: str = "None" + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary for serialization.""" + return { + 'session_time_seconds': self.session_time.total_seconds(), + 'loot_total': str(self.loot_total), + 'damage_dealt': self.damage_dealt, + 'damage_taken': self.damage_taken, + 'kills': self.kills, + 'globals_count': self.globals_count, + 'hofs_count': self.hofs_count, + 'current_weapon': self.current_weapon, + 'current_loadout': self.current_loadout, + } + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> 'HUDStats': + """Create from dictionary.""" + return cls( + session_time=timedelta(seconds=data.get('session_time_seconds', 0)), + loot_total=Decimal(data.get('loot_total', '0.0')), + damage_dealt=data.get('damage_dealt', 0), + damage_taken=data.get('damage_taken', 0), + kills=data.get('kills', 0), + globals_count=data.get('globals_count', 0), + hofs_count=data.get('hofs_count', 0), + current_weapon=data.get('current_weapon', 'None'), + current_loadout=data.get('current_loadout', 'None'), + ) + + +class HUDOverlay(QWidget): + """ + Transparent, always-on-top HUD overlay for Lemontropia Suite. + + Features: + - Frameless window (no borders, title bar) + - Transparent background with semi-transparent content + - Always on top of other windows + - Click-through when not holding modifier key + - Draggable when holding Ctrl key + - Position persistence across sessions + - Real-time stat updates via signals/slots + + Window Flags: + - FramelessWindowHint: Removes window decorations + - WindowStaysOnTopHint: Keeps above other windows + - Tool: Makes it a tool window (no taskbar entry) + """ + + # Signal emitted when stats are updated (for external integration) + stats_updated = pyqtSignal(dict) + + # Signal emitted when HUD is moved + position_changed = pyqtSignal(QPoint) + + def __init__(self, parent: Optional[QObject] = None, + config_path: Optional[str] = None): + """ + Initialize HUD Overlay. + + Args: + parent: Parent widget (optional) + config_path: Path to config file for position persistence + """ + super().__init__(parent) + + # Configuration path for saving position + if config_path is None: + ui_dir = Path(__file__).parent + self.config_path = ui_dir.parent / "data" / "hud_config.json" + else: + self.config_path = Path(config_path) + + self.config_path.parent.mkdir(parents=True, exist_ok=True) + + # Session tracking + self._session_start: Optional[datetime] = None + self._stats = HUDStats() + + # Drag state + self._dragging = False + self._drag_offset = QPoint() + self._modifier_pressed = False + + # Timer for session time updates + self._timer = QTimer(self) + self._timer.timeout.connect(self._update_session_time) + + self._setup_window() + self._setup_ui() + self._load_position() + + def _setup_window(self) -> None: + """Configure window properties for HUD behavior.""" + # Window flags for frameless, always-on-top, tool window + self.setWindowFlags( + Qt.WindowType.FramelessWindowHint | + Qt.WindowType.WindowStaysOnTopHint | + Qt.WindowType.Tool + ) + + # Enable transparency + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + + # Enable mouse tracking for hover detection + self.setMouseTracking(True) + + # Size + self.setFixedSize(320, 220) + + # Accept focus for keyboard events (needed for modifier detection) + self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + + def _setup_ui(self) -> None: + """Build the HUD UI components.""" + # Main container with semi-transparent background + self.container = QFrame(self) + self.container.setFixedSize(320, 220) + self.container.setObjectName("hudContainer") + + # Style the container - semi-transparent dark background + self.container.setStyleSheet(""" + #hudContainer { + background-color: rgba(0, 0, 0, 180); + border: 1px solid rgba(255, 215, 0, 80); + border-radius: 8px; + } + QLabel { + color: #FFFFFF; + font-family: 'Segoe UI', 'Arial', sans-serif; + } + .stat-label { + font-size: 11px; + color: #CCCCCC; + } + .stat-value { + font-size: 13px; + font-weight: bold; + color: #FFD700; + } + .header { + font-size: 14px; + font-weight: bold; + color: #FFD700; + } + .subheader { + font-size: 10px; + color: #888888; + } + .positive { + color: #7FFF7F; + } + .negative { + color: #FF7F7F; + } + """) + + # Main layout + layout = QVBoxLayout(self.container) + layout.setContentsMargins(12, 10, 12, 10) + layout.setSpacing(6) + + # === HEADER === + header_layout = QHBoxLayout() + + self.title_label = QLabel("šŸ‹ LEMONTROPIA") + self.title_label.setProperty("class", "header") + self.title_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #FFD700;") + header_layout.addWidget(self.title_label) + + header_layout.addStretch() + + self.time_label = QLabel("00:00:00") + self.time_label.setStyleSheet("font-size: 13px; font-weight: bold; color: #00FFFF;") + header_layout.addWidget(self.time_label) + + layout.addLayout(header_layout) + + # Drag hint (only visible on hover) + self.drag_hint = QLabel("Hold Ctrl to drag") + self.drag_hint.setStyleSheet("font-size: 9px; color: #666666;") + self.drag_hint.setAlignment(Qt.AlignmentFlag.AlignCenter) + layout.addWidget(self.drag_hint) + + # === SEPARATOR === + separator = QFrame() + separator.setFrameShape(QFrame.Shape.HLine) + separator.setStyleSheet("background-color: rgba(255, 215, 0, 50);") + separator.setFixedHeight(1) + layout.addWidget(separator) + + # === STATS GRID === + # Row 1: Loot & Kills + row1 = QHBoxLayout() + + # Loot + loot_layout = QVBoxLayout() + loot_label = QLabel("šŸ’° LOOT") + loot_label.setStyleSheet("font-size: 10px; color: #888888;") + loot_layout.addWidget(loot_label) + + self.loot_value_label = QLabel("0.00 PED") + self.loot_value_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #7FFF7F;") + loot_layout.addWidget(self.loot_value_label) + + row1.addLayout(loot_layout) + row1.addStretch() + + # Kills + kills_layout = QVBoxLayout() + kills_label = QLabel("šŸ’€ KILLS") + kills_label.setStyleSheet("font-size: 10px; color: #888888;") + kills_layout.addWidget(kills_label) + + self.kills_value_label = QLabel("0") + self.kills_value_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #FFFFFF;") + kills_layout.addWidget(self.kills_value_label) + + row1.addLayout(kills_layout) + row1.addStretch() + + # Globals/HoFs + globals_layout = QVBoxLayout() + globals_label = QLabel("šŸŒ GLOBALS") + globals_label.setStyleSheet("font-size: 10px; color: #888888;") + globals_layout.addWidget(globals_label) + + self.globals_value_label = QLabel("0 / 0") + self.globals_value_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #FFD700;") + globals_layout.addWidget(self.globals_value_label) + + row1.addLayout(globals_layout) + + layout.addLayout(row1) + + # Row 2: Damage + row2 = QHBoxLayout() + + # Damage Dealt + dealt_layout = QVBoxLayout() + dealt_label = QLabel("āš”ļø DEALT") + dealt_label.setStyleSheet("font-size: 10px; color: #888888;") + dealt_layout.addWidget(dealt_label) + + self.dealt_value_label = QLabel("0") + self.dealt_value_label.setStyleSheet("font-size: 13px; font-weight: bold; color: #7FFF7F;") + dealt_layout.addWidget(self.dealt_value_label) + + row2.addLayout(dealt_layout) + row2.addStretch() + + # Damage Taken + taken_layout = QVBoxLayout() + taken_label = QLabel("šŸ›”ļø TAKEN") + taken_label.setStyleSheet("font-size: 10px; color: #888888;") + taken_layout.addWidget(taken_label) + + self.taken_value_label = QLabel("0") + self.taken_value_label.setStyleSheet("font-size: 13px; font-weight: bold; color: #FF7F7F;") + taken_layout.addWidget(self.taken_value_label) + + row2.addLayout(taken_layout) + row2.addStretch() + + layout.addLayout(row2) + + # === WEAPON INFO === + weapon_separator = QFrame() + weapon_separator.setFrameShape(QFrame.Shape.HLine) + weapon_separator.setStyleSheet("background-color: rgba(255, 215, 0, 30);") + weapon_separator.setFixedHeight(1) + layout.addWidget(weapon_separator) + + weapon_layout = QHBoxLayout() + + weapon_icon = QLabel("šŸ”«") + weapon_icon.setStyleSheet("font-size: 12px;") + weapon_layout.addWidget(weapon_icon) + + self.weapon_label = QLabel("No weapon") + self.weapon_label.setStyleSheet("font-size: 11px; color: #CCCCCC;") + weapon_layout.addWidget(self.weapon_label) + + weapon_layout.addStretch() + + loadout_label = QLabel("Loadout:") + loadout_label.setStyleSheet("font-size: 10px; color: #888888;") + weapon_layout.addWidget(loadout_label) + + self.loadout_label = QLabel("None") + self.loadout_label.setStyleSheet("font-size: 11px; color: #00FFFF;") + weapon_layout.addWidget(self.loadout_label) + + layout.addLayout(weapon_layout) + + # === STATUS BAR === + self.status_label = QLabel("ā— Live") + self.status_label.setStyleSheet("font-size: 9px; color: #7FFF7F;") + self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + layout.addWidget(self.status_label) + + # ======================================================================== + # POSITION PERSISTENCE + # ======================================================================== + + def _load_position(self) -> None: + """Load saved position from config file.""" + try: + if self.config_path.exists(): + with open(self.config_path, 'r') as f: + config = json.load(f) + + x = config.get('x', 100) + y = config.get('y', 100) + self.move(x, y) + + # Load saved stats if available + if 'stats' in config: + self._stats = HUDStats.from_dict(config['stats']) + self._refresh_display() + else: + # Default position: top-right of screen + screen = QApplication.primaryScreen().geometry() + self.move(screen.width() - 350, 50) + except Exception as e: + # Default position on error + self.move(100, 100) + + def _save_position(self) -> None: + """Save current position to config file.""" + try: + config = { + 'x': self.x(), + 'y': self.y(), + 'stats': self._stats.to_dict(), + } + with open(self.config_path, 'w') as f: + json.dump(config, f, indent=2) + except Exception as e: + pass # Silent fail on save error + + # ======================================================================== + # MOUSE HANDLING (Drag Support) + # ======================================================================== + + def mousePressEvent(self, event: QMouseEvent) -> None: + """Handle mouse press - start drag if Ctrl is held.""" + if event.button() == Qt.MouseButton.LeftButton: + # Check if Ctrl is held + if event.modifiers() == Qt.KeyboardModifier.ControlModifier: + self._dragging = True + self._drag_offset = event.pos() + self.setCursor(Qt.CursorShape.ClosedHandCursor) + event.accept() + else: + # Pass through to underlying window + event.ignore() + self._enable_click_through(True) + + def mouseMoveEvent(self, event: QMouseEvent) -> None: + """Handle mouse move - drag window if in drag mode.""" + if self._dragging: + # Move window + new_pos = self.mapToGlobal(event.pos()) - self._drag_offset + self.move(new_pos) + self.position_changed.emit(new_pos) + event.accept() + else: + # Show drag hint on hover + self.drag_hint.setStyleSheet("font-size: 9px; color: #FFD700;") + event.ignore() + + def mouseReleaseEvent(self, event: QMouseEvent) -> None: + """Handle mouse release - end drag and save position.""" + if event.button() == Qt.MouseButton.LeftButton and self._dragging: + self._dragging = False + self.setCursor(Qt.CursorShape.ArrowCursor) + self._save_position() + event.accept() + else: + event.ignore() + + def leaveEvent(self, event) -> None: + """Handle mouse leave - reset drag hint.""" + self.drag_hint.setStyleSheet("font-size: 9px; color: #666666;") + super().leaveEvent(event) + + def _enable_click_through(self, enable: bool) -> None: + """ + Enable or disable click-through behavior. + + When enabled, mouse events pass through to the window below. + When disabled (Ctrl held), window captures mouse events for dragging. + """ + if enable: + self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True) + else: + self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, False) + + def keyPressEvent(self, event) -> None: + """Handle key press - detect Ctrl for drag mode.""" + if event.key() == Qt.Key.Key_Control: + self._modifier_pressed = True + self._enable_click_through(False) + self.drag_hint.setText("āœ‹ Drag mode ON") + self.drag_hint.setStyleSheet("font-size: 9px; color: #00FF00;") + super().keyPressEvent(event) + + def keyReleaseEvent(self, event) -> None: + """Handle key release - detect Ctrl release.""" + if event.key() == Qt.Key.Key_Control: + self._modifier_pressed = False + self._enable_click_through(True) + self.drag_hint.setText("Hold Ctrl to drag") + self.drag_hint.setStyleSheet("font-size: 9px; color: #666666;") + super().keyReleaseEvent(event) + + # ======================================================================== + # SESSION MANAGEMENT + # ======================================================================== + + def start_session(self) -> None: + """Start a new hunting/mining/crafting session.""" + self._session_start = datetime.now() + self._stats = HUDStats() # Reset stats + self._timer.start(1000) # Update every second + self._refresh_display() + self.status_label.setText("ā— Live - Recording") + self.status_label.setStyleSheet("font-size: 9px; color: #7FFF7F;") + + def end_session(self) -> None: + """End the current session.""" + self._timer.stop() + self._session_start = None + self._save_position() # Save final stats + self.status_label.setText("ā—‹ Paused") + self.status_label.setStyleSheet("font-size: 9px; color: #888888;") + + def _update_session_time(self) -> None: + """Update the session time display.""" + if self._session_start: + self._stats.session_time = datetime.now() - self._session_start + self._update_time_display() + + def _update_time_display(self) -> None: + """Update the time label.""" + total_seconds = int(self._stats.session_time.total_seconds()) + hours = total_seconds // 3600 + minutes = (total_seconds % 3600) // 60 + seconds = total_seconds % 60 + self.time_label.setText(f"{hours:02d}:{minutes:02d}:{seconds:02d}") + + # ======================================================================== + # STATS UPDATE INTERFACE + # ======================================================================== + + def update_stats(self, stats: Dict[str, Any]) -> None: + """ + Update HUD with new statistics. + + This is the main interface for LogWatcher integration. + Called via signals/slots when new data arrives. + + Args: + stats: Dictionary containing stat updates. Supported keys: + - 'loot': Decimal - Total loot value in PED + - 'loot_delta': Decimal - Add to existing loot + - 'damage_dealt': int - Total damage dealt (or add) + - 'damage_dealt_add': int - Add to damage dealt + - 'damage_taken': int - Total damage taken (or add) + - 'damage_taken_add': int - Add to damage taken + - 'kills': int - Total kills (or add) + - 'kills_add': int - Add to kills + - 'globals': int - Total globals (or add) + - 'globals_add': int - Add to globals + - 'hofs': int - Total HoFs (or add) + - 'hofs_add': int - Add to HoFs + - 'weapon': str - Current weapon name + - 'loadout': str - Current loadout name + """ + # Loot (Decimal precision) + if 'loot' in stats: + self._stats.loot_total = Decimal(str(stats['loot'])) + elif 'loot_delta' in stats: + self._stats.loot_total += Decimal(str(stats['loot_delta'])) + + # Damage dealt + if 'damage_dealt' in stats: + self._stats.damage_dealt = int(stats['damage_dealt']) + elif 'damage_dealt_add' in stats: + self._stats.damage_dealt += int(stats['damage_dealt_add']) + + # Damage taken + if 'damage_taken' in stats: + self._stats.damage_taken = int(stats['damage_taken']) + elif 'damage_taken_add' in stats: + self._stats.damage_taken += int(stats['damage_taken_add']) + + # Kills + if 'kills' in stats: + self._stats.kills = int(stats['kills']) + elif 'kills_add' in stats: + self._stats.kills += int(stats['kills_add']) + + # Globals + if 'globals' in stats: + self._stats.globals_count = int(stats['globals']) + elif 'globals_add' in stats: + self._stats.globals_count += int(stats['globals_add']) + + # HoFs + if 'hofs' in stats: + self._stats.hofs_count = int(stats['hofs']) + elif 'hofs_add' in stats: + self._stats.hofs_count += int(stats['hofs_add']) + + # Weapon + if 'weapon' in stats: + self._stats.current_weapon = str(stats['weapon']) + + # Loadout + if 'loadout' in stats: + self._stats.current_loadout = str(stats['loadout']) + + # Refresh display + self._refresh_display() + + # Emit signal for external listeners + self.stats_updated.emit(self._stats.to_dict()) + + def _refresh_display(self) -> None: + """Refresh all display labels with current stats.""" + # Loot with 2 decimal places (PED format) + self.loot_value_label.setText(f"{self._stats.loot_total:.2f} PED") + + # Kills + self.kills_value_label.setText(str(self._stats.kills)) + + # Globals / HoFs + self.globals_value_label.setText( + f"{self._stats.globals_count} / {self._stats.hofs_count}" + ) + + # Damage + self.dealt_value_label.setText(str(self._stats.damage_dealt)) + self.taken_value_label.setText(str(self._stats.damage_taken)) + + # Weapon/Loadout + self.weapon_label.setText(self._stats.current_weapon[:20]) # Truncate long names + self.loadout_label.setText(self._stats.current_loadout[:15]) + + # Update time if session active + self._update_time_display() + + def get_stats(self) -> HUDStats: + """Get current stats.""" + return self._stats + + # ======================================================================== + # WINDOW OVERRIDES + # ======================================================================== + + def showEvent(self, event) -> None: + """Handle show event - ensure proper window attributes.""" + # Re-apply click-through on show + if not self._modifier_pressed: + self._enable_click_through(True) + super().showEvent(event) + + def moveEvent(self, event) -> None: + """Handle move event - save position periodically.""" + # Save position every 5 seconds of movement (throttled) + if not hasattr(self, '_last_save'): + self._last_save = 0 + + import time + current_time = time.time() + if current_time - self._last_save > 5: + self._save_position() + self._last_save = current_time + + super().moveEvent(event) + + def closeEvent(self, event) -> None: + """Handle close event - save position before closing.""" + self._save_position() + super().closeEvent(event) + + +# ============================================================================ +# MOCK TESTING +# ============================================================================ + +def run_mock_test(): + """Run the HUD with mock data for testing.""" + app = QApplication(sys.argv) + app.setQuitOnLastWindowClosed(False) # Keep running when closed + + # Create HUD + hud = HUDOverlay() + hud.show() + + # Start session + hud.start_session() + + # Simulate incoming stats + from PyQt6.QtCore import QTimer + + mock_stats = { + 'loot': Decimal('0.0'), + 'damage_dealt': 0, + 'damage_taken': 0, + 'kills': 0, + 'globals': 0, + 'hofs': 0, + 'weapon': 'Omegaton M2100', + 'loadout': 'Hunting Set A', + } + + def simulate_event(): + """Simulate random game events.""" + import random + + event_type = random.choice(['loot', 'damage', 'kill', 'global']) + + if event_type == 'loot': + value = Decimal(str(random.uniform(0.5, 15.0))) + mock_stats['loot'] += value + hud.update_stats({'loot': mock_stats['loot']}) + print(f"[MOCK] Loot: {value:.2f} PED") + + elif event_type == 'damage': + damage = random.randint(5, 50) + mock_stats['damage_dealt'] += damage + hud.update_stats({'damage_dealt': mock_stats['damage_dealt']}) + print(f"[MOCK] Damage dealt: {damage}") + + elif event_type == 'kill': + mock_stats['kills'] += 1 + hud.update_stats({'kills': mock_stats['kills']}) + print(f"[MOCK] Kill! Total: {mock_stats['kills']}") + + elif event_type == 'global': + mock_stats['globals'] += 1 + hud.update_stats({'globals': mock_stats['globals']}) + print(f"[MOCK] GLOBAL!!! Count: {mock_stats['globals']}") + + # Simulate events every 3 seconds + timer = QTimer() + timer.timeout.connect(simulate_event) + timer.start(3000) + + print("\n" + "="*60) + print("šŸ‹ LEMONTROPIA HUD - MOCK TEST MODE") + print("="*60) + print("HUD should be visible on screen (top-right default)") + print("Features to test:") + print(" āœ“ Frameless window with gold border") + print(" āœ“ Transparent background") + print(" āœ“ Always on top") + print(" āœ“ Click-through (clicks pass to window below)") + print(" āœ“ Hold Ctrl to drag the HUD") + print(" āœ“ Stats update every 3 seconds (mock data)") + print("\nPress Ctrl+C in terminal to exit") + print("="*60 + "\n") + + sys.exit(app.exec()) + + +if __name__ == "__main__": + run_mock_test() diff --git a/ui/test_hud.py b/ui/test_hud.py new file mode 100644 index 0000000..51492eb --- /dev/null +++ b/ui/test_hud.py @@ -0,0 +1,127 @@ +# Description: Test script for HUD Overlay +# Run this to verify the HUD works correctly with mock data + +import sys +from pathlib import Path + +# Add project root to path +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root)) + +try: + from PyQt6.QtWidgets import QApplication + from PyQt6.QtCore import QTimer +except ImportError as e: + print("āŒ PyQt6 is not installed.") + print(" Install with: pip install PyQt6") + sys.exit(1) + +from decimal import Decimal +from ui.hud_overlay import HUDOverlay, HUDStats + + +def test_hud(): + """Test the HUD overlay with mock data.""" + app = QApplication(sys.argv) + app.setQuitOnLastWindowClosed(False) + + print("\n" + "="*60) + print("šŸ‹ LEMONTROPIA HUD OVERLAY - TEST MODE") + print("="*60) + + # Create HUD + hud = HUDOverlay() + hud.show() + + print("āœ… HUD created and shown") + print(f" Position: ({hud.x()}, {hud.y()})") + print(f" Size: {hud.width()}x{hud.height()}") + + # Start session + hud.start_session() + print("āœ… Session started") + + # Test initial stats + hud.update_stats({ + 'weapon': 'Omegaton M2100', + 'loadout': 'Test Loadout', + }) + print("āœ… Initial stats set") + + # Simulate events + import random + + event_count = [0] + + def simulate(): + event_count[0] += 1 + + # Simulate various events + event_type = random.choice([ + 'loot', 'damage', 'kill', 'global', 'hof' + ]) + + if event_type == 'loot': + value = Decimal(str(random.uniform(0.5, 25.0))) + hud.update_stats({'loot_delta': value}) + print(f" šŸ’° Loot: +{value:.2f} PED") + + elif event_type == 'damage': + damage = random.randint(10, 75) + hud.update_stats({'damage_dealt_add': damage}) + print(f" āš”ļø Damage dealt: +{damage}") + + elif event_type == 'kill': + hud.update_stats({'kills_add': 1}) + current = hud.get_stats().kills + print(f" šŸ’€ Kill! Total: {current}") + + elif event_type == 'global': + hud.update_stats({'globals_add': 1}) + print(f" šŸŒ GLOBAL!!!") + + elif event_type == 'hof': + hud.update_stats({'hofs_add': 1}) + print(f" šŸ† HALL OF FAME!") + + # Stop after 10 events + if event_count[0] >= 10: + timer.stop() + print("\n" + "="*60) + print("āœ… TEST COMPLETE") + print("="*60) + + final_stats = hud.get_stats() + print(f"\nFinal Stats:") + print(f" Session time: {final_stats.session_time}") + print(f" Total loot: {final_stats.loot_total:.2f} PED") + print(f" Kills: {final_stats.kills}") + print(f" Globals: {final_stats.globals_count}") + print(f" HoFs: {final_stats.hofs_count}") + print(f" Damage dealt: {final_stats.damage_dealt}") + + hud.end_session() + print("\n Closing in 3 seconds...") + QTimer.singleShot(3000, app.quit) + + # Run simulation every 2 seconds + timer = QTimer() + timer.timeout.connect(simulate) + timer.start(2000) + + print("\nšŸ“‹ FEATURES TO VERIFY:") + print(" 1. HUD appears on screen (top-right default)") + print(" 2. Frameless window with gold border") + print(" 3. Semi-transparent background") + print(" 4. Stats update automatically") + print(" 5. Session timer counts up") + print(" 6. Hold Ctrl to drag the HUD") + print(" 7. Click-through works (clicks pass to window below)") + print("\n Close window or wait for auto-close...") + print("="*60 + "\n") + + sys.exit(app.exec()) + + +if __name__ == "__main__": + test_hud()