From 6cc10b14f1b5c1cc3d54067807bd6d5cb477b7a7 Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Mon, 9 Feb 2026 22:27:33 +0000 Subject: [PATCH] fix: HUD drag functionality - auto-manage click-through mode - Don't enable click-through on show (allows initial interaction) - Disable click-through when mouse enters HUD (allows clicking/dragging) - Enable click-through when mouse leaves (so HUD doesn't block game) - Remove duplicate leaveEvent handler - User can now hold Ctrl and drag without issues --- ui/hud_overlay.py | 526 +++++++++++++++++++++++----------------------- 1 file changed, 267 insertions(+), 259 deletions(-) diff --git a/ui/hud_overlay.py b/ui/hud_overlay.py index cdad8fb..c0386be 100644 --- a/ui/hud_overlay.py +++ b/ui/hud_overlay.py @@ -15,24 +15,24 @@ from typing import Optional, Dict, Any, List if sys.platform == 'win32': import ctypes from ctypes import wintypes - + # Windows API constants for window detection GW_OWNER = 4 - + # GetForegroundWindow function user32 = ctypes.windll.user32 GetForegroundWindow = user32.GetForegroundWindow GetForegroundWindow.restype = wintypes.HWND - + # GetWindowText functions GetWindowTextLengthW = user32.GetWindowTextLengthW GetWindowTextLengthW.argtypes = [wintypes.HWND] GetWindowTextLengthW.restype = ctypes.c_int - + GetWindowTextW = user32.GetWindowTextW GetWindowTextW.argtypes = [wintypes.HWND, wintypes.LPWSTR, ctypes.c_int] GetWindowTextW.restype = ctypes.c_int - + # FindWindow function FindWindowW = user32.FindWindowW FindWindowW.argtypes = [wintypes.LPCWSTR, wintypes.LPCWSTR] @@ -53,13 +53,13 @@ from PyQt6.QtGui import QFont, QColor, QPalette, QMouseEvent class HUDStats: """Data structure for HUD statistics - Enhanced for hunting sessions.""" session_time: timedelta = field(default_factory=lambda: timedelta(0)) - + # Financial tracking loot_total: Decimal = Decimal('0.0') # All loot including shrapnel loot_other: Decimal = Decimal('0.0') # Excluding shrapnel/UA shrapnel_total: Decimal = Decimal('0.0') universal_ammo_total: Decimal = Decimal('0.0') - + # Cost tracking weapon_cost_total: Decimal = Decimal('0.0') armor_cost_total: Decimal = Decimal('0.0') # NEW: Official armor decay @@ -68,13 +68,13 @@ class HUDStats: enhancer_cost_total: Decimal = Decimal('0.0') mindforce_cost_total: Decimal = Decimal('0.0') cost_total: Decimal = Decimal('0.0') - + # Profit/Loss (calculated from other_loot - total_cost) profit_loss: Decimal = Decimal('0.0') - + # Return percentage return_percentage: Decimal = Decimal('0.0') - + # Combat stats damage_dealt: Decimal = Decimal('0.0') damage_taken: Decimal = Decimal('0.0') @@ -85,28 +85,28 @@ class HUDStats: kills: int = 0 globals_count: int = 0 hofs_count: int = 0 - + # Loadout-based cost metrics cost_per_shot: Decimal = Decimal('0.0') cost_per_hit: Decimal = Decimal('0.0') cost_per_heal: Decimal = Decimal('0.0') - + # Efficiency metrics cost_per_kill: Decimal = Decimal('0.0') dpp: Decimal = Decimal('0.0') # Damage Per PED - + # Current gear current_weapon: str = "None" current_loadout: str = "None" loadout_id: Optional[int] = None current_armor: str = "None" # NEW current_fap: str = "None" - + # Gear stats weapon_dpp: Decimal = Decimal('0.0') weapon_cost_per_hour: Decimal = Decimal('0.0') armor_durability: int = 2000 # Default Ghost - + def to_dict(self) -> Dict[str, Any]: """Convert to dictionary for serialization.""" return { @@ -140,7 +140,7 @@ class HUDStats: 'weapon_cost_per_hour': str(self.weapon_cost_per_hour), 'armor_durability': self.armor_durability, } - + @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'HUDStats': """Create from dictionary.""" @@ -175,40 +175,40 @@ class HUDStats: weapon_cost_per_hour=Decimal(data.get('weapon_cost_per_hour', '0.0')), armor_durability=data.get('armor_durability', 2000), ) - + def recalculate(self): """Recalculate derived statistics.""" # Total cost including mindforce self.cost_total = ( - self.weapon_cost_total + - self.armor_cost_total + + self.weapon_cost_total + + self.armor_cost_total + self.healing_cost_total + self.plates_cost_total + self.enhancer_cost_total + self.mindforce_cost_total ) - + # Profit/loss (excluding shrapnel from loot value for accurate profit calc) self.profit_loss = self.loot_other - self.cost_total - + # Return percentage if self.cost_total > 0: self.return_percentage = (self.loot_other / self.cost_total) * Decimal('100') else: self.return_percentage = Decimal('0.0') - + # Cost per kill if self.kills > 0: self.cost_per_kill = self.cost_total / self.kills else: self.cost_per_kill = Decimal('0.0') - + # DPP (Damage Per PED) if self.cost_total > 0: self.dpp = self.damage_dealt / self.cost_total else: self.dpp = Decimal('0.0') - + def update_from_cost_tracker(self, summary: Dict[str, Any]): """Update stats from SessionCostTracker summary.""" self.weapon_cost_total = summary.get('weapon_cost', Decimal('0')) @@ -228,7 +228,7 @@ class HUDStats: 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 @@ -238,75 +238,75 @@ class HUDOverlay(QWidget): - Position persistence across sessions - Real-time stat updates via signals/slots - Enhanced hunting session tracking with armor decay - + 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, + + 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() self.session_active = False - + # Armor decay tracker (imported here to avoid circular imports) self._armor_tracker = None - + # Session cost tracker for loadout-based tracking self._cost_tracker = None - + # Drag state self._dragging = False self._drag_offset = QPoint() self._modifier_pressed = False - + # Game window detection self._auto_hide_with_game = False self._game_window_title = "Entropia Universe" self._was_visible_before_unfocus = False self._debug_window_detection = False - + # Timer for session time updates self._timer = QTimer(self) self._timer.timeout.connect(self._update_session_time) - + # Timer for game window detection (Windows only) self._window_check_timer = QTimer(self) self._window_check_timer.timeout.connect(self._check_game_window) if sys.platform == 'win32': self._window_check_timer.start(500) - + 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 @@ -315,26 +315,26 @@ class HUDOverlay(QWidget): Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.Tool ) - + # Enable transparency self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) - + # Enable mouse tracking for hover detection self.setMouseTracking(True) - + # Size - increased to accommodate all rows self.setFixedSize(340, 320) - + # Accept focus for keyboard events 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(340, 320) self.container.setObjectName("hudContainer") - + # Style the container - semi-transparent dark background self.container.setStyleSheet(""" #hudContainer { @@ -374,362 +374,362 @@ class HUDOverlay(QWidget): color: #FFFFFF; } """) - + # Main layout layout = QVBoxLayout(self.container) layout.setContentsMargins(12, 8, 12, 8) layout.setSpacing(4) - + # === HEADER === header_layout = QHBoxLayout() - + self.title_label = QLabel("🍋 LEMONTROPIA") self.title_label.setProperty("class", "header") self.title_label.setStyleSheet("font-size: 13px; 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: 12px; font-weight: bold; color: #00FFFF;") header_layout.addWidget(self.time_label) - + layout.addLayout(header_layout) - + # Drag hint self.drag_hint = QLabel("Hold Ctrl to drag") self.drag_hint.setStyleSheet("font-size: 8px; 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) - + # === FINANCIALS ROW === row0 = QHBoxLayout() - + # Loot (marketable only, excluding shrapnel) loot_layout = QVBoxLayout() loot_label = QLabel("💰 LOOT") loot_label.setStyleSheet("font-size: 9px; color: #888888;") loot_layout.addWidget(loot_label) - + self.loot_value_label = QLabel("0.00 PED") self.loot_value_label.setStyleSheet("font-size: 13px; font-weight: bold; color: #7FFF7F;") loot_layout.addWidget(self.loot_value_label) - + row0.addLayout(loot_layout) row0.addStretch() - + # Total Cost cost_layout = QVBoxLayout() cost_label = QLabel("💸 COST") cost_label.setStyleSheet("font-size: 9px; color: #888888;") cost_layout.addWidget(cost_label) - + self.cost_value_label = QLabel("0.00 PED") self.cost_value_label.setStyleSheet("font-size: 13px; font-weight: bold; color: #FF7F7F;") cost_layout.addWidget(self.cost_value_label) - + row0.addLayout(cost_layout) row0.addStretch() - + # Profit/Loss with return % profit_layout = QVBoxLayout() profit_label = QLabel("📊 P/L") profit_label.setStyleSheet("font-size: 9px; color: #888888;") profit_layout.addWidget(profit_label) - + self.profit_value_label = QLabel("0.00 PED") self.profit_value_label.setStyleSheet("font-size: 13px; font-weight: bold; color: #FFFFFF;") profit_layout.addWidget(self.profit_value_label) - + row0.addLayout(profit_layout) - + layout.addLayout(row0) - + # === RETURN % BAR === return_layout = QHBoxLayout() return_label = QLabel("📈 Return:") return_label.setStyleSheet("font-size: 9px; color: #888888;") return_layout.addWidget(return_label) - + self.return_label = QLabel("0.0%") self.return_label.setStyleSheet("font-size: 11px; font-weight: bold; color: #FFFFFF;") return_layout.addWidget(self.return_label) - + return_layout.addStretch() - + # Shrapnel indicator self.shrapnel_label = QLabel("💎 Shrapnel: 0.00") self.shrapnel_label.setStyleSheet("font-size: 9px; color: #AAAAAA;") return_layout.addWidget(self.shrapnel_label) - + layout.addLayout(return_layout) - + # === COMBAT STATS ROW 1 === row1 = QHBoxLayout() - + # Kills kills_layout = QVBoxLayout() kills_label = QLabel("💀 KILLS") kills_label.setStyleSheet("font-size: 9px; color: #888888;") kills_layout.addWidget(kills_label) - + self.kills_value_label = QLabel("0") self.kills_value_label.setStyleSheet("font-size: 13px; 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("🌍 G/H") globals_label.setStyleSheet("font-size: 9px; color: #888888;") globals_layout.addWidget(globals_label) - + self.globals_value_label = QLabel("0 / 0") self.globals_value_label.setStyleSheet("font-size: 13px; font-weight: bold; color: #FFD700;") globals_layout.addWidget(self.globals_value_label) - + row1.addLayout(globals_layout) row1.addStretch() - + # DPP dpp_layout = QVBoxLayout() dpp_label = QLabel("⚡ DPP") dpp_label.setStyleSheet("font-size: 9px; color: #888888;") dpp_layout.addWidget(dpp_label) - + self.dpp_value_label = QLabel("0.0") self.dpp_value_label.setStyleSheet("font-size: 13px; font-weight: bold; color: #00FFFF;") dpp_layout.addWidget(self.dpp_value_label) - + row1.addLayout(dpp_layout) - + layout.addLayout(row1) - + # === COMBAT STATS ROW 2 === row2 = QHBoxLayout() - + # Damage Dealt dealt_layout = QVBoxLayout() dealt_label = QLabel("⚔️ DEALT") dealt_label.setStyleSheet("font-size: 9px; color: #888888;") dealt_layout.addWidget(dealt_label) - + self.dealt_value_label = QLabel("0") self.dealt_value_label.setStyleSheet("font-size: 12px; 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: 9px; color: #888888;") taken_layout.addWidget(taken_label) - + self.taken_value_label = QLabel("0") self.taken_value_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #FF7F7F;") taken_layout.addWidget(self.taken_value_label) - + row2.addLayout(taken_layout) row2.addStretch() - + # Shots shots_layout = QVBoxLayout() shots_label = QLabel("🔫 SHOTS") shots_label.setStyleSheet("font-size: 9px; color: #888888;") shots_layout.addWidget(shots_label) - + self.shots_value_label = QLabel("0") self.shots_value_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #FFD700;") shots_layout.addWidget(self.shots_value_label) - + row2.addLayout(shots_layout) - + layout.addLayout(row2) - + # === COST BREAKDOWN ROW === row3 = QHBoxLayout() - + # Weapon cost wep_cost_layout = QVBoxLayout() wep_cost_label = QLabel("🔫 WEP") wep_cost_label.setStyleSheet("font-size: 8px; color: #888888;") wep_cost_layout.addWidget(wep_cost_label) - + self.wep_cost_value_label = QLabel("0.00") self.wep_cost_value_label.setStyleSheet("font-size: 10px; color: #FFAAAA;") wep_cost_layout.addWidget(self.wep_cost_value_label) - + row3.addLayout(wep_cost_layout) - + # Armor cost arm_cost_layout = QVBoxLayout() arm_cost_label = QLabel("🛡️ ARM") arm_cost_label.setStyleSheet("font-size: 8px; color: #888888;") arm_cost_layout.addWidget(arm_cost_label) - + self.arm_cost_value_label = QLabel("0.00") self.arm_cost_value_label.setStyleSheet("font-size: 10px; color: #FFAAAA;") arm_cost_layout.addWidget(self.arm_cost_value_label) - + row3.addLayout(arm_cost_layout) - + # Healing cost heal_cost_layout = QVBoxLayout() heal_cost_label = QLabel("💊 FAP") heal_cost_label.setStyleSheet("font-size: 8px; color: #888888;") heal_cost_layout.addWidget(heal_cost_label) - + self.heal_cost_value_label = QLabel("0.00") self.heal_cost_value_label.setStyleSheet("font-size: 10px; color: #FFAAAA;") heal_cost_layout.addWidget(self.heal_cost_value_label) - + row3.addLayout(heal_cost_layout) - + # Cost per kill cpk_layout = QVBoxLayout() cpk_label = QLabel("📊 CPK") cpk_label.setStyleSheet("font-size: 8px; color: #888888;") cpk_layout.addWidget(cpk_label) - + self.cpk_value_label = QLabel("0.00") self.cpk_value_label.setStyleSheet("font-size: 10px; color: #FFD700;") cpk_layout.addWidget(self.cpk_value_label) - + row3.addLayout(cpk_layout) - + layout.addLayout(row3) - + # === LOADOUT COST METRICS ROW === row4 = QHBoxLayout() - + # Cost per shot cps_layout = QVBoxLayout() cps_label = QLabel("$/SHOT") cps_label.setStyleSheet("font-size: 8px; color: #888888;") cps_layout.addWidget(cps_label) - + self.cps_value_label = QLabel("0.00") self.cps_value_label.setStyleSheet("font-size: 10px; color: #FFAAAA;") cps_layout.addWidget(self.cps_value_label) - + row4.addLayout(cps_layout) - + # Cost per hit cph_layout = QVBoxLayout() cph_label = QLabel("$/HIT") cph_label.setStyleSheet("font-size: 8px; color: #888888;") cph_layout.addWidget(cph_label) - + self.cph_value_label = QLabel("0.00") self.cph_value_label.setStyleSheet("font-size: 10px; color: #FFAAAA;") cph_layout.addWidget(self.cph_value_label) - + row4.addLayout(cph_layout) - + # Cost per heal cphl_layout = QVBoxLayout() cphl_label = QLabel("$/HEAL") cphl_label.setStyleSheet("font-size: 8px; color: #888888;") cphl_layout.addWidget(cphl_label) - + self.cphl_value_label = QLabel("0.00") self.cphl_value_label.setStyleSheet("font-size: 10px; color: #FFAAAA;") cphl_layout.addWidget(self.cphl_value_label) - + row4.addLayout(cphl_layout) - + # Hits taken hits_layout = QVBoxLayout() hits_label = QLabel("HITS") hits_label.setStyleSheet("font-size: 8px; color: #888888;") hits_layout.addWidget(hits_label) - + self.hits_value_label = QLabel("0") self.hits_value_label.setStyleSheet("font-size: 10px; color: #FFD700;") hits_layout.addWidget(self.hits_value_label) - + row4.addLayout(hits_layout) - + # Heals used heals_layout = QVBoxLayout() heals_label = QLabel("HEALS") heals_label.setStyleSheet("font-size: 8px; color: #888888;") heals_layout.addWidget(heals_label) - + self.heals_value_label = QLabel("0") self.heals_value_label.setStyleSheet("font-size: 10px; color: #FFD700;") heals_layout.addWidget(self.heals_value_label) - + row4.addLayout(heals_layout) - + layout.addLayout(row4) - + # === EQUIPMENT INFO === equip_separator = QFrame() equip_separator.setFrameShape(QFrame.Shape.HLine) equip_separator.setStyleSheet("background-color: rgba(255, 215, 0, 30);") equip_separator.setFixedHeight(1) layout.addWidget(equip_separator) - + equip_layout = QHBoxLayout() - + # Weapon self.weapon_label = QLabel("🔫 None") self.weapon_label.setStyleSheet("font-size: 10px; color: #CCCCCC;") equip_layout.addWidget(self.weapon_label) - + equip_layout.addStretch() - + # Armor self.armor_label = QLabel("🛡️ None") self.armor_label.setStyleSheet("font-size: 10px; color: #CCCCCC;") equip_layout.addWidget(self.armor_label) - + equip_layout.addStretch() - + # Loadout loadout_label = QLabel("Loadout:") loadout_label.setStyleSheet("font-size: 9px; color: #888888;") equip_layout.addWidget(loadout_label) - + self.loadout_label = QLabel("Default") self.loadout_label.setStyleSheet("font-size: 10px; color: #00FFFF;") equip_layout.addWidget(self.loadout_label) - + layout.addLayout(equip_layout) - + # === STATUS BAR === self.status_label = QLabel("● Ready") 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']) @@ -741,7 +741,7 @@ class HUDOverlay(QWidget): except Exception as e: # Default position on error self.move(100, 100) - + def _save_position(self) -> None: """Save current position to config file.""" try: @@ -754,11 +754,11 @@ class HUDOverlay(QWidget): json.dump(config, f, indent=2) except Exception as e: pass - + # ======================================================================== # 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: @@ -771,7 +771,7 @@ class HUDOverlay(QWidget): else: self._enable_click_through(True) event.ignore() - + def mouseMoveEvent(self, event: QMouseEvent) -> None: """Handle mouse move - drag window if in drag mode.""" if self._dragging: @@ -782,7 +782,7 @@ class HUDOverlay(QWidget): else: self.drag_hint.setStyleSheet("font-size: 8px; 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: @@ -793,40 +793,35 @@ class HUDOverlay(QWidget): event.accept() else: event.ignore() - - def leaveEvent(self, event) -> None: - """Handle mouse leave - reset drag hint.""" - self.drag_hint.setStyleSheet("font-size: 8px; color: #666666;") - super().leaveEvent(event) - + def _enable_click_through(self, enable: bool) -> None: """Enable or disable click-through behavior.""" if sys.platform == 'win32': self._set_click_through_win32(enable) else: self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, enable) - + def _set_click_through_win32(self, enabled: bool) -> None: """Enable/disable click-through on Windows using WinAPI.""" GWL_EXSTYLE = -20 WS_EX_TRANSPARENT = 0x00000020 WS_EX_LAYERED = 0x00080000 - + SWP_FRAMECHANGED = 0x0020 SWP_NOMOVE = 0x0002 SWP_NOSIZE = 0x0001 SWP_NOZORDER = 0x0004 SWP_SHOWWINDOW = 0x0040 - + try: hwnd = self.winId().__int__() style = ctypes.windll.user32.GetWindowLongW(hwnd, GWL_EXSTYLE) - + if enabled: style |= WS_EX_TRANSPARENT | WS_EX_LAYERED else: style &= ~(WS_EX_TRANSPARENT | WS_EX_LAYERED) - + ctypes.windll.user32.SetWindowLongW(hwnd, GWL_EXSTYLE, style) ctypes.windll.user32.SetWindowPos( hwnd, 0, 0, 0, 0, 0, @@ -834,7 +829,7 @@ class HUDOverlay(QWidget): ) except Exception: self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, enabled) - + def keyPressEvent(self, event) -> None: """Handle key press - detect Ctrl for drag mode.""" if event.key() == Qt.Key.Key_Control: @@ -843,7 +838,7 @@ class HUDOverlay(QWidget): self.drag_hint.setText("✋ Drag mode ON") self.drag_hint.setStyleSheet("font-size: 8px; color: #00FF00;") super().keyPressEvent(event) - + def keyReleaseEvent(self, event) -> None: """Handle key release - detect Ctrl release.""" if event.key() == Qt.Key.Key_Control: @@ -852,11 +847,11 @@ class HUDOverlay(QWidget): self.drag_hint.setText("Hold Ctrl to drag") self.drag_hint.setStyleSheet("font-size: 8px; color: #666666;") super().keyReleaseEvent(event) - + # ======================================================================== # SESSION MANAGEMENT # ======================================================================== - + def start_session(self, weapon: str = "Unknown", loadout: str = "Default", weapon_dpp: Decimal = Decimal('0.0'), weapon_cost_per_hour: Decimal = Decimal('0.0'), @@ -869,7 +864,7 @@ class HUDOverlay(QWidget): cost_per_heal: Decimal = Decimal('0.0')) -> None: """ Start a new hunting session with loadout-based cost tracking. - + Args: weapon: Name of the current weapon loadout: Name of the current loadout @@ -897,7 +892,7 @@ class HUDOverlay(QWidget): self._stats.cost_per_hit = cost_per_hit self._stats.cost_per_heal = cost_per_heal self.session_active = True - + # Initialize armor decay tracker try: from core.armor_decay import ArmorDecayTracker @@ -905,27 +900,27 @@ class HUDOverlay(QWidget): self._armor_tracker.start_session() except ImportError: self._armor_tracker = None - + self._timer.start(1000) self._refresh_display() self.status_label.setText("● Live - Recording") self.status_label.setStyleSheet("font-size: 9px; color: #7FFF7F;") - + def set_cost_tracker(self, cost_tracker: 'SessionCostTracker') -> None: """ Connect to a SessionCostTracker for real-time cost updates. - + Args: cost_tracker: SessionCostTracker instance """ self._cost_tracker = cost_tracker cost_tracker.register_callback(self._on_cost_update) - + def _on_cost_update(self, state: 'SessionCostState') -> None: """Handle cost update from SessionCostTracker.""" if not self.session_active: return - + # Update stats from cost tracker state self._stats.weapon_cost_total = state.weapon_cost self._stats.armor_cost_total = state.armor_cost @@ -935,22 +930,22 @@ class HUDOverlay(QWidget): self._stats.shots_fired = state.shots_fired self._stats.hits_taken = state.hits_taken self._stats.heals_used = state.heals_used - + self._stats.recalculate() self._refresh_display() self.stats_updated.emit(self._stats.to_dict()) - + def update_cost(self, cost_ped: Decimal, cost_type: str = 'weapon') -> None: """ Update total cost spent. - + Args: cost_ped: Cost in PED to add cost_type: Type of cost ('weapon', 'armor', 'healing', 'plates', 'enhancer', 'mindforce') """ if not self.session_active: return - + if cost_type == 'weapon': self._stats.weapon_cost_total += cost_ped elif cost_type == 'armor': @@ -963,36 +958,36 @@ class HUDOverlay(QWidget): self._stats.enhancer_cost_total += cost_ped elif cost_type == 'mindforce': self._stats.mindforce_cost_total += cost_ped - + self._stats.recalculate() self._refresh_display() self.stats_updated.emit(self._stats.to_dict()) - + def end_session(self) -> None: """End the current session.""" self._timer.stop() - + # End armor tracking if self._armor_tracker: armor_summary = self._armor_tracker.end_session() self._armor_tracker = None - + self._session_start = None self.session_active = False self._save_position() self.status_label.setText("○ Paused") self.status_label.setStyleSheet("font-size: 9px; color: #888888;") - + # ======================================================================== # EVENT HANDLERS (Called from LogWatcher) # ======================================================================== - - def on_loot_event(self, item_name: str, value_ped: Decimal, - is_shrapnel: bool = False, + + def on_loot_event(self, item_name: str, value_ped: Decimal, + is_shrapnel: bool = False, is_universal_ammo: bool = False) -> None: """ Called when loot is received from LogWatcher. - + Args: item_name: Name of the looted item value_ped: Value in PED (Decimal for precision) @@ -1001,10 +996,10 @@ class HUDOverlay(QWidget): """ if not self.session_active: return - + # Always add to total loot self._stats.loot_total += value_ped - + # Track separately if is_shrapnel: self._stats.shrapnel_total += value_ped @@ -1013,117 +1008,117 @@ class HUDOverlay(QWidget): else: # Marketable loot - counts toward profit/loss self._stats.loot_other += value_ped - + self._stats.recalculate() self._refresh_display() self.stats_updated.emit(self._stats.to_dict()) - + def on_kill_event(self, creature_name: str = "") -> None: """ Called when a creature is killed. - + Args: creature_name: Name of the killed creature """ if not self.session_active: return - + self._stats.kills += 1 self._stats.recalculate() self._refresh_display() self.stats_updated.emit(self._stats.to_dict()) - + def on_damage_dealt(self, damage: Decimal) -> None: """ Called when damage is dealt. - + Args: damage: Amount of damage dealt """ if not self.session_active: return - + self._stats.damage_dealt += damage self._stats.shots_fired += 1 self._stats.recalculate() self._refresh_display() self.stats_updated.emit(self._stats.to_dict()) - + def on_damage_taken(self, damage: Decimal) -> None: """ Called when damage is taken. Calculates armor decay using official formula. - + Args: damage: Amount of damage taken (absorbed by armor) """ if not self.session_active: return - + self._stats.damage_taken += damage - + # Calculate armor decay using tracker if self._armor_tracker and damage > 0: armor_decay_ped = self._armor_tracker.record_damage_taken(damage) self._stats.armor_cost_total += armor_decay_ped - + self._stats.recalculate() self._refresh_display() self.stats_updated.emit(self._stats.to_dict()) - + def on_global(self, value_ped: Decimal = Decimal('0.0')) -> None: """Called on global event.""" if not self.session_active: return - + self._stats.globals_count += 1 self._refresh_display() self.stats_updated.emit(self._stats.to_dict()) - + def on_hof(self, value_ped: Decimal = Decimal('0.0')) -> None: """Called on Hall of Fame event.""" if not self.session_active: return - + self._stats.hofs_count += 1 self._refresh_display() self.stats_updated.emit(self._stats.to_dict()) - + def on_heal_event(self, heal_amount: Decimal, decay_cost: Decimal = Decimal('0')) -> None: """ Called when healing is done from LogWatcher. - + Args: heal_amount: Amount of HP healed decay_cost: Cost of the heal in PED (based on FAP decay) """ if not self.session_active: return - + self._stats.healing_done += heal_amount - + if decay_cost > 0: self._stats.healing_cost_total += decay_cost self._stats.recalculate() - + self._refresh_display() self.stats_updated.emit(self._stats.to_dict()) - + def update_display(self) -> None: """Public method to refresh display.""" self._refresh_display() - + 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 _check_game_window(self) -> None: """Check if Entropia Universe window is in foreground.""" if not self._auto_hide_with_game or sys.platform != 'win32': return - + try: hwnd = GetForegroundWindow() if hwnd: @@ -1132,9 +1127,9 @@ class HUDOverlay(QWidget): buffer = ctypes.create_unicode_buffer(length + 1) GetWindowTextW(hwnd, buffer, length + 1) title = buffer.value - + is_game_focused = self._game_window_title.lower() in title.lower() - + if is_game_focused: if not self.isVisible(): self.show() @@ -1143,7 +1138,7 @@ class HUDOverlay(QWidget): self.hide() except Exception: pass - + def _update_time_display(self) -> None: """Update the time label.""" total_seconds = int(self._stats.session_time.total_seconds()) @@ -1151,15 +1146,15 @@ class HUDOverlay(QWidget): 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. - + Args: stats: Dictionary containing stat updates """ @@ -1168,56 +1163,56 @@ class HUDOverlay(QWidget): self._stats.loot_other = Decimal(str(stats['loot'])) elif 'loot_delta' in stats: self._stats.loot_other += Decimal(str(stats['loot_delta'])) - + if 'shrapnel' in stats: self._stats.shrapnel_total = Decimal(str(stats['shrapnel'])) elif 'shrapnel_delta' in stats: self._stats.shrapnel_total += Decimal(str(stats['shrapnel_delta'])) - + # Damage if 'damage_dealt' in stats: self._stats.damage_dealt = Decimal(str(stats['damage_dealt'])) elif 'damage_dealt_add' in stats: self._stats.damage_dealt += Decimal(str(stats['damage_dealt_add'])) - + if 'damage_taken' in stats: self._stats.damage_taken = Decimal(str(stats['damage_taken'])) elif 'damage_taken_add' in stats: self._stats.damage_taken += Decimal(str(stats['damage_taken_add'])) - + # Healing if 'healing_done' in stats: self._stats.healing_done = Decimal(str(stats['healing_done'])) elif 'healing_add' in stats: self._stats.healing_done += Decimal(str(stats['healing_add'])) - + if 'healing_cost' in stats: self._stats.healing_cost_total = Decimal(str(stats['healing_cost'])) elif 'healing_cost_add' in stats: self._stats.healing_cost_total += Decimal(str(stats['healing_cost_add'])) - + # Shots & Kills if 'shots_fired' in stats: self._stats.shots_fired = int(stats['shots_fired']) elif 'shots_add' in stats: self._stats.shots_fired += int(stats['shots_add']) - + if 'kills' in stats: self._stats.kills = int(stats['kills']) elif 'kills_add' in stats: self._stats.kills += int(stats['kills_add']) - + # Events if 'globals' in stats: self._stats.globals_count = int(stats['globals']) elif 'globals_add' in stats: self._stats.globals_count += int(stats['globals_add']) - + if 'hofs' in stats: self._stats.hofs_count = int(stats['hofs']) elif 'hofs_add' in stats: self._stats.hofs_count += int(stats['hofs_add']) - + # Equipment if 'weapon' in stats: self._stats.current_weapon = str(stats['weapon']) @@ -1227,19 +1222,19 @@ class HUDOverlay(QWidget): self._stats.current_fap = str(stats['fap']) if 'loadout' in stats: self._stats.current_loadout = str(stats['loadout']) - + self._stats.recalculate() self._refresh_display() self.stats_updated.emit(self._stats.to_dict()) - + def _refresh_display(self) -> None: """Refresh all display labels with current stats.""" # Loot (marketable only) self.loot_value_label.setText(f"{self._stats.loot_other:.2f} PED") - + # Total Cost self.cost_value_label.setText(f"{self._stats.cost_total:.2f} PED") - + # Profit/Loss with color coding profit = self._stats.profit_loss self.profit_value_label.setText(f"{profit:+.2f}") @@ -1249,7 +1244,7 @@ class HUDOverlay(QWidget): self.profit_value_label.setStyleSheet("font-size: 13px; font-weight: bold; color: #FF7F7F;") else: self.profit_value_label.setStyleSheet("font-size: 13px; font-weight: bold; color: #FFFFFF;") - + # Return percentage with color coding ret_pct = self._stats.return_percentage self.return_label.setText(f"{ret_pct:.1f}%") @@ -1259,70 +1254,83 @@ class HUDOverlay(QWidget): self.return_label.setStyleSheet("font-size: 11px; font-weight: bold; color: #FFFF7F;") else: self.return_label.setStyleSheet("font-size: 11px; font-weight: bold; color: #FF7F7F;") - + # Shrapnel indicator self.shrapnel_label.setText(f"💎 Shrapnel: {self._stats.shrapnel_total:.2f}") - + # Combat stats self.kills_value_label.setText(str(self._stats.kills)) self.globals_value_label.setText(f"{self._stats.globals_count} / {self._stats.hofs_count}") self.dpp_value_label.setText(f"{self._stats.dpp:.1f}") - + self.dealt_value_label.setText(f"{int(self._stats.damage_dealt)}") self.taken_value_label.setText(f"{int(self._stats.damage_taken)}") self.shots_value_label.setText(str(self._stats.shots_fired)) - + # Cost breakdown self.wep_cost_value_label.setText(f"{self._stats.weapon_cost_total:.2f}") self.arm_cost_value_label.setText(f"{self._stats.armor_cost_total:.3f}") self.heal_cost_value_label.setText(f"{self._stats.healing_cost_total:.2f}") self.cpk_value_label.setText(f"{self._stats.cost_per_kill:.2f}") - + # Loadout cost metrics self.cps_value_label.setText(f"{self._stats.cost_per_shot:.3f}") self.cph_value_label.setText(f"{self._stats.cost_per_hit:.3f}") self.cphl_value_label.setText(f"{self._stats.cost_per_heal:.3f}") self.hits_value_label.setText(str(self._stats.hits_taken)) self.heals_value_label.setText(str(self._stats.heals_used)) - + # Equipment info weapon_short = self._stats.current_weapon[:12] if len(self._stats.current_weapon) > 12 else self._stats.current_weapon armor_short = self._stats.current_armor[:12] if len(self._stats.current_armor) > 12 else self._stats.current_armor - + self.weapon_label.setText(f"🔫 {weapon_short}") self.armor_label.setText(f"🛡️ {armor_short}") self.loadout_label.setText(self._stats.current_loadout[:12]) - + # Update time 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.""" - if not self._modifier_pressed: - self._enable_click_through(True) + # Don't enable click-through immediately - let user interact first + # Click-through will be enabled after mouse leaves the window super().showEvent(event) - + + def enterEvent(self, event) -> None: + """Handle mouse enter - disable click-through so user can interact.""" + self._enable_click_through(False) + super().enterEvent(event) + + def leaveEvent(self, event) -> None: + """Handle mouse leave - reset drag hint and enable click-through.""" + self.drag_hint.setStyleSheet("font-size: 8px; color: #666666;") + # Enable click-through when mouse leaves so it doesn't block game + if not self._dragging: + self._enable_click_through(True) + super().leaveEvent(event) + def moveEvent(self, event) -> None: """Handle move event - save position periodically.""" 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() @@ -1337,11 +1345,11 @@ def run_mock_test(): """Run the HUD with mock data for testing.""" app = QApplication(sys.argv) app.setQuitOnLastWindowClosed(False) - + # Create HUD hud = HUDOverlay() hud.show() - + # Start session hud.start_session( weapon="Omegaton M2100", @@ -1350,10 +1358,10 @@ def run_mock_test(): fap="Vivo T10", loadout="Hunting Set A" ) - + # Simulate incoming stats from PyQt6.QtCore import QTimer - + mock_stats = { 'loot': Decimal('0.0'), 'shrapnel': Decimal('0.0'), @@ -1363,53 +1371,53 @@ def run_mock_test(): 'globals': 0, 'hofs': 0, } - + def simulate_event(): """Simulate random game events.""" import random - + event_type = random.choice(['loot', 'damage', 'kill', 'global', 'shrapnel', 'armor_hit']) - + 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 == 'shrapnel': value = Decimal(str(random.uniform(0.1, 2.0))) mock_stats['shrapnel'] += value hud._stats.shrapnel_total += value hud._refresh_display() print(f"[MOCK] Shrapnel: {value:.2f} PED") - + elif event_type == 'damage': damage = Decimal(str(random.randint(5, 50))) mock_stats['damage_dealt'] += damage hud.update_stats({'damage_dealt': mock_stats['damage_dealt'], 'shots_add': 1}) print(f"[MOCK] Damage dealt: {damage}") - + elif event_type == 'armor_hit': damage = Decimal(str(random.randint(5, 20))) mock_stats['damage_taken'] += damage hud.on_damage_taken(damage) print(f"[MOCK] Damage taken (armor): {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) @@ -1424,7 +1432,7 @@ def run_mock_test(): print(" ✓ Armor decay calculated using official formula") print("\nPress Ctrl+C in terminal to exit") print("="*60 + "\n") - + sys.exit(app.exec())