From 60fbf8d2572bd94a1813fe7a09d32b0f48b9c3b5 Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Mon, 9 Feb 2026 22:34:16 +0000 Subject: [PATCH] feat: new clean customizable HUD overlay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - hud_overlay_clean.py: Completely redesigned HUD - Default view shows only essentials (P/L, Return %, Cost metrics, Gear) - Optional stats hidden by default (cost breakdown, combat, damage) - Settings button (⚙️) to customize visible elements - Compact mode option - Auto-sizing based on enabled features - HUD_REDESIGN.md: Documentation and migration guide --- ui/HUD_REDESIGN.md | 125 +++++++ ui/hud_overlay_clean.py | 722 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 847 insertions(+) create mode 100644 ui/HUD_REDESIGN.md create mode 100644 ui/hud_overlay_clean.py diff --git a/ui/HUD_REDESIGN.md b/ui/HUD_REDESIGN.md new file mode 100644 index 0000000..b5cf728 --- /dev/null +++ b/ui/HUD_REDESIGN.md @@ -0,0 +1,125 @@ +# HUD Redesign - Clean & Customizable + +## New Features + +### 1. **Simplified Default View** +By default, the new HUD shows only the essentials: +- Status indicator (● Live) +- Current gear (weapon, armor, loadout) +- **Profit/Loss** (big, prominent) +- **Return %** (big, prominent) +- Cost per shot/hit/heal +- Session time +- Drag hint + +### 2. **Customizable Display** +Click the **⚙️ (settings)** button to choose what to show: +- ✅ Core Stats (recommended on) + - Session Time + - Profit/Loss + - Return % + - Cost per Shot/Hit/Heal + - Current Gear +- ⬜ Optional Stats (off by default) + - Cost Breakdown (weapon/armor/heal separate costs) + - Combat Stats (kills, globals) + - Damage Stats (dealt/taken) + - Shrapnel Amount +- 📏 Display Mode + - Compact Mode (smaller font, narrower) + +### 3. **Auto-Size Based on Content** +The HUD automatically adjusts its height based on what you're showing. No wasted space! + +## Files + +| File | Purpose | +|------|---------| +| `ui/hud_overlay_clean.py` | New clean HUD implementation | + +## How to Test + +Replace the import in `main_window.py`: + +```python +# OLD: +from ui.hud_overlay import HUDOverlay + +# NEW: +from ui.hud_overlay_clean import HUDOverlay +``` + +## Default Layout (Clean) + +``` +┌─────────────────────────┐ +│ ● Ready [⚙️] │ +├─────────────────────────┤ +│ 🔫 ArMatrix BP-25 │ +│ 🛡️ Frontier, Adjusted │ +│ 📋 Test Loadout │ +├─────────────────────────┤ +│ P/L: +12.50 PED 105% │ +├─────────────────────────┤ +│ Shot: 0.091 Hit: 0.0001│ +├─────────────────────────┤ +│ 00:23:45 Ctrl+drag │ +└─────────────────────────┘ +``` + +## Expanded Layout (All Stats) + +``` +┌─────────────────────────┐ +│ ● Live [⚙️] │ +├─────────────────────────┤ +│ 🔫 ArMatrix BP-25 │ +│ 🛡️ Frontier, Adjusted │ +│ 📋 Test Loadout │ +├─────────────────────────┤ +│ P/L: +12.50 PED 105% │ +├─────────────────────────┤ +│ Shot: 0.091 Hit: 0.0001│ +├─────────────────────────┤ +│ Weapon: 2.15 │ +│ Armor: 0.50 │ +│ Healing: 0.25 │ +├─────────────────────────┤ +│ Kills: 15 Globals: 2 │ +├─────────────────────────┤ +│ 00:23:45 Ctrl+drag │ +└─────────────────────────┘ +``` + +## Migration + +To migrate to the new HUD: + +1. Backup your current `hud_overlay.py` +2. Replace import in `main_window.py` +3. Test the new layout +4. Use ⚙️ button to customize +5. Settings are saved automatically + +## Settings File + +Settings are saved to: +``` +%USERPROFILE%\.lemontropia\hud_config.json +``` + +Example: +```json +{ + "show_session_time": true, + "show_profit_loss": true, + "show_return_pct": true, + "show_cost_breakdown": false, + "show_combat_stats": false, + "show_damage_stats": false, + "show_cost_metrics": true, + "show_shrapnel": false, + "show_gear_info": true, + "compact_mode": false +} +``` diff --git a/ui/hud_overlay_clean.py b/ui/hud_overlay_clean.py new file mode 100644 index 0000000..e87939f --- /dev/null +++ b/ui/hud_overlay_clean.py @@ -0,0 +1,722 @@ +""" +Lemontropia Suite - HUD Overlay v2.0 +Cleaner, customizable HUD with collapsible sections. +""" + +import sys +import json +from pathlib import Path +from decimal import Decimal +from datetime import datetime, timedelta +from dataclasses import dataclass, asdict, field +from typing import Optional, Dict, Any, List, Set + +# Windows-specific imports for click-through support +if sys.platform == 'win32': + import ctypes + from ctypes import wintypes + + user32 = ctypes.windll.user32 + GetForegroundWindow = user32.GetForegroundWindow + GetForegroundWindow.restype = wintypes.HWND + +from PyQt6.QtWidgets import ( + QApplication, QWidget, QVBoxLayout, QHBoxLayout, + QLabel, QFrame, QSizePolicy, QPushButton, QMenu, + QDialog, QCheckBox, QFormLayout, QScrollArea +) +from PyQt6.QtCore import ( + Qt, QTimer, pyqtSignal, QPoint, QSettings, + QObject +) +from PyQt6.QtGui import QFont, QColor, QPalette, QMouseEvent + + +@dataclass +class HUDConfig: + """Configuration for which HUD elements to show.""" + show_session_time: bool = True + show_profit_loss: bool = True + show_return_pct: bool = True + show_cost_breakdown: bool = False # Weapon/Armor/Heal costs + show_combat_stats: bool = False # Kills, Globals, DPP + show_damage_stats: bool = False # Damage dealt/taken + show_cost_metrics: bool = True # Cost per shot/hit/heal + show_shrapnel: bool = False + show_gear_info: bool = True + + compact_mode: bool = False + + def to_dict(self) -> dict: + return { + 'show_session_time': self.show_session_time, + 'show_profit_loss': self.show_profit_loss, + 'show_return_pct': self.show_return_pct, + 'show_cost_breakdown': self.show_cost_breakdown, + 'show_combat_stats': self.show_combat_stats, + 'show_damage_stats': self.show_damage_stats, + 'show_cost_metrics': self.show_cost_metrics, + 'show_shrapnel': self.show_shrapnel, + 'show_gear_info': self.show_gear_info, + 'compact_mode': self.compact_mode, + } + + @classmethod + def from_dict(cls, data: dict) -> "HUDConfig": + return cls( + show_session_time=data.get('show_session_time', True), + show_profit_loss=data.get('show_profit_loss', True), + show_return_pct=data.get('show_return_pct', True), + show_cost_breakdown=data.get('show_cost_breakdown', False), + show_combat_stats=data.get('show_combat_stats', False), + show_damage_stats=data.get('show_damage_stats', False), + show_cost_metrics=data.get('show_cost_metrics', True), + show_shrapnel=data.get('show_shrapnel', False), + show_gear_info=data.get('show_gear_info', True), + compact_mode=data.get('compact_mode', False), + ) + + +@dataclass +class HUDStats: + """Simplified stats for HUD display.""" + session_time: timedelta = field(default_factory=lambda: timedelta(0)) + + # Financial (core) + loot_total: Decimal = Decimal('0.0') + cost_total: Decimal = Decimal('0.0') + profit_loss: Decimal = Decimal('0.0') + return_percentage: Decimal = Decimal('0.0') + + # Cost breakdown (optional) + weapon_cost_total: Decimal = Decimal('0.0') + armor_cost_total: Decimal = Decimal('0.0') + healing_cost_total: Decimal = Decimal('0.0') + + # Combat (optional) + kills: int = 0 + globals_count: int = 0 + hofs_count: int = 0 + + # Cost metrics (core) + cost_per_shot: Decimal = Decimal('0.0') + cost_per_hit: Decimal = Decimal('0.0') + cost_per_heal: Decimal = Decimal('0.0') + + # Gear + current_weapon: str = "None" + current_armor: str = "None" + current_fap: str = "None" + current_loadout: str = "None" + + def recalculate(self): + """Recalculate derived values.""" + self.profit_loss = self.loot_total - self.cost_total + if self.cost_total > 0: + self.return_percentage = (self.loot_total / self.cost_total) * Decimal('100') + else: + self.return_percentage = Decimal('0') + + def to_dict(self) -> dict: + return { + 'session_time': str(self.session_time), + 'loot_total': str(self.loot_total), + 'cost_total': str(self.cost_total), + 'profit_loss': str(self.profit_loss), + 'return_percentage': str(self.return_percentage), + 'weapon_cost_total': str(self.weapon_cost_total), + 'armor_cost_total': str(self.armor_cost_total), + 'healing_cost_total': str(self.healing_cost_total), + 'kills': self.kills, + 'globals_count': self.globals_count, + 'hofs_count': self.hofs_count, + 'cost_per_shot': str(self.cost_per_shot), + 'cost_per_hit': str(self.cost_per_hit), + 'cost_per_heal': str(self.cost_per_heal), + 'current_weapon': self.current_weapon, + 'current_armor': self.current_armor, + 'current_fap': self.current_fap, + 'current_loadout': self.current_loadout, + } + + +class HUDSettingsDialog(QDialog): + """Dialog to configure which HUD elements to show.""" + + def __init__(self, config: HUDConfig, parent=None): + super().__init__(parent) + self.setWindowTitle("HUD Settings") + self.setMinimumWidth(300) + + self.config = config + self._setup_ui() + + def _setup_ui(self): + layout = QVBoxLayout(self) + + scroll = QScrollArea() + scroll.setWidgetResizable(True) + content = QWidget() + form = QFormLayout(content) + + # Core stats (always recommended) + form.addRow(QLabel("Core Stats (Recommended)")) + + self.cb_time = QCheckBox("Session Time") + self.cb_time.setChecked(self.config.show_session_time) + form.addRow(self.cb_time) + + self.cb_profit = QCheckBox("Profit/Loss") + self.cb_profit.setChecked(self.config.show_profit_loss) + form.addRow(self.cb_profit) + + self.cb_return = QCheckBox("Return %") + self.cb_return.setChecked(self.config.show_return_pct) + form.addRow(self.cb_return) + + self.cb_cost_metrics = QCheckBox("Cost per Shot/Hit/Heal") + self.cb_cost_metrics.setChecked(self.config.show_cost_metrics) + form.addRow(self.cb_cost_metrics) + + self.cb_gear = QCheckBox("Current Gear") + self.cb_gear.setChecked(self.config.show_gear_info) + form.addRow(self.cb_gear) + + # Optional stats + form.addRow(QLabel("Optional Stats")) + + self.cb_cost_breakdown = QCheckBox("Cost Breakdown (Weapon/Armor/Heal)") + self.cb_cost_breakdown.setChecked(self.config.show_cost_breakdown) + form.addRow(self.cb_cost_breakdown) + + self.cb_combat = QCheckBox("Combat Stats (Kills, Globals)") + self.cb_combat.setChecked(self.config.show_combat_stats) + form.addRow(self.cb_combat) + + self.cb_damage = QCheckBox("Damage Stats (Dealt/Taken)") + self.cb_damage.setChecked(self.config.show_damage_stats) + form.addRow(self.cb_damage) + + self.cb_shrapnel = QCheckBox("Shrapnel Amount") + self.cb_shrapnel.setChecked(self.config.show_shrapnel) + form.addRow(self.cb_shrapnel) + + # Display mode + form.addRow(QLabel("Display Mode")) + + self.cb_compact = QCheckBox("Compact Mode (smaller font)") + self.cb_compact.setChecked(self.config.compact_mode) + form.addRow(self.cb_compact) + + scroll.setWidget(content) + layout.addWidget(scroll) + + # Buttons + btn_layout = QHBoxLayout() + btn_layout.addStretch() + + save_btn = QPushButton("Save") + save_btn.clicked.connect(self._on_save) + btn_layout.addWidget(save_btn) + + cancel_btn = QPushButton("Cancel") + cancel_btn.clicked.connect(self.reject) + btn_layout.addWidget(cancel_btn) + + layout.addLayout(btn_layout) + + def _on_save(self): + self.config.show_session_time = self.cb_time.isChecked() + self.config.show_profit_loss = self.cb_profit.isChecked() + self.config.show_return_pct = self.cb_return.isChecked() + self.config.show_cost_metrics = self.cb_cost_metrics.isChecked() + self.config.show_gear_info = self.cb_gear.isChecked() + self.config.show_cost_breakdown = self.cb_cost_breakdown.isChecked() + self.config.show_combat_stats = self.cb_combat.isChecked() + self.config.show_damage_stats = self.cb_damage.isChecked() + self.config.show_shrapnel = self.cb_shrapnel.isChecked() + self.config.compact_mode = self.cb_compact.isChecked() + self.accept() + + def get_config(self) -> HUDConfig: + return self.config + + +class HUDOverlay(QWidget): + """ + Clean, customizable HUD Overlay for Lemontropia Suite. + + Features: + - Collapsible sections + - Customizable display elements + - Compact mode + - Draggable with Ctrl+click + """ + + position_changed = pyqtSignal(QPoint) + stats_updated = pyqtSignal(dict) + + def __init__(self, parent=None, config_path: Optional[str] = None): + super().__init__(parent) + + self.config_path = Path(config_path) if config_path else Path.home() / ".lemontropia" / "hud_config.json" + self.config_path.parent.mkdir(parents=True, exist_ok=True) + + # Load HUD config + self.hud_config = self._load_hud_config() + + # Session state + self._session_start: Optional[datetime] = None + self._stats = HUDStats() + self.session_active = False + + # Drag state + self._dragging = False + self._drag_offset = QPoint() + + # Setup + self._setup_window() + self._setup_ui() + self._load_position() + + # Timer for session time + self._timer = QTimer(self) + self._timer.timeout.connect(self._update_session_time) + + def _load_hud_config(self) -> HUDConfig: + """Load HUD configuration.""" + try: + if self.config_path.exists(): + with open(self.config_path, 'r') as f: + data = json.load(f) + return HUDConfig.from_dict(data) + except Exception as e: + pass + return HUDConfig() # Default config + + def _save_hud_config(self): + """Save HUD configuration.""" + try: + with open(self.config_path, 'w') as f: + json.dump(self.hud_config.to_dict(), f, indent=2) + except Exception as e: + pass + + def _setup_window(self): + """Configure window properties.""" + self.setWindowFlags( + Qt.WindowType.FramelessWindowHint | + Qt.WindowType.WindowStaysOnTopHint | + Qt.WindowType.Tool + ) + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + self.setMouseTracking(True) + self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + + def _setup_ui(self): + """Build the HUD UI.""" + # Calculate size based on what's shown + self._update_window_size() + + # Main container + self.container = QFrame(self) + self.container.setObjectName("hudContainer") + self.container.setStyleSheet(self._get_stylesheet()) + + layout = QVBoxLayout(self.container) + layout.setContentsMargins(12, 12, 12, 8) + layout.setSpacing(4) + + # === HEADER: Status + Gear === + header = QHBoxLayout() + + self.status_label = QLabel("● Ready") + self.status_label.setStyleSheet("color: #7FFF7F; font-weight: bold;") + header.addWidget(self.status_label) + + header.addStretch() + + # Settings button + self.settings_btn = QPushButton("⚙️") + self.settings_btn.setFixedSize(24, 24) + self.settings_btn.setStyleSheet(""" + QPushButton { + background: transparent; + border: none; + font-size: 14px; + } + QPushButton:hover { + background: rgba(255, 255, 255, 30); + border-radius: 4px; + } + """) + self.settings_btn.clicked.connect(self._show_settings) + header.addWidget(self.settings_btn) + + layout.addLayout(header) + + # === GEAR INFO (if enabled) === + if self.hud_config.show_gear_info: + self.gear_frame = QFrame() + gear_layout = QVBoxLayout(self.gear_frame) + gear_layout.setContentsMargins(0, 0, 0, 0) + gear_layout.setSpacing(2) + + self.weapon_label = QLabel("🔫 None") + self.armor_label = QLabel("🛡️ None") + self.loadout_label = QLabel("📋 No Loadout") + + gear_layout.addWidget(self.weapon_label) + gear_layout.addWidget(self.armor_label) + gear_layout.addWidget(self.loadout_label) + + layout.addWidget(self.gear_frame) + + # Separator + sep = QFrame() + sep.setFrameShape(QFrame.Shape.HLine) + sep.setStyleSheet("background-color: rgba(255, 215, 0, 50);") + sep.setFixedHeight(1) + layout.addWidget(sep) + + # === CORE: P/L + Return % === + if self.hud_config.show_profit_loss or self.hud_config.show_return_pct: + core_frame = QFrame() + core_layout = QHBoxLayout(core_frame) + core_layout.setContentsMargins(0, 4, 0, 4) + + if self.hud_config.show_profit_loss: + self.profit_label = QLabel("P/L: 0.00 PED") + self.profit_label.setStyleSheet("font-size: 18px; font-weight: bold;") + core_layout.addWidget(self.profit_label) + + core_layout.addStretch() + + if self.hud_config.show_return_pct: + self.return_label = QLabel("0.0%") + self.return_label.setStyleSheet("font-size: 16px; font-weight: bold;") + core_layout.addWidget(self.return_label) + + layout.addWidget(core_frame) + + # === COST METRICS (if enabled) === + if self.hud_config.show_cost_metrics: + metrics_frame = QFrame() + metrics_frame.setStyleSheet("background-color: rgba(0, 0, 0, 100); border-radius: 4px;") + metrics_layout = QHBoxLayout(metrics_frame) + metrics_layout.setContentsMargins(8, 4, 8, 4) + + self.cps_label = QLabel("Shot: 0.0000") + self.cph_label = QLabel("Hit: 0.0000") + self.cphl_label = QLabel("Heal: 0.0000") + + for lbl in [self.cps_label, self.cph_label, self.cphl_label]: + lbl.setStyleSheet("font-size: 10px; color: #AAAAAA;") + metrics_layout.addWidget(lbl) + + metrics_layout.addStretch() + layout.addWidget(metrics_frame) + + # === OPTIONAL: Cost Breakdown === + if self.hud_config.show_cost_breakdown: + breakdown_frame = QFrame() + breakdown_layout = QFormLayout(breakdown_frame) + breakdown_layout.setContentsMargins(0, 0, 0, 0) + breakdown_layout.setSpacing(2) + + self.wep_cost_label = QLabel("0.00") + self.arm_cost_label = QLabel("0.00") + self.heal_cost_label = QLabel("0.00") + + breakdown_layout.addRow("Weapon:", self.wep_cost_label) + breakdown_layout.addRow("Armor:", self.arm_cost_label) + breakdown_layout.addRow("Healing:", self.heal_cost_label) + + layout.addWidget(breakdown_frame) + + # === OPTIONAL: Combat Stats === + if self.hud_config.show_combat_stats: + combat_frame = QFrame() + combat_layout = QHBoxLayout(combat_frame) + combat_layout.setContentsMargins(0, 0, 0, 0) + + self.kills_label = QLabel("Kills: 0") + self.globals_label = QLabel("Globals: 0") + + combat_layout.addWidget(self.kills_label) + combat_layout.addStretch() + combat_layout.addWidget(self.globals_label) + + layout.addWidget(combat_frame) + + # === FOOTER: Session Time + Drag Hint === + footer = QHBoxLayout() + + if self.hud_config.show_session_time: + self.time_label = QLabel("00:00:00") + self.time_label.setStyleSheet("color: #888; font-size: 11px;") + footer.addWidget(self.time_label) + else: + footer.addStretch() + + footer.addStretch() + + self.drag_hint = QLabel("Ctrl+drag to move") + self.drag_hint.setStyleSheet("font-size: 9px; color: #666;") + footer.addWidget(self.drag_hint) + + layout.addLayout(footer) + + def _get_stylesheet(self) -> str: + """Get stylesheet based on compact mode.""" + font_size = "11px" if self.hud_config.compact_mode else "12px" + + return f""" + #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; + font-size: {font_size}; + }} + """ + + def _update_window_size(self): + """Calculate window size based on enabled features.""" + height = 80 # Base height + + if self.hud_config.show_gear_info: + height += 70 + if self.hud_config.show_profit_loss or self.hud_config.show_return_pct: + height += 40 + if self.hud_config.show_cost_metrics: + height += 30 + if self.hud_config.show_cost_breakdown: + height += 70 + if self.hud_config.show_combat_stats: + height += 25 + if self.hud_config.show_session_time: + height += 20 + + width = 280 if self.hud_config.compact_mode else 320 + + self.setFixedSize(width, height) + self.container.setFixedSize(width, height) + + def _show_settings(self): + """Show settings dialog.""" + dialog = HUDSettingsDialog(self.hud_config, self) + if dialog.exec() == QDialog.DialogCode.Accepted: + self.hud_config = dialog.get_config() + self._save_hud_config() + # Rebuild UI with new settings + self._rebuild_ui() + + def _rebuild_ui(self): + """Rebuild UI with current config.""" + # Clear container + while self.container.layout().count(): + item = self.container.layout().takeAt(0) + if item.widget(): + item.widget().deleteLater() + + self._update_window_size() + self._setup_ui() + self._refresh_display() + + # === Session Management === + + def start_session(self, weapon: str = "Unknown", armor: str = "None", + fap: str = "None", loadout: str = "Default", + cost_per_shot: Decimal = Decimal('0.0'), + cost_per_hit: Decimal = Decimal('0.0'), + cost_per_heal: Decimal = Decimal('0.0')) -> None: + """Start a new hunting session.""" + self._session_start = datetime.now() + self._stats = HUDStats() + self._stats.current_weapon = weapon + self._stats.current_armor = armor + self._stats.current_fap = fap + self._stats.current_loadout = loadout + self._stats.cost_per_shot = cost_per_shot + self._stats.cost_per_hit = cost_per_hit + self._stats.cost_per_heal = cost_per_heal + self.session_active = True + + self._timer.start(1000) + self._refresh_display() + self.status_label.setText("● Live") + self.status_label.setStyleSheet("color: #7FFF7F; font-weight: bold;") + + def stop_session(self) -> None: + """Stop the current session.""" + self.session_active = False + self._timer.stop() + self.status_label.setText("● Stopped") + self.status_label.setStyleSheet("color: #FF7F7F; font-weight: bold;") + + # === Event Handlers === + + def _update_session_time(self): + """Update session time display.""" + if self._session_start and self.session_active: + elapsed = datetime.now() - self._session_start + hours, remainder = divmod(elapsed.seconds, 3600) + minutes, seconds = divmod(remainder, 60) + + if hasattr(self, 'time_label'): + self.time_label.setText(f"{hours:02d}:{minutes:02d}:{seconds:02d}") + + def _refresh_display(self): + """Refresh all display labels.""" + # Profit/Loss + if hasattr(self, 'profit_label'): + profit = self._stats.profit_loss + color = "#7FFF7F" if profit >= 0 else "#FF7F7F" + self.profit_label.setText(f"{profit:+.2f} PED") + self.profit_label.setStyleSheet(f"font-size: 18px; font-weight: bold; color: {color};") + + # Return % + if hasattr(self, 'return_label'): + ret = self._stats.return_percentage + if ret >= 100: + color = "#7FFF7F" + elif ret >= 90: + color = "#FFFF7F" + else: + color = "#FF7F7F" + self.return_label.setText(f"{ret:.1f}%") + self.return_label.setStyleSheet(f"font-size: 16px; font-weight: bold; color: {color};") + + # Cost metrics + if hasattr(self, 'cps_label'): + self.cps_label.setText(f"Shot: {self._stats.cost_per_shot:.4f}") + if hasattr(self, 'cph_label'): + self.cph_label.setText(f"Hit: {self._stats.cost_per_hit:.4f}") + if hasattr(self, 'cphl_label'): + self.cphl_label.setText(f"Heal: {self._stats.cost_per_heal:.4f}") + + # Gear + if hasattr(self, 'weapon_label'): + self.weapon_label.setText(f"🔫 {self._stats.current_weapon[:20]}") + if hasattr(self, 'armor_label'): + self.armor_label.setText(f"🛡️ {self._stats.current_armor[:20]}") + if hasattr(self, 'loadout_label'): + self.loadout_label.setText(f"📋 {self._stats.current_loadout[:20]}") + + # Cost breakdown + if hasattr(self, 'wep_cost_label'): + self.wep_cost_label.setText(f"{self._stats.weapon_cost_total:.2f}") + if hasattr(self, 'arm_cost_label'): + self.arm_cost_label.setText(f"{self._stats.armor_cost_total:.2f}") + if hasattr(self, 'heal_cost_label'): + self.heal_cost_label.setText(f"{self._stats.healing_cost_total:.2f}") + + # Combat + if hasattr(self, 'kills_label'): + self.kills_label.setText(f"Kills: {self._stats.kills}") + if hasattr(self, 'globals_label'): + self.globals_label.setText(f"Globals: {self._stats.globals_count}") + + # === Public Update Methods === + + def update_loot(self, value_ped: Decimal): + """Update loot value.""" + if self.session_active: + self._stats.loot_total += value_ped + self._stats.recalculate() + self._refresh_display() + + def update_weapon_cost(self, cost_ped: Decimal): + """Update weapon cost.""" + if self.session_active: + self._stats.weapon_cost_total += cost_ped + self._stats.cost_total += cost_ped + self._stats.recalculate() + self._refresh_display() + + def update_armor_cost(self, cost_ped: Decimal): + """Update armor cost.""" + if self.session_active: + self._stats.armor_cost_total += cost_ped + self._stats.cost_total += cost_ped + self._stats.recalculate() + self._refresh_display() + + def update_healing_cost(self, cost_ped: Decimal): + """Update healing cost.""" + if self.session_active: + self._stats.healing_cost_total += cost_ped + self._stats.cost_total += cost_ped + self._stats.recalculate() + self._refresh_display() + + def update_kills(self, count: int = 1): + """Update kill count.""" + if self.session_active: + self._stats.kills += count + self._refresh_display() + + def update_globals(self): + """Update global count.""" + if self.session_active: + self._stats.globals_count += 1 + self._refresh_display() + + # === Mouse Handling === + + def mousePressEvent(self, event: QMouseEvent): + if event.button() == Qt.MouseButton.LeftButton: + if event.modifiers() == Qt.KeyboardModifier.ControlModifier: + self._dragging = True + self._drag_offset = event.pos() + self.setCursor(Qt.CursorShape.ClosedHandCursor) + event.accept() + + def mouseMoveEvent(self, event: QMouseEvent): + if self._dragging: + new_pos = self.mapToGlobal(event.pos()) - self._drag_offset + self.move(new_pos) + self.position_changed.emit(new_pos) + event.accept() + + def mouseReleaseEvent(self, event: QMouseEvent): + if event.button() == Qt.MouseButton.LeftButton and self._dragging: + self._dragging = False + self.setCursor(Qt.CursorShape.ArrowCursor) + self._save_position() + event.accept() + + def enterEvent(self, event): + """Mouse entered - enable interaction.""" + super().enterEvent(event) + + def leaveEvent(self, event): + """Mouse left - disable interaction.""" + super().leaveEvent(event) + + def _save_position(self): + """Save window position.""" + try: + pos_file = self.config_path.parent / "hud_position.json" + with open(pos_file, 'w') as f: + json.dump({'x': self.x(), 'y': self.y()}, f) + except: + pass + + def _load_position(self): + """Load window position.""" + try: + pos_file = self.config_path.parent / "hud_position.json" + if pos_file.exists(): + with open(pos_file, 'r') as f: + pos = json.load(f) + self.move(pos.get('x', 100), pos.get('y', 100)) + else: + screen = QApplication.primaryScreen().geometry() + self.move(screen.width() - 360, 50) + except: + self.move(100, 100)