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
This commit is contained in:
LemonNexus 2026-02-08 23:22:13 +00:00
parent e9dc72df23
commit 7c38b398f3
2 changed files with 110 additions and 24 deletions

View File

@ -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))

View File

@ -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)