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:
parent
e9dc72df23
commit
7c38b398f3
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue