# Description: Transparent HUD Overlay for Lemontropia Suite # Implements frameless, always-on-top, click-through overlay with draggable support # Updated: Integrated armor decay tracking and hunting session metrics # 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, field from typing import Optional, Dict, Any, List # Windows-specific imports for click-through support 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] FindWindowW.restype = wintypes.HWND 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 - 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 healing_cost_total: Decimal = Decimal('0.0') plates_cost_total: Decimal = Decimal('0.0') 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') healing_done: Decimal = Decimal('0.0') shots_fired: int = 0 hits_taken: int = 0 heals_used: int = 0 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 { 'session_time_seconds': self.session_time.total_seconds(), 'loot_total': str(self.loot_total), 'loot_other': str(self.loot_other), 'shrapnel_total': str(self.shrapnel_total), 'universal_ammo_total': str(self.universal_ammo_total), 'weapon_cost_total': str(self.weapon_cost_total), 'armor_cost_total': str(self.armor_cost_total), 'healing_cost_total': str(self.healing_cost_total), 'plates_cost_total': str(self.plates_cost_total), 'enhancer_cost_total': str(self.enhancer_cost_total), 'cost_total': str(self.cost_total), 'profit_loss': str(self.profit_loss), 'return_percentage': str(self.return_percentage), 'damage_dealt': float(self.damage_dealt), 'damage_taken': float(self.damage_taken), 'healing_done': float(self.healing_done), 'shots_fired': self.shots_fired, 'kills': self.kills, 'globals_count': self.globals_count, 'hofs_count': self.hofs_count, 'cost_per_kill': str(self.cost_per_kill), 'dpp': str(self.dpp), 'current_weapon': self.current_weapon, 'current_loadout': self.current_loadout, 'current_armor': self.current_armor, 'current_fap': self.current_fap, 'weapon_dpp': str(self.weapon_dpp), '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.""" return cls( session_time=timedelta(seconds=data.get('session_time_seconds', 0)), loot_total=Decimal(data.get('loot_total', '0.0')), loot_other=Decimal(data.get('loot_other', '0.0')), shrapnel_total=Decimal(data.get('shrapnel_total', '0.0')), universal_ammo_total=Decimal(data.get('universal_ammo_total', '0.0')), weapon_cost_total=Decimal(data.get('weapon_cost_total', '0.0')), armor_cost_total=Decimal(data.get('armor_cost_total', '0.0')), healing_cost_total=Decimal(data.get('healing_cost_total', '0.0')), plates_cost_total=Decimal(data.get('plates_cost_total', '0.0')), enhancer_cost_total=Decimal(data.get('enhancer_cost_total', '0.0')), cost_total=Decimal(data.get('cost_total', '0.0')), profit_loss=Decimal(data.get('profit_loss', '0.0')), return_percentage=Decimal(data.get('return_percentage', '0.0')), damage_dealt=Decimal(str(data.get('damage_dealt', 0))), damage_taken=Decimal(str(data.get('damage_taken', 0))), healing_done=Decimal(str(data.get('healing_done', 0))), shots_fired=data.get('shots_fired', 0), kills=data.get('kills', 0), globals_count=data.get('globals_count', 0), hofs_count=data.get('hofs_count', 0), cost_per_kill=Decimal(data.get('cost_per_kill', '0.0')), dpp=Decimal(data.get('dpp', '0.0')), current_weapon=data.get('current_weapon', 'None'), current_loadout=data.get('current_loadout', 'None'), current_armor=data.get('current_armor', 'None'), current_fap=data.get('current_fap', 'None'), weapon_dpp=Decimal(data.get('weapon_dpp', '0.0')), 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.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')) self.armor_cost_total = summary.get('armor_cost', Decimal('0')) self.healing_cost_total = summary.get('healing_cost', Decimal('0')) self.enhancer_cost_total = summary.get('enhancer_cost', Decimal('0')) self.mindforce_cost_total = summary.get('mindforce_cost', Decimal('0')) self.shots_fired = summary.get('shots_fired', 0) self.hits_taken = summary.get('hits_taken', 0) self.heals_used = summary.get('heals_used', 0) self.cost_per_shot = summary.get('cost_per_shot', Decimal('0')) self.cost_per_hit = summary.get('cost_per_hit', Decimal('0')) self.cost_per_heal = summary.get('cost_per_heal', Decimal('0')) self.recalculate() 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 - 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, 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 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 - 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 { 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: 10px; color: #AAAAAA; } .stat-value { font-size: 13px; font-weight: bold; color: #FFD700; } .header { font-size: 14px; font-weight: bold; color: #FFD700; } .subheader { font-size: 9px; color: #888888; } .positive { color: #7FFF7F; } .negative { color: #FF7F7F; } .neutral { 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']) self._refresh_display() else: # Default position: top-right of screen screen = QApplication.primaryScreen().geometry() self.move(screen.width() - 360, 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 # ======================================================================== # 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: if event.modifiers() == Qt.KeyboardModifier.ControlModifier: self._dragging = True self._drag_offset = event.pos() self.setCursor(Qt.CursorShape.ClosedHandCursor) self._enable_click_through(False) event.accept() 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: new_pos = self.mapToGlobal(event.pos()) - self._drag_offset self.move(new_pos) self.position_changed.emit(new_pos) event.accept() 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: self._dragging = False self.setCursor(Qt.CursorShape.ArrowCursor) self._save_position() self._enable_click_through(True) 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, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED | SWP_SHOWWINDOW ) 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: self._modifier_pressed = True self._enable_click_through(False) 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: self._modifier_pressed = False self._enable_click_through(True) 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'), armor: str = "None", armor_durability: int = 2000, fap: str = "None", loadout_id: Optional[int] = None, 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 with loadout-based cost tracking. Args: weapon: Name of the current weapon loadout: Name of the current loadout weapon_dpp: Weapon DPP for cost calculations weapon_cost_per_hour: Weapon cost per hour in PED armor: Name of the equipped armor armor_durability: Armor durability rating fap: Name of the equipped FAP loadout_id: Database ID of the loadout configuration cost_per_shot: Cost per weapon shot from loadout cost_per_hit: Cost per armor hit from loadout cost_per_heal: Cost per heal from loadout """ self._session_start = datetime.now() self._stats = HUDStats() self._stats.current_weapon = weapon self._stats.current_loadout = loadout self._stats.loadout_id = loadout_id self._stats.current_armor = armor self._stats.current_fap = fap self._stats.weapon_dpp = weapon_dpp self._stats.weapon_cost_per_hour = weapon_cost_per_hour self._stats.armor_durability = armor_durability 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 # Initialize armor decay tracker try: from core.armor_decay import ArmorDecayTracker self._armor_tracker = ArmorDecayTracker(armor if armor != "None" else "Ghost") 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 self._stats.healing_cost_total = state.healing_cost self._stats.enhancer_cost_total = state.enhancer_cost self._stats.mindforce_cost_total = state.mindforce_cost 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': self._stats.armor_cost_total += cost_ped elif cost_type == 'healing': self._stats.healing_cost_total += cost_ped elif cost_type == 'plates': self._stats.plates_cost_total += cost_ped elif cost_type == 'enhancer': 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, 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) is_shrapnel: Whether this is shrapnel is_universal_ammo: Whether this is universal ammo """ 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 elif is_universal_ammo: self._stats.universal_ammo_total += value_ped 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: length = GetWindowTextLengthW(hwnd) if length > 0: 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() else: if self.isVisible(): self.hide() except Exception: pass 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. Args: stats: Dictionary containing stat updates """ # Loot if 'loot' in stats: 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']) if 'armor' in stats: self._stats.current_armor = str(stats['armor']) if 'fap' in stats: 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}") if profit > 0: self.profit_value_label.setStyleSheet("font-size: 13px; font-weight: bold; color: #7FFF7F;") elif profit < 0: 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}%") if ret_pct >= 100: self.return_label.setStyleSheet("font-size: 11px; font-weight: bold; color: #7FFF7F;") elif ret_pct >= 90: 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) super().showEvent(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() super().closeEvent(event) # ============================================================================ # MOCK TESTING # ============================================================================ 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", armor="Ghost", armor_durability=2000, 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'), 'damage_dealt': Decimal('0.0'), 'damage_taken': Decimal('0.0'), 'kills': 0, '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) 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(" ✓ Armor decay calculated using official formula") print("\nPress Ctrl+C in terminal to exit") print("="*60 + "\n") sys.exit(app.exec()) if __name__ == "__main__": run_mock_test()