From 7c38b398f34e2ca9ad354af7ec2676894a1bbb4f Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Sun, 8 Feb 2026 23:22:13 +0000 Subject: [PATCH] feat(hud): cost tracking and profit/loss calculation - HUD now shows: LOOT, COST, and PROFIT/LOSS (P/L) - Profit/Loss color-coded: Green=profit, Red=loss, White=break-even - Cost estimated from weapon DPP and damage dealt - Weapon stats (DPP, cost/hour) passed when starting session - Tracks cost per shot based on damage output - All stats persisted in HUDStats dataclass --- ui/hud_overlay.py | 103 +++++++++++++++++++++++++++++++++++++--------- ui/main_window.py | 31 ++++++++++++-- 2 files changed, 110 insertions(+), 24 deletions(-) diff --git a/ui/hud_overlay.py b/ui/hud_overlay.py index 476b162..82388a1 100644 --- a/ui/hud_overlay.py +++ b/ui/hud_overlay.py @@ -52,20 +52,32 @@ from PyQt6.QtGui import QFont, QColor, QPalette, QMouseEvent class HUDStats: """Data structure for HUD statistics.""" session_time: timedelta = timedelta(0) + + # Financial tracking loot_total: Decimal = Decimal('0.0') + cost_total: Decimal = Decimal('0.0') # Weapon decay + ammo + profit_loss: Decimal = Decimal('0.0') # loot - cost + + # Combat stats damage_dealt: int = 0 damage_taken: int = 0 kills: int = 0 globals_count: int = 0 hofs_count: int = 0 + + # Current gear current_weapon: str = "None" current_loadout: str = "None" + weapon_dpp: Decimal = Decimal('0.0') + weapon_cost_per_hour: Decimal = Decimal('0.0') 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), + 'profit_loss': str(self.profit_loss), 'damage_dealt': self.damage_dealt, 'damage_taken': self.damage_taken, 'kills': self.kills, @@ -73,6 +85,8 @@ class HUDStats: 'hofs_count': self.hofs_count, 'current_weapon': self.current_weapon, 'current_loadout': self.current_loadout, + 'weapon_dpp': str(self.weapon_dpp), + 'weapon_cost_per_hour': str(self.weapon_cost_per_hour), } @classmethod @@ -81,6 +95,8 @@ 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')), + profit_loss=Decimal(data.get('profit_loss', '0.0')), damage_dealt=data.get('damage_dealt', 0), damage_taken=data.get('damage_taken', 0), kills=data.get('kills', 0), @@ -88,6 +104,8 @@ class HUDStats: hofs_count=data.get('hofs_count', 0), current_weapon=data.get('current_weapon', 'None'), current_loadout=data.get('current_loadout', 'None'), + weapon_dpp=Decimal(data.get('weapon_dpp', '0.0')), + weapon_cost_per_hour=Decimal(data.get('weapon_cost_per_hour', '0.0')), ) @@ -265,9 +283,8 @@ class HUDOverlay(QWidget): separator.setFixedHeight(1) layout.addWidget(separator) - # === STATS GRID === - # Row 1: Loot & Kills - row1 = QHBoxLayout() + # === FINANCIALS ROW === + row0 = QHBoxLayout() # Loot loot_layout = QVBoxLayout() @@ -279,26 +296,39 @@ class HUDOverlay(QWidget): self.loot_value_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #7FFF7F;") loot_layout.addWidget(self.loot_value_label) - row1.addLayout(loot_layout) - row1.addStretch() + row0.addLayout(loot_layout) + row0.addStretch() - # Kills - kills_layout = QVBoxLayout() - kills_label = QLabel("💀 KILLS") - kills_label.setStyleSheet("font-size: 10px; color: #888888;") - kills_layout.addWidget(kills_label) + # Cost + cost_layout = QVBoxLayout() + cost_label = QLabel("💸 COST") + cost_label.setStyleSheet("font-size: 10px; color: #888888;") + cost_layout.addWidget(cost_label) - self.kills_value_label = QLabel("0") - self.kills_value_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #FFFFFF;") - kills_layout.addWidget(self.kills_value_label) + self.cost_value_label = QLabel("0.00 PED") + self.cost_value_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #FF7F7F;") + cost_layout.addWidget(self.cost_value_label) - row1.addLayout(kills_layout) - row1.addStretch() + row0.addLayout(cost_layout) + row0.addStretch() - # Globals/HoFs - globals_layout = QVBoxLayout() - globals_label = QLabel("🌍 GLOBALS") - globals_label.setStyleSheet("font-size: 10px; color: #888888;") + # Profit/Loss + profit_layout = QVBoxLayout() + profit_label = QLabel("📊 P/L") + profit_label.setStyleSheet("font-size: 10px; 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;") + profit_layout.addWidget(self.profit_value_label) + + row0.addLayout(profit_layout) + + layout.addLayout(row0) + + # === STATS GRID === + # Row 1: Damage & Kills + row1 = QHBoxLayout() globals_layout.addWidget(globals_label) self.globals_value_label = QLabel("0 / 0") @@ -546,23 +576,43 @@ class HUDOverlay(QWidget): # SESSION MANAGEMENT # ======================================================================== - def start_session(self, weapon: str = "Unknown", loadout: str = "Default") -> None: + 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. 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 """ self._session_start = datetime.now() self._stats = HUDStats() # Reset stats self._stats.current_weapon = weapon self._stats.current_loadout = loadout + self._stats.weapon_dpp = weapon_dpp + self._stats.weapon_cost_per_hour = weapon_cost_per_hour self.session_active = True self._timer.start(1000) # Update every second self._refresh_display() self.status_label.setText("● Live - Recording") self.status_label.setStyleSheet("font-size: 9px; color: #7FFF7F;") + def update_cost(self, cost_ped: Decimal) -> None: + """Update total cost spent. + + Args: + cost_ped: Cost in PED to add + """ + if not self.session_active: + return + + self._stats.cost_total += cost_ped + self._stats.profit_loss = self._stats.loot_total - self._stats.cost_total + self._refresh_display() + self.stats_updated.emit(self._stats.to_dict()) + def end_session(self) -> None: """End the current session.""" self._timer.stop() @@ -824,6 +874,19 @@ class HUDOverlay(QWidget): # Loot with 2 decimal places (PED format) self.loot_value_label.setText(f"{self._stats.loot_total:.2f} PED") + # Cost with 2 decimal places + 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} PED") + if profit > 0: + self.profit_value_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #7FFF7F;") # Green + elif profit < 0: + self.profit_value_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #FF7F7F;") # Red + else: + self.profit_value_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #FFFFFF;") # White + # Kills self.kills_value_label.setText(str(self._stats.kills)) diff --git a/ui/main_window.py b/ui/main_window.py index 6572697..dc4a626 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -928,8 +928,17 @@ class MainWindow(QMainWindow): # Show HUD and start session tracking self.hud.show() weapon_name = self._selected_weapon or "Unknown" - self.hud.start_session(weapon=weapon_name, loadout="Default") - self.log_info("HUD", f"HUD shown - Weapon: {weapon_name}") + weapon_stats = self._selected_weapon_stats or {} + weapon_dpp = Decimal(str(weapon_stats.get('dpp', 0))) + weapon_cost_per_hour = Decimal(str(weapon_stats.get('cost_per_hour', 0))) + + self.hud.start_session( + weapon=weapon_name, + loadout="Default", + weapon_dpp=weapon_dpp, + weapon_cost_per_hour=weapon_cost_per_hour + ) + self.log_info("HUD", f"HUD shown - Weapon: {weapon_name} (DPP: {weapon_dpp:.2f}, Cost/h: {weapon_cost_per_hour:.2f} PED)") def _setup_log_watcher_callbacks(self): """Setup LogWatcher event callbacks.""" @@ -993,9 +1002,23 @@ class MainWindow(QMainWindow): self.log_info("Skill", f"{skill_name} +{gained}") def on_damage_dealt(event): - """Handle damage dealt.""" + """Handle damage dealt - also track weapon cost.""" damage = event.data.get('damage', 0) self.hud.on_damage_dealt(float(damage)) + + # Estimate cost per shot based on weapon stats + if self._selected_weapon_stats: + # Rough estimate: if we know DPP and damage, we can estimate cost + dpp = self._selected_weapon_stats.get('dpp', 0) + if dpp > 0: + # Cost per shot in PEC = damage / DPP + cost_pec = damage / dpp + cost_ped = cost_pec / 100 # Convert to PED + self.hud.update_cost(Decimal(str(cost_ped))) + + def on_critical_hit(event): + """Handle critical hit - same as damage dealt.""" + on_damage_dealt(event) def on_damage_taken(event): """Handle damage taken.""" @@ -1014,7 +1037,7 @@ class MainWindow(QMainWindow): self.log_watcher.subscribe('hof', on_hof) self.log_watcher.subscribe('skill', on_skill) self.log_watcher.subscribe('damage_dealt', on_damage_dealt) - self.log_watcher.subscribe('critical_hit', on_damage_dealt) # Count as damage + self.log_watcher.subscribe('critical_hit', on_critical_hit) self.log_watcher.subscribe('damage_taken', on_damage_taken) self.log_watcher.subscribe('evade', on_evade)