From 95a511bbfdc3c955fd43fdf69eff95f15c19fb22 Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Mon, 9 Feb 2026 10:51:32 +0000 Subject: [PATCH] feat(gear): add real healing tools and Frontier armor set - LoadoutManager now uses real healing_tools.py database (25+ tools) - Added categorized healing tools: Medical Tools and Restoration Chips - Added Frontier Set (Adjusted) with all 7 pieces - Includes Regeneration Chip IV (L) for test run --- core/armor_system.py | 78 ++++ ui/hud_overlay.py | 832 +++++++++++++++++++++++------------------- ui/loadout_manager.py | 61 +++- 3 files changed, 582 insertions(+), 389 deletions(-) diff --git a/core/armor_system.py b/core/armor_system.py index bfc83ca..0576121 100644 --- a/core/armor_system.py +++ b/core/armor_system.py @@ -1191,6 +1191,83 @@ def get_mock_plates() -> List[ArmorPlate]: # Helper Functions # ============================================================================ +def create_frontier_set() -> ArmorSet: + """Create the Frontier armor set (Arkadia mid-level, good mobility).""" + # Frontier Adjusted has 3800 durability = 20.83 hp/pec economy + frontier_economy = Decimal("0.048") # ~20.8 hp/pec + + pieces = { + ArmorSlot.HEAD: ArmorPiece( + name="Frontier Helmet, Adjusted", + item_id="frontier_helmet_adj", + slot=ArmorSlot.HEAD, + set_name="Frontier", + decay_per_hp=frontier_economy, + protection=ProtectionProfile(impact=Decimal("4"), cut=Decimal("3"), stab=Decimal("3")), + weight=Decimal("0.5"), + ), + ArmorSlot.CHEST: ArmorPiece( + name="Frontier Harness, Adjusted", + item_id="frontier_harness_adj", + slot=ArmorSlot.CHEST, + set_name="Frontier", + decay_per_hp=frontier_economy, + protection=ProtectionProfile(impact=Decimal("8"), cut=Decimal("6"), stab=Decimal("6")), + weight=Decimal("1.0"), + ), + ArmorSlot.LEFT_ARM: ArmorPiece( + name="Frontier Arm Guards, Adjusted (L)", + item_id="frontier_arm_l_adj", + slot=ArmorSlot.LEFT_ARM, + set_name="Frontier", + decay_per_hp=frontier_economy, + protection=ProtectionProfile(impact=Decimal("3"), cut=Decimal("2"), stab=Decimal("2")), + weight=Decimal("0.4"), + ), + ArmorSlot.RIGHT_ARM: ArmorPiece( + name="Frontier Arm Guards, Adjusted (R)", + item_id="frontier_arm_r_adj", + slot=ArmorSlot.RIGHT_ARM, + set_name="Frontier", + decay_per_hp=frontier_economy, + protection=ProtectionProfile(impact=Decimal("3"), cut=Decimal("2"), stab=Decimal("2")), + weight=Decimal("0.4"), + ), + ArmorSlot.LEFT_HAND: ArmorPiece( + name="Frontier Gloves, Adjusted (L)", + item_id="frontier_gloves_l_adj", + slot=ArmorSlot.LEFT_HAND, + set_name="Frontier", + decay_per_hp=frontier_economy, + protection=ProtectionProfile(impact=Decimal("2"), cut=Decimal("1"), stab=Decimal("1")), + weight=Decimal("0.3"), + ), + ArmorSlot.RIGHT_HAND: ArmorPiece( + name="Frontier Gloves, Adjusted (R)", + item_id="frontier_gloves_r_adj", + slot=ArmorSlot.RIGHT_HAND, + set_name="Frontier", + decay_per_hp=frontier_economy, + protection=ProtectionProfile(impact=Decimal("2"), cut=Decimal("1"), stab=Decimal("1")), + weight=Decimal("0.3"), + ), + ArmorSlot.LEGS: ArmorPiece( + name="Frontier Thigh+Shin Guards, Adjusted", + item_id="frontier_legs_adj", + slot=ArmorSlot.LEGS, + set_name="Frontier", + decay_per_hp=frontier_economy, + protection=ProtectionProfile(impact=Decimal("7"), cut=Decimal("5"), stab=Decimal("5")), + weight=Decimal("1.1"), + ), + } + return ArmorSet( + name="Frontier Set (Adjusted)", + set_id="frontier_set_adj", + pieces=pieces, + ) + + def get_all_armor_sets() -> List[ArmorSet]: """Get all available armor sets.""" return [ @@ -1198,6 +1275,7 @@ def get_all_armor_sets() -> List[ArmorSet]: create_vigilante_set(), create_ghost_set(), create_shogun_set(), + create_frontier_set(), create_hermes_set(), ] diff --git a/ui/hud_overlay.py b/ui/hud_overlay.py index 736bc78..0e416f9 100644 --- a/ui/hud_overlay.py +++ b/ui/hud_overlay.py @@ -1,5 +1,6 @@ # 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 @@ -7,8 +8,8 @@ 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 +from dataclasses import dataclass, asdict, field +from typing import Optional, Dict, Any, List # Windows-specific imports for click-through support if sys.platform == 'win32': @@ -50,57 +51,85 @@ from PyQt6.QtGui import QFont, QColor, QPalette, QMouseEvent @dataclass class HUDStats: - """Data structure for HUD statistics.""" - session_time: timedelta = timedelta(0) + """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') - cost_total: Decimal = Decimal('0.0') # Weapon decay + ammo - healing_cost_total: Decimal = Decimal('0.0') # NEW: Healing/FAP decay cost - profit_loss: Decimal = Decimal('0.0') # loot - cost - healing_cost + 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: int = 0 - damage_taken: int = 0 + 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 - # Healing stats (NEW) - healing_done: Decimal = Decimal('0.0') # Total HP healed - heals_count: int = 0 # Number of heal actions + # 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_medical_tool: str = "None" # NEW: Current FAP + 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') - medical_tool_decay: Decimal = Decimal('0.0') # NEW: FAP decay per use in PEC + 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), - 'cost_total': str(self.cost_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), - 'damage_dealt': self.damage_dealt, - 'damage_taken': self.damage_taken, + '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, - 'healing_done': str(self.healing_done), - 'heals_count': self.heals_count, + 'cost_per_kill': str(self.cost_per_kill), + 'dpp': str(self.dpp), 'current_weapon': self.current_weapon, 'current_loadout': self.current_loadout, - 'current_medical_tool': self.current_medical_tool, + '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), - 'medical_tool_decay': str(self.medical_tool_decay), + 'armor_durability': self.armor_durability, } @classmethod @@ -109,24 +138,66 @@ class HUDStats: return cls( session_time=timedelta(seconds=data.get('session_time_seconds', 0)), loot_total=Decimal(data.get('loot_total', '0.0')), - cost_total=Decimal(data.get('cost_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')), - damage_dealt=data.get('damage_dealt', 0), - damage_taken=data.get('damage_taken', 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), - healing_done=Decimal(data.get('healing_done', '0.0')), - heals_count=data.get('heals_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_medical_tool=data.get('current_medical_tool', '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')), - medical_tool_decay=Decimal(data.get('medical_tool_decay', '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): @@ -141,6 +212,7 @@ class HUDOverlay(QWidget): - 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 @@ -177,18 +249,21 @@ class HUDOverlay(QWidget): # Session tracking self._session_start: Optional[datetime] = None self._stats = HUDStats() - self.session_active = False # Public flag for session state + 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 (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 + # 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 # Set to True to debug + self._debug_window_detection = False # Timer for session time updates self._timer = QTimer(self) @@ -198,7 +273,7 @@ class HUDOverlay(QWidget): 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._window_check_timer.start(500) self._setup_window() self._setup_ui() @@ -219,17 +294,17 @@ class HUDOverlay(QWidget): # Enable mouse tracking for hover detection self.setMouseTracking(True) - # Size - increased to accommodate healing stats row - self.setFixedSize(320, 260) + # Size - increased to accommodate all rows + self.setFixedSize(340, 320) - # Accept focus for keyboard events (needed for modifier detection) + # 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 - increased height for healing row + # Main container with semi-transparent background self.container = QFrame(self) - self.container.setFixedSize(320, 260) + self.container.setFixedSize(340, 320) self.container.setObjectName("hudContainer") # Style the container - semi-transparent dark background @@ -244,8 +319,8 @@ class HUDOverlay(QWidget): font-family: 'Segoe UI', 'Arial', sans-serif; } .stat-label { - font-size: 11px; - color: #CCCCCC; + font-size: 10px; + color: #AAAAAA; } .stat-value { font-size: 13px; @@ -258,7 +333,7 @@ class HUDOverlay(QWidget): color: #FFD700; } .subheader { - font-size: 10px; + font-size: 9px; color: #888888; } .positive { @@ -267,32 +342,35 @@ class HUDOverlay(QWidget): .negative { color: #FF7F7F; } + .neutral { + color: #FFFFFF; + } """) # Main layout layout = QVBoxLayout(self.container) - layout.setContentsMargins(12, 10, 12, 10) - layout.setSpacing(6) + 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: 14px; font-weight: bold; color: #FFD700;") + 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: 13px; font-weight: bold; color: #00FFFF;") + self.time_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #00FFFF;") header_layout.addWidget(self.time_label) layout.addLayout(header_layout) - # Drag hint (only visible on hover) + # Drag hint self.drag_hint = QLabel("Hold Ctrl to drag") - self.drag_hint.setStyleSheet("font-size: 9px; color: #666666;") + self.drag_hint.setStyleSheet("font-size: 8px; color: #666666;") self.drag_hint.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(self.drag_hint) @@ -306,58 +384,76 @@ class HUDOverlay(QWidget): # === FINANCIALS ROW === row0 = QHBoxLayout() - # Loot + # Loot (marketable only, excluding shrapnel) loot_layout = QVBoxLayout() loot_label = QLabel("💰 LOOT") - loot_label.setStyleSheet("font-size: 10px; color: #888888;") + 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: 14px; font-weight: bold; color: #7FFF7F;") + 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() - # Cost + # Total Cost cost_layout = QVBoxLayout() cost_label = QLabel("💸 COST") - cost_label.setStyleSheet("font-size: 10px; color: #888888;") + 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: 14px; font-weight: bold; color: #FF7F7F;") + 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 + # Profit/Loss with return % profit_layout = QVBoxLayout() profit_label = QLabel("📊 P/L") - profit_label.setStyleSheet("font-size: 10px; color: #888888;") + 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: 14px; font-weight: bold; color: #FFFFFF;") + 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) - # === STATS GRID === - # Row 1: Damage & Kills & Globals + # === 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 (Estimated) + # Kills kills_layout = QVBoxLayout() - kills_label = QLabel("💀 EST. KILLS") - kills_label.setStyleSheet("font-size: 10px; color: #888888;") + 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: 14px; font-weight: bold; color: #FFFFFF;") + self.kills_value_label.setStyleSheet("font-size: 13px; font-weight: bold; color: #FFFFFF;") kills_layout.addWidget(self.kills_value_label) row1.addLayout(kills_layout) @@ -365,43 +461,42 @@ class HUDOverlay(QWidget): # Globals/HoFs globals_layout = QVBoxLayout() - globals_label = QLabel("🌍 GLOBALS") - globals_label.setStyleSheet("font-size: 10px; color: #888888;") + 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: 14px; font-weight: bold; color: #FFD700;") + 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) - # Row 2: Shots, Damage Dealt, Damage Taken + # === COMBAT STATS ROW 2 === row2 = QHBoxLayout() - # Shots Fired - shots_layout = QVBoxLayout() - shots_label = QLabel("🔫 SHOTS") - shots_label.setStyleSheet("font-size: 10px; color: #888888;") - shots_layout.addWidget(shots_label) - - self.shots_value_label = QLabel("0") - self.shots_value_label.setStyleSheet("font-size: 13px; font-weight: bold; color: #FFD700;") - shots_layout.addWidget(self.shots_value_label) - - row2.addLayout(shots_layout) - row2.addStretch() - # Damage Dealt dealt_layout = QVBoxLayout() dealt_label = QLabel("⚔️ DEALT") - dealt_label.setStyleSheet("font-size: 10px; color: #888888;") + 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: 13px; font-weight: bold; color: #7FFF7F;") + self.dealt_value_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #7FFF7F;") dealt_layout.addWidget(self.dealt_value_label) row2.addLayout(dealt_layout) @@ -410,98 +505,119 @@ class HUDOverlay(QWidget): # Damage Taken taken_layout = QVBoxLayout() taken_label = QLabel("🛡️ TAKEN") - taken_label.setStyleSheet("font-size: 10px; color: #888888;") + 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: 13px; font-weight: bold; color: #FF7F7F;") + 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) - # Row 3: Healing Stats (NEW) + # === COST BREAKDOWN ROW === row3 = QHBoxLayout() - # Healing Done (HP) - healing_layout = QVBoxLayout() - healing_label = QLabel("❤️ HEALING") - healing_label.setStyleSheet("font-size: 10px; color: #888888;") - healing_layout.addWidget(healing_label) + # 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.healing_value_label = QLabel("0 HP") - self.healing_value_label.setStyleSheet("font-size: 13px; font-weight: bold; color: #7FFF7F;") - healing_layout.addWidget(self.healing_value_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(healing_layout) - row3.addStretch() + row3.addLayout(wep_cost_layout) - # Number of Heals - heals_layout = QVBoxLayout() - heals_label = QLabel("💉 HEALS") - heals_label.setStyleSheet("font-size: 10px; color: #888888;") - heals_layout.addWidget(heals_label) + # 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.heals_value_label = QLabel("0") - self.heals_value_label.setStyleSheet("font-size: 13px; font-weight: bold; color: #FFFFFF;") - heals_layout.addWidget(self.heals_value_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(heals_layout) - row3.addStretch() + row3.addLayout(arm_cost_layout) - # Healing Cost + # Healing cost heal_cost_layout = QVBoxLayout() - heal_cost_label = QLabel("💊 HEAL COST") - heal_cost_label.setStyleSheet("font-size: 10px; color: #888888;") + heal_cost_label = QLabel("💊 FAP") + heal_cost_label.setStyleSheet("font-size: 8px; color: #888888;") heal_cost_layout.addWidget(heal_cost_label) - self.healing_cost_value_label = QLabel("0.00 PED") - self.healing_cost_value_label.setStyleSheet("font-size: 13px; font-weight: bold; color: #FF7F7F;") - heal_cost_layout.addWidget(self.healing_cost_value_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) - row3.addStretch() + + # 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) - # === 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) + # === 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) - weapon_layout = QHBoxLayout() + equip_layout = QHBoxLayout() - weapon_icon = QLabel("🔫") - weapon_icon.setStyleSheet("font-size: 12px;") - weapon_layout.addWidget(weapon_icon) + # Weapon + self.weapon_label = QLabel("🔫 None") + self.weapon_label.setStyleSheet("font-size: 10px; color: #CCCCCC;") + equip_layout.addWidget(self.weapon_label) - self.weapon_label = QLabel("No weapon") - self.weapon_label.setStyleSheet("font-size: 11px; color: #CCCCCC;") - weapon_layout.addWidget(self.weapon_label) + equip_layout.addStretch() - # DPP display - self.dpp_label = QLabel("DPP: --") - self.dpp_label.setStyleSheet("font-size: 10px; color: #FFD700;") - weapon_layout.addWidget(self.dpp_label) + # Armor + self.armor_label = QLabel("🛡️ None") + self.armor_label.setStyleSheet("font-size: 10px; color: #CCCCCC;") + equip_layout.addWidget(self.armor_label) - weapon_layout.addStretch() + equip_layout.addStretch() + # Loadout loadout_label = QLabel("Loadout:") - loadout_label.setStyleSheet("font-size: 10px; color: #888888;") - weapon_layout.addWidget(loadout_label) + loadout_label.setStyleSheet("font-size: 9px; color: #888888;") + equip_layout.addWidget(loadout_label) - self.loadout_label = QLabel("None") - self.loadout_label.setStyleSheet("font-size: 11px; color: #00FFFF;") - weapon_layout.addWidget(self.loadout_label) + self.loadout_label = QLabel("Default") + self.loadout_label.setStyleSheet("font-size: 10px; color: #00FFFF;") + equip_layout.addWidget(self.loadout_label) - layout.addLayout(weapon_layout) + layout.addLayout(equip_layout) # === STATUS BAR === - self.status_label = QLabel("● Live") + 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) @@ -528,7 +644,7 @@ class HUDOverlay(QWidget): else: # Default position: top-right of screen screen = QApplication.primaryScreen().geometry() - self.move(screen.width() - 350, 50) + self.move(screen.width() - 360, 50) except Exception as e: # Default position on error self.move(100, 100) @@ -544,7 +660,7 @@ class HUDOverlay(QWidget): with open(self.config_path, 'w') as f: json.dump(config, f, indent=2) except Exception as e: - pass # Silent fail on save error + pass # ======================================================================== # MOUSE HANDLING (Drag Support) @@ -553,29 +669,25 @@ class HUDOverlay(QWidget): 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 + self._enable_click_through(False) 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;") + self.drag_hint.setStyleSheet("font-size: 8px; color: #FFD700;") event.ignore() def mouseReleaseEvent(self, event: QMouseEvent) -> None: @@ -584,7 +696,6 @@ class HUDOverlay(QWidget): 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: @@ -592,41 +703,22 @@ class HUDOverlay(QWidget): def leaveEvent(self, event) -> None: """Handle mouse leave - reset drag hint.""" - self.drag_hint.setStyleSheet("font-size: 9px; color: #666666;") + 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. - - 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. - """ + """Enable or disable click-through behavior.""" 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 - """ + """Enable/disable click-through on Windows using WinAPI.""" GWL_EXSTYLE = -20 WS_EX_TRANSPARENT = 0x00000020 WS_EX_LAYERED = 0x00080000 - # Window refresh flags SWP_FRAMECHANGED = 0x0020 SWP_NOMOVE = 0x0002 SWP_NOSIZE = 0x0001 @@ -635,8 +727,6 @@ class HUDOverlay(QWidget): try: hwnd = self.winId().__int__() - - # Get current extended style style = ctypes.windll.user32.GetWindowLongW(hwnd, GWL_EXSTYLE) if enabled: @@ -645,14 +735,11 @@ class HUDOverlay(QWidget): 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: @@ -661,7 +748,7 @@ class HUDOverlay(QWidget): 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;") + self.drag_hint.setStyleSheet("font-size: 8px; color: #00FF00;") super().keyPressEvent(event) def keyReleaseEvent(self, event) -> None: @@ -670,7 +757,7 @@ class HUDOverlay(QWidget): 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;") + self.drag_hint.setStyleSheet("font-size: 8px; color: #666666;") super().keyReleaseEvent(event) # ======================================================================== @@ -679,47 +766,84 @@ class HUDOverlay(QWidget): def start_session(self, weapon: str = "Unknown", loadout: str = "Default", weapon_dpp: Decimal = Decimal('0.0'), - weapon_cost_per_hour: Decimal = Decimal('0.0')) -> None: - """Start a new hunting/mining/crafting session. + 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() # Reset stats + 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 - self._timer.start(1000) # Update every second + + # 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) -> None: - """Update total cost spent. + 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 - self._stats.cost_total += cost_ped - self._stats.profit_loss = self._stats.loot_total - self._stats.cost_total + 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() # Save final stats + self._save_position() self.status_label.setText("○ Paused") self.status_label.setStyleSheet("font-size: 9px; color: #888888;") @@ -727,26 +851,55 @@ class HUDOverlay(QWidget): # EVENT HANDLERS (Called from LogWatcher) # ======================================================================== - def on_loot_event(self, item_name: str, value_ped: Decimal) -> None: - """Called when loot is received 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 - # Note: Kill counting should be based on actual kill messages from log - # For now, we don't auto-increment kills on loot to avoid overcounting - # (Multiple items from one kill would count as multiple kills) + # 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_damage_dealt(self, damage: float) -> None: - """Called when damage is dealt. + 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 @@ -754,29 +907,36 @@ class HUDOverlay(QWidget): if not self.session_active: return - self._stats.damage_dealt += int(damage) + 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: float) -> None: - """Called when damage is taken. + 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 + damage: Amount of damage taken (absorbed by armor) """ if not self.session_active: return - self._stats.damage_taken += int(damage) + 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. - - Args: - value_ped: Value of the global in PED - """ + """Called on global event.""" if not self.session_active: return @@ -785,11 +945,7 @@ class HUDOverlay(QWidget): 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 - """ + """Called on Hall of Fame event.""" if not self.session_active: return @@ -798,7 +954,8 @@ class HUDOverlay(QWidget): 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. + """ + Called when healing is done from LogWatcher. Args: heal_amount: Amount of HP healed @@ -808,23 +965,16 @@ class HUDOverlay(QWidget): return self._stats.healing_done += heal_amount - self._stats.heals_count += 1 - # Add healing cost to total healing cost and update profit/loss if decay_cost > 0: self._stats.healing_cost_total += decay_cost - # Recalculate profit/loss including healing costs - self._stats.profit_loss = ( - self._stats.loot_total - - self._stats.cost_total - - self._stats.healing_cost_total - ) + self._stats.recalculate() 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).""" + """Public method to refresh display.""" self._refresh_display() def _update_session_time(self) -> None: @@ -834,15 +984,11 @@ class HUDOverlay(QWidget): 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). - """ + """Check if Entropia Universe window is in foreground.""" if not self._auto_hide_with_game or sys.platform != 'win32': return try: - # Get foreground window title hwnd = GetForegroundWindow() if hwnd: length = GetWindowTextLengthW(hwnd) @@ -851,64 +997,16 @@ class HUDOverlay(QWidget): 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.""" @@ -926,166 +1024,133 @@ class HUDOverlay(QWidget): """ 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 - - 'healing_done': Decimal - Total HP healed (or add) - - 'healing_add': Decimal - Add to healing done - - 'heals_count': int - Total number of heals (or add) - - 'heals_add': int - Add to heal count - - 'healing_cost': Decimal - Total healing cost in PED (or add) - - 'healing_cost_add': Decimal - Add to healing cost - - '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 - - 'medical_tool': str - Current medical tool (FAP) name + stats: Dictionary containing stat updates """ - # Loot (Decimal precision) + # Loot if 'loot' in stats: - self._stats.loot_total = Decimal(str(stats['loot'])) + self._stats.loot_other = Decimal(str(stats['loot'])) elif 'loot_delta' in stats: - self._stats.loot_total += Decimal(str(stats['loot_delta'])) + self._stats.loot_other += Decimal(str(stats['loot_delta'])) - # Damage dealt + 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 = int(stats['damage_dealt']) + self._stats.damage_dealt = Decimal(str(stats['damage_dealt'])) elif 'damage_dealt_add' in stats: - self._stats.damage_dealt += int(stats['damage_dealt_add']) + self._stats.damage_dealt += Decimal(str(stats['damage_dealt_add'])) - # Damage taken if 'damage_taken' in stats: - self._stats.damage_taken = int(stats['damage_taken']) + self._stats.damage_taken = Decimal(str(stats['damage_taken'])) elif 'damage_taken_add' in stats: - self._stats.damage_taken += int(stats['damage_taken_add']) + self._stats.damage_taken += Decimal(str(stats['damage_taken_add'])) - # Healing done (NEW) + # 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'])) - # Healing count (NEW) - if 'heals_count' in stats: - self._stats.heals_count = int(stats['heals_count']) - elif 'heals_add' in stats: - self._stats.heals_count += int(stats['heals_add']) - - # Healing cost (NEW) 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 fired + # 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']) - # Kills if 'kills' in stats: self._stats.kills = int(stats['kills']) elif 'kills_add' in stats: self._stats.kills += int(stats['kills_add']) - # Globals + # 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']) - # 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 + # Equipment if 'weapon' in stats: self._stats.current_weapon = str(stats['weapon']) - - # Loadout + 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']) - # Medical Tool (NEW) - if 'medical_tool' in stats: - self._stats.current_medical_tool = str(stats['medical_tool']) - - # Recalculate profit/loss if costs changed - if any(k in stats for k in ['loot', 'loot_delta', 'healing_cost', 'healing_cost_add']): - self._stats.profit_loss = ( - self._stats.loot_total - - self._stats.cost_total - - self._stats.healing_cost_total - ) - - # Refresh display + self._stats.recalculate() 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") + # Loot (marketable only) + self.loot_value_label.setText(f"{self._stats.loot_other:.2f} PED") - # Cost with 2 decimal places (weapon cost only) + # Total Cost self.cost_value_label.setText(f"{self._stats.cost_total:.2f} PED") - # Profit/Loss with color coding (includes healing costs) + # Profit/Loss with color coding profit = self._stats.profit_loss - self.profit_value_label.setText(f"{profit:+.2f} PED") + self.profit_value_label.setText(f"{profit:+.2f}") if profit > 0: - self.profit_value_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #7FFF7F;") # Green + self.profit_value_label.setStyleSheet("font-size: 13px; font-weight: bold; color: #7FFF7F;") elif profit < 0: - self.profit_value_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #FF7F7F;") # Red + self.profit_value_label.setStyleSheet("font-size: 13px; font-weight: bold; color: #FF7F7F;") else: - self.profit_value_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #FFFFFF;") # White + self.profit_value_label.setStyleSheet("font-size: 13px; font-weight: bold; color: #FFFFFF;") - # Kills + # 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}") - # Globals / HoFs - self.globals_value_label.setText( - f"{self._stats.globals_count} / {self._stats.hofs_count}" - ) - - # Shots Fired + 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)) - # Damage - self.dealt_value_label.setText(str(self._stats.damage_dealt)) - self.taken_value_label.setText(str(self._stats.damage_taken)) + # 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}") - # Healing Stats (NEW) - self.healing_value_label.setText(f"{self._stats.healing_done:.0f} HP") - self.heals_value_label.setText(str(self._stats.heals_count)) - self.healing_cost_value_label.setText(f"{self._stats.healing_cost_total:.2f} PED") + # 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 - # Weapon/Loadout/DPP - self.weapon_label.setText(self._stats.current_weapon[:20]) - self.loadout_label.setText(self._stats.current_loadout[:15]) - if self._stats.weapon_dpp > 0: - self.dpp_label.setText(f"DPP: {self._stats.weapon_dpp:.2f}") - else: - self.dpp_label.setText("DPP: --") + 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 if session active + # Update time self._update_time_display() def get_stats(self) -> HUDStats: @@ -1098,14 +1163,12 @@ class HUDOverlay(QWidget): 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 @@ -1130,34 +1193,39 @@ class HUDOverlay(QWidget): def run_mock_test(): """Run the HUD with mock data for testing.""" app = QApplication(sys.argv) - app.setQuitOnLastWindowClosed(False) # Keep running when closed + app.setQuitOnLastWindowClosed(False) # Create HUD hud = HUDOverlay() hud.show() # Start session - hud.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'), - 'damage_dealt': 0, - 'damage_taken': 0, + 'shrapnel': Decimal('0.0'), + 'damage_dealt': Decimal('0.0'), + 'damage_taken': Decimal('0.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']) + event_type = random.choice(['loot', 'damage', 'kill', 'global', 'shrapnel', 'armor_hit']) if event_type == 'loot': value = Decimal(str(random.uniform(0.5, 15.0))) @@ -1165,12 +1233,25 @@ def run_mock_test(): 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 = random.randint(5, 50) + damage = Decimal(str(random.randint(5, 50))) mock_stats['damage_dealt'] += damage - hud.update_stats({'damage_dealt': mock_stats['damage_dealt']}) + 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']}) @@ -1197,6 +1278,7 @@ def run_mock_test(): 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") diff --git a/ui/loadout_manager.py b/ui/loadout_manager.py index 30473b9..7b1f88b 100644 --- a/ui/loadout_manager.py +++ b/ui/loadout_manager.py @@ -286,19 +286,35 @@ class LoadoutConfig: # ============================================================================ -# Mock Data for Healing +# Healing Tools Data - Using real database # ============================================================================ -MOCK_HEALING = [ - {"name": "Vivo T10", "cost": Decimal("2.0"), "amount": Decimal("12")}, - {"name": "Vivo T15", "cost": Decimal("3.5"), "amount": Decimal("18")}, - {"name": "Vivo S10", "cost": Decimal("4.0"), "amount": Decimal("25")}, - {"name": "Refurbished H.E.A.R.T.", "cost": Decimal("1.5"), "amount": Decimal("8")}, - {"name": "Restoration Chip I", "cost": Decimal("5.0"), "amount": Decimal("30")}, - {"name": "Restoration Chip II", "cost": Decimal("8.0"), "amount": Decimal("50")}, - {"name": "Restoration Chip III", "cost": Decimal("12.0"), "amount": Decimal("80")}, - {"name": "Mod 2350", "cost": Decimal("15.0"), "amount": Decimal("100")}, -] +def get_healing_tools_data(): + """Get healing tools from the real database.""" + try: + from core.healing_tools import HEALING_TOOLS + return [ + { + "name": tool.name, + "cost": tool.decay_pec, + "amount": tool.heal_amount, + "is_chip": tool.is_chip + } + for tool in HEALING_TOOLS + ] + except ImportError: + # Fallback to mock data if import fails + return [ + {"name": "Vivo T10", "cost": Decimal("0.815"), "amount": Decimal("10")}, + {"name": "Vivo S10", "cost": Decimal("1.705"), "amount": Decimal("21")}, + {"name": "Hedoc MM10", "cost": Decimal("2.09"), "amount": Decimal("44")}, + {"name": "Adjusted Restoration Chip", "cost": Decimal("2.88"), "amount": Decimal("60")}, + {"name": "Restoration Chip IV (L)", "cost": Decimal("2.8"), "amount": Decimal("45")}, + ] + + +# Legacy mock data for compatibility +MOCK_HEALING = get_healing_tools_data() # ============================================================================ @@ -1310,11 +1326,28 @@ class LoadoutManagerDialog(QDialog): self.armor_set_combo.addItem(display, armor_set) def _populate_healing_data(self): - """Populate healing combo with data.""" + """Populate healing combo with real data from database.""" self.heal_combo.clear() self.heal_combo.addItem("-- Custom --") - for heal in MOCK_HEALING: - self.heal_combo.addItem(heal["name"]) + + # Get real healing tools + healing_tools = get_healing_tools_data() + + # Sort by category (chips last) + medical_tools = [h for h in healing_tools if not h.get("is_chip", False)] + chips = [h for h in healing_tools if h.get("is_chip", False)] + + # Add medical tools first + if medical_tools: + self.heal_combo.addItem("--- Medical Tools ---") + for tool in medical_tools: + self.heal_combo.addItem(tool["name"]) + + # Add restoration chips + if chips: + self.heal_combo.addItem("--- Restoration Chips ---") + for chip in sorted(chips, key=lambda x: x["amount"]): + self.heal_combo.addItem(chip["name"]) def _on_select_weapon(self): """Open weapon selector dialog."""