From d7f1e615338bf4de46d722a353ddc1a4fea0e4ac Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Mon, 9 Feb 2026 20:44:28 +0000 Subject: [PATCH] feat: connect loadout selection to session cost tracking - start_session now uses _session_loadout_name for HUD display - Added _setup_session_cost_tracker to initialize SessionCostTracker - Added _on_cost_update callback to update HUD with live costs - Loadout name now appears in HUD instead of 'Default' --- ui/main_window.py | 565 +++++++++++++++++++++++++--------------------- 1 file changed, 304 insertions(+), 261 deletions(-) diff --git a/ui/main_window.py b/ui/main_window.py index 6f6d845..bc0d602 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -88,30 +88,30 @@ from core.database import DatabaseManager class NewProjectDialog(QDialog): """Dialog for creating a new project.""" - + def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("New Project") self.setMinimumWidth(400) self.setup_ui() - + def setup_ui(self): layout = QVBoxLayout(self) - + # Form layout for inputs form_layout = QFormLayout() - + self.name_input = QLineEdit() self.name_input.setPlaceholderText("Enter project name...") form_layout.addRow("Name:", self.name_input) - + self.desc_input = QLineEdit() self.desc_input.setPlaceholderText("Enter description (optional)...") form_layout.addRow("Description:", self.desc_input) - + layout.addLayout(form_layout) layout.addSpacing(10) - + # Button box button_box = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel @@ -119,11 +119,11 @@ class NewProjectDialog(QDialog): button_box.accepted.connect(self.accept) button_box.rejected.connect(self.reject) layout.addWidget(button_box) - + def get_project_data(self) -> tuple: """Get the entered project data.""" return self.name_input.text().strip(), self.desc_input.text().strip() - + def accept(self): """Validate before accepting.""" name = self.name_input.text().strip() @@ -135,35 +135,35 @@ class NewProjectDialog(QDialog): class ProjectStatsDialog(QDialog): """Dialog for displaying project statistics.""" - + def __init__(self, project: Project, parent=None): super().__init__(parent) self.project = project self.setWindowTitle(f"Project Statistics - {project.name}") self.setMinimumWidth(350) self.setup_ui() - + def setup_ui(self): layout = QVBoxLayout(self) - + # Stats display stats_group = QGroupBox("Project Information") stats_layout = QFormLayout(stats_group) - + stats_layout.addRow("ID:", QLabel(str(self.project.id))) stats_layout.addRow("Name:", QLabel(self.project.name)) stats_layout.addRow("Type:", QLabel(self.project.type)) stats_layout.addRow("Status:", QLabel(self.project.status)) - + # Description from metadata description = self.project.metadata.get('description', 'N/A') if self.project.metadata else 'N/A' stats_layout.addRow("Description:", QLabel(description)) - + created = self.project.created_at.strftime("%Y-%m-%d %H:%M") if self.project.created_at else "N/A" stats_layout.addRow("Created:", QLabel(created)) - + layout.addWidget(stats_group) - + # Close button button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) button_box.rejected.connect(self.reject) @@ -172,22 +172,22 @@ class ProjectStatsDialog(QDialog): class SettingsDialog(QDialog): """Dialog for application settings.""" - + def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Settings") self.setMinimumWidth(400) self.setup_ui() - + def setup_ui(self): layout = QVBoxLayout(self) - + info_label = QLabel("Settings configuration would go here.") info_label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(info_label) - + layout.addStretch() - + button_box = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) @@ -203,54 +203,54 @@ class SettingsDialog(QDialog): class MainWindow(QMainWindow): """ Main application window for Lemontropia Suite. - + Provides project management, session control, and log viewing capabilities. """ - + # Signals session_started = pyqtSignal(int) # project_id session_stopped = pyqtSignal() session_paused = pyqtSignal() session_resumed = pyqtSignal() - + def __init__(self): super().__init__() - + # Window configuration self.setWindowTitle("Lemontropia Suite") self.setMinimumSize(1200, 800) self.resize(1400, 900) - + # Initialize database and real project manager self.db = DatabaseManager() if not self.db.initialize(): QMessageBox.critical(self, "Error", "Failed to initialize database!") sys.exit(1) - + self.project_manager = ProjectManager(self.db) - + # Initialize HUD self.hud = HUDOverlay() - + # Log watcher - created when session starts self.log_watcher: Optional[LogWatcher] = None self._log_watcher_task = None - + # Thread-safe queue for cross-thread communication from queue import Queue self._event_queue = Queue() - + # Timer to process queued events in main thread self._queue_timer = QTimer(self) self._queue_timer.timeout.connect(self._process_queued_events) self._queue_timer.start(100) # Check every 100ms - + # State self.current_project: Optional[Project] = None self.session_state = SessionState.IDLE self.current_session_id: Optional[int] = None self._current_db_session_id: Optional[int] = None - + # Selected gear self._selected_weapon: Optional[str] = None self._selected_weapon_stats: Optional[dict] = None @@ -261,76 +261,76 @@ class MainWindow(QMainWindow): self._selected_medical_tool: Optional[str] = None self._selected_medical_tool_stats: Optional[dict] = None self._selected_loadout: Optional[Any] = None - + # Setup UI self.setup_ui() self.apply_dark_theme() self.create_menu_bar() self.create_status_bar() - + # Load initial data self.refresh_project_list() - + # Welcome message self.log_info("Application", "Lemontropia Suite initialized") self.log_info("Database", f"Database ready: {self.db.db_path}") - + # ======================================================================== # UI Setup # ======================================================================== - + def setup_ui(self): """Setup the main UI layout.""" # Central widget central_widget = QWidget() self.setCentralWidget(central_widget) - + # Main layout main_layout = QVBoxLayout(central_widget) main_layout.setContentsMargins(10, 10, 10, 10) main_layout.setSpacing(10) - + # Main splitter (horizontal: left panels | log panel) self.main_splitter = QSplitter(Qt.Orientation.Horizontal) main_layout.addWidget(self.main_splitter) - + # Left side container left_container = QWidget() left_layout = QVBoxLayout(left_container) left_layout.setContentsMargins(0, 0, 0, 0) left_layout.setSpacing(10) - + # Left splitter (vertical: projects | session control) left_splitter = QSplitter(Qt.Orientation.Vertical) left_layout.addWidget(left_splitter) - + # Project panel self.project_panel = self.create_project_panel() left_splitter.addWidget(self.project_panel) - + # Session control panel self.session_panel = self.create_session_panel() left_splitter.addWidget(self.session_panel) - + # Set splitter proportions left_splitter.setSizes([400, 300]) - + # Add left container to main splitter self.main_splitter.addWidget(left_container) - + # Log output panel self.log_panel = self.create_log_panel() self.main_splitter.addWidget(self.log_panel) - + # Set main splitter proportions (30% left, 70% log) self.main_splitter.setSizes([400, 900]) - + def create_project_panel(self) -> QGroupBox: """Create the project management panel.""" panel = QGroupBox("Project Management") layout = QVBoxLayout(panel) layout.setSpacing(8) - + # Project list self.project_list = QTreeWidget() self.project_list.setHeaderLabels(["ID", "Name", "Type", "Status"]) @@ -339,7 +339,7 @@ class MainWindow(QMainWindow): self.project_list.setRootIsDecorated(False) self.project_list.itemSelectionChanged.connect(self.on_project_selected) self.project_list.itemDoubleClicked.connect(self.on_project_double_clicked) - + # Adjust column widths header = self.project_list.header() header.setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed) @@ -349,51 +349,51 @@ class MainWindow(QMainWindow): header.resizeSection(0, 50) header.resizeSection(2, 70) header.resizeSection(3, 80) - + layout.addWidget(self.project_list) - + # Button row button_layout = QHBoxLayout() - + self.new_project_btn = QPushButton("βž• New Project") self.new_project_btn.setToolTip("Create a new project") self.new_project_btn.clicked.connect(self.on_new_project) button_layout.addWidget(self.new_project_btn) - + self.view_stats_btn = QPushButton("πŸ“Š View Stats") self.view_stats_btn.setToolTip("View selected project statistics") self.view_stats_btn.clicked.connect(self.on_view_stats) self.view_stats_btn.setEnabled(False) button_layout.addWidget(self.view_stats_btn) - + self.refresh_btn = QPushButton("πŸ”„ Refresh") self.refresh_btn.setToolTip("Refresh project list") self.refresh_btn.clicked.connect(self.refresh_project_list) button_layout.addWidget(self.refresh_btn) - + layout.addLayout(button_layout) - + return panel - + def create_session_panel(self) -> QGroupBox: """Create the session control panel.""" panel = QGroupBox("Session Control") layout = QVBoxLayout(panel) layout.setSpacing(10) - + # Current project display project_info_layout = QFormLayout() self.current_project_label = QLabel("No project selected") self.current_project_label.setStyleSheet("font-weight: bold; color: #888;") project_info_layout.addRow("Selected Project:", self.current_project_label) layout.addLayout(project_info_layout) - + # Separator line separator = QFrame() separator.setFrameShape(QFrame.Shape.HLine) separator.setStyleSheet("background-color: #444;") layout.addWidget(separator) - + # Session status status_layout = QHBoxLayout() status_layout.addWidget(QLabel("Status:")) @@ -411,211 +411,211 @@ class MainWindow(QMainWindow): status_layout.addWidget(self.session_status_label) status_layout.addStretch() layout.addLayout(status_layout) - + # Control buttons button_layout = QHBoxLayout() - + self.start_session_btn = QPushButton("▢️ Start Session") self.start_session_btn.setToolTip("Start a new session with selected project") self.start_session_btn.clicked.connect(self.on_start_session) self.start_session_btn.setEnabled(False) button_layout.addWidget(self.start_session_btn) - + self.stop_session_btn = QPushButton("⏹️ Stop") self.stop_session_btn.setToolTip("Stop current session") self.stop_session_btn.clicked.connect(self.on_stop_session) self.stop_session_btn.setEnabled(False) button_layout.addWidget(self.stop_session_btn) - + self.pause_session_btn = QPushButton("⏸️ Pause") self.pause_session_btn.setToolTip("Pause/Resume current session") self.pause_session_btn.clicked.connect(self.on_pause_session) self.pause_session_btn.setEnabled(False) button_layout.addWidget(self.pause_session_btn) - + layout.addLayout(button_layout) - + # Session info self.session_info_label = QLabel("Ready to start") self.session_info_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.session_info_label.setStyleSheet("color: #666; padding: 10px;") layout.addWidget(self.session_info_label) - + layout.addStretch() - + return panel - + def create_log_panel(self) -> QGroupBox: """Create the log output panel.""" panel = QGroupBox("Event Log") layout = QVBoxLayout(panel) layout.setSpacing(8) - + # Log text edit self.log_output = QTextEdit() self.log_output.setReadOnly(True) self.log_output.setFont(QFont("Consolas", 10)) self.log_output.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap) layout.addWidget(self.log_output) - + # Log controls controls_layout = QHBoxLayout() - + self.clear_log_btn = QPushButton("πŸ—‘οΈ Clear") self.clear_log_btn.setToolTip("Clear log output") self.clear_log_btn.clicked.connect(self.log_output.clear) controls_layout.addWidget(self.clear_log_btn) - + controls_layout.addStretch() - + self.auto_scroll_check = QLabel("βœ“ Auto-scroll") self.auto_scroll_check.setStyleSheet("color: #888;") controls_layout.addWidget(self.auto_scroll_check) - + layout.addLayout(controls_layout) - + return panel - + def create_menu_bar(self): """Create the application menu bar.""" menubar = self.menuBar() - + # File menu file_menu = menubar.addMenu("&File") - + new_project_action = QAction("&New Project", self) new_project_action.setShortcut("Ctrl+N") new_project_action.triggered.connect(self.on_new_project) file_menu.addAction(new_project_action) - + open_project_action = QAction("&Open Project", self) open_project_action.setShortcut("Ctrl+O") open_project_action.triggered.connect(self.on_open_project) file_menu.addAction(open_project_action) - + file_menu.addSeparator() - + exit_action = QAction("E&xit", self) exit_action.setShortcut("Alt+F4") exit_action.triggered.connect(self.close) file_menu.addAction(exit_action) - + # Session menu session_menu = menubar.addMenu("&Session") - + start_action = QAction("&Start", self) start_action.setShortcut("F5") start_action.triggered.connect(self.on_start_session) session_menu.addAction(start_action) self.start_action = start_action - + stop_action = QAction("St&op", self) stop_action.setShortcut("Shift+F5") stop_action.triggered.connect(self.on_stop_session) session_menu.addAction(stop_action) self.stop_action = stop_action - + pause_action = QAction("&Pause", self) pause_action.setShortcut("F6") pause_action.triggered.connect(self.on_pause_session) session_menu.addAction(pause_action) self.pause_action = pause_action - + # View menu view_menu = menubar.addMenu("&View") - + show_hud_action = QAction("Show &HUD", self) show_hud_action.setShortcut("F9") show_hud_action.triggered.connect(self.on_show_hud) view_menu.addAction(show_hud_action) - + hide_hud_action = QAction("&Hide HUD", self) hide_hud_action.setShortcut("F10") hide_hud_action.triggered.connect(self.on_hide_hud) view_menu.addAction(hide_hud_action) - + view_menu.addSeparator() - + settings_action = QAction("&Settings", self) settings_action.setShortcut("Ctrl+,") settings_action.triggered.connect(self.on_settings) view_menu.addAction(settings_action) - + # Tools menu tools_menu = menubar.addMenu("&Tools") - + # Select Gear submenu select_gear_menu = tools_menu.addMenu("Select &Gear") - + select_weapon_action = QAction("&Weapon", self) select_weapon_action.setShortcut("Ctrl+W") select_weapon_action.triggered.connect(lambda: self.on_select_gear("weapon")) select_gear_menu.addAction(select_weapon_action) - + select_armor_action = QAction("&Armor", self) select_armor_action.setShortcut("Ctrl+Shift+A") select_armor_action.triggered.connect(lambda: self.on_select_gear("armor")) select_gear_menu.addAction(select_armor_action) - + select_finder_action = QAction("&Finder", self) select_finder_action.setShortcut("Ctrl+Shift+F") select_finder_action.triggered.connect(lambda: self.on_select_gear("finder")) select_gear_menu.addAction(select_finder_action) - + select_medical_action = QAction("&Medical Tool", self) select_medical_action.setShortcut("Ctrl+M") select_medical_action.triggered.connect(lambda: self.on_select_gear("medical_tool")) select_gear_menu.addAction(select_medical_action) - + tools_menu.addSeparator() - + loadout_action = QAction("&Loadout Manager", self) loadout_action.setShortcut("Ctrl+L") loadout_action.triggered.connect(self.on_loadout_manager) tools_menu.addAction(loadout_action) - + # Help menu help_menu = menubar.addMenu("&Help") - + about_action = QAction("&About", self) about_action.triggered.connect(self.on_about) help_menu.addAction(about_action) - + def create_status_bar(self): """Create the status bar.""" self.status_bar = QStatusBar() self.setStatusBar(self.status_bar) - + # Permanent widgets self.status_state_label = QLabel("● Idle") self.status_state_label.setStyleSheet("color: #888; padding: 0 10px;") self.status_bar.addPermanentWidget(self.status_state_label) - + self.status_project_label = QLabel("No project") self.status_project_label.setStyleSheet("color: #888; padding: 0 10px;") self.status_bar.addPermanentWidget(self.status_project_label) - + # Message area self.status_bar.showMessage("Ready") - + # ======================================================================== # Theme # ======================================================================== - + def apply_dark_theme(self): """Apply dark theme styling.""" dark_stylesheet = """ QMainWindow { background-color: #1e1e1e; } - + QWidget { background-color: #1e1e1e; color: #e0e0e0; font-family: 'Segoe UI', Arial, sans-serif; font-size: 10pt; } - + QGroupBox { font-weight: bold; border: 1px solid #444; @@ -624,14 +624,14 @@ class MainWindow(QMainWindow): padding-top: 10px; padding: 10px; } - + QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px; color: #888; } - + QPushButton { background-color: #2d2d2d; border: 1px solid #444; @@ -639,61 +639,61 @@ class MainWindow(QMainWindow): padding: 8px 16px; color: #e0e0e0; } - + QPushButton:hover { background-color: #3d3d3d; border-color: #555; } - + QPushButton:pressed { background-color: #4d4d4d; } - + QPushButton:disabled { background-color: #252525; color: #666; border-color: #333; } - + QPushButton#start_button { background-color: #1b5e20; border-color: #2e7d32; } - + QPushButton#start_button:hover { background-color: #2e7d32; } - + QPushButton#stop_button { background-color: #b71c1c; border-color: #c62828; } - + QPushButton#stop_button:hover { background-color: #c62828; } - + QTreeWidget { background-color: #252525; border: 1px solid #444; border-radius: 4px; outline: none; } - + QTreeWidget::item { padding: 6px; border-bottom: 1px solid #333; } - + QTreeWidget::item:selected { background-color: #0d47a1; color: white; } - + QTreeWidget::item:alternate { background-color: #2a2a2a; } - + QHeaderView::section { background-color: #2d2d2d; padding: 6px; @@ -701,7 +701,7 @@ class MainWindow(QMainWindow): border-right: 1px solid #444; font-weight: bold; } - + QTextEdit { background-color: #151515; border: 1px solid #444; @@ -709,7 +709,7 @@ class MainWindow(QMainWindow): padding: 8px; color: #d0d0d0; } - + QLineEdit { background-color: #252525; border: 1px solid #444; @@ -717,86 +717,86 @@ class MainWindow(QMainWindow): padding: 6px; color: #e0e0e0; } - + QLineEdit:focus { border-color: #0d47a1; } - + QMenuBar { background-color: #1e1e1e; border-bottom: 1px solid #444; } - + QMenuBar::item { background-color: transparent; padding: 6px 12px; } - + QMenuBar::item:selected { background-color: #2d2d2d; } - + QMenu { background-color: #2d2d2d; border: 1px solid #444; padding: 4px; } - + QMenu::item { padding: 6px 24px; border-radius: 2px; } - + QMenu::item:selected { background-color: #0d47a1; } - + QMenu::separator { height: 1px; background-color: #444; margin: 4px 8px; } - + QStatusBar { background-color: #1e1e1e; border-top: 1px solid #444; } - + QSplitter::handle { background-color: #444; } - + QSplitter::handle:horizontal { width: 2px; } - + QSplitter::handle:vertical { height: 2px; } - + QDialog { background-color: #1e1e1e; } - + QLabel { color: #e0e0e0; } - + QFormLayout QLabel { color: #888; } """ self.setStyleSheet(dark_stylesheet) - + # ======================================================================== # Project Management # ======================================================================== - + def refresh_project_list(self): """Refresh the project list display.""" self.project_list.clear() projects = self.project_manager.list_projects() - + for project in projects: item = QTreeWidgetItem([ str(project.id), @@ -806,16 +806,16 @@ class MainWindow(QMainWindow): ]) item.setData(0, Qt.ItemDataRole.UserRole, project.id) self.project_list.addTopLevelItem(item) - + self.log_debug("ProjectManager", f"Loaded {len(projects)} projects") - + def on_project_selected(self): """Handle project selection change.""" selected = self.project_list.selectedItems() if selected: project_id = selected[0].data(0, Qt.ItemDataRole.UserRole) self.current_project = self.project_manager.load_project(project_id) - + if self.current_project: self.current_project_label.setText(self.current_project.name) self.current_project_label.setStyleSheet("font-weight: bold; color: #4caf50;") @@ -830,14 +830,14 @@ class MainWindow(QMainWindow): self.view_stats_btn.setEnabled(False) self.start_session_btn.setEnabled(False) self.status_project_label.setText("No project") - + def on_project_double_clicked(self, item: QTreeWidgetItem, column: int): """Handle double-click on project.""" project_id = item.data(0, Qt.ItemDataRole.UserRole) project = self.project_manager.load_project(project_id) if project: self.show_project_stats(project) - + def on_new_project(self): """Handle new project creation.""" dialog = NewProjectDialog(self) @@ -848,36 +848,36 @@ class MainWindow(QMainWindow): self.refresh_project_list() self.log_info("ProjectManager", f"Created project: {project.name}") self.status_bar.showMessage(f"Project '{name}' created", 3000) - + def on_open_project(self): """Handle open project action.""" # For now, just focus the project list self.project_list.setFocus() self.status_bar.showMessage("Select a project from the list", 3000) - + def on_view_stats(self): """Handle view stats button.""" if self.current_project: self.show_project_stats(self.current_project) - + def show_project_stats(self, project: Project): """Show project statistics dialog.""" dialog = ProjectStatsDialog(project, self) dialog.exec() - + # ======================================================================== # Session Control # ======================================================================== - + def start_session(self, project_id: int): """ Start a new session with the given project. - + Args: project_id: The ID of the project to start session for """ from core.project_manager import ProjectData - + # Get real project from database projects = self.project_manager.list_projects() project = None @@ -885,34 +885,34 @@ class MainWindow(QMainWindow): if p.id == project_id: project = p break - + if not project: self.log_error("Session", f"Project {project_id} not found") return - + if self.session_state != SessionState.IDLE: self.log_warning("Session", "Cannot start: session already active") return - + # Update state self.set_session_state(SessionState.RUNNING) self.current_session_id = project_id - + # Emit signal self.session_started.emit(project_id) - + # Log self.log_info("Session", f"Started session for project: {project.name}") self.session_info_label.setText(f"Session active: {project.name}") - + # Start real session in database session = self.project_manager.start_session(project_id) self._current_db_session_id = session.id if session else None - + # Setup LogWatcher use_mock = os.getenv('USE_MOCK_DATA', 'false').lower() in ('true', '1', 'yes') log_path = os.getenv('EU_CHAT_LOG_PATH', '') - + if use_mock or not log_path: # Use mock log for testing test_data_dir = Path(__file__).parent.parent / "test-data" @@ -926,44 +926,53 @@ class MainWindow(QMainWindow): else: self.log_watcher = LogWatcher(log_path, poll_interval=1.0, mock_mode=False) self.log_info("LogWatcher", f"Using REAL log: {log_path}") - + # Subscribe to events self._setup_log_watcher_callbacks() - + # Start LogWatcher in background self._start_log_watcher() - + # Show HUD and start session tracking self.hud.show() weapon_name = self._selected_weapon or "Unknown" 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))) - + + # Get loadout name from session selection + loadout_name = getattr(self, '_session_loadout_name', None) or "Default" + loadout_id = getattr(self, '_session_loadout_id', None) + self.hud.start_session( weapon=weapon_name, - loadout="Default", + loadout=loadout_name, 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)") - + + # Set up cost tracker if loadout selected + if loadout_id: + self._setup_session_cost_tracker(loadout_id) + + self.log_info("HUD", f"HUD shown - Weapon: {weapon_name}, Loadout: {loadout_name} (DPP: {weapon_dpp:.2f}, Cost/h: {weapon_cost_per_hour:.2f} PED)") + def _setup_log_watcher_callbacks(self): """Setup LogWatcher event callbacks.""" if not self.log_watcher: return - + from core.project_manager import LootEvent from decimal import Decimal - + def on_heal(event): """Handle heal events from chat.log. - + Pattern: "You healed yourself X points" Calculates healing cost based on FAP decay and updates HUD. """ heal_amount = event.data.get('heal_amount', Decimal('0')) - + # Calculate heal cost based on selected medical tool decay # Get decay per heal from loadout or use default decay_cost = Decimal('0') @@ -977,26 +986,26 @@ class MainWindow(QMainWindow): else: # Default estimate: 2 PEC per heal decay_cost = Decimal('0.02') - + # Update HUD with heal event self.hud.on_heal_event(heal_amount, decay_cost) - + # Log to UI self.log_info("Heal", f"Healed {heal_amount} HP (Cost: {decay_cost:.4f} PED)") - + def on_loot(event): """Handle loot events.""" item_name = event.data.get('item_name', 'Unknown') value_ped = event.data.get('value_ped', Decimal('0.0')) quantity = event.data.get('quantity', 1) - + # Skip Universal Ammo if item_name == 'Universal Ammo': return - + # Estimated Kills: Every loot event = 1 mob killed self.hud.update_stats({'kills_add': 1}) - + # Queue database write for main thread (SQLite thread safety) if self._current_db_session_id: self._event_queue.put({ @@ -1007,47 +1016,47 @@ class MainWindow(QMainWindow): 'value_ped': value_ped, 'raw_line': event.raw_line }) - + # Update HUD (thread-safe) self.hud.on_loot_event(item_name, value_ped) - + # Log to UI (main thread only - use signal/slot or queue) # We'll log this in _process_queued_events instead - + def on_global(event): """Handle global events.""" value_ped = event.data.get('value_ped', Decimal('0.0')) player = event.data.get('player_name', 'Unknown') self.hud.on_global(value_ped) self.log_info("Global", f"{player} found {value_ped} PED!") - + def on_personal_global(event): """Handle personal global events.""" value_ped = event.data.get('value_ped', Decimal('0.0')) creature = event.data.get('creature', 'Unknown') self.hud.on_global(value_ped) self.log_info("Global", f"πŸŽ‰ YOUR GLOBAL: {creature} for {value_ped} PED!!!") - + def on_hof(event): """Handle HoF events.""" value_ped = event.data.get('value_ped', Decimal('0.0')) self.hud.on_hof(value_ped) self.log_info("HoF", f"πŸ† HALL OF FAME: {value_ped} PED!") - + def on_skill(event): """Handle skill events.""" skill_name = event.data.get('skill_name', 'Unknown') gained = event.data.get('gained', 0) self.log_info("Skill", f"{skill_name} +{gained}") - + def on_damage_dealt(event): """Handle damage dealt - also track weapon cost and shots fired.""" damage = event.data.get('damage', 0) self.hud.on_damage_dealt(float(damage)) - + # Track shots fired (1 shot per damage event) self.hud.update_stats({'shots_add': 1}) - + # Track weapon decay cost per shot # Only count as one shot fired per damage event if self._selected_weapon_stats: @@ -1058,18 +1067,18 @@ class MainWindow(QMainWindow): # Convert PEC to PED cost_ped = Decimal(str(decay)) / Decimal('100') self.hud.update_cost(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 - track armor decay cost.""" from decimal import Decimal - + damage = event.data.get('damage', 0) self.hud.on_damage_taken(float(damage)) - + # Calculate armor decay cost per hit # Formula: cost_per_hit = armor_decay_pec / 100 (PED) if self._selected_armor_stats and self._selected_armor_stats.get('decay'): @@ -1079,12 +1088,12 @@ class MainWindow(QMainWindow): cost_ped = armor_decay_pec / Decimal('100') self.hud.update_cost(cost_ped) self.log_debug("Armor", f"Armor decay: {cost_ped:.4f} PED (decay: {armor_decay_pec} PEC)") - + def on_evade(event): """Handle evade.""" evade_type = event.data.get('type', 'Evade') self.log_info("Evade", evade_type) - + # Subscribe to all event types self.log_watcher.subscribe('loot', on_loot) self.log_watcher.subscribe('global', on_global) @@ -1096,18 +1105,18 @@ class MainWindow(QMainWindow): self.log_watcher.subscribe('damage_taken', on_damage_taken) self.log_watcher.subscribe('evade', on_evade) self.log_watcher.subscribe('heal', on_heal) # NEW: Heal event tracking - + def _start_log_watcher(self): """Start LogWatcher in background thread.""" import asyncio from PyQt6.QtCore import QThread - + class LogWatcherThread(QThread): def __init__(self, watcher): super().__init__() self.watcher = watcher self._running = True - + def run(self): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) @@ -1120,14 +1129,14 @@ class MainWindow(QMainWindow): finally: loop.run_until_complete(self.watcher.stop()) loop.close() - + def stop(self): self._running = False - + self._log_watcher_thread = LogWatcherThread(self.log_watcher) self._log_watcher_thread.start() self.log_info("LogWatcher", "Started watching for events") - + def _stop_log_watcher(self): """Stop LogWatcher.""" if hasattr(self, '_log_watcher_thread') and self._log_watcher_thread: @@ -1135,17 +1144,51 @@ class MainWindow(QMainWindow): self._log_watcher_thread.wait(2000) # Wait up to 2 seconds self._log_watcher_thread = None self.log_info("LogWatcher", "Stopped") + + def _setup_session_cost_tracker(self, loadout_id: int): + """Set up session cost tracker with selected loadout.""" + from core.session_cost_tracker import SessionCostTracker + from core.database import DatabaseManager + + try: + db = DatabaseManager() + self._cost_tracker = SessionCostTracker( + session_id=self._current_db_session_id, + loadout_id=loadout_id, + db_manager=db + ) + self._cost_tracker.register_callback(self._on_cost_update) + self.log_info("CostTracker", f"Cost tracking enabled for loadout ID: {loadout_id}") + except Exception as e: + self.log_error("CostTracker", f"Failed to set up cost tracker: {e}") + def _on_cost_update(self, state): + """Handle cost update from SessionCostTracker.""" + # Update HUD with new cost state + if hasattr(self, 'hud') and self.hud: + summary = { + 'weapon_cost': state.weapon_cost, + 'armor_cost': state.armor_cost, + 'healing_cost': state.healing_cost, + 'enhancer_cost': state.enhancer_cost, + 'mindforce_cost': state.mindforce_cost, + 'shots_fired': state.shots_fired, + 'hits_taken': state.hits_taken, + 'heals_used': state.heals_used, + } + self.hud._stats.update_from_cost_tracker(summary) + self.hud._refresh_display() + def _process_queued_events(self): """Process events from the queue in the main thread (SQLite thread safety).""" from core.project_manager import LootEvent from decimal import Decimal - + processed = 0 while not self._event_queue.empty() and processed < 10: # Process max 10 per tick try: event = self._event_queue.get_nowait() - + if event['type'] == 'loot': # Record to database (now in main thread - safe) loot = LootEvent( @@ -1156,14 +1199,14 @@ class MainWindow(QMainWindow): raw_log_line=event['raw_line'] ) self.project_manager.record_loot(loot) - + # Log to UI self.log_info("Loot", f"{event['item_name']} x{event['quantity']} ({event['value_ped']} PED)") - + processed += 1 except Exception as e: self.log_error("EventQueue", f"Error processing event: {e}") - + def on_start_session(self): """Handle start session button - shows loadout selection first.""" if self.current_project and self.session_state == SessionState.IDLE: @@ -1185,36 +1228,36 @@ class MainWindow(QMainWindow): self.log_info("Session", "Starting session without loadout") self._session_loadout_id = None self._session_loadout_name = None - + # Now start the session if self.current_project: self.start_session(self.current_project.id) - + def on_stop_session(self): """Handle stop session button.""" if self.session_state in (SessionState.RUNNING, SessionState.PAUSED): # Stop LogWatcher self._stop_log_watcher() - + # End session in database if self._current_db_session_id: self.project_manager.end_session(self._current_db_session_id) self._current_db_session_id = None - + self.set_session_state(SessionState.IDLE) self.current_session_id = None - + self.session_stopped.emit() - + self.log_info("Session", "Session stopped") self.session_info_label.setText("Session stopped") - + # End HUD session self.hud.end_session() - + # Hide HUD self.hud.hide() - + def on_pause_session(self): """Handle pause/resume session button.""" if self.session_state == SessionState.RUNNING: @@ -1229,16 +1272,16 @@ class MainWindow(QMainWindow): self.log_info("Session", "Session resumed") self.session_info_label.setText("Session resumed") self.pause_session_btn.setText("⏸️ Pause") - + def set_session_state(self, state: SessionState): """ Update the session state and UI. - + Args: state: New session state """ self.session_state = state - + # Update status label colors = { SessionState.IDLE: "#888", @@ -1247,7 +1290,7 @@ class MainWindow(QMainWindow): SessionState.ERROR: "#f44336", SessionState.STOPPING: "#ff5722" } - + self.session_status_label.setText(state.value) self.session_status_label.setStyleSheet(f""" QLabel {{ @@ -1259,34 +1302,34 @@ class MainWindow(QMainWindow): border: 1px solid #444; }} """) - + # Update status bar self.status_state_label.setText(f"● {state.value}") self.status_state_label.setStyleSheet(f"color: {colors.get(state, '#888')}; padding: 0 10px;") - + # Update buttons self.start_session_btn.setEnabled( state == SessionState.IDLE and self.current_project is not None ) self.stop_session_btn.setEnabled(state in (SessionState.RUNNING, SessionState.PAUSED)) self.pause_session_btn.setEnabled(state in (SessionState.RUNNING, SessionState.PAUSED)) - + # Update menu actions self.start_action.setEnabled(self.start_session_btn.isEnabled()) self.stop_action.setEnabled(self.stop_session_btn.isEnabled()) self.pause_action.setEnabled(self.pause_session_btn.isEnabled()) - + if state == SessionState.IDLE: self.pause_session_btn.setText("⏸️ Pause") - + # ======================================================================== # Log Handling # ======================================================================== - + def on_log_event(self, event: LogEvent): """ Handle incoming log events. - + Args: event: The log event to display """ @@ -1298,32 +1341,32 @@ class MainWindow(QMainWindow): "ERROR": "#f44336", "CRITICAL": "#e91e63" } - + color = colors.get(event.level, "#e0e0e0") html = f'{self.escape_html(str(event))}' - + self.log_output.append(html) - + # Auto-scroll to bottom scrollbar = self.log_output.verticalScrollBar() scrollbar.setValue(scrollbar.maximum()) - + def log_debug(self, source: str, message: str): """Log a debug message.""" self._append_log("DEBUG", source, message) - + def log_info(self, source: str, message: str): """Log an info message.""" self._append_log("INFO", source, message) - + def log_warning(self, source: str, message: str): """Log a warning message.""" self._append_log("WARNING", source, message) - + def log_error(self, source: str, message: str): """Log an error message.""" self._append_log("ERROR", source, message) - + def _append_log(self, level: str, source: str, message: str): """Append log message to output.""" timestamp = datetime.now().strftime("%H:%M:%S") @@ -1333,52 +1376,52 @@ class MainWindow(QMainWindow): "WARNING": "#ff9800", "ERROR": "#f44336" }.get(level, "#ffffff") - + log_entry = f'[{timestamp}] [{level}] [{source}] {self.escape_html(message)}' self.log_output.append(log_entry) - + # Auto-scroll to bottom scrollbar = self.log_output.verticalScrollBar() scrollbar.setValue(scrollbar.maximum()) - + def escape_html(self, text: str) -> str: """Escape HTML special characters.""" return (text .replace("&", "&") .replace("<", "<") .replace(">", ">")) - + # ======================================================================== # Menu Actions # ======================================================================== - + def on_show_hud(self): """Show the HUD overlay.""" self.hud.show() self.log_info("HUD", "HUD overlay shown") - + def on_hide_hud(self): """Hide the HUD overlay.""" self.hud.hide() self.log_info("HUD", "HUD overlay hidden") - + def on_settings(self): """Open settings dialog.""" dialog = SettingsDialog(self) dialog.exec() - + def on_loadout_manager(self): """Open Loadout Manager dialog.""" from ui.loadout_manager import LoadoutManagerDialog dialog = LoadoutManagerDialog(self) dialog.loadout_saved.connect(self.on_loadout_selected) dialog.exec() - + def on_loadout_selected(self, loadout): """Handle loadout selection from Loadout Manager.""" self._selected_loadout = loadout self.log_info("Loadout", f"Selected loadout: {loadout.name}") - + # Update selected gear from loadout if hasattr(loadout, 'weapon_name'): self._selected_weapon = loadout.weapon_name @@ -1389,19 +1432,19 @@ class MainWindow(QMainWindow): 'decay': float(loadout.heal_cost_pec), 'cost_per_heal': float(loadout.heal_cost_pec) / 100.0, # Convert PEC to PED } - + def on_select_gear(self, gear_type: str = "weapon"): """Open Gear Selector dialog.""" from ui.gear_selector import GearSelectorDialog - + dialog = GearSelectorDialog(gear_type, self) dialog.gear_selected.connect(self.on_gear_selected) dialog.exec() - + def on_gear_selected(self, gear_type: str, name: str, stats: dict): """Handle gear selection.""" self.log_info("Gear", f"Selected {gear_type}: {name}") - + if gear_type == "weapon": self._selected_weapon = name self._selected_weapon_stats = stats @@ -1418,7 +1461,7 @@ class MainWindow(QMainWindow): self._selected_medical_tool_stats = stats if self.session_state == SessionState.RUNNING: self.hud.update_stats({'medical_tool': name}) - + def on_about(self): """Show about dialog.""" QMessageBox.about( @@ -1436,11 +1479,11 @@ class MainWindow(QMainWindow): """ ) - + # ======================================================================== # Event Overrides # ======================================================================== - + def closeEvent(self, event): """Handle window close event.""" if self.session_state == SessionState.RUNNING: @@ -1451,7 +1494,7 @@ class MainWindow(QMainWindow): QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No ) - + if reply == QMessageBox.StandardButton.Yes: self.on_stop_session() event.accept() @@ -1468,15 +1511,15 @@ class MainWindow(QMainWindow): def main(): """Main entry point for testing.""" app = QApplication(sys.argv) - + # Set application-wide font font = QFont("Segoe UI", 10) app.setFont(font) - + # Create and show main window window = MainWindow() window.show() - + # Simulate some log activity for demonstration def simulate_logs(): import random @@ -1490,7 +1533,7 @@ def main(): "Buffer cleared", "Sync complete" ] - + if window.session_state == SessionState.RUNNING: if random.random() < 0.3: # 30% chance each tick event = LogEvent( @@ -1500,12 +1543,12 @@ def main(): message=random.choice(messages) ) window.log_watcher.emit(event) - + # Timer to simulate log activity timer = QTimer() timer.timeout.connect(simulate_logs) timer.start(1000) # Every second - + sys.exit(app.exec())