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