963 lines
35 KiB
Python
963 lines
35 KiB
Python
# Description: Transparent HUD Overlay for Lemontropia Suite
|
|
# Implements frameless, always-on-top, click-through overlay with draggable support
|
|
# Follows Never-Break Rules: Decimal precision, 60+ FPS, Observer Pattern integration
|
|
|
|
import sys
|
|
import json
|
|
from pathlib import Path
|
|
from decimal import Decimal
|
|
from datetime import datetime, timedelta
|
|
from dataclasses import dataclass, asdict
|
|
from typing import Optional, Dict, Any
|
|
|
|
# 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."""
|
|
session_time: timedelta = timedelta(0)
|
|
loot_total: Decimal = Decimal('0.0')
|
|
damage_dealt: int = 0
|
|
damage_taken: int = 0
|
|
kills: int = 0
|
|
globals_count: int = 0
|
|
hofs_count: int = 0
|
|
current_weapon: str = "None"
|
|
current_loadout: str = "None"
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Convert to dictionary for serialization."""
|
|
return {
|
|
'session_time_seconds': self.session_time.total_seconds(),
|
|
'loot_total': str(self.loot_total),
|
|
'damage_dealt': self.damage_dealt,
|
|
'damage_taken': self.damage_taken,
|
|
'kills': self.kills,
|
|
'globals_count': self.globals_count,
|
|
'hofs_count': self.hofs_count,
|
|
'current_weapon': self.current_weapon,
|
|
'current_loadout': self.current_loadout,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> 'HUDStats':
|
|
"""Create from dictionary."""
|
|
return cls(
|
|
session_time=timedelta(seconds=data.get('session_time_seconds', 0)),
|
|
loot_total=Decimal(data.get('loot_total', '0.0')),
|
|
damage_dealt=data.get('damage_dealt', 0),
|
|
damage_taken=data.get('damage_taken', 0),
|
|
kills=data.get('kills', 0),
|
|
globals_count=data.get('globals_count', 0),
|
|
hofs_count=data.get('hofs_count', 0),
|
|
current_weapon=data.get('current_weapon', 'None'),
|
|
current_loadout=data.get('current_loadout', 'None'),
|
|
)
|
|
|
|
|
|
class HUDOverlay(QWidget):
|
|
"""
|
|
Transparent, always-on-top HUD overlay for Lemontropia Suite.
|
|
|
|
Features:
|
|
- Frameless window (no borders, title bar)
|
|
- Transparent background with semi-transparent content
|
|
- Always on top of other windows
|
|
- Click-through when not holding modifier key
|
|
- Draggable when holding Ctrl key
|
|
- Position persistence across sessions
|
|
- Real-time stat updates via signals/slots
|
|
|
|
Window Flags:
|
|
- FramelessWindowHint: Removes window decorations
|
|
- WindowStaysOnTopHint: Keeps above other windows
|
|
- Tool: Makes it a tool window (no taskbar entry)
|
|
"""
|
|
|
|
# Signal emitted when stats are updated (for external integration)
|
|
stats_updated = pyqtSignal(dict)
|
|
|
|
# Signal emitted when HUD is moved
|
|
position_changed = pyqtSignal(QPoint)
|
|
|
|
def __init__(self, parent: Optional[QObject] = None,
|
|
config_path: Optional[str] = None):
|
|
"""
|
|
Initialize HUD Overlay.
|
|
|
|
Args:
|
|
parent: Parent widget (optional)
|
|
config_path: Path to config file for position persistence
|
|
"""
|
|
super().__init__(parent)
|
|
|
|
# Configuration path for saving position
|
|
if config_path is None:
|
|
ui_dir = Path(__file__).parent
|
|
self.config_path = ui_dir.parent / "data" / "hud_config.json"
|
|
else:
|
|
self.config_path = Path(config_path)
|
|
|
|
self.config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Session tracking
|
|
self._session_start: Optional[datetime] = None
|
|
self._stats = HUDStats()
|
|
self.session_active = False # Public flag for session state
|
|
|
|
# Drag state
|
|
self._dragging = False
|
|
self._drag_offset = QPoint()
|
|
self._modifier_pressed = False
|
|
|
|
# Game window detection (disabled by default - enable after testing)
|
|
self._auto_hide_with_game = False # Auto-hide when game not focused
|
|
self._game_window_title = "Entropia Universe" # Window title to track
|
|
self._was_visible_before_unfocus = False
|
|
self._debug_window_detection = False # Set to True to debug
|
|
|
|
# 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) # Check every 500ms
|
|
|
|
self._setup_window()
|
|
self._setup_ui()
|
|
self._load_position()
|
|
|
|
def _setup_window(self) -> None:
|
|
"""Configure window properties for HUD behavior."""
|
|
# Window flags for frameless, always-on-top, tool window
|
|
self.setWindowFlags(
|
|
Qt.WindowType.FramelessWindowHint |
|
|
Qt.WindowType.WindowStaysOnTopHint |
|
|
Qt.WindowType.Tool
|
|
)
|
|
|
|
# Enable transparency
|
|
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
|
|
|
# Enable mouse tracking for hover detection
|
|
self.setMouseTracking(True)
|
|
|
|
# Size
|
|
self.setFixedSize(320, 220)
|
|
|
|
# Accept focus for keyboard events (needed for modifier detection)
|
|
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
|
|
|
def _setup_ui(self) -> None:
|
|
"""Build the HUD UI components."""
|
|
# Main container with semi-transparent background
|
|
self.container = QFrame(self)
|
|
self.container.setFixedSize(320, 220)
|
|
self.container.setObjectName("hudContainer")
|
|
|
|
# Style the container - semi-transparent dark background
|
|
self.container.setStyleSheet("""
|
|
#hudContainer {
|
|
background-color: rgba(0, 0, 0, 180);
|
|
border: 1px solid rgba(255, 215, 0, 80);
|
|
border-radius: 8px;
|
|
}
|
|
QLabel {
|
|
color: #FFFFFF;
|
|
font-family: 'Segoe UI', 'Arial', sans-serif;
|
|
}
|
|
.stat-label {
|
|
font-size: 11px;
|
|
color: #CCCCCC;
|
|
}
|
|
.stat-value {
|
|
font-size: 13px;
|
|
font-weight: bold;
|
|
color: #FFD700;
|
|
}
|
|
.header {
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
color: #FFD700;
|
|
}
|
|
.subheader {
|
|
font-size: 10px;
|
|
color: #888888;
|
|
}
|
|
.positive {
|
|
color: #7FFF7F;
|
|
}
|
|
.negative {
|
|
color: #FF7F7F;
|
|
}
|
|
""")
|
|
|
|
# Main layout
|
|
layout = QVBoxLayout(self.container)
|
|
layout.setContentsMargins(12, 10, 12, 10)
|
|
layout.setSpacing(6)
|
|
|
|
# === HEADER ===
|
|
header_layout = QHBoxLayout()
|
|
|
|
self.title_label = QLabel("🍋 LEMONTROPIA")
|
|
self.title_label.setProperty("class", "header")
|
|
self.title_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #FFD700;")
|
|
header_layout.addWidget(self.title_label)
|
|
|
|
header_layout.addStretch()
|
|
|
|
self.time_label = QLabel("00:00:00")
|
|
self.time_label.setStyleSheet("font-size: 13px; font-weight: bold; color: #00FFFF;")
|
|
header_layout.addWidget(self.time_label)
|
|
|
|
layout.addLayout(header_layout)
|
|
|
|
# Drag hint (only visible on hover)
|
|
self.drag_hint = QLabel("Hold Ctrl to drag")
|
|
self.drag_hint.setStyleSheet("font-size: 9px; color: #666666;")
|
|
self.drag_hint.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
layout.addWidget(self.drag_hint)
|
|
|
|
# === SEPARATOR ===
|
|
separator = QFrame()
|
|
separator.setFrameShape(QFrame.Shape.HLine)
|
|
separator.setStyleSheet("background-color: rgba(255, 215, 0, 50);")
|
|
separator.setFixedHeight(1)
|
|
layout.addWidget(separator)
|
|
|
|
# === STATS GRID ===
|
|
# Row 1: Loot & Kills
|
|
row1 = QHBoxLayout()
|
|
|
|
# Loot
|
|
loot_layout = QVBoxLayout()
|
|
loot_label = QLabel("💰 LOOT")
|
|
loot_label.setStyleSheet("font-size: 10px; color: #888888;")
|
|
loot_layout.addWidget(loot_label)
|
|
|
|
self.loot_value_label = QLabel("0.00 PED")
|
|
self.loot_value_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #7FFF7F;")
|
|
loot_layout.addWidget(self.loot_value_label)
|
|
|
|
row1.addLayout(loot_layout)
|
|
row1.addStretch()
|
|
|
|
# Kills
|
|
kills_layout = QVBoxLayout()
|
|
kills_label = QLabel("💀 KILLS")
|
|
kills_label.setStyleSheet("font-size: 10px; color: #888888;")
|
|
kills_layout.addWidget(kills_label)
|
|
|
|
self.kills_value_label = QLabel("0")
|
|
self.kills_value_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #FFFFFF;")
|
|
kills_layout.addWidget(self.kills_value_label)
|
|
|
|
row1.addLayout(kills_layout)
|
|
row1.addStretch()
|
|
|
|
# Globals/HoFs
|
|
globals_layout = QVBoxLayout()
|
|
globals_label = QLabel("🌍 GLOBALS")
|
|
globals_label.setStyleSheet("font-size: 10px; color: #888888;")
|
|
globals_layout.addWidget(globals_label)
|
|
|
|
self.globals_value_label = QLabel("0 / 0")
|
|
self.globals_value_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #FFD700;")
|
|
globals_layout.addWidget(self.globals_value_label)
|
|
|
|
row1.addLayout(globals_layout)
|
|
|
|
layout.addLayout(row1)
|
|
|
|
# Row 2: Damage
|
|
row2 = QHBoxLayout()
|
|
|
|
# Damage Dealt
|
|
dealt_layout = QVBoxLayout()
|
|
dealt_label = QLabel("⚔️ DEALT")
|
|
dealt_label.setStyleSheet("font-size: 10px; color: #888888;")
|
|
dealt_layout.addWidget(dealt_label)
|
|
|
|
self.dealt_value_label = QLabel("0")
|
|
self.dealt_value_label.setStyleSheet("font-size: 13px; font-weight: bold; color: #7FFF7F;")
|
|
dealt_layout.addWidget(self.dealt_value_label)
|
|
|
|
row2.addLayout(dealt_layout)
|
|
row2.addStretch()
|
|
|
|
# Damage Taken
|
|
taken_layout = QVBoxLayout()
|
|
taken_label = QLabel("🛡️ TAKEN")
|
|
taken_label.setStyleSheet("font-size: 10px; color: #888888;")
|
|
taken_layout.addWidget(taken_label)
|
|
|
|
self.taken_value_label = QLabel("0")
|
|
self.taken_value_label.setStyleSheet("font-size: 13px; font-weight: bold; color: #FF7F7F;")
|
|
taken_layout.addWidget(self.taken_value_label)
|
|
|
|
row2.addLayout(taken_layout)
|
|
row2.addStretch()
|
|
|
|
layout.addLayout(row2)
|
|
|
|
# === WEAPON INFO ===
|
|
weapon_separator = QFrame()
|
|
weapon_separator.setFrameShape(QFrame.Shape.HLine)
|
|
weapon_separator.setStyleSheet("background-color: rgba(255, 215, 0, 30);")
|
|
weapon_separator.setFixedHeight(1)
|
|
layout.addWidget(weapon_separator)
|
|
|
|
weapon_layout = QHBoxLayout()
|
|
|
|
weapon_icon = QLabel("🔫")
|
|
weapon_icon.setStyleSheet("font-size: 12px;")
|
|
weapon_layout.addWidget(weapon_icon)
|
|
|
|
self.weapon_label = QLabel("No weapon")
|
|
self.weapon_label.setStyleSheet("font-size: 11px; color: #CCCCCC;")
|
|
weapon_layout.addWidget(self.weapon_label)
|
|
|
|
weapon_layout.addStretch()
|
|
|
|
loadout_label = QLabel("Loadout:")
|
|
loadout_label.setStyleSheet("font-size: 10px; color: #888888;")
|
|
weapon_layout.addWidget(loadout_label)
|
|
|
|
self.loadout_label = QLabel("None")
|
|
self.loadout_label.setStyleSheet("font-size: 11px; color: #00FFFF;")
|
|
weapon_layout.addWidget(self.loadout_label)
|
|
|
|
layout.addLayout(weapon_layout)
|
|
|
|
# === STATUS BAR ===
|
|
self.status_label = QLabel("● Live")
|
|
self.status_label.setStyleSheet("font-size: 9px; color: #7FFF7F;")
|
|
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
layout.addWidget(self.status_label)
|
|
|
|
# ========================================================================
|
|
# POSITION PERSISTENCE
|
|
# ========================================================================
|
|
|
|
def _load_position(self) -> None:
|
|
"""Load saved position from config file."""
|
|
try:
|
|
if self.config_path.exists():
|
|
with open(self.config_path, 'r') as f:
|
|
config = json.load(f)
|
|
|
|
x = config.get('x', 100)
|
|
y = config.get('y', 100)
|
|
self.move(x, y)
|
|
|
|
# Load saved stats if available
|
|
if 'stats' in config:
|
|
self._stats = HUDStats.from_dict(config['stats'])
|
|
self._refresh_display()
|
|
else:
|
|
# Default position: top-right of screen
|
|
screen = QApplication.primaryScreen().geometry()
|
|
self.move(screen.width() - 350, 50)
|
|
except Exception as e:
|
|
# Default position on error
|
|
self.move(100, 100)
|
|
|
|
def _save_position(self) -> None:
|
|
"""Save current position to config file."""
|
|
try:
|
|
config = {
|
|
'x': self.x(),
|
|
'y': self.y(),
|
|
'stats': self._stats.to_dict(),
|
|
}
|
|
with open(self.config_path, 'w') as f:
|
|
json.dump(config, f, indent=2)
|
|
except Exception as e:
|
|
pass # Silent fail on save error
|
|
|
|
# ========================================================================
|
|
# MOUSE HANDLING (Drag Support)
|
|
# ========================================================================
|
|
|
|
def mousePressEvent(self, event: QMouseEvent) -> None:
|
|
"""Handle mouse press - start drag if Ctrl is held."""
|
|
if event.button() == Qt.MouseButton.LeftButton:
|
|
# Check if Ctrl is held
|
|
if event.modifiers() == Qt.KeyboardModifier.ControlModifier:
|
|
self._dragging = True
|
|
self._drag_offset = event.pos()
|
|
self.setCursor(Qt.CursorShape.ClosedHandCursor)
|
|
self._enable_click_through(False) # Disable click-through for dragging
|
|
event.accept()
|
|
else:
|
|
# Enable click-through and pass to underlying window
|
|
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:
|
|
# Move window
|
|
new_pos = self.mapToGlobal(event.pos()) - self._drag_offset
|
|
self.move(new_pos)
|
|
self.position_changed.emit(new_pos)
|
|
event.accept()
|
|
else:
|
|
# Show drag hint on hover
|
|
self.drag_hint.setStyleSheet("font-size: 9px; color: #FFD700;")
|
|
event.ignore()
|
|
|
|
def mouseReleaseEvent(self, event: QMouseEvent) -> None:
|
|
"""Handle mouse release - end drag and save position."""
|
|
if event.button() == Qt.MouseButton.LeftButton and self._dragging:
|
|
self._dragging = False
|
|
self.setCursor(Qt.CursorShape.ArrowCursor)
|
|
self._save_position()
|
|
# Re-enable click-through after drag
|
|
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: 9px; color: #666666;")
|
|
super().leaveEvent(event)
|
|
|
|
def _enable_click_through(self, enable: bool) -> None:
|
|
"""
|
|
Enable or disable click-through behavior.
|
|
|
|
When enabled, mouse events pass through to the window below.
|
|
When disabled (Ctrl held), window captures mouse events for dragging.
|
|
|
|
On Windows: Uses WinAPI for proper click-through support.
|
|
On other platforms: Uses Qt's WA_TransparentForMouseEvents.
|
|
"""
|
|
if sys.platform == 'win32':
|
|
self._set_click_through_win32(enable)
|
|
else:
|
|
# Use Qt's built-in for non-Windows platforms
|
|
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, enable)
|
|
|
|
def _set_click_through_win32(self, enabled: bool) -> None:
|
|
"""
|
|
Enable/disable click-through on Windows using WinAPI.
|
|
|
|
Uses SetWindowLongW to modify the window's extended style flags:
|
|
- WS_EX_TRANSPARENT (0x00000020): Allows mouse events to pass through
|
|
- WS_EX_LAYERED (0x00080000): Required for transparency effects
|
|
|
|
Args:
|
|
enabled: True to enable click-through, False to capture mouse events
|
|
"""
|
|
GWL_EXSTYLE = -20
|
|
WS_EX_TRANSPARENT = 0x00000020
|
|
WS_EX_LAYERED = 0x00080000
|
|
|
|
# Window refresh flags
|
|
SWP_FRAMECHANGED = 0x0020
|
|
SWP_NOMOVE = 0x0002
|
|
SWP_NOSIZE = 0x0001
|
|
SWP_NOZORDER = 0x0004
|
|
SWP_SHOWWINDOW = 0x0040
|
|
|
|
try:
|
|
hwnd = self.winId().__int__()
|
|
|
|
# Get current extended style
|
|
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)
|
|
|
|
# Refresh the window to apply style changes
|
|
ctypes.windll.user32.SetWindowPos(
|
|
hwnd, 0, 0, 0, 0, 0,
|
|
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED | SWP_SHOWWINDOW
|
|
)
|
|
except Exception:
|
|
# Fallback to Qt method if WinAPI fails
|
|
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: 9px; color: #00FF00;")
|
|
super().keyPressEvent(event)
|
|
|
|
def keyReleaseEvent(self, event) -> None:
|
|
"""Handle key release - detect Ctrl release."""
|
|
if event.key() == Qt.Key.Key_Control:
|
|
self._modifier_pressed = False
|
|
self._enable_click_through(True)
|
|
self.drag_hint.setText("Hold Ctrl to drag")
|
|
self.drag_hint.setStyleSheet("font-size: 9px; color: #666666;")
|
|
super().keyReleaseEvent(event)
|
|
|
|
# ========================================================================
|
|
# SESSION MANAGEMENT
|
|
# ========================================================================
|
|
|
|
def start_session(self, weapon: str = "Unknown", loadout: str = "Default") -> None:
|
|
"""Start a new hunting/mining/crafting session.
|
|
|
|
Args:
|
|
weapon: Name of the current weapon
|
|
loadout: Name of the current loadout
|
|
"""
|
|
self._session_start = datetime.now()
|
|
self._stats = HUDStats() # Reset stats
|
|
self._stats.current_weapon = weapon
|
|
self._stats.current_loadout = loadout
|
|
self.session_active = True
|
|
self._timer.start(1000) # Update every second
|
|
self._refresh_display()
|
|
self.status_label.setText("● Live - Recording")
|
|
self.status_label.setStyleSheet("font-size: 9px; color: #7FFF7F;")
|
|
|
|
def end_session(self) -> None:
|
|
"""End the current session."""
|
|
self._timer.stop()
|
|
self._session_start = None
|
|
self.session_active = False
|
|
self._save_position() # Save final stats
|
|
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) -> None:
|
|
"""Called when loot is received from LogWatcher.
|
|
|
|
Args:
|
|
item_name: Name of the looted item
|
|
value_ped: Value in PED (Decimal for precision)
|
|
"""
|
|
if not self.session_active:
|
|
return
|
|
|
|
self._stats.loot_total += value_ped
|
|
# Count actual loot as kills (exclude Shrapnel and Universal Ammo)
|
|
if item_name not in ('Shrapnel', 'Universal Ammo'):
|
|
self._stats.kills += 1
|
|
|
|
self._refresh_display()
|
|
self.stats_updated.emit(self._stats.to_dict())
|
|
|
|
def on_damage_dealt(self, damage: float) -> None:
|
|
"""Called when damage is dealt.
|
|
|
|
Args:
|
|
damage: Amount of damage dealt
|
|
"""
|
|
if not self.session_active:
|
|
return
|
|
|
|
self._stats.damage_dealt += int(damage)
|
|
self._refresh_display()
|
|
self.stats_updated.emit(self._stats.to_dict())
|
|
|
|
def on_damage_taken(self, damage: float) -> None:
|
|
"""Called when damage is taken.
|
|
|
|
Args:
|
|
damage: Amount of damage taken
|
|
"""
|
|
if not self.session_active:
|
|
return
|
|
|
|
self._stats.damage_taken += int(damage)
|
|
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.
|
|
|
|
Args:
|
|
value_ped: Value of the global in PED
|
|
"""
|
|
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.
|
|
|
|
Args:
|
|
value_ped: Value of the HoF in PED
|
|
"""
|
|
if not self.session_active:
|
|
return
|
|
|
|
self._stats.hofs_count += 1
|
|
self._refresh_display()
|
|
self.stats_updated.emit(self._stats.to_dict())
|
|
|
|
def update_display(self) -> None:
|
|
"""Public method to refresh display (alias for _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.
|
|
Auto-hide HUD when game is not focused (optional feature).
|
|
"""
|
|
if not self._auto_hide_with_game or sys.platform != 'win32':
|
|
return
|
|
|
|
try:
|
|
# Get foreground window title
|
|
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
|
|
|
|
if self._debug_window_detection:
|
|
print(f"[HUD DEBUG] Foreground window: '{title}'")
|
|
|
|
# Check if game is in foreground (case insensitive)
|
|
is_game_focused = self._game_window_title.lower() in title.lower()
|
|
|
|
if is_game_focused:
|
|
# Game is focused - show HUD if needed
|
|
if not self.isVisible():
|
|
self.show()
|
|
if self._debug_window_detection:
|
|
print(f"[HUD DEBUG] Game focused - showing HUD")
|
|
else:
|
|
# Game not focused - hide HUD if visible
|
|
if self.isVisible():
|
|
self.hide()
|
|
if self._debug_window_detection:
|
|
print(f"[HUD DEBUG] Game unfocused - hiding HUD")
|
|
except Exception as e:
|
|
if self._debug_window_detection:
|
|
print(f"[HUD DEBUG] Error: {e}")
|
|
pass
|
|
|
|
def get_foreground_window_title(self) -> Optional[str]:
|
|
"""
|
|
Get the title of the current foreground window.
|
|
Useful for debugging window detection.
|
|
|
|
Returns:
|
|
Window title string or None if unavailable
|
|
"""
|
|
if sys.platform != 'win32':
|
|
return None
|
|
|
|
try:
|
|
hwnd = GetForegroundWindow()
|
|
if hwnd:
|
|
length = GetWindowTextLengthW(hwnd)
|
|
if length > 0:
|
|
buffer = ctypes.create_unicode_buffer(length + 1)
|
|
GetWindowTextW(hwnd, buffer, length + 1)
|
|
return buffer.value
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
def set_debug_window_detection(self, enabled: bool) -> None:
|
|
"""Enable/disable debug output for window detection."""
|
|
self._debug_window_detection = enabled
|
|
|
|
def set_auto_hide_with_game(self, enabled: bool) -> None:
|
|
"""
|
|
Enable/disable auto-hide when game not focused.
|
|
|
|
Args:
|
|
enabled: True to auto-hide HUD when game loses focus
|
|
"""
|
|
self._auto_hide_with_game = enabled
|
|
|
|
def _update_time_display(self) -> None:
|
|
"""Update the time label."""
|
|
total_seconds = int(self._stats.session_time.total_seconds())
|
|
hours = total_seconds // 3600
|
|
minutes = (total_seconds % 3600) // 60
|
|
seconds = total_seconds % 60
|
|
self.time_label.setText(f"{hours:02d}:{minutes:02d}:{seconds:02d}")
|
|
|
|
# ========================================================================
|
|
# STATS UPDATE INTERFACE
|
|
# ========================================================================
|
|
|
|
def update_stats(self, stats: Dict[str, Any]) -> None:
|
|
"""
|
|
Update HUD with new statistics.
|
|
|
|
This is the main interface for LogWatcher integration.
|
|
Called via signals/slots when new data arrives.
|
|
|
|
Args:
|
|
stats: Dictionary containing stat updates. Supported keys:
|
|
- 'loot': Decimal - Total loot value in PED
|
|
- 'loot_delta': Decimal - Add to existing loot
|
|
- 'damage_dealt': int - Total damage dealt (or add)
|
|
- 'damage_dealt_add': int - Add to damage dealt
|
|
- 'damage_taken': int - Total damage taken (or add)
|
|
- 'damage_taken_add': int - Add to damage taken
|
|
- 'kills': int - Total kills (or add)
|
|
- 'kills_add': int - Add to kills
|
|
- 'globals': int - Total globals (or add)
|
|
- 'globals_add': int - Add to globals
|
|
- 'hofs': int - Total HoFs (or add)
|
|
- 'hofs_add': int - Add to HoFs
|
|
- 'weapon': str - Current weapon name
|
|
- 'loadout': str - Current loadout name
|
|
"""
|
|
# Loot (Decimal precision)
|
|
if 'loot' in stats:
|
|
self._stats.loot_total = Decimal(str(stats['loot']))
|
|
elif 'loot_delta' in stats:
|
|
self._stats.loot_total += Decimal(str(stats['loot_delta']))
|
|
|
|
# Damage dealt
|
|
if 'damage_dealt' in stats:
|
|
self._stats.damage_dealt = int(stats['damage_dealt'])
|
|
elif 'damage_dealt_add' in stats:
|
|
self._stats.damage_dealt += int(stats['damage_dealt_add'])
|
|
|
|
# Damage taken
|
|
if 'damage_taken' in stats:
|
|
self._stats.damage_taken = int(stats['damage_taken'])
|
|
elif 'damage_taken_add' in stats:
|
|
self._stats.damage_taken += int(stats['damage_taken_add'])
|
|
|
|
# Kills
|
|
if 'kills' in stats:
|
|
self._stats.kills = int(stats['kills'])
|
|
elif 'kills_add' in stats:
|
|
self._stats.kills += int(stats['kills_add'])
|
|
|
|
# Globals
|
|
if 'globals' in stats:
|
|
self._stats.globals_count = int(stats['globals'])
|
|
elif 'globals_add' in stats:
|
|
self._stats.globals_count += int(stats['globals_add'])
|
|
|
|
# HoFs
|
|
if 'hofs' in stats:
|
|
self._stats.hofs_count = int(stats['hofs'])
|
|
elif 'hofs_add' in stats:
|
|
self._stats.hofs_count += int(stats['hofs_add'])
|
|
|
|
# Weapon
|
|
if 'weapon' in stats:
|
|
self._stats.current_weapon = str(stats['weapon'])
|
|
|
|
# Loadout
|
|
if 'loadout' in stats:
|
|
self._stats.current_loadout = str(stats['loadout'])
|
|
|
|
# Refresh display
|
|
self._refresh_display()
|
|
|
|
# Emit signal for external listeners
|
|
self.stats_updated.emit(self._stats.to_dict())
|
|
|
|
def _refresh_display(self) -> None:
|
|
"""Refresh all display labels with current stats."""
|
|
# Loot with 2 decimal places (PED format)
|
|
self.loot_value_label.setText(f"{self._stats.loot_total:.2f} PED")
|
|
|
|
# Kills
|
|
self.kills_value_label.setText(str(self._stats.kills))
|
|
|
|
# Globals / HoFs
|
|
self.globals_value_label.setText(
|
|
f"{self._stats.globals_count} / {self._stats.hofs_count}"
|
|
)
|
|
|
|
# Damage
|
|
self.dealt_value_label.setText(str(self._stats.damage_dealt))
|
|
self.taken_value_label.setText(str(self._stats.damage_taken))
|
|
|
|
# Weapon/Loadout
|
|
self.weapon_label.setText(self._stats.current_weapon[:20]) # Truncate long names
|
|
self.loadout_label.setText(self._stats.current_loadout[:15])
|
|
|
|
# Update time if session active
|
|
self._update_time_display()
|
|
|
|
def get_stats(self) -> HUDStats:
|
|
"""Get current stats."""
|
|
return self._stats
|
|
|
|
# ========================================================================
|
|
# WINDOW OVERRIDES
|
|
# ========================================================================
|
|
|
|
def showEvent(self, event) -> None:
|
|
"""Handle show event - ensure proper window attributes."""
|
|
# Re-apply click-through on show
|
|
if not self._modifier_pressed:
|
|
self._enable_click_through(True)
|
|
super().showEvent(event)
|
|
|
|
def moveEvent(self, event) -> None:
|
|
"""Handle move event - save position periodically."""
|
|
# Save position every 5 seconds of movement (throttled)
|
|
if not hasattr(self, '_last_save'):
|
|
self._last_save = 0
|
|
|
|
import time
|
|
current_time = time.time()
|
|
if current_time - self._last_save > 5:
|
|
self._save_position()
|
|
self._last_save = current_time
|
|
|
|
super().moveEvent(event)
|
|
|
|
def closeEvent(self, event) -> None:
|
|
"""Handle close event - save position before closing."""
|
|
self._save_position()
|
|
super().closeEvent(event)
|
|
|
|
|
|
# ============================================================================
|
|
# MOCK TESTING
|
|
# ============================================================================
|
|
|
|
def run_mock_test():
|
|
"""Run the HUD with mock data for testing."""
|
|
app = QApplication(sys.argv)
|
|
app.setQuitOnLastWindowClosed(False) # Keep running when closed
|
|
|
|
# Create HUD
|
|
hud = HUDOverlay()
|
|
hud.show()
|
|
|
|
# Start session
|
|
hud.start_session()
|
|
|
|
# Simulate incoming stats
|
|
from PyQt6.QtCore import QTimer
|
|
|
|
mock_stats = {
|
|
'loot': Decimal('0.0'),
|
|
'damage_dealt': 0,
|
|
'damage_taken': 0,
|
|
'kills': 0,
|
|
'globals': 0,
|
|
'hofs': 0,
|
|
'weapon': 'Omegaton M2100',
|
|
'loadout': 'Hunting Set A',
|
|
}
|
|
|
|
def simulate_event():
|
|
"""Simulate random game events."""
|
|
import random
|
|
|
|
event_type = random.choice(['loot', 'damage', 'kill', 'global'])
|
|
|
|
if event_type == 'loot':
|
|
value = Decimal(str(random.uniform(0.5, 15.0)))
|
|
mock_stats['loot'] += value
|
|
hud.update_stats({'loot': mock_stats['loot']})
|
|
print(f"[MOCK] Loot: {value:.2f} PED")
|
|
|
|
elif event_type == 'damage':
|
|
damage = random.randint(5, 50)
|
|
mock_stats['damage_dealt'] += damage
|
|
hud.update_stats({'damage_dealt': mock_stats['damage_dealt']})
|
|
print(f"[MOCK] Damage dealt: {damage}")
|
|
|
|
elif event_type == 'kill':
|
|
mock_stats['kills'] += 1
|
|
hud.update_stats({'kills': mock_stats['kills']})
|
|
print(f"[MOCK] Kill! Total: {mock_stats['kills']}")
|
|
|
|
elif event_type == 'global':
|
|
mock_stats['globals'] += 1
|
|
hud.update_stats({'globals': mock_stats['globals']})
|
|
print(f"[MOCK] GLOBAL!!! Count: {mock_stats['globals']}")
|
|
|
|
# Simulate events every 3 seconds
|
|
timer = QTimer()
|
|
timer.timeout.connect(simulate_event)
|
|
timer.start(3000)
|
|
|
|
print("\n" + "="*60)
|
|
print("🍋 LEMONTROPIA HUD - MOCK TEST MODE")
|
|
print("="*60)
|
|
print("HUD should be visible on screen (top-right default)")
|
|
print("Features to test:")
|
|
print(" ✓ Frameless window with gold border")
|
|
print(" ✓ Transparent background")
|
|
print(" ✓ Always on top")
|
|
print(" ✓ Click-through (clicks pass to window below)")
|
|
print(" ✓ Hold Ctrl to drag the HUD")
|
|
print(" ✓ Stats update every 3 seconds (mock data)")
|
|
print("\nPress Ctrl+C in terminal to exit")
|
|
print("="*60 + "\n")
|
|
|
|
sys.exit(app.exec())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
run_mock_test()
|