1290 lines
48 KiB
Python
1290 lines
48 KiB
Python
# 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')
|
|
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
|
|
kills: int = 0
|
|
globals_count: int = 0
|
|
hofs_count: int = 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"
|
|
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
|
|
self.cost_total = (
|
|
self.weapon_cost_total +
|
|
self.armor_cost_total +
|
|
self.healing_cost_total +
|
|
self.plates_cost_total +
|
|
self.enhancer_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')
|
|
|
|
|
|
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
|
|
|
|
# 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)
|
|
|
|
# === 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") -> None:
|
|
"""
|
|
Start a new hunting session.
|
|
|
|
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
|
|
"""
|
|
self._session_start = datetime.now()
|
|
self._stats = HUDStats()
|
|
self._stats.current_weapon = weapon
|
|
self._stats.current_loadout = loadout
|
|
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.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 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')
|
|
"""
|
|
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
|
|
|
|
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}")
|
|
|
|
# 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()
|