""" Lemontropia Suite - Main Application Window PyQt6 GUI for managing game automation projects and sessions. """ import sys from datetime import datetime from enum import Enum, auto from typing import Optional, List, Callable from dataclasses import dataclass from PyQt6.QtWidgets import ( QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QSplitter, QPushButton, QListWidget, QListWidgetItem, QTextEdit, QLabel, QStatusBar, QMenuBar, QMenu, QDialog, QLineEdit, QFormLayout, QDialogButtonBox, QMessageBox, QGroupBox, QFrame, QApplication, QTreeWidget, QTreeWidgetItem, QHeaderView ) from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QSize from PyQt6.QtGui import QAction, QFont, QColor, QPalette, QIcon # ============================================================================ # Data Models # ============================================================================ class SessionState(Enum): """Session state enumeration.""" IDLE = "Idle" RUNNING = "Running" PAUSED = "Paused" ERROR = "Error" STOPPING = "Stopping" @dataclass class Project: """Project data model.""" id: int name: str description: str = "" created_at: Optional[datetime] = None session_count: int = 0 last_session: Optional[datetime] = None @dataclass class LogEvent: """Log event data model.""" timestamp: datetime level: str # DEBUG, INFO, WARNING, ERROR, CRITICAL source: str message: str def __str__(self) -> str: time_str = self.timestamp.strftime("%H:%M:%S.%f")[:-3] return f"[{time_str}] [{self.level}] [{self.source}] {self.message}" # ============================================================================ # HUD Overlay # ============================================================================ from ui.hud_overlay import HUDOverlay # ============================================================================ # Core Integration # ============================================================================ import os import asyncio from pathlib import Path from decimal import Decimal # Add core to path for imports sys.path.insert(0, str(Path(__file__).parent.parent / "core")) from core.log_watcher import LogWatcher from core.project_manager import ProjectManager from core.database import DatabaseManager # ============================================================================ # Custom Dialogs # ============================================================================ 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 ) 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() if not name: QMessageBox.warning(self, "Validation Error", "Project name is required.") return super().accept() 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) layout.addWidget(button_box) 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 ) button_box.accepted.connect(self.accept) button_box.rejected.connect(self.reject) layout.addWidget(button_box) # ============================================================================ # Main Window # ============================================================================ 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 self._selected_armor: Optional[str] = None self._selected_armor_stats: Optional[dict] = None self._selected_finder: Optional[str] = None self._selected_finder_stats: Optional[dict] = None 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"]) self.project_list.setAlternatingRowColors(True) self.project_list.setSelectionMode(QTreeWidget.SelectionMode.SingleSelection) 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) header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) header.setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed) header.setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed) 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:")) self.session_status_label = QLabel("Idle") self.session_status_label.setStyleSheet(""" QLabel { font-weight: bold; color: #888; padding: 5px 15px; background-color: #2a2a2a; border-radius: 4px; border: 1px solid #444; } """) 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; border-radius: 6px; margin-top: 10px; 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; border-radius: 4px; 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; border: none; border-right: 1px solid #444; font-weight: bold; } QTextEdit { background-color: #151515; border: 1px solid #444; border-radius: 4px; padding: 8px; color: #d0d0d0; } QLineEdit { background-color: #252525; border: 1px solid #444; border-radius: 4px; 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), project.name, project.type, project.status ]) 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;") self.view_stats_btn.setEnabled(True) self.start_session_btn.setEnabled(self.session_state == SessionState.IDLE) self.status_project_label.setText(f"Project: {self.current_project.name}") self.log_debug("ProjectManager", f"Selected project: {self.current_project.name}") else: self.current_project = None self.current_project_label.setText("No project selected") self.current_project_label.setStyleSheet("font-weight: bold; color: #888;") 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) if dialog.exec() == QDialog.DialogCode.Accepted: name, description = dialog.get_project_data() metadata = {"description": description} if description else None project = self.project_manager.create_project(name, 'hunt', metadata) 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 for p in projects: 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" test_data_dir.mkdir(exist_ok=True) mock_log = test_data_dir / "mock-chat.log" if not mock_log.exists(): from core.log_watcher import MockLogGenerator MockLogGenerator.create_mock_file(mock_log, lines=50) self.log_watcher = LogWatcher(str(mock_log), poll_interval=2.0, mock_mode=True) self.log_info("LogWatcher", "Using MOCK data for testing") 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))) 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.""" 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') if self._selected_loadout and hasattr(self._selected_loadout, 'heal_cost_pec'): # heal_cost_pec is the decay per heal in PEC # Convert to PED for cost calculation decay_cost = self._selected_loadout.heal_cost_pec / Decimal('100') elif self._selected_medical_tool_stats and 'decay' in self._selected_medical_tool_stats: decay_pec = Decimal(str(self._selected_medical_tool_stats['decay'])) decay_cost = decay_pec / Decimal('100') 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({ 'type': 'loot', 'session_id': self._current_db_session_id, 'item_name': item_name, 'quantity': quantity, '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: # Get decay per shot from weapon stats (in PEC) decay = self._selected_weapon_stats.get('decay', 0) if decay: from decimal import Decimal # 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'): armor_decay_pec = Decimal(str(self._selected_armor_stats.get('decay', 0))) if armor_decay_pec > 0: # Convert PEC to PED (1 PED = 100 PEC) 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) self.log_watcher.subscribe('personal_global', on_personal_global) 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_critical_hit) 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) try: loop.run_until_complete(self.watcher.start()) while self._running: loop.run_until_complete(asyncio.sleep(0.1)) except Exception as e: print(f"LogWatcher error: {e}") 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: self._log_watcher_thread.stop() self._log_watcher_thread.wait(2000) # Wait up to 2 seconds self._log_watcher_thread = None self.log_info("LogWatcher", "Stopped") 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( item_name=event['item_name'], quantity=event['quantity'], value_ped=event['value_ped'], event_type='regular', 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.""" if self.current_project and self.session_state == SessionState.IDLE: 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: self.set_session_state(SessionState.PAUSED) self.session_paused.emit() self.log_info("Session", "Session paused") self.session_info_label.setText("Session paused") self.pause_session_btn.setText("βΆοΈ Resume") elif self.session_state == SessionState.PAUSED: self.set_session_state(SessionState.RUNNING) self.session_resumed.emit() 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", SessionState.RUNNING: "#4caf50", SessionState.PAUSED: "#ff9800", SessionState.ERROR: "#f44336", SessionState.STOPPING: "#ff5722" } self.session_status_label.setText(state.value) self.session_status_label.setStyleSheet(f""" QLabel {{ font-weight: bold; color: {colors.get(state, '#888')}; padding: 5px 15px; background-color: #2a2a2a; border-radius: 4px; 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 """ # Color mapping colors = { "DEBUG": "#888", "INFO": "#4fc3f7", "WARNING": "#ff9800", "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") color = { "DEBUG": "#888888", "INFO": "#4caf50", "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 if hasattr(loadout, 'heal_cost_pec'): # Create medical tool stats from loadout heal cost self._selected_medical_tool = loadout.heal_name self._selected_medical_tool_stats = { '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 if self.session_state == SessionState.RUNNING: self.hud.update_stats({'weapon': name}) elif gear_type == "armor": self._selected_armor = name self._selected_armor_stats = stats elif gear_type == "finder": self._selected_finder = name self._selected_finder_stats = stats elif gear_type == "medical_tool": self._selected_medical_tool = name 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( self, "About Lemontropia Suite", """
Version 1.0.0
A PyQt6-based GUI for game automation and session management.
Features: