From 1b176b96a8ebb731a30b9320a82b9d31b49338be Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Mon, 9 Feb 2026 16:14:51 +0000 Subject: [PATCH] feat(hud): integrate loadout-based cost tracking in HUD - Added LoadoutSelectionDialog for choosing loadout when starting session - Added set_cost_tracker() method to HUDOverlay for SessionCostTracker integration - Added new display row for loadout metrics: $/shot, $/hit, $/heal, hits, heals - Added mindforce cost support - Updated start_session() to accept loadout_id and per-action costs - Updated _refresh_display() to show new cost metrics --- ui/hud_overlay.py | 124 +++++++++++++++- ui/loadout_selection_dialog.py | 254 +++++++++++++++++++++++++++++++++ 2 files changed, 375 insertions(+), 3 deletions(-) create mode 100644 ui/loadout_selection_dialog.py diff --git a/ui/hud_overlay.py b/ui/hud_overlay.py index 1e92822..cdad8fb 100644 --- a/ui/hud_overlay.py +++ b/ui/hud_overlay.py @@ -279,6 +279,9 @@ class HUDOverlay(QWidget): # Armor decay tracker (imported here to avoid circular imports) self._armor_tracker = None + # Session cost tracker for loadout-based tracking + self._cost_tracker = None + # Drag state self._dragging = False self._drag_offset = QPoint() @@ -607,6 +610,71 @@ class HUDOverlay(QWidget): layout.addLayout(row3) + # === LOADOUT COST METRICS ROW === + row4 = QHBoxLayout() + + # Cost per shot + cps_layout = QVBoxLayout() + cps_label = QLabel("$/SHOT") + cps_label.setStyleSheet("font-size: 8px; color: #888888;") + cps_layout.addWidget(cps_label) + + self.cps_value_label = QLabel("0.00") + self.cps_value_label.setStyleSheet("font-size: 10px; color: #FFAAAA;") + cps_layout.addWidget(self.cps_value_label) + + row4.addLayout(cps_layout) + + # Cost per hit + cph_layout = QVBoxLayout() + cph_label = QLabel("$/HIT") + cph_label.setStyleSheet("font-size: 8px; color: #888888;") + cph_layout.addWidget(cph_label) + + self.cph_value_label = QLabel("0.00") + self.cph_value_label.setStyleSheet("font-size: 10px; color: #FFAAAA;") + cph_layout.addWidget(self.cph_value_label) + + row4.addLayout(cph_layout) + + # Cost per heal + cphl_layout = QVBoxLayout() + cphl_label = QLabel("$/HEAL") + cphl_label.setStyleSheet("font-size: 8px; color: #888888;") + cphl_layout.addWidget(cphl_label) + + self.cphl_value_label = QLabel("0.00") + self.cphl_value_label.setStyleSheet("font-size: 10px; color: #FFAAAA;") + cphl_layout.addWidget(self.cphl_value_label) + + row4.addLayout(cphl_layout) + + # Hits taken + hits_layout = QVBoxLayout() + hits_label = QLabel("HITS") + hits_label.setStyleSheet("font-size: 8px; color: #888888;") + hits_layout.addWidget(hits_label) + + self.hits_value_label = QLabel("0") + self.hits_value_label.setStyleSheet("font-size: 10px; color: #FFD700;") + hits_layout.addWidget(self.hits_value_label) + + row4.addLayout(hits_layout) + + # Heals used + heals_layout = QVBoxLayout() + heals_label = QLabel("HEALS") + heals_label.setStyleSheet("font-size: 8px; color: #888888;") + heals_layout.addWidget(heals_label) + + self.heals_value_label = QLabel("0") + self.heals_value_label.setStyleSheet("font-size: 10px; color: #FFD700;") + heals_layout.addWidget(self.heals_value_label) + + row4.addLayout(heals_layout) + + layout.addLayout(row4) + # === EQUIPMENT INFO === equip_separator = QFrame() equip_separator.setFrameShape(QFrame.Shape.HLine) @@ -794,9 +862,13 @@ class HUDOverlay(QWidget): weapon_cost_per_hour: Decimal = Decimal('0.0'), armor: str = "None", armor_durability: int = 2000, - fap: str = "None") -> None: + fap: str = "None", + loadout_id: Optional[int] = None, + 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. + Start a new hunting session with loadout-based cost tracking. Args: weapon: Name of the current weapon @@ -806,16 +878,24 @@ class HUDOverlay(QWidget): armor: Name of the equipped armor armor_durability: Armor durability rating fap: Name of the equipped FAP + loadout_id: Database ID of the loadout configuration + cost_per_shot: Cost per weapon shot from loadout + cost_per_hit: Cost per armor hit from loadout + cost_per_heal: Cost per heal from loadout """ self._session_start = datetime.now() self._stats = HUDStats() self._stats.current_weapon = weapon self._stats.current_loadout = loadout + self._stats.loadout_id = loadout_id 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._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 # Initialize armor decay tracker @@ -831,13 +911,42 @@ class HUDOverlay(QWidget): self.status_label.setText("● Live - Recording") self.status_label.setStyleSheet("font-size: 9px; color: #7FFF7F;") + def set_cost_tracker(self, cost_tracker: 'SessionCostTracker') -> None: + """ + Connect to a SessionCostTracker for real-time cost updates. + + Args: + cost_tracker: SessionCostTracker instance + """ + self._cost_tracker = cost_tracker + cost_tracker.register_callback(self._on_cost_update) + + def _on_cost_update(self, state: 'SessionCostState') -> None: + """Handle cost update from SessionCostTracker.""" + if not self.session_active: + return + + # Update stats from cost tracker state + self._stats.weapon_cost_total = state.weapon_cost + self._stats.armor_cost_total = state.armor_cost + self._stats.healing_cost_total = state.healing_cost + self._stats.enhancer_cost_total = state.enhancer_cost + self._stats.mindforce_cost_total = state.mindforce_cost + self._stats.shots_fired = state.shots_fired + self._stats.hits_taken = state.hits_taken + self._stats.heals_used = state.heals_used + + self._stats.recalculate() + self._refresh_display() + self.stats_updated.emit(self._stats.to_dict()) + 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') + cost_type: Type of cost ('weapon', 'armor', 'healing', 'plates', 'enhancer', 'mindforce') """ if not self.session_active: return @@ -852,6 +961,8 @@ class HUDOverlay(QWidget): self._stats.plates_cost_total += cost_ped elif cost_type == 'enhancer': self._stats.enhancer_cost_total += cost_ped + elif cost_type == 'mindforce': + self._stats.mindforce_cost_total += cost_ped self._stats.recalculate() self._refresh_display() @@ -1167,6 +1278,13 @@ class HUDOverlay(QWidget): 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}") + # Loadout cost metrics + self.cps_value_label.setText(f"{self._stats.cost_per_shot:.3f}") + self.cph_value_label.setText(f"{self._stats.cost_per_hit:.3f}") + self.cphl_value_label.setText(f"{self._stats.cost_per_heal:.3f}") + self.hits_value_label.setText(str(self._stats.hits_taken)) + self.heals_value_label.setText(str(self._stats.heals_used)) + # 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 diff --git a/ui/loadout_selection_dialog.py b/ui/loadout_selection_dialog.py new file mode 100644 index 0000000..d87ebad --- /dev/null +++ b/ui/loadout_selection_dialog.py @@ -0,0 +1,254 @@ +# Description: Loadout selection dialog for hunting sessions +# Allows choosing a loadout when starting a new session +# Standards: Python 3.11+, PyQt6, type hints + +from decimal import Decimal +from PyQt6.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, + QListWidget, QListWidgetItem, QDialogButtonBox, QGroupBox, + QFormLayout, QMessageBox +) +from PyQt6.QtCore import Qt, pyqtSignal +from typing import Optional, Dict, Any, List + +from core.loadout_db import LoadoutDatabase +from core.database import DatabaseManager + + +class LoadoutSelectionDialog(QDialog): + """ + Dialog for selecting a loadout when starting a hunting session. + + Shows saved loadouts with their per-action cost metrics. + """ + + loadout_selected = pyqtSignal(int, str) # loadout_id, loadout_name + + def __init__(self, parent=None, db_manager: Optional[DatabaseManager] = None): + """ + Initialize loadout selection dialog. + + Args: + parent: Parent widget + db_manager: Optional database manager + """ + super().__init__(parent) + self.setWindowTitle("Select Loadout - Hunting Session") + self.setMinimumSize(500, 400) + + self.db = db_manager or DatabaseManager() + self.loadout_db = LoadoutDatabase(self.db) + + self.selected_loadout_id: Optional[int] = None + self.selected_loadout_name: Optional[str] = None + + self._setup_ui() + self._load_loadouts() + + def _setup_ui(self): + """Setup the dialog UI.""" + layout = QVBoxLayout(self) + layout.setSpacing(10) + + # Header + header = QLabel("⚙️ Select Your Hunting Loadout") + header.setStyleSheet("font-size: 16px; font-weight: bold; color: #FFD700;") + layout.addWidget(header) + + description = QLabel( + "Choose a loadout to track your costs accurately during this hunting session.\n" + "Costs will be calculated based on your configured gear." + ) + description.setStyleSheet("color: #888888;") + description.setWordWrap(True) + layout.addWidget(description) + + # Loadout list + self.loadout_list = QListWidget() + self.loadout_list.setAlternatingRowColors(True) + self.loadout_list.itemSelectionChanged.connect(self._on_selection_changed) + self.loadout_list.itemDoubleClicked.connect(self._on_double_click) + layout.addWidget(self.loadout_list) + + # Preview panel + self.preview_group = QGroupBox("Loadout Details") + preview_layout = QFormLayout(self.preview_group) + + self.preview_weapon = QLabel("-") + self.preview_armor = QLabel("-") + self.preview_healing = QLabel("-") + self.preview_cost_shot = QLabel("-") + self.preview_cost_hit = QLabel("-") + self.preview_cost_heal = QLabel("-") + + preview_layout.addRow("🗡️ Weapon:", self.preview_weapon) + preview_layout.addRow("🛡️ Armor:", self.preview_armor) + preview_layout.addRow("💚 Healing:", self.preview_healing) + preview_layout.addRow("💰 Cost/Shot:", self.preview_cost_shot) + preview_layout.addRow("💰 Cost/Hit:", self.preview_cost_hit) + preview_layout.addRow("💰 Cost/Heal:", self.preview_cost_heal) + + layout.addWidget(self.preview_group) + + # Buttons + buttons = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + ) + buttons.accepted.connect(self._on_accept) + buttons.rejected.connect(self.reject) + + self.ok_button = buttons.button(QDialogButtonBox.StandardButton.Ok) + self.ok_button.setText("Start Session") + self.ok_button.setEnabled(False) + + # Add "No Loadout" option button + no_loadout_btn = QPushButton("Skip (No Loadout)") + no_loadout_btn.setToolTip("Start session without cost tracking") + no_loadout_btn.clicked.connect(self._on_no_loadout) + + btn_layout = QHBoxLayout() + btn_layout.addWidget(no_loadout_btn) + btn_layout.addStretch() + btn_layout.addWidget(buttons) + + layout.addLayout(btn_layout) + + def _load_loadouts(self): + """Load saved loadouts from database.""" + loadouts = self.loadout_db.list_loadouts() + + if not loadouts: + # No loadouts saved + item = QListWidgetItem("No loadouts saved yet") + item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsSelectable) + item.setForeground(Qt.GlobalColor.gray) + self.loadout_list.addItem(item) + return + + for loadout in loadouts: + item = QListWidgetItem() + item.setText(loadout['name']) + + # Build tooltip with gear info + tooltip = f"Weapon: {loadout['weapon_name'] or 'None'}\n" + tooltip += f"Armor: {loadout['armor_name'] or 'None'}\n" + tooltip += f"Healing: {loadout['healing_tool_name'] or 'None'}\n" + tooltip += f"Cost/Shot: {loadout['cost_per_shot_ped']:.4f} PED\n" + tooltip += f"Cost/Hit: {loadout['cost_per_hit_ped']:.4f} PED\n" + tooltip += f"Cost/Heal: {loadout['cost_per_heal_ped']:.4f} PED" + item.setToolTip(tooltip) + + # Store loadout ID + item.setData(Qt.ItemDataRole.UserRole, loadout['id']) + + # Highlight active loadout + if loadout.get('is_active'): + item.setText(f"⭐ {loadout['name']} (Active)") + font = item.font() + font.setBold(True) + item.setFont(font) + + self.loadout_list.addItem(item) + + def _on_selection_changed(self): + """Handle loadout selection change.""" + items = self.loadout_list.selectedItems() + if not items: + self.ok_button.setEnabled(False) + self._clear_preview() + return + + item = items[0] + loadout_id = item.data(Qt.ItemDataRole.UserRole) + + if loadout_id is None: + self.ok_button.setEnabled(False) + return + + self.selected_loadout_id = loadout_id + self.selected_loadout_name = item.text().replace("⭐ ", "").replace(" (Active)", "") + + # Load and display preview + loadout = self.loadout_db.get_loadout(self.selected_loadout_name) + if loadout: + self._update_preview(loadout) + self.ok_button.setEnabled(True) + + def _update_preview(self, loadout: Dict[str, Any]): + """Update preview panel with loadout details.""" + self.preview_weapon.setText(loadout.get('weapon_name') or "None") + self.preview_armor.setText(loadout.get('armor_name') or "None") + self.preview_healing.setText(loadout.get('healing_tool_name') or "None") + + # Format costs + cost_shot = Decimal(str(loadout.get('cost_per_shot_ped', 0))) + cost_hit = Decimal(str(loadout.get('cost_per_hit_ped', 0))) + cost_heal = Decimal(str(loadout.get('cost_per_heal_ped', 0))) + + self.preview_cost_shot.setText(f"{cost_shot:.4f} PED") + self.preview_cost_hit.setText(f"{cost_hit:.4f} PED") + self.preview_cost_heal.setText(f"{cost_heal:.4f} PED") + + # Color code based on cost + if cost_shot > Decimal("0.5"): + self.preview_cost_shot.setStyleSheet("color: #FF7F7F;") + else: + self.preview_cost_shot.setStyleSheet("color: #7FFF7F;") + + def _clear_preview(self): + """Clear preview panel.""" + self.preview_weapon.setText("-") + self.preview_armor.setText("-") + self.preview_healing.setText("-") + self.preview_cost_shot.setText("-") + self.preview_cost_hit.setText("-") + self.preview_cost_heal.setText("-") + + def _on_double_click(self, item: QListWidgetItem): + """Handle double click on loadout.""" + if item.data(Qt.ItemDataRole.UserRole): + self._on_accept() + + def _on_accept(self): + """Handle OK button - start session with selected loadout.""" + if self.selected_loadout_id: + self.loadout_selected.emit(self.selected_loadout_id, self.selected_loadout_name or "") + self.accept() + + def _on_no_loadout(self): + """Handle skip button - start session without loadout.""" + reply = QMessageBox.question( + self, + "No Loadout Selected", + "Start session without cost tracking?\n\n" + "You won't be able to track accurate weapon/armor/healing costs.", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + self.loadout_selected.emit(0, "No Loadout") + self.accept() + + +# Main entry for testing +if __name__ == "__main__": + import sys + import logging + from PyQt6.QtWidgets import QApplication + + logging.basicConfig(level=logging.INFO) + + app = QApplication(sys.argv) + app.setStyle('Fusion') + + dialog = LoadoutSelectionDialog() + + # Connect signal for testing + dialog.loadout_selected.connect( + lambda id, name: print(f"Selected loadout: {name} (ID: {id})") + ) + + if dialog.exec() == QDialog.DialogCode.Accepted: + print("Session starting!") + + sys.exit(0)