diff --git a/docs/CODEBASE_AUDIT_REPORT.md b/docs/CODEBASE_AUDIT_REPORT.md new file mode 100644 index 0000000..0c7b514 --- /dev/null +++ b/docs/CODEBASE_AUDIT_REPORT.md @@ -0,0 +1,486 @@ +# Lemontropia Suite - Comprehensive Codebase Audit Report +**Date:** 2026-02-11 +**Auditor:** Sub-agent Analysis +**Project Path:** `/home/impulsivefps/.openclaw/workspace/projects/Lemontropia-Suite` + +--- + +## π EXECUTIVE SUMMARY + +| Category | Count | Status | +|----------|-------|--------| +| **Total Features Implemented** | 28 | β | +| **Wired to Main UI** | 14 | β Connected | +| **Partially Wired** | 3 | β οΈ Needs Work | +| **Orphaned (Not Wired)** | 11 | π Not Connected | + +--- + +## π COMPLETE FEATURE INVENTORY + +### 1. CORE MODULES (`core/`) + +| Module | Description | Status | Wiring | +|--------|-------------|--------|--------| +| `database.py` | SQLite database management | β Implemented | β Auto-initialized on startup | +| `project_manager.py` | Project CRUD operations | β Implemented | β Full UI integration | +| `log_watcher.py` | Chat.log parsing & events | β Implemented | β Active during sessions | +| `hunting_session.py` | Session state management | β Implemented | β Via project_manager | +| `session_cost_tracker.py` | Cost tracking calculations | β Implemented | β οΈ Partial (HUD uses simplified) | +| `armor_system.py` | Armor protection calculations | β Implemented | β Via Armor selectors | +| `armor_decay.py` | Armor decay formulas | β Implemented | β Integrated | +| `entropia_nexus.py` | Nexus API client | β Implemented | β Used by gear selectors | +| `nexus_api.py` | Extended Nexus API | β Implemented | β Used by gear selectors | +| `nexus_full_api.py` | Full Nexus API with models | β Implemented | β Used by loadout manager | +| `healing_tools.py` | Healing/FAP calculations | β Implemented | β Via healing selector | +| `attachments.py` | Weapon attachment stats | β Implemented | β Via attachment selector | +| `loadout_db.py` | Loadout database storage | β Implemented | β Used by loadout manager | + +### 2. UI COMPONENTS (`ui/`) + +| Component | Description | Status | Wiring | +|-----------|-------------|--------|--------| +| `main_window.py` | Primary application window | β Implemented | β Root component | +| `hud_overlay_clean.py` | In-game HUD overlay | β Implemented | β Menu + auto-show on session | +| `hud_overlay.py` | Legacy HUD (superseded) | β Implemented | β Not used (clean version preferred) | +| `loadout_manager_simple.py` | Simplified loadout editor | β Implemented | β Tools menu | +| `loadout_manager.py` | Full loadout manager (legacy) | β Implemented | β Superseded by simple version | +| `loadout_selection_dialog_simple.py` | Pre-session loadout picker | β Implemented | β Auto-shows on Start Session | +| `loadout_selection_dialog.py` | Legacy selection dialog | β Implemented | β Not used | +| `gear_selector.py` | Generic gear selector | β Implemented | β Tools > Select Gear | +| `weapon_selector.py` | Weapon picker | β Implemented | β Via gear selector | +| `armor_selector.py` | Armor picker | β Implemented | β Via gear selector | +| `armor_set_selector.py` | Full armor set picker | β Implemented | β Not directly wired | +| `armor_selection_dialog.py` | Armor selection dialog | β Implemented | β Not directly wired | +| `healing_selector.py` | FAP/Healing picker | β Implemented | β Via gear selector | +| `attachment_selector.py` | Weapon attachments | β Implemented | β Via weapon selector | +| `enhancer_selector.py` | Weapon enhancers | β Implemented | β Via weapon selector | +| `plate_selector.py` | Armor plate picker | β Implemented | β Via armor selector | +| `mindforce_selector.py` | Mindforce chips | β Implemented | β οΈ Exists but rarely used | +| `accessories_selector.py` | Hunting accessories | β Implemented | β Not wired to main UI | +| `icon_price_dialogs.py` | Icon browser + Price tracker | β Implemented | β **ORPHANED** | + +### 3. FEATURE MODULES (`modules/`) + +| Module | Description | Status | Wiring | +|--------|-------------|--------|--------| +| `icon_manager.py` | Download/cache item icons | β Implemented | β **ORPHANED** | +| `market_prices.py` | Price tracking & profit calc | β Implemented | β **ORPHANED** | +| `loot_analyzer.py` | Loot breakdown analysis | β Implemented | β **ORPHANED** | +| `crafting_tracker.py` | Crafting session tracker | β Implemented | β **ORPHANED** | +| `game_vision.py` | Screen capture & OCR | β Implemented | β **ORPHANED** | +| `notifications.py` | Discord/Telegram alerts | β Implemented | β **ORPHANED** | +| `auto_screenshot.py` | Auto-capture on events | β Implemented | β **ORPHANED** | + +--- + +## π WIRING DIAGRAM + +``` +βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ +β LEMONTROPIA SUITE MAIN UI β +β (main_window.py) β +βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ +β β +β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β +β β FILE MENU SESSION MENU VIEW MENU β β +β β βββ New Project βββ Start βββ Show/Hide HUD β β +β β βββ Open Project βββ Stop βββ Settings β β +β β βββ Exit βββ Pause β β +β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β +β β +β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β +β β TOOLS MENU β β +β β βββ Select Gear ββββββββββββββββββββββββββββ β β +β β βββ Weapon ββββββββΊ weapon_selector.py β β β +β β βββ Armor βββββββββΊ armor_selector.py β β β +β β βββ Finder ββββββββΊ gear_selector.py β β β +β β βββ Medical Tool ββΊ healing_selector.pyβ β β +β β β β +β β βββ Loadout Manager βββΊ loadout_manager_simple.py ββββββ β β +β β (Opens for editing/saving loadouts) β β β +β β β β β +β β [ORPHANED - NO MENU ENTRY]: β β β +β β βββ Icon Browser ββββββΊ icon_price_dialogs.py β β β +β β βββ Price Tracker βββββΊ icon_price_dialogs.py β β β +β β βββ Loot Analyzer βββββΊ loot_analyzer.py β β β +β β βββ Crafting Tracker ββΊ crafting_tracker.py β β β +β β βββ Notification ConfigβΊ notifications.py β β β +β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β +β β +β βββββββββββββββββββββββ βββββββββββββββββββββββ ββββββββββββββββββ β +β β PROJECT PANEL β β SESSION PANEL β β LOG PANEL β β +β β βββββββββββββ β β βββββββββββββ β β βββββββββ β β +β β List of projects β β Start/Stop/Pause β β Event log β β +β β Create/View Stats β β Status display β β Real-time β β +β ββββββββββββ¬βββββββββββ ββββββββββββ¬βββββββββββ ββββββββββββββββββ β +β β β β +β βΌ βΌ β +β βββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββββββββ β +β β project_manager.py β β Start Session Button Pressed β β +β β database.py β β β β β +β β (CRUD operations) β β βΌ β β +β βββββββββββββββββββββββ β loadout_selection_dialog_simple.py β β +β β β β β +β β βΌ β β +β β User selects loadout ββββ β β +β β β β β +β β ββββββββββββββββββββ β β +β β βΌ β β +β β Session starts β β +β β β β β +β βΌ βΌ β β +β ββββββββββββββββββββββββββββ β β +β β HUD OVERLAY SYSTEM β β β +β β (hud_overlay_clean.py) β β β +β ββββββββββββββββββββββββββββ€ β β +β β β’ Live profit/loss β β β +β β β’ Return % β β β +β β β’ Cost tracking β β β +β β β’ Loot tracking β β β +β β β’ Gear info β β β +β ββββββββββββββ¬ββββββββββββββ β β +β β β β +β βΌ β β +β ββββββββββββββββββββββββββββ β β +β β log_watcher.py β β β +β β (parses chat.log) β β β +β ββββββββββββββββββββββββββββ β β +β β +βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ + +βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ +β ORPHANED FEATURES (Not Wired) β +βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ +β β +β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ β +β β ICON MANAGER β β MARKET PRICES β β LOOT ANALYZER β β +β β ββββββββββββ β β ββββββββββββ β β ββββββββββββ β β +β β β’ Download β β β’ Track prices β β β’ Mob stats β β +β β from Wiki β β β’ Calculate β β β’ Category β β +β β β’ Local cache β β profit β β breakdown β β +β β β’ Export icons β β β’ Markup % β β β’ Top loot β β +β β β β β β β’ CSV export β β +β β Status: FULLY β β Status: FULLY β β Status: FULLY β β +β β IMPLEMENTED β β IMPLEMENTED β β IMPLEMENTED β β +β β NOT ACCESSIBLE β β NOT ACCESSIBLE β β NOT ACCESSIBLE β β +β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ β +β β +β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ β +β β CRAFTING TRACKERβ β GAME VISION β β NOTIFICATIONS β β +β β βββββββββββββββ β β ββββββββββββ β β ββββββββββββ β β +β β β’ Blueprint QR β β β’ Screen captureβ β β’ Discord β β +β β β’ Success rates β β β’ OCR (Paddle) β β β’ Telegram β β +β β β’ Material inv β β β’ Template matchβ β β’ Sound alerts β β +β β β’ Profit calc β β β’ Gear detectionβ β β’ Custom msgs β β +β β β β β β β β +β β Status: FULLY β β Status: MOSTLY β β Status: FULLY β β +β β IMPLEMENTED β β IMPLEMENTED β β IMPLEMENTED β β +β β NOT ACCESSIBLE β β NOT ACCESSIBLE β β NOT ACCESSIBLE β β +β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ β +β β +β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β +β β AUTO SCREENSHOT β β +β β βββββββββββββ β β +β β β’ Capture on globals/HoFs β β +β β β’ Region capture β β +β β β’ Screenshot viewer β β +β β β β +β β Status: FULLY IMPLEMENTED β β +β β NOT ACCESSIBLE β β +β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β +β β +βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ +``` + +--- + +## π΄ IDENTIFIED ORPHANED FEATURES + +### Critical Priority (High Value, Ready to Wire) + +| # | Feature | Module | UI Dialog | Integration Point | Effort | +|---|---------|--------|-----------|-------------------|--------| +| 1 | **Icon Browser** | `icon_manager.py` | `icon_price_dialogs.py` | Tools menu | Low | +| 2 | **Price Tracker** | `market_prices.py` | `icon_price_dialogs.py` | Tools menu | Low | +| 3 | **Loot Analyzer** | `loot_analyzer.py` | Built-in | Session end / Tools menu | Medium | +| 4 | **Auto Screenshot** | `auto_screenshot.py` | Built-in | Settings dialog + events | Medium | + +### Medium Priority (Good Value, May Need Work) + +| # | Feature | Module | UI Dialog | Integration Point | Effort | +|---|---------|--------|-----------|-------------------|--------| +| 5 | **Notifications** | `notifications.py` | Settings dialog | Settings dialog + event hooks | Medium | +| 6 | **Crafting Tracker** | `crafting_tracker.py` | New dialog needed | Separate crafting mode | High | + +### Lower Priority (Niche Use Cases) + +| # | Feature | Module | UI Dialog | Integration Point | Effort | +|---|---------|--------|-----------|-------------------|--------| +| 7 | **Game Vision** | `game_vision.py` | Template capture tool | Settings or calibration wizard | High | +| 8 | **Armor Set Selector** | `armor_set_selector.py` | Built-in | Replace armor_selector | Low | +| 9 | **Mindforce Selector** | `mindforce_selector.py` | Built-in | Tools > Select Gear menu | Low | +| 10 | **Accessories Selector** | `accessories_selector.py` | Built-in | Tools > Select Gear menu | Low | + +--- + +## π SPECIFIC WIRING RECOMMENDATIONS + +### 1. Icon Browser & Price Tracker (Quick Win) + +**Location:** Add to `main_window.py` `create_menu_bar()` Tools menu + +```python +# In create_menu_bar() under tools_menu: +tools_menu.addSeparator() + +icon_browser_action = QAction("&Icon Browser", self) +icon_browser_action.triggered.connect(self.on_icon_browser) +tools_menu.addAction(icon_browser_action) + +price_tracker_action = QAction("&Price Tracker", self) +price_tracker_action.triggered.connect(self.on_price_tracker) +tools_menu.addAction(price_tracker_action) +``` + +**Add handlers:** +```python +def on_icon_browser(self): + from ui.icon_price_dialogs import IconBrowserDialog + dialog = IconBrowserDialog(self) + dialog.exec() + +def on_price_tracker(self): + from ui.icon_price_dialogs import PriceTrackerDialog + dialog = PriceTrackerDialog(self) + dialog.exec() +``` + +**Effort:** 5 minutes +**Value:** High - Complete UI already exists + +--- + +### 2. Loot Analyzer Integration + +**Option A - Session End Report:** +```python +# In on_stop_session() after ending session: +def on_stop_session(self): + # ... existing code ... + + # Generate loot analysis + from modules.loot_analyzer import LootAnalyzer + analyzer = LootAnalyzer() + + # Load loot from database for this session + loot_data = self.db.get_session_loot(self._current_db_session_id) + for item in loot_data: + analyzer.record_loot(item['name'], item['quantity'], + Decimal(str(item['value']))) + + # Show report or save to file + report = analyzer.generate_report() + self.log_info("LootAnalysis", "\n" + report) + analyzer.export_to_csv(Path.home() / ".lemontropia" / "loot_report.csv") +``` + +**Option B - Tools Menu:** +```python +loot_analyzer_action = QAction("&Loot Analyzer", self) +loot_analyzer_action.triggered.connect(self.on_loot_analyzer) +tools_menu.addAction(loot_analyzer_action) +``` + +**Effort:** 15-30 minutes +**Value:** High - Session analysis capability + +--- + +### 3. Auto Screenshot Integration + +**Add to SettingsDialog:** +```python +# In SettingsDialog setup_ui(): +screenshot_group = QGroupBox("Auto Screenshot") +screenshot_layout = QFormLayout(screenshot_group) + +self.screenshot_global_cb = QCheckBox("Capture on Global") +self.screenshot_hof_cb = QCheckBox("Capture on HoF") +screenshot_layout.addRow(self.screenshot_global_cb) +screenshot_layout.addRow(self.screenshot_hof_cb) +``` + +**Wire to events in main_window.py:** +```python +# In _setup_log_watcher_callbacks(), enhance existing handlers: +def on_personal_global(event): + value_ped = event.data.get('value_ped', Decimal('0')) + # ... existing code ... + + # Trigger auto-screenshot + if self.auto_screenshot and self.auto_screenshot.on_global: + self.auto_screenshot.on_global("Global", float(value_ped)) +``` + +**Effort:** 20 minutes +**Value:** Medium - Documentation capability + +--- + +### 4. Notifications Integration + +**Add to SettingsDialog:** +```python +notifications_group = QGroupBox("Notifications") +notifications_layout = QFormLayout(notifications_group) + +self.discord_webhook_edit = QLineEdit() +self.discord_webhook_edit.setPlaceholderText("Discord webhook URL...") +notifications_layout.addRow("Discord:", self.discord_webhook_edit) + +self.notify_global_cb = QCheckBox("Notify on Global") +self.notify_hof_cb = QCheckBox("Notify on HoF") +notifications_layout.addRow(self.notify_global_cb) +notifications_layout.addRow(self.notify_hof_cb) +``` + +**Initialize in MainWindow:** +```python +def __init__(self): + # ... after settings load ... + from modules.notifications import NotificationManager, NotificationConfig + notif_config = NotificationConfig( + discord_webhook=settings.value("notifications/discord", ""), + notify_on_global=settings.value("notifications/on_global", True), + notify_on_hof=settings.value("notifications/on_hof", True) + ) + self.notification_manager = NotificationManager(notif_config) +``` + +**Wire to events:** +```python +def on_personal_global(self, value_ped): + # ... existing code ... + self.notification_manager.on_global("Global", value_ped) + +def on_hof(self, value_ped): + # ... existing code ... + self.notification_manager.on_hof("HoF", value_ped) +``` + +**Effort:** 30 minutes +**Value:** High - External alerts + +--- + +### 5. Crafting Tracker (Separate Mode) + +**Recommendation:** Create a "Mode Switcher" in main UI + +```python +# Add to main_window.py setup_ui(): +mode_layout = QHBoxLayout() +mode_layout.addWidget(QLabel("Mode:")) +self.mode_combo = QComboBox() +self.mode_combo.addItems(["Hunting", "Crafting", "Mining"]) +self.mode_combo.currentTextChanged.connect(self.on_mode_changed) +mode_layout.addWidget(self.mode_combo) +main_layout.addLayout(mode_layout) + +def on_mode_changed(self, mode): + if mode == "Crafting": + from modules.crafting_tracker import CraftingTracker + self.crafting_tracker = CraftingTracker() + # Show crafting-specific UI +``` + +**Effort:** 2-4 hours +**Value:** Medium - For crafters + +--- + +## π οΈ IMPLEMENTATION PRIORITY MATRIX + +| Feature | User Value | Dev Effort | Priority | Quick Win? | +|---------|-----------|------------|----------|------------| +| Icon Browser | High | Low | **P1** | β Yes | +| Price Tracker | High | Low | **P1** | β Yes | +| Loot Analyzer | High | Medium | **P2** | β οΈ Medium | +| Auto Screenshot | Medium | Medium | **P2** | β οΈ Medium | +| Notifications | High | Medium | **P2** | β οΈ Medium | +| Crafting Tracker | Medium | High | **P3** | β No | +| Game Vision | Medium | High | **P3** | β No | + +--- + +## π FILE STRUCTURE REFERENCE + +``` +projects/Lemontropia-Suite/ +βββ main.py # CLI entry point +βββ gui_main.py # GUI launcher stub +βββ ui/ +β βββ main_window.py β WIRED - Main UI +β βββ hud_overlay_clean.py β WIRED - Active HUD +β βββ hud_overlay.py β Legacy (unused) +β βββ loadout_manager_simple.py β WIRED - Tools menu +β βββ loadout_manager.py β Legacy (unused) +β βββ icon_price_dialogs.py β ORPHANED - Ready to wire +β βββ gear_selector.py β WIRED +β βββ weapon_selector.py β WIRED +β βββ armor_selector.py β WIRED +β βββ healing_selector.py β WIRED +β βββ armor_set_selector.py β οΈ Partially wired +β βββ armor_selection_dialog.py β Unused +β βββ attachment_selector.py β WIRED +β βββ enhancer_selector.py β WIRED +β βββ plate_selector.py β WIRED +β βββ mindforce_selector.py β ORPHANED +β βββ accessories_selector.py β ORPHANED +β βββ ... +βββ core/ +β βββ database.py β WIRED +β βββ project_manager.py β WIRED +β βββ log_watcher.py β WIRED +β βββ session_cost_tracker.py β οΈ Partially used +β βββ ... +βββ modules/ + βββ icon_manager.py β ORPHANED + βββ market_prices.py β ORPHANED + βββ loot_analyzer.py β ORPHANED + βββ crafting_tracker.py β ORPHANED + βββ game_vision.py β ORPHANED + βββ notifications.py β ORPHANED + βββ auto_screenshot.py β ORPHANED +``` + +--- + +## β CONCLUSION + +### What's Working Well +- **Core hunting loop** is fully functional (Projects β Sessions β HUD) +- **Loadout system** is well-integrated with cost tracking +- **Log parsing** is robust with event detection +- **HUD overlay** provides real-time session metrics + +### Quick Wins (Do These First) +1. **Wire Icon Browser** - 5 minutes, complete UI exists +2. **Wire Price Tracker** - 5 minutes, same dialog file +3. **Add Loot Analyzer report** on session end +4. **Add Notifications settings** with Discord/Telegram + +### Technical Debt +- Two versions of HUD exist (clean version preferred) +- Two versions of loadout manager exist (simple version preferred) +- Some selectors exist but aren't in the Select Gear menu + +### Estimated Time to Full Integration +- **Minimum viable:** 1 hour (Icon Browser + Price Tracker) +- **Recommended:** 2-3 hours (+ Loot Analyzer + Auto Screenshot) +- **Complete:** 1-2 days (+ Notifications + Crafting + Vision) + +--- + +*Report generated by Sub-agent on 2026-02-11* diff --git a/gui_main.py b/gui_main.py index 5af85b6..5485182 100644 --- a/gui_main.py +++ b/gui_main.py @@ -16,6 +16,7 @@ sys.path.insert(0, str(Path(__file__).parent)) from PyQt6.QtWidgets import QApplication from PyQt6.QtCore import Qt from ui.main_window import MainWindow +from ui.setup_wizard import SetupWizard def main(): @@ -28,6 +29,14 @@ def main(): app.setApplicationName("Lemontropia Suite") app.setApplicationVersion("0.2.0") + # Check if first run - show setup wizard + if SetupWizard.is_first_run(): + wizard = SetupWizard(first_run=True) + if wizard.exec() != SetupWizard.DialogCode.Accepted: + # User cancelled wizard, exit + print("Setup cancelled by user") + sys.exit(0) + # Create and show main window window = MainWindow() window.show() @@ -36,4 +45,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/ui/amplifier_selector.py b/ui/amplifier_selector.py new file mode 100644 index 0000000..79513ba --- /dev/null +++ b/ui/amplifier_selector.py @@ -0,0 +1,166 @@ +""" +Weapon Amplifier Selector Dialog +Uses /weaponamplifiers API endpoint +""" + +from PyQt6.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, + QListWidget, QListWidgetItem, QLineEdit, QGroupBox, + QFormLayout, QMessageBox +) +from PyQt6.QtCore import Qt +from decimal import Decimal + +from core.nexus_full_api import get_nexus_api + + +class AmplifierSelectorDialog(QDialog): + """Dialog for selecting weapon amplifiers.""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Select Weapon Amplifier") + self.setMinimumSize(600, 500) + + self._selected_amplifier = None + self._amplifiers_cache = [] + + self._setup_ui() + self._load_amplifiers() + + def _setup_ui(self): + layout = QVBoxLayout(self) + + # Search bar + search_layout = QHBoxLayout() + search_layout.addWidget(QLabel("Search:")) + self.search_edit = QLineEdit() + self.search_edit.setPlaceholderText("Type to search amplifiers...") + self.search_edit.textChanged.connect(self._on_search) + search_layout.addWidget(self.search_edit) + layout.addLayout(search_layout) + + # Amplifier list + self.list_widget = QListWidget() + self.list_widget.itemClicked.connect(self._on_item_selected) + self.list_widget.itemDoubleClicked.connect(self.accept) + layout.addWidget(self.list_widget) + + # Info panel + info_group = QGroupBox("Amplifier Info") + info_layout = QFormLayout(info_group) + + self.info_name = QLabel("Select an amplifier") + self.info_damage = QLabel("-") + self.info_decay = QLabel("-") + self.info_ammo = QLabel("-") + self.info_efficiency = QLabel("-") + self.info_cost = QLabel("-") + + info_layout.addRow("Name:", self.info_name) + info_layout.addRow("Damage Bonus:", self.info_damage) + info_layout.addRow("Decay (PEC):", self.info_decay) + info_layout.addRow("Ammo Burn:", self.info_ammo) + info_layout.addRow("Efficiency:", self.info_efficiency) + info_layout.addRow("Cost/Shot:", self.info_cost) + + layout.addWidget(info_group) + + # Buttons + button_layout = QHBoxLayout() + button_layout.addStretch() + + self.select_btn = QPushButton("Select") + self.select_btn.clicked.connect(self.accept) + self.select_btn.setEnabled(False) + button_layout.addWidget(self.select_btn) + + cancel_btn = QPushButton("Cancel") + cancel_btn.clicked.connect(self.reject) + button_layout.addWidget(cancel_btn) + + layout.addLayout(button_layout) + + def _load_amplifiers(self): + """Load amplifiers from API.""" + try: + api = get_nexus_api() + self._amplifiers_cache = api.get_all_amplifiers() + self._populate_list(self._amplifiers_cache) + except Exception as e: + QMessageBox.warning(self, "Error", f"Failed to load amplifiers: {e}") + + def _populate_list(self, amplifiers): + """Populate the list widget.""" + self.list_widget.clear() + + for amp in amplifiers: + # Format display text + decay_str = f"{amp.decay} PEC" if amp.decay else "N/A" + item_text = f"{amp.name} | Decay: {decay_str}" + + item = QListWidgetItem(item_text) + item.setData(Qt.ItemDataRole.UserRole, amp) + + # Tooltip with more info + tooltip = f"Type: {amp.attachment_type}\nDecay: {amp.decay} PEC" + if amp.damage_bonus > 0: + tooltip += f"\nDamage: +{amp.damage_bonus}" + if amp.efficiency_bonus > 0: + tooltip += f"\nEfficiency: {amp.efficiency_bonus}%" + item.setToolTip(tooltip) + + self.list_widget.addItem(item) + + def _on_search(self, text): + """Filter list based on search text.""" + if not text: + self._populate_list(self._amplifiers_cache) + return + + filtered = [amp for amp in self._amplifiers_cache + if text.lower() in amp.name.lower()] + self._populate_list(filtered) + + def _on_item_selected(self, item): + """Handle item selection.""" + amp = item.data(Qt.ItemDataRole.UserRole) + if not amp: + return + + # Store selection + decay_pec = Decimal(str(amp.decay)) if amp.decay else Decimal("0") + damage_bonus = Decimal(str(amp.damage_bonus)) if hasattr(amp, 'damage_bonus') else Decimal("0") + + self._selected_amplifier = { + 'name': amp.name, + 'api_id': amp.id, + 'decay_pec': decay_pec, + 'damage_bonus': damage_bonus, + 'efficiency': amp.efficiency_bonus if hasattr(amp, 'efficiency_bonus') else Decimal("0"), + } + + # Update info panel + self.info_name.setText(amp.name) + self.info_damage.setText(f"+{damage_bonus}") + self.info_decay.setText(f"{decay_pec} PEC") + + # Get ammo burn from properties if available + ammo_burn = "N/A" + if hasattr(amp, 'ammo_burn') and amp.ammo_burn: + ammo_burn = str(amp.ammo_burn) + self._selected_amplifier['ammo_burn'] = amp.ammo_burn + self.info_ammo.setText(ammo_burn) + + efficiency = f"{amp.efficiency_bonus}%" if hasattr(amp, 'efficiency_bonus') and amp.efficiency_bonus else "N/A" + self.info_efficiency.setText(efficiency) + + # Calculate cost per shot (decay in PEC converted to PED) + cost_per_shot = decay_pec / Decimal("100") + self.info_cost.setText(f"{cost_per_shot:.4f} PED") + + self.select_btn.setEnabled(True) + + def get_selected_amplifier(self): + """Get the selected amplifier data.""" + return self._selected_amplifier diff --git a/ui/gallery_dialog.py b/ui/gallery_dialog.py new file mode 100644 index 0000000..2c6ebb2 --- /dev/null +++ b/ui/gallery_dialog.py @@ -0,0 +1,835 @@ +""" +Lemontropia Suite - Gallery Dialog +Browse and manage screenshots captured during hunting sessions. +""" + +import os +import json +from datetime import datetime +from pathlib import Path +from typing import Optional, List, Dict, Any + +# PIL is optional - screenshots won't work without it but gallery can still view +try: + from PIL import Image, ImageGrab + PIL_AVAILABLE = True +except ImportError: + PIL_AVAILABLE = False + Image = None + ImageGrab = None + +from PyQt6.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, + QLabel, QPushButton, QListWidget, QListWidgetItem, + QSplitter, QWidget, QGroupBox, QComboBox, + QMessageBox, QFileDialog, QScrollArea, QFrame, + QSizePolicy, QGridLayout +) +from PyQt6.QtCore import Qt, pyqtSignal, QSize +from PyQt6.QtGui import QPixmap, QImage, QColor + +from core.database import DatabaseManager + + +class ImageLabel(QLabel): + """Custom label for displaying images with click handling.""" + + clicked = pyqtSignal() + + def __init__(self, parent=None): + super().__init__(parent) + self.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.setStyleSheet("background-color: #252525; border: 1px solid #444;") + self.setMinimumSize(400, 300) + self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + + def mousePressEvent(self, event): + self.clicked.emit() + + +class GalleryDialog(QDialog): + """ + Dialog for viewing and managing captured screenshots. + + Features: + - Browse all screenshots with thumbnails + - Filter by type (global, hof, all) + - View full-size image with metadata + - Delete screenshots + - Open in external viewer + """ + + screenshot_deleted = pyqtSignal(int) # Emits screenshot_id when deleted + + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Screenshot Gallery") + self.setMinimumSize(1400, 900) + self.resize(1600, 1000) + + # Initialize database + self.db = DatabaseManager() + + # Ensure screenshots directory exists + self.screenshots_dir = Path(__file__).parent.parent / "data" / "screenshots" + self.screenshots_dir.mkdir(parents=True, exist_ok=True) + + # State + self.current_screenshot_id: Optional[int] = None + self.screenshots_data: List[Dict[str, Any]] = [] + self.current_pixmap: Optional[QPixmap] = None + + self._setup_ui() + self._load_screenshots() + self._apply_dark_theme() + + def _setup_ui(self): + """Setup the dialog UI.""" + layout = QVBoxLayout(self) + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(10) + + # Top controls + controls_layout = QHBoxLayout() + + # Filter by type + controls_layout.addWidget(QLabel("Filter:")) + self.type_filter = QComboBox() + self.type_filter.addItem("π· All Screenshots", "all") + self.type_filter.addItem("π Globals", "global") + self.type_filter.addItem("π HoFs", "hof") + self.type_filter.currentIndexChanged.connect(self._on_filter_changed) + controls_layout.addWidget(self.type_filter) + + controls_layout.addSpacing(20) + + # Filter by session + controls_layout.addWidget(QLabel("Session:")) + self.session_filter = QComboBox() + self.session_filter.addItem("All Sessions", None) + self.session_filter.currentIndexChanged.connect(self._on_filter_changed) + controls_layout.addWidget(self.session_filter) + + controls_layout.addStretch() + + # Stats label + self.stats_label = QLabel("0 screenshots") + controls_layout.addWidget(self.stats_label) + + controls_layout.addSpacing(20) + + # Refresh button + self.refresh_btn = QPushButton("π Refresh") + self.refresh_btn.clicked.connect(self._load_screenshots) + controls_layout.addWidget(self.refresh_btn) + + # Open folder button + self.open_folder_btn = QPushButton("π Open Folder") + self.open_folder_btn.clicked.connect(self._open_screenshots_folder) + controls_layout.addWidget(self.open_folder_btn) + + layout.addLayout(controls_layout) + + # Main splitter + splitter = QSplitter(Qt.Orientation.Horizontal) + layout.addWidget(splitter) + + # Left side - Thumbnail list + left_panel = QWidget() + left_layout = QVBoxLayout(left_panel) + left_layout.setContentsMargins(0, 0, 0, 0) + + # Screenshots list + self.screenshots_list = QListWidget() + self.screenshots_list.setIconSize(QSize(120, 90)) + self.screenshots_list.setViewMode(QListWidget.ViewMode.IconMode) + self.screenshots_list.setResizeMode(QListWidget.ResizeMode.Adjust) + self.screenshots_list.setSpacing(10) + self.screenshots_list.setWrapping(True) + self.screenshots_list.setMinimumWidth(400) + self.screenshots_list.itemClicked.connect(self._on_screenshot_selected) + self.screenshots_list.itemDoubleClicked.connect(self._on_screenshot_double_clicked) + + left_layout.addWidget(self.screenshots_list) + + splitter.addWidget(left_panel) + + # Right side - Preview and details + right_panel = QWidget() + right_layout = QVBoxLayout(right_panel) + right_layout.setContentsMargins(0, 0, 0, 0) + + # Image preview area + self.preview_group = QGroupBox("Preview") + preview_layout = QVBoxLayout(self.preview_group) + + # Scroll area for image + self.scroll_area = QScrollArea() + self.scroll_area.setWidgetResizable(True) + self.scroll_area.setStyleSheet("background-color: #151515; border: none;") + + self.image_label = ImageLabel() + self.image_label.setText("Select a screenshot to preview") + self.image_label.clicked.connect(self._open_external_viewer) + self.scroll_area.setWidget(self.image_label) + + preview_layout.addWidget(self.scroll_area) + right_layout.addWidget(self.preview_group, stretch=1) + + # Details panel + self.details_group = QGroupBox("Details") + details_layout = QFormLayout(self.details_group) + + self.detail_id = QLabel("-") + details_layout.addRow("ID:", self.detail_id) + + self.detail_timestamp = QLabel("-") + details_layout.addRow("Captured:", self.detail_timestamp) + + self.detail_event_type = QLabel("-") + details_layout.addRow("Event Type:", self.detail_event_type) + + self.detail_value = QLabel("-") + details_layout.addRow("Value:", self.detail_value) + + self.detail_mob = QLabel("-") + details_layout.addRow("Mob:", self.detail_mob) + + self.detail_session = QLabel("-") + details_layout.addRow("Session:", self.detail_session) + + self.detail_file_path = QLabel("-") + self.detail_file_path.setWordWrap(True) + self.detail_file_path.setStyleSheet("font-size: 9px; color: #888;") + details_layout.addRow("File:", self.detail_file_path) + + self.detail_file_size = QLabel("-") + details_layout.addRow("Size:", self.detail_file_size) + + self.detail_dimensions = QLabel("-") + details_layout.addRow("Dimensions:", self.detail_dimensions) + + right_layout.addWidget(self.details_group) + + # Action buttons + actions_layout = QHBoxLayout() + + self.open_external_btn = QPushButton("π Open External") + self.open_external_btn.clicked.connect(self._open_external_viewer) + self.open_external_btn.setEnabled(False) + actions_layout.addWidget(self.open_external_btn) + + self.save_as_btn = QPushButton("πΎ Save As...") + self.save_as_btn.clicked.connect(self._save_as) + self.save_as_btn.setEnabled(False) + actions_layout.addWidget(self.save_as_btn) + + actions_layout.addStretch() + + self.delete_btn = QPushButton("ποΈ Delete") + self.delete_btn.clicked.connect(self._delete_screenshot) + self.delete_btn.setEnabled(False) + actions_layout.addWidget(self.delete_btn) + + right_layout.addLayout(actions_layout) + + splitter.addWidget(right_panel) + + # Set splitter sizes + splitter.setSizes([500, 700]) + + # Close button + close_layout = QHBoxLayout() + close_layout.addStretch() + + self.close_btn = QPushButton("Close") + self.close_btn.clicked.connect(self.accept) + close_layout.addWidget(self.close_btn) + + layout.addLayout(close_layout) + + def _load_screenshots(self): + """Load screenshots from database.""" + self.screenshots_list.clear() + self.screenshots_data = [] + + try: + # Load sessions for filter + self._load_sessions() + + # Get filter values + event_filter = self.type_filter.currentData() + session_filter = self.session_filter.currentData() + + # Build query - check both screenshots table and loot_events with screenshots + query = """ + SELECT + s.id, + s.session_id, + s.timestamp, + s.file_path, + s.trigger_event, + s.trigger_value_ped as value, + le.creature_name as mob_name, + le.event_type as loot_event_type, + p.name as project_name, + hs.started_at as session_date + FROM screenshots s + JOIN sessions ses ON s.session_id = ses.id + JOIN projects p ON ses.project_id = p.id + LEFT JOIN hunting_sessions hs ON hs.session_id = s.session_id + LEFT JOIN loot_events le ON le.session_id = s.session_id + AND le.screenshot_path = s.file_path + WHERE 1=1 + """ + params = [] + + # Apply event type filter + if event_filter == "global": + query += " AND (s.trigger_event LIKE '%global%' OR le.event_type = 'global')" + elif event_filter == "hof": + query += " AND (s.trigger_event LIKE '%hof%' OR s.trigger_event LIKE '%hall%' OR le.event_type = 'hof')" + + # Apply session filter + if session_filter: + query += " AND s.session_id = ?" + params.append(session_filter) + + query += " ORDER BY s.timestamp DESC" + + cursor = self.db.execute(query, tuple(params)) + rows = cursor.fetchall() + + for row in rows: + screenshot_data = dict(row) + self.screenshots_data.append(screenshot_data) + + # Create list item with thumbnail + item = QListWidgetItem() + item.setData(Qt.ItemDataRole.UserRole, screenshot_data['id']) + + # Set text with timestamp and value + timestamp = datetime.fromisoformat(screenshot_data['timestamp']) + time_str = timestamp.strftime("%m/%d %H:%M") + + value = screenshot_data['value'] or 0 + event_type = screenshot_data['trigger_event'] or screenshot_data['loot_event_type'] or "Manual" + + if value > 0: + item.setText(f"{time_str}\n{value:.0f} PED") + else: + item.setText(f"{time_str}\n{event_type}") + + # Load thumbnail + file_path = screenshot_data['file_path'] + if file_path and os.path.exists(file_path): + pixmap = QPixmap(file_path) + if not pixmap.isNull(): + # Scale to thumbnail size + scaled = pixmap.scaled(120, 90, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) + item.setIcon(scaled) + + self.screenshots_list.addItem(item) + + # Update stats + self.stats_label.setText(f"{len(self.screenshots_data)} screenshot(s)") + + except Exception as e: + QMessageBox.warning(self, "Error", f"Failed to load screenshots: {e}") + + def _load_sessions(self): + """Load sessions for filter dropdown.""" + current = self.session_filter.currentData() + self.session_filter.clear() + self.session_filter.addItem("All Sessions", None) + + try: + cursor = self.db.execute(""" + SELECT DISTINCT s.id, p.name, s.started_at + FROM sessions s + JOIN projects p ON s.project_id = p.id + JOIN screenshots sc ON sc.session_id = s.id + ORDER BY s.started_at DESC + """) + + for row in cursor.fetchall(): + started = datetime.fromisoformat(row['started_at']) + label = f"{row['name']} - {started.strftime('%Y-%m-%d %H:%M')}" + self.session_filter.addItem(label, row['id']) + + # Restore selection + if current: + idx = self.session_filter.findData(current) + if idx >= 0: + self.session_filter.setCurrentIndex(idx) + except Exception: + pass + + def _on_filter_changed(self): + """Handle filter changes.""" + self._load_screenshots() + + def _on_screenshot_selected(self, item: QListWidgetItem): + """Handle screenshot selection.""" + screenshot_id = item.data(Qt.ItemDataRole.UserRole) + self.current_screenshot_id = screenshot_id + self._load_screenshot_details(screenshot_id) + + def _on_screenshot_double_clicked(self, item: QListWidgetItem): + """Handle double-click on screenshot.""" + self._open_external_viewer() + + def _load_screenshot_details(self, screenshot_id: int): + """Load and display screenshot details.""" + try: + screenshot = next((s for s in self.screenshots_data if s['id'] == screenshot_id), None) + if not screenshot: + return + + file_path = screenshot['file_path'] + + # Update details + self.detail_id.setText(str(screenshot['id'])) + + timestamp = datetime.fromisoformat(screenshot['timestamp']) + self.detail_timestamp.setText(timestamp.strftime("%Y-%m-%d %H:%M:%S")) + + event_type = screenshot['trigger_event'] or screenshot['loot_event_type'] or "Manual" + self.detail_event_type.setText(event_type.capitalize()) + + value = screenshot['value'] or 0 + if value > 0: + self.detail_value.setText(f"{value:.2f} PED") + self.detail_value.setStyleSheet("color: #4caf50; font-weight: bold;") + else: + self.detail_value.setText("-") + self.detail_value.setStyleSheet("") + + mob = screenshot['mob_name'] or "Unknown" + self.detail_mob.setText(mob) + + session_info = f"{screenshot['project_name'] or 'Unknown'}" + if screenshot['session_date']: + session_date = datetime.fromisoformat(screenshot['session_date']) + session_info += f" ({session_date.strftime('%Y-%m-%d')})" + self.detail_session.setText(session_info) + + self.detail_file_path.setText(file_path or "-") + + # Load and display image + if file_path and os.path.exists(file_path): + self._display_image(file_path) + + # Get file info + file_size = os.path.getsize(file_path) + self.detail_file_size.setText(self._format_file_size(file_size)) + + # Enable buttons + self.open_external_btn.setEnabled(True) + self.save_as_btn.setEnabled(True) + self.delete_btn.setEnabled(True) + else: + self.image_label.setText(f"File not found:\n{file_path}") + self.image_label.setPixmap(QPixmap()) + self.current_pixmap = None + self.detail_file_size.setText("-") + self.detail_dimensions.setText("-") + + # Disable buttons + self.open_external_btn.setEnabled(False) + self.save_as_btn.setEnabled(False) + self.delete_btn.setEnabled(True) # Still allow delete if DB record exists + + except Exception as e: + QMessageBox.warning(self, "Error", f"Failed to load screenshot details: {e}") + + def _display_image(self, file_path: str): + """Load and display the image.""" + pixmap = QPixmap(file_path) + + if pixmap.isNull(): + self.image_label.setText("Failed to load image") + self.current_pixmap = None + return + + self.current_pixmap = pixmap + + # Update dimensions + self.detail_dimensions.setText(f"{pixmap.width()} x {pixmap.height()}") + + # Scale to fit while maintaining aspect ratio + scroll_size = self.scroll_area.size() + scaled = pixmap.scaled( + scroll_size.width() - 20, + scroll_size.height() - 20, + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation + ) + + self.image_label.setPixmap(scaled) + self.image_label.setText("") + + def _format_file_size(self, size_bytes: int) -> str: + """Format file size to human readable.""" + if size_bytes < 1024: + return f"{size_bytes} B" + elif size_bytes < 1024 * 1024: + return f"{size_bytes / 1024:.1f} KB" + else: + return f"{size_bytes / (1024 * 1024):.1f} MB" + + def _open_external_viewer(self): + """Open the screenshot in external viewer.""" + if not self.current_screenshot_id: + return + + screenshot = next( + (s for s in self.screenshots_data if s['id'] == self.current_screenshot_id), + None + ) + + if screenshot and screenshot['file_path'] and os.path.exists(screenshot['file_path']): + import subprocess + import platform + + file_path = screenshot['file_path'] + + try: + if platform.system() == 'Windows': + os.startfile(file_path) + elif platform.system() == 'Darwin': # macOS + subprocess.run(['open', file_path]) + else: # Linux + subprocess.run(['xdg-open', file_path]) + except Exception as e: + QMessageBox.warning(self, "Error", f"Failed to open image: {e}") + + def _save_as(self): + """Save the screenshot to a new location.""" + if not self.current_screenshot_id: + return + + screenshot = next( + (s for s in self.screenshots_data if s['id'] == self.current_screenshot_id), + None + ) + + if not screenshot or not screenshot['file_path']: + return + + source_path = Path(screenshot['file_path']) + + file_path, _ = QFileDialog.getSaveFileName( + self, + "Save Screenshot", + f"screenshot_{screenshot['id']}.png", + "PNG Images (*.png);;JPEG Images (*.jpg *.jpeg);;All Files (*)" + ) + + if not file_path: + return + + try: + import shutil + shutil.copy2(source_path, file_path) + QMessageBox.information(self, "Success", f"Screenshot saved to {file_path}") + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to save: {e}") + + def _delete_screenshot(self): + """Delete the selected screenshot.""" + if not self.current_screenshot_id: + return + + reply = QMessageBox.question( + self, "Confirm Delete", + "Are you sure you want to delete this screenshot?\n\n" + "This will permanently delete the file and its metadata.", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No + ) + + if reply != QMessageBox.StandardButton.Yes: + return + + try: + screenshot = next( + (s for s in self.screenshots_data if s['id'] == self.current_screenshot_id), + None + ) + + # Delete file if exists + if screenshot and screenshot['file_path'] and os.path.exists(screenshot['file_path']): + os.remove(screenshot['file_path']) + + # Delete from database + self.db.execute( + "DELETE FROM screenshots WHERE id = ?", + (self.current_screenshot_id,) + ) + self.db.commit() + + # Emit signal + self.screenshot_deleted.emit(self.current_screenshot_id) + + # Reload + self._load_screenshots() + self.current_screenshot_id = None + + # Clear details + self.image_label.setText("Select a screenshot to preview") + self.image_label.setPixmap(QPixmap()) + self.current_pixmap = None + + self.detail_id.setText("-") + self.detail_timestamp.setText("-") + self.detail_event_type.setText("-") + self.detail_value.setText("-") + self.detail_value.setStyleSheet("") + self.detail_mob.setText("-") + self.detail_session.setText("-") + self.detail_file_path.setText("-") + self.detail_file_size.setText("-") + self.detail_dimensions.setText("-") + + self.open_external_btn.setEnabled(False) + self.save_as_btn.setEnabled(False) + self.delete_btn.setEnabled(False) + + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to delete screenshot: {e}") + + def _open_screenshots_folder(self): + """Open the screenshots folder in file manager.""" + import subprocess + import platform + + folder_path = str(self.screenshots_dir) + + try: + if platform.system() == 'Windows': + os.startfile(folder_path) + elif platform.system() == 'Darwin': # macOS + subprocess.run(['open', folder_path]) + else: # Linux + subprocess.run(['xdg-open', folder_path]) + except Exception as e: + QMessageBox.warning(self, "Error", f"Failed to open folder: {e}") + + def resizeEvent(self, event): + """Handle resize to rescale image.""" + super().resizeEvent(event) + if self.current_pixmap: + scroll_size = self.scroll_area.size() + scaled = self.current_pixmap.scaled( + scroll_size.width() - 20, + scroll_size.height() - 20, + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation + ) + self.image_label.setPixmap(scaled) + + def _apply_dark_theme(self): + """Apply dark theme styling.""" + dark_stylesheet = """ + QDialog { + 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; + } + + QListWidget { + background-color: #252525; + border: 1px solid #444; + border-radius: 4px; + outline: none; + padding: 10px; + } + + QListWidget::item { + padding: 5px; + border-radius: 4px; + margin: 2px; + background-color: #2d2d2d; + } + + QListWidget::item:selected { + background-color: #0d47a1; + color: white; + } + + QListWidget::item:hover:!selected { + background-color: #3d3d3d; + } + + QComboBox { + background-color: #252525; + border: 1px solid #444; + border-radius: 4px; + padding: 6px; + color: #e0e0e0; + min-width: 150px; + } + + QComboBox:focus { + border-color: #0d47a1; + } + + QComboBox::drop-down { + border: none; + padding-right: 10px; + } + + QScrollArea { + border: none; + } + + QLabel { + color: #e0e0e0; + } + + QFormLayout QLabel { + color: #888; + } + + QSplitter::handle { + background-color: #444; + } + """ + self.setStyleSheet(dark_stylesheet) + + +class ScreenshotCapture: + """ + Utility class for capturing screenshots. + + Handles: + - Screen capture + - Saving to file with metadata + - Database recording + """ + + def __init__(self, db: Optional[DatabaseManager] = None): + self.db = db or DatabaseManager() + self.screenshots_dir = Path(__file__).parent.parent / "data" / "screenshots" + self.screenshots_dir.mkdir(parents=True, exist_ok=True) + + def capture(self, session_id: int, trigger_event: str = "manual", + value_ped: float = 0.0, mob_name: str = "") -> Optional[str]: + """ + Capture a screenshot and save it. + + Args: + session_id: The current session ID + trigger_event: What triggered the screenshot (global, hof, manual) + value_ped: Value of the event in PED + mob_name: Name of the mob/creature + + Returns: + Path to the saved screenshot, or None if failed + """ + if not PIL_AVAILABLE: + print("Screenshot capture requires PIL (Pillow). Install with: pip install Pillow") + return None + + try: + # Generate filename with timestamp + timestamp = datetime.now() + filename = f"{timestamp.strftime('%Y%m%d_%H%M%S')}_{trigger_event}" + if value_ped > 0: + filename += f"_{value_ped:.0f}PED" + filename += ".png" + + file_path = self.screenshots_dir / filename + + # Capture screenshot using PIL + screenshot = ImageGrab.grab() + screenshot.save(file_path, "PNG") + + # Save metadata to database + cursor = self.db.execute(""" + INSERT INTO screenshots + (session_id, timestamp, file_path, trigger_event, trigger_value_ped) + VALUES (?, ?, ?, ?, ?) + """, ( + session_id, + timestamp.isoformat(), + str(file_path), + trigger_event, + value_ped + )) + + # Also update loot_events if there's a matching recent event + self.db.execute(""" + UPDATE loot_events + SET screenshot_path = ? + WHERE session_id = ? + AND screenshot_path IS NULL + AND event_type IN ('global', 'hof') + ORDER BY timestamp DESC + LIMIT 1 + """, (str(file_path), session_id)) + + self.db.commit() + + return str(file_path) + + except Exception as e: + print(f"Screenshot capture failed: {e}") + return None + + def capture_global(self, session_id: int, value_ped: float, mob_name: str = "") -> Optional[str]: + """Capture screenshot for a global event.""" + return self.capture(session_id, "global", value_ped, mob_name) + + def capture_hof(self, session_id: int, value_ped: float, mob_name: str = "") -> Optional[str]: + """Capture screenshot for a HoF event.""" + return self.capture(session_id, "hof", value_ped, mob_name) + + def capture_manual(self, session_id: int) -> Optional[str]: + """Capture manual screenshot.""" + return self.capture(session_id, "manual", 0.0, "") diff --git a/ui/loadout_manager_simple.py b/ui/loadout_manager_simple.py index 992b976..ede9099 100644 --- a/ui/loadout_manager_simple.py +++ b/ui/loadout_manager_simple.py @@ -1,6 +1,6 @@ """ -Lemontropia Suite - Loadout Manager UI v4.0 -Simplified cost-focused loadout system. +Lemontropia Suite - Loadout Manager UI v5.0 +Full gear support with weapon amplifiers, mindforce implants, and armor platings. """ import json @@ -15,7 +15,7 @@ from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QLineEdit, QLabel, QPushButton, QGroupBox, QMessageBox, QListWidget, QListWidgetItem, - QSplitter, QWidget, QFrame, QGridLayout, + QSplitter, QWidget, QFrame, QGridLayout, QScrollArea, ) from PyQt6.QtCore import Qt, pyqtSignal @@ -25,62 +25,94 @@ logger = logging.getLogger(__name__) # ============================================================================ -# Simple Cost-Focused Loadout Config +# Full Gear Loadout Config # ============================================================================ @dataclass class LoadoutConfig: - """Simple loadout configuration focused on cost tracking. + """Complete loadout configuration with full gear support. - Core principle: Only store what's needed for cost calculations. - Everything else is display metadata. + Core principle: Store all gear types needed for comprehensive cost tracking. """ # Identity name: str = "Unnamed" - version: int = 2 # Version 2 = simplified format + version: int = 3 # Version 3 = full gear support - # === COST DATA (Required for tracking) === - # All values in PED (not PEC) + # === WEAPON & ATTACHMENTS === weapon_cost_per_shot: Decimal = Decimal("0") - armor_cost_per_hit: Decimal = Decimal("0") - healing_cost_per_heal: Decimal = Decimal("0") - - # === DISPLAY METADATA (For UI only) === weapon_name: str = "None" weapon_damage: Decimal = Decimal("0") - weapon_decay_pec: Decimal = Decimal("0") # Raw for reference - weapon_ammo_pec: Decimal = Decimal("0") # Raw for reference - - armor_name: str = "None" - armor_decay_pec: Decimal = Decimal("0") # Raw for reference - - healing_name: str = "None" - healing_decay_pec: Decimal = Decimal("0") # Raw for reference - - # === API REFERENCES (For re-loading from API) === + weapon_decay_pec: Decimal = Decimal("0") + weapon_ammo_pec: Decimal = Decimal("0") weapon_api_id: Optional[int] = None + + # Weapon Amplifier + weapon_amp_id: Optional[int] = None + weapon_amp_name: str = "None" + weapon_amp_decay: Decimal = Decimal("0") + weapon_amp_damage_bonus: Decimal = Decimal("0") + + # === ARMOR & PLATINGS === + armor_cost_per_hit: Decimal = Decimal("0") + armor_name: str = "None" + armor_decay_pec: Decimal = Decimal("0") armor_api_id: Optional[int] = None + + # Armor Plating + plating_id: Optional[int] = None + plating_name: str = "None" + plating_decay: Decimal = Decimal("0") + plating_protection_summary: str = "" + + # === HEALING & MINDFORCE === + healing_cost_per_heal: Decimal = Decimal("0") + healing_name: str = "None" + healing_decay_pec: Decimal = Decimal("0") healing_api_id: Optional[int] = None + # Mindforce Implant (for healing chips) + mindforce_implant_id: Optional[int] = None + mindforce_implant_name: str = "None" + mindforce_implant_decay: Decimal = Decimal("0") + mindforce_implant_heal_amount: Decimal = Decimal("0") + def to_dict(self) -> dict: - """Serialize to simple dictionary.""" + """Serialize to dictionary.""" return { 'name': self.name, 'version': self.version, + # Weapon 'weapon_cost_per_shot': str(self.weapon_cost_per_shot), - 'armor_cost_per_hit': str(self.armor_cost_per_hit), - 'healing_cost_per_heal': str(self.healing_cost_per_heal), 'weapon_name': self.weapon_name, 'weapon_damage': str(self.weapon_damage), 'weapon_decay_pec': str(self.weapon_decay_pec), 'weapon_ammo_pec': str(self.weapon_ammo_pec), + 'weapon_api_id': self.weapon_api_id, + # Weapon Amplifier + 'weapon_amp_id': self.weapon_amp_id, + 'weapon_amp_name': self.weapon_amp_name, + 'weapon_amp_decay': str(self.weapon_amp_decay), + 'weapon_amp_damage_bonus': str(self.weapon_amp_damage_bonus), + # Armor + 'armor_cost_per_hit': str(self.armor_cost_per_hit), 'armor_name': self.armor_name, 'armor_decay_pec': str(self.armor_decay_pec), + 'armor_api_id': self.armor_api_id, + # Plating + 'plating_id': self.plating_id, + 'plating_name': self.plating_name, + 'plating_decay': str(self.plating_decay), + 'plating_protection_summary': self.plating_protection_summary, + # Healing + 'healing_cost_per_heal': str(self.healing_cost_per_heal), 'healing_name': self.healing_name, 'healing_decay_pec': str(self.healing_decay_pec), - 'weapon_api_id': self.weapon_api_id, - 'armor_api_id': self.armor_api_id, 'healing_api_id': self.healing_api_id, + # Mindforce + 'mindforce_implant_id': self.mindforce_implant_id, + 'mindforce_implant_name': self.mindforce_implant_name, + 'mindforce_implant_decay': str(self.mindforce_implant_decay), + 'mindforce_implant_heal_amount': str(self.mindforce_implant_heal_amount), } @classmethod @@ -89,13 +121,15 @@ class LoadoutConfig: version = data.get('version', 1) if version == 1: - return cls._from_legacy(data) - else: + return cls._from_legacy_v1(data) + elif version == 2: return cls._from_v2(data) + else: + return cls._from_v3(data) @classmethod - def _from_v2(cls, data: dict) -> "LoadoutConfig": - """Parse version 2 (current) format.""" + def _from_v3(cls, data: dict) -> "LoadoutConfig": + """Parse version 3 (current) format.""" def get_decimal(key: str, default: str = "0") -> Decimal: try: return Decimal(str(data.get(key, default))) @@ -104,26 +138,72 @@ class LoadoutConfig: return cls( name=data.get('name', 'Unnamed'), - version=2, + version=3, + # Weapon weapon_cost_per_shot=get_decimal('weapon_cost_per_shot'), - armor_cost_per_hit=get_decimal('armor_cost_per_hit'), - healing_cost_per_heal=get_decimal('healing_cost_per_heal'), weapon_name=data.get('weapon_name', 'None'), weapon_damage=get_decimal('weapon_damage'), weapon_decay_pec=get_decimal('weapon_decay_pec'), weapon_ammo_pec=get_decimal('weapon_ammo_pec'), + weapon_api_id=data.get('weapon_api_id'), + # Weapon Amplifier + weapon_amp_id=data.get('weapon_amp_id'), + weapon_amp_name=data.get('weapon_amp_name', 'None'), + weapon_amp_decay=get_decimal('weapon_amp_decay'), + weapon_amp_damage_bonus=get_decimal('weapon_amp_damage_bonus'), + # Armor + armor_cost_per_hit=get_decimal('armor_cost_per_hit'), armor_name=data.get('armor_name', 'None'), armor_decay_pec=get_decimal('armor_decay_pec'), + armor_api_id=data.get('armor_api_id'), + # Plating + plating_id=data.get('plating_id'), + plating_name=data.get('plating_name', 'None'), + plating_decay=get_decimal('plating_decay'), + plating_protection_summary=data.get('plating_protection_summary', ''), + # Healing + healing_cost_per_heal=get_decimal('healing_cost_per_heal'), healing_name=data.get('healing_name', 'None'), healing_decay_pec=get_decimal('healing_decay_pec'), + healing_api_id=data.get('healing_api_id'), + # Mindforce + mindforce_implant_id=data.get('mindforce_implant_id'), + mindforce_implant_name=data.get('mindforce_implant_name', 'None'), + mindforce_implant_decay=get_decimal('mindforce_implant_decay'), + mindforce_implant_heal_amount=get_decimal('mindforce_implant_heal_amount'), + ) + + @classmethod + def _from_v2(cls, data: dict) -> "LoadoutConfig": + """Convert version 2 to version 3.""" + def get_decimal(key: str, default: str = "0") -> Decimal: + try: + return Decimal(str(data.get(key, default))) + except Exception: + return Decimal(default) + + return cls( + name=data.get('name', 'Unnamed'), + version=3, + weapon_cost_per_shot=get_decimal('weapon_cost_per_shot'), + weapon_name=data.get('weapon_name', 'None'), + weapon_damage=get_decimal('weapon_damage'), + weapon_decay_pec=get_decimal('weapon_decay_pec'), + weapon_ammo_pec=get_decimal('weapon_ammo_pec'), weapon_api_id=data.get('weapon_api_id'), + armor_cost_per_hit=get_decimal('armor_cost_per_hit'), + armor_name=data.get('armor_name', 'None'), + armor_decay_pec=get_decimal('armor_decay_pec'), armor_api_id=data.get('armor_api_id'), + healing_cost_per_heal=get_decimal('healing_cost_per_heal'), + healing_name=data.get('healing_name', 'None'), + healing_decay_pec=get_decimal('healing_decay_pec'), healing_api_id=data.get('healing_api_id'), ) @classmethod - def _from_legacy(cls, data: dict) -> "LoadoutConfig": - """Convert legacy format to new simple format.""" + def _from_legacy_v1(cls, data: dict) -> "LoadoutConfig": + """Convert legacy format to new format.""" def get_decimal(key: str, default: str = "0") -> Decimal: try: return Decimal(str(data.get(key, default))) @@ -143,62 +223,78 @@ class LoadoutConfig: return cls( name=data.get('name', 'Unnamed'), - version=2, + version=3, weapon_cost_per_shot=weapon_cost_per_shot, - armor_cost_per_hit=armor_cost_per_hit, - healing_cost_per_heal=healing_cost_per_heal, weapon_name=data.get('weapon_name', data.get('weapon', 'None')), weapon_damage=get_decimal('weapon_damage'), weapon_decay_pec=weapon_decay, weapon_ammo_pec=weapon_ammo, + armor_cost_per_hit=armor_cost_per_hit, armor_name=data.get('armor_set_name', data.get('armor_name', 'None')), armor_decay_pec=armor_decay, + healing_cost_per_heal=healing_cost_per_heal, healing_name=data.get('heal_name', 'None'), healing_decay_pec=heal_decay, ) + def get_total_weapon_cost_per_shot(self) -> Decimal: + """Calculate total weapon cost including amplifier.""" + base_cost = self.weapon_cost_per_shot + amp_cost = self.weapon_amp_decay / Decimal("100") # Convert PEC to PED + return base_cost + amp_cost + + def get_total_healing_cost_per_heal(self) -> Decimal: + """Calculate total healing cost including mindforce implant.""" + base_cost = self.healing_cost_per_heal + implant_cost = self.mindforce_implant_decay / Decimal("100") # Convert PEC to PED + return base_cost + implant_cost + + def get_total_armor_cost_per_hit(self) -> Decimal: + """Calculate total armor cost including plating.""" + base_cost = self.armor_cost_per_hit + plating_cost = self.plating_decay / Decimal("100") # Convert PEC to PED + return base_cost + plating_cost + def get_summary(self) -> Dict[str, Any]: """Get cost summary for display.""" return { 'name': self.name, 'weapon': self.weapon_name, + 'weapon_amp': self.weapon_amp_name if self.weapon_amp_id else "None", 'armor': self.armor_name, + 'plating': self.plating_name if self.plating_id else "None", 'healing': self.healing_name, - 'cost_per_shot': self.weapon_cost_per_shot, - 'cost_per_hit': self.armor_cost_per_hit, - 'cost_per_heal': self.healing_cost_per_heal, + 'mindforce': self.mindforce_implant_name if self.mindforce_implant_id else "None", + 'cost_per_shot': self.get_total_weapon_cost_per_shot(), + 'cost_per_hit': self.get_total_armor_cost_per_hit(), + 'cost_per_heal': self.get_total_healing_cost_per_heal(), } # ============================================================================ -# Simple Loadout Manager Dialog +# Full Gear Loadout Manager Dialog # ============================================================================ class LoadoutManagerDialog(QDialog): - """Simplified loadout manager focused on cost configuration.""" + """Full-featured loadout manager with all gear types.""" loadout_saved = pyqtSignal(LoadoutConfig) def __init__(self, parent=None): super().__init__(parent) - self.setWindowTitle("Loadout Manager") - self.setMinimumSize(600, 500) + self.setWindowTitle("Loadout Manager - Full Gear Configuration") + self.setMinimumSize(800, 700) # State self.config_dir = Path.home() / ".lemontropia" / "loadouts" self.config_dir.mkdir(parents=True, exist_ok=True) self.current_config: Optional[LoadoutConfig] = None - # Cached API data - self._cached_weapons: Optional[list] = None - self._cached_armors: Optional[list] = None - self._cached_healing: Optional[list] = None - self._setup_ui() self._load_saved_loadouts() def _setup_ui(self): - """Setup simplified UI.""" + """Setup full gear UI.""" layout = QVBoxLayout(self) layout.setSpacing(10) @@ -206,7 +302,7 @@ class LoadoutManagerDialog(QDialog): name_layout = QHBoxLayout() name_layout.addWidget(QLabel("Loadout Name:")) self.name_edit = QLineEdit() - self.name_edit.setPlaceholderText("e.g., ArMatrix Ghost Hunt") + self.name_edit.setPlaceholderText("e.g., ArMatrix Ghost Hunt with Dante") name_layout.addWidget(self.name_edit) layout.addLayout(name_layout) @@ -235,13 +331,16 @@ class LoadoutManagerDialog(QDialog): splitter.addWidget(left_widget) - # Right: Configuration + # Right: Configuration (in a scroll area) + scroll = QScrollArea() + scroll.setWidgetResizable(True) right_widget = QWidget() right_layout = QVBoxLayout(right_widget) right_layout.setContentsMargins(0, 0, 0, 0) + right_layout.setSpacing(10) # -- Weapon Section -- - weapon_group = QGroupBox("βοΈ Weapon") + weapon_group = QGroupBox("βοΈ Weapon & Amplifier") weapon_layout = QFormLayout(weapon_group) self.weapon_btn = QPushButton("Select Weapon...") @@ -252,20 +351,31 @@ class LoadoutManagerDialog(QDialog): self.weapon_info.setStyleSheet("color: #888;") weapon_layout.addRow(self.weapon_info) - self.weapon_decay_label = QLabel("0 PEC") - weapon_layout.addRow("Decay:", self.weapon_decay_label) - - self.weapon_ammo_label = QLabel("0") - weapon_layout.addRow("Ammo:", self.weapon_ammo_label) - self.weapon_cost_label = QLabel("0.0000 PED") self.weapon_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F;") - weapon_layout.addRow("Cost/Shot:", self.weapon_cost_label) + weapon_layout.addRow("Base Cost/Shot:", self.weapon_cost_label) + + # Amplifier + self.amp_btn = QPushButton("Select Amplifier...") + self.amp_btn.clicked.connect(self._select_amplifier) + weapon_layout.addRow("Amplifier:", self.amp_btn) + + self.amp_info = QLabel("None selected") + self.amp_info.setStyleSheet("color: #888;") + weapon_layout.addRow(self.amp_info) + + self.amp_cost_label = QLabel("0.0000 PED") + self.amp_cost_label.setStyleSheet("color: #FFA07A;") + weapon_layout.addRow("Amp Cost/Shot:", self.amp_cost_label) + + self.total_weapon_cost_label = QLabel("0.0000 PED") + self.total_weapon_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F; font-size: 12px;") + weapon_layout.addRow("Total Cost/Shot:", self.total_weapon_cost_label) right_layout.addWidget(weapon_group) # -- Armor Section -- - armor_group = QGroupBox("π‘οΈ Armor") + armor_group = QGroupBox("π‘οΈ Armor & Plating") armor_layout = QFormLayout(armor_group) self.armor_btn = QPushButton("Select Armor...") @@ -278,17 +388,34 @@ class LoadoutManagerDialog(QDialog): self.armor_cost_label = QLabel("0.0000 PED") self.armor_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F;") - armor_layout.addRow("Cost/Hit:", self.armor_cost_label) + armor_layout.addRow("Base Cost/Hit:", self.armor_cost_label) + + # Plating + self.plating_btn = QPushButton("Select Plating...") + self.plating_btn.clicked.connect(self._select_plating) + armor_layout.addRow("Plating:", self.plating_btn) + + self.plating_info = QLabel("None selected") + self.plating_info.setStyleSheet("color: #888;") + armor_layout.addRow(self.plating_info) + + self.plating_cost_label = QLabel("0.0000 PED") + self.plating_cost_label.setStyleSheet("color: #FFA07A;") + armor_layout.addRow("Plating Cost/Hit:", self.plating_cost_label) + + self.total_armor_cost_label = QLabel("0.0000 PED") + self.total_armor_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F; font-size: 12px;") + armor_layout.addRow("Total Cost/Hit:", self.total_armor_cost_label) right_layout.addWidget(armor_group) # -- Healing Section -- - healing_group = QGroupBox("π Healing") + healing_group = QGroupBox("π Healing & Mindforce") healing_layout = QFormLayout(healing_group) - self.healing_btn = QPushButton("Select Healing...") + self.healing_btn = QPushButton("Select Healing Tool...") self.healing_btn.clicked.connect(self._select_healing) - healing_layout.addRow("Healing:", self.healing_btn) + healing_layout.addRow("Healing Tool:", self.healing_btn) self.healing_info = QLabel("None selected") self.healing_info.setStyleSheet("color: #888;") @@ -296,24 +423,44 @@ class LoadoutManagerDialog(QDialog): self.healing_cost_label = QLabel("0.0000 PED") self.healing_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F;") - healing_layout.addRow("Cost/Heal:", self.healing_cost_label) + healing_layout.addRow("Base Cost/Heal:", self.healing_cost_label) + + # Mindforce Implant + self.mindforce_btn = QPushButton("Select Mindforce Chip...") + self.mindforce_btn.clicked.connect(self._select_mindforce) + healing_layout.addRow("Mindforce Chip:", self.mindforce_btn) + + self.mindforce_info = QLabel("None selected") + self.mindforce_info.setStyleSheet("color: #888;") + healing_layout.addRow(self.mindforce_info) + + self.mindforce_cost_label = QLabel("0.0000 PED") + self.mindforce_cost_label.setStyleSheet("color: #FFA07A;") + healing_layout.addRow("Chip Cost/Heal:", self.mindforce_cost_label) + + self.total_healing_cost_label = QLabel("0.0000 PED") + self.total_healing_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F; font-size: 12px;") + healing_layout.addRow("Total Cost/Heal:", self.total_healing_cost_label) right_layout.addWidget(healing_group) # -- Summary Section -- - summary_group = QGroupBox("π° Session Cost Summary") + summary_group = QGroupBox("π° Total Session Cost Summary") summary_layout = QGridLayout(summary_group) summary_layout.addWidget(QLabel("Cost per Shot:"), 0, 0) self.summary_shot = QLabel("0.0000 PED") + self.summary_shot.setStyleSheet("font-weight: bold; color: #7FFF7F;") summary_layout.addWidget(self.summary_shot, 0, 1) summary_layout.addWidget(QLabel("Cost per Hit:"), 1, 0) self.summary_hit = QLabel("0.0000 PED") + self.summary_hit.setStyleSheet("font-weight: bold; color: #7FFF7F;") summary_layout.addWidget(self.summary_hit, 1, 1) summary_layout.addWidget(QLabel("Cost per Heal:"), 2, 0) self.summary_heal = QLabel("0.0000 PED") + self.summary_heal.setStyleSheet("font-weight: bold; color: #7FFF7F;") summary_layout.addWidget(self.summary_heal, 2, 1) summary_layout.setColumnStretch(1, 1) @@ -321,8 +468,10 @@ class LoadoutManagerDialog(QDialog): right_layout.addStretch() - splitter.addWidget(right_widget) - splitter.setSizes([200, 400]) + scroll.setWidget(right_widget) + splitter.addWidget(scroll) + + splitter.setSizes([250, 550]) layout.addWidget(splitter) @@ -371,8 +520,6 @@ class LoadoutManagerDialog(QDialog): # Update UI self.weapon_btn.setText(weapon.name[:30]) self.weapon_info.setText(f"Damage: {weapon.damage} | Range: {weapon.range_val}") - self.weapon_decay_label.setText(f"{decay_pec} PEC") - self.weapon_ammo_label.setText(f"{ammo}") self.weapon_cost_label.setText(f"{cost_per_shot:.4f} PED") # Store for saving @@ -385,8 +532,54 @@ class LoadoutManagerDialog(QDialog): 'cost_per_shot': cost_per_shot, } + self._update_weapon_total() self._update_summary() + def _select_amplifier(self): + """Open weapon amplifier selector.""" + from ui.amplifier_selector import AmplifierSelectorDialog + + dialog = AmplifierSelectorDialog(self) + if dialog.exec() == QDialog.DialogCode.Accepted: + amp = dialog.get_selected_amplifier() + if amp: + self._set_amplifier(amp) + + def _set_amplifier(self, amp_data: dict): + """Set weapon amplifier.""" + name = amp_data.get('name', 'Unknown') + decay_pec = Decimal(str(amp_data.get('decay_pec', 0))) + damage_bonus = Decimal(str(amp_data.get('damage_bonus', 0))) + cost_per_shot = decay_pec / Decimal("100") + + # Update UI + self.amp_btn.setText(name[:30]) + self.amp_info.setText(f"+{damage_bonus} Damage") + self.amp_cost_label.setText(f"{cost_per_shot:.4f} PED") + + # Store for saving + self._pending_amplifier = { + 'name': name, + 'api_id': amp_data.get('api_id'), + 'decay_pec': decay_pec, + 'damage_bonus': damage_bonus, + 'cost_per_shot': cost_per_shot, + } + + self._update_weapon_total() + self._update_summary() + + def _update_weapon_total(self): + """Update total weapon cost display.""" + weapon = getattr(self, '_pending_weapon', {}) + amp = getattr(self, '_pending_amplifier', {}) + + weapon_cost = weapon.get('cost_per_shot', Decimal("0")) + amp_cost = amp.get('cost_per_shot', Decimal("0")) + total = weapon_cost + amp_cost + + self.total_weapon_cost_label.setText(f"{total:.4f} PED") + def _select_armor(self): """Open simplified armor selector.""" from ui.armor_selector import ArmorSelectorDialog @@ -399,7 +592,6 @@ class LoadoutManagerDialog(QDialog): def _set_armor(self, armor_data: dict): """Set armor and calculate cost.""" - # armor_data has: name, decay_pec, protection_summary name = armor_data.get('name', 'Unknown') decay_pec = Decimal(str(armor_data.get('decay_pec', 0))) cost_per_hit = decay_pec / Decimal("100") @@ -418,8 +610,54 @@ class LoadoutManagerDialog(QDialog): 'cost_per_hit': cost_per_hit, } + self._update_armor_total() self._update_summary() + def _select_plating(self): + """Open armor plating selector.""" + from ui.plate_selector import PlateSelectorDialog + + dialog = PlateSelectorDialog(self) + if dialog.exec() == QDialog.DialogCode.Accepted: + plate = dialog.get_selected_plate() + if plate: + self._set_plating(plate) + + def _set_plating(self, plate_data: dict): + """Set armor plating.""" + name = plate_data.get('name', 'Unknown') + decay_pec = Decimal(str(plate_data.get('decay_pec', 0))) + cost_per_hit = decay_pec / Decimal("100") + + # Update UI + self.plating_btn.setText(name[:30]) + prot_summary = plate_data.get('protection_summary', '') + self.plating_info.setText(prot_summary[:40] if prot_summary else "No data") + self.plating_cost_label.setText(f"{cost_per_hit:.4f} PED") + + # Store for saving + self._pending_plating = { + 'name': name, + 'api_id': plate_data.get('api_id'), + 'decay_pec': decay_pec, + 'protection_summary': prot_summary, + 'cost_per_hit': cost_per_hit, + } + + self._update_armor_total() + self._update_summary() + + def _update_armor_total(self): + """Update total armor cost display.""" + armor = getattr(self, '_pending_armor', {}) + plating = getattr(self, '_pending_plating', {}) + + armor_cost = armor.get('cost_per_hit', Decimal("0")) + plating_cost = plating.get('cost_per_hit', Decimal("0")) + total = armor_cost + plating_cost + + self.total_armor_cost_label.setText(f"{total:.4f} PED") + def _select_healing(self): """Open healing selector.""" from ui.healing_selector import HealingSelectorDialog @@ -451,13 +689,66 @@ class LoadoutManagerDialog(QDialog): 'cost_per_heal': cost_per_heal, } + self._update_healing_total() self._update_summary() + def _select_mindforce(self): + """Open mindforce implant selector.""" + from ui.mindforce_selector import MindforceSelectorDialog + + dialog = MindforceSelectorDialog(self) + if dialog.exec() == QDialog.DialogCode.Accepted: + chip = dialog.get_selected_chip() + if chip: + self._set_mindforce(chip) + + def _set_mindforce(self, chip_data: dict): + """Set mindforce implant.""" + name = chip_data.get('name', 'Unknown') + decay_pec = Decimal(str(chip_data.get('decay_pec', 0))) + heal_amount = Decimal(str(chip_data.get('heal_amount', 0))) + cost_per_heal = decay_pec / Decimal("100") + + # Update UI + self.mindforce_btn.setText(name[:30]) + self.mindforce_info.setText(f"Heal: {heal_amount} HP") + self.mindforce_cost_label.setText(f"{cost_per_heal:.4f} PED") + + # Store for saving + self._pending_mindforce = { + 'name': name, + 'api_id': chip_data.get('api_id'), + 'decay_pec': decay_pec, + 'heal_amount': heal_amount, + 'cost_per_heal': cost_per_heal, + } + + self._update_healing_total() + self._update_summary() + + def _update_healing_total(self): + """Update total healing cost display.""" + healing = getattr(self, '_pending_healing', {}) + mindforce = getattr(self, '_pending_mindforce', {}) + + healing_cost = healing.get('cost_per_heal', Decimal("0")) + mindforce_cost = mindforce.get('cost_per_heal', Decimal("0")) + total = healing_cost + mindforce_cost + + self.total_healing_cost_label.setText(f"{total:.4f} PED") + def _update_summary(self): """Update cost summary display.""" - shot = getattr(self, '_pending_weapon', {}).get('cost_per_shot', Decimal("0")) - hit = getattr(self, '_pending_armor', {}).get('cost_per_hit', Decimal("0")) - heal = getattr(self, '_pending_healing', {}).get('cost_per_heal', Decimal("0")) + weapon = getattr(self, '_pending_weapon', {}) + amp = getattr(self, '_pending_amplifier', {}) + armor = getattr(self, '_pending_armor', {}) + plating = getattr(self, '_pending_plating', {}) + healing = getattr(self, '_pending_healing', {}) + mindforce = getattr(self, '_pending_mindforce', {}) + + shot = weapon.get('cost_per_shot', Decimal("0")) + amp.get('cost_per_shot', Decimal("0")) + hit = armor.get('cost_per_hit', Decimal("0")) + plating.get('cost_per_hit', Decimal("0")) + heal = healing.get('cost_per_heal', Decimal("0")) + mindforce.get('cost_per_heal', Decimal("0")) self.summary_shot.setText(f"{shot:.4f} PED") self.summary_hit.setText(f"{hit:.4f} PED") @@ -472,25 +763,46 @@ class LoadoutManagerDialog(QDialog): # Build config from pending data weapon = getattr(self, '_pending_weapon', {}) + amp = getattr(self, '_pending_amplifier', {}) armor = getattr(self, '_pending_armor', {}) + plating = getattr(self, '_pending_plating', {}) healing = getattr(self, '_pending_healing', {}) + mindforce = getattr(self, '_pending_mindforce', {}) config = LoadoutConfig( name=name, + # Weapon weapon_cost_per_shot=weapon.get('cost_per_shot', Decimal("0")), - armor_cost_per_hit=armor.get('cost_per_hit', Decimal("0")), - healing_cost_per_heal=healing.get('cost_per_heal', Decimal("0")), weapon_name=weapon.get('name', 'None'), weapon_damage=weapon.get('damage', Decimal("0")), weapon_decay_pec=weapon.get('decay_pec', Decimal("0")), weapon_ammo_pec=weapon.get('ammo_pec', Decimal("0")), + weapon_api_id=weapon.get('api_id'), + # Weapon Amplifier + weapon_amp_id=amp.get('api_id'), + weapon_amp_name=amp.get('name', 'None'), + weapon_amp_decay=amp.get('decay_pec', Decimal("0")), + weapon_amp_damage_bonus=amp.get('damage_bonus', Decimal("0")), + # Armor + armor_cost_per_hit=armor.get('cost_per_hit', Decimal("0")), armor_name=armor.get('name', 'None'), armor_decay_pec=armor.get('decay_pec', Decimal("0")), + armor_api_id=armor.get('api_id'), + # Plating + plating_id=plating.get('api_id'), + plating_name=plating.get('name', 'None'), + plating_decay=plating.get('decay_pec', Decimal("0")), + plating_protection_summary=plating.get('protection_summary', ''), + # Healing + healing_cost_per_heal=healing.get('cost_per_heal', Decimal("0")), healing_name=healing.get('name', 'None'), healing_decay_pec=healing.get('decay_pec', Decimal("0")), - weapon_api_id=weapon.get('api_id'), - armor_api_id=armor.get('api_id'), healing_api_id=healing.get('api_id'), + # Mindforce + mindforce_implant_id=mindforce.get('api_id'), + mindforce_implant_name=mindforce.get('name', 'None'), + mindforce_implant_decay=mindforce.get('decay_pec', Decimal("0")), + mindforce_implant_heal_amount=mindforce.get('heal_amount', Decimal("0")), ) # Save to file @@ -529,10 +841,14 @@ class LoadoutManagerDialog(QDialog): # Tooltip with costs tooltip = ( f"Weapon: {config.weapon_name}\n" + f" + Amp: {config.weapon_amp_name if config.weapon_amp_id else 'None'}\n" f"Armor: {config.armor_name}\n" - f"Cost/Shot: {config.weapon_cost_per_shot:.4f} PED\n" - f"Cost/Hit: {config.armor_cost_per_hit:.4f} PED\n" - f"Cost/Heal: {config.healing_cost_per_heal:.4f} PED" + f" + Plating: {config.plating_name if config.plating_id else 'None'}\n" + f"Healing: {config.healing_name}\n" + f" + Chip: {config.mindforce_implant_name if config.mindforce_implant_id else 'None'}\n" + f"Cost/Shot: {config.get_total_weapon_cost_per_shot():.4f} PED\n" + f"Cost/Hit: {config.get_total_armor_cost_per_hit():.4f} PED\n" + f"Cost/Heal: {config.get_total_healing_cost_per_heal():.4f} PED" ) item.setToolTip(tooltip) self.saved_list.addItem(item) @@ -569,8 +885,6 @@ class LoadoutManagerDialog(QDialog): if config.weapon_name != "None": self.weapon_btn.setText(config.weapon_name[:30]) self.weapon_info.setText(f"Damage: {config.weapon_damage}") - self.weapon_decay_label.setText(f"{config.weapon_decay_pec} PEC") - self.weapon_ammo_label.setText(f"{config.weapon_ammo_pec}") self.weapon_cost_label.setText(f"{config.weapon_cost_per_shot:.4f} PED") self._pending_weapon = { 'name': config.weapon_name, @@ -581,6 +895,20 @@ class LoadoutManagerDialog(QDialog): 'cost_per_shot': config.weapon_cost_per_shot, } + # Weapon Amplifier + if config.weapon_amp_id: + self.amp_btn.setText(config.weapon_amp_name[:30]) + self.amp_info.setText(f"+{config.weapon_amp_damage_bonus} Damage") + amp_cost = config.weapon_amp_decay / Decimal("100") + self.amp_cost_label.setText(f"{amp_cost:.4f} PED") + self._pending_amplifier = { + 'name': config.weapon_amp_name, + 'api_id': config.weapon_amp_id, + 'decay_pec': config.weapon_amp_decay, + 'damage_bonus': config.weapon_amp_damage_bonus, + 'cost_per_shot': amp_cost, + } + # Armor if config.armor_name != "None": self.armor_btn.setText(config.armor_name[:30]) @@ -593,6 +921,20 @@ class LoadoutManagerDialog(QDialog): 'cost_per_hit': config.armor_cost_per_hit, } + # Plating + if config.plating_id: + self.plating_btn.setText(config.plating_name[:30]) + self.plating_info.setText(config.plating_protection_summary[:40] if config.plating_protection_summary else "No data") + plating_cost = config.plating_decay / Decimal("100") + self.plating_cost_label.setText(f"{plating_cost:.4f} PED") + self._pending_plating = { + 'name': config.plating_name, + 'api_id': config.plating_id, + 'decay_pec': config.plating_decay, + 'protection_summary': config.plating_protection_summary, + 'cost_per_hit': plating_cost, + } + # Healing if config.healing_name != "None": self.healing_btn.setText(config.healing_name[:30]) @@ -604,29 +946,64 @@ class LoadoutManagerDialog(QDialog): 'cost_per_heal': config.healing_cost_per_heal, } + # Mindforce + if config.mindforce_implant_id: + self.mindforce_btn.setText(config.mindforce_implant_name[:30]) + mindforce_cost = config.mindforce_implant_decay / Decimal("100") + self.mindforce_cost_label.setText(f"{mindforce_cost:.4f} PED") + self._pending_mindforce = { + 'name': config.mindforce_implant_name, + 'api_id': config.mindforce_implant_id, + 'decay_pec': config.mindforce_implant_decay, + 'heal_amount': config.mindforce_implant_heal_amount, + 'cost_per_heal': mindforce_cost, + } + + self._update_weapon_total() + self._update_armor_total() + self._update_healing_total() self._update_summary() self.current_config = config def _new_loadout(self): """Clear all fields for new loadout.""" self.name_edit.clear() + + # Reset weapon section self.weapon_btn.setText("Select Weapon...") self.weapon_info.setText("None selected") - self.weapon_decay_label.setText("0 PEC") - self.weapon_ammo_label.setText("0") self.weapon_cost_label.setText("0.0000 PED") + self.amp_btn.setText("Select Amplifier...") + self.amp_info.setText("None selected") + self.amp_cost_label.setText("0.0000 PED") + self.total_weapon_cost_label.setText("0.0000 PED") + # Reset armor section self.armor_btn.setText("Select Armor...") self.armor_info.setText("None selected") self.armor_cost_label.setText("0.0000 PED") + self.plating_btn.setText("Select Plating...") + self.plating_info.setText("None selected") + self.plating_cost_label.setText("0.0000 PED") + self.total_armor_cost_label.setText("0.0000 PED") + # Reset healing section self.healing_btn.setText("Select Healing...") self.healing_info.setText("None selected") self.healing_cost_label.setText("0.0000 PED") + self.mindforce_btn.setText("Select Mindforce Chip...") + self.mindforce_info.setText("None selected") + self.mindforce_cost_label.setText("0.0000 PED") + self.total_healing_cost_label.setText("0.0000 PED") + # Clear pending data self._pending_weapon = None + self._pending_amplifier = None self._pending_armor = None + self._pending_plating = None self._pending_healing = None + self._pending_mindforce = None + self._update_summary() self.current_config = None diff --git a/ui/main_window.py b/ui/main_window.py index b28e01d..f3a5efa 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -1,13 +1,13 @@ """ -Lemontropia Suite - Main Application Window -PyQt6 GUI for managing game automation projects and sessions. +Lemontropia Suite - Main Application Window (Session-Focused Redesign) +PyQt6 GUI for managing game automation sessions and activities. """ import sys import logging from datetime import datetime from enum import Enum, auto -from typing import Optional, List, Callable +from typing import Optional, List, Callable, Any from dataclasses import dataclass # Setup logger @@ -19,7 +19,8 @@ from PyQt6.QtWidgets import ( QTextEdit, QLabel, QStatusBar, QMenuBar, QMenu, QDialog, QLineEdit, QFormLayout, QDialogButtonBox, QMessageBox, QGroupBox, QFrame, QApplication, - QTreeWidget, QTreeWidgetItem, QHeaderView + QTreeWidget, QTreeWidgetItem, QHeaderView, QComboBox, + QGridLayout, QToolButton ) from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QSize, QSettings from PyQt6.QtGui import QAction, QFont, QColor, QPalette, QIcon @@ -38,22 +39,55 @@ class SessionState(Enum): STOPPING = "Stopping" +class ActivityType(Enum): + """Activity type enumeration.""" + HUNTING = ("hunting", "π― Hunting", "#4caf50") + MINING = ("mining", "βοΈ Mining", "#ff9800") + CRAFTING = ("crafting", "βοΈ Crafting", "#2196f3") + + def __init__(self, value, display_name, color): + self._value_ = value + self.display_name = display_name + self.color = color + + @classmethod + def from_string(cls, value: str) -> "ActivityType": + for item in cls: + if item.value == value.lower(): + return item + return cls.HUNTING + + @dataclass -class Project: - """Project data model.""" +class SessionTemplate: + """Session template data model (replaces Project).""" id: int name: str + activity_type: ActivityType description: str = "" created_at: Optional[datetime] = None session_count: int = 0 last_session: Optional[datetime] = None +@dataclass +class RecentSession: + """Recent session data model.""" + id: int + template_name: str + activity_type: ActivityType + started_at: datetime + duration_minutes: int + total_cost: float + total_return: float + status: str + + @dataclass class LogEvent: """Log event data model.""" timestamp: datetime - level: str # DEBUG, INFO, WARNING, ERROR, CRITICAL + level: str source: str message: str @@ -69,6 +103,14 @@ class LogEvent: from ui.hud_overlay_clean import HUDOverlay +# ============================================================================ +# Session History & Gallery Integration +# ============================================================================ + +from ui.session_history import SessionHistoryDialog +from ui.gallery_dialog import GalleryDialog, ScreenshotCapture + + # ============================================================================ # Core Integration # ============================================================================ @@ -82,7 +124,6 @@ from decimal import Decimal 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 @@ -90,12 +131,12 @@ from core.database import DatabaseManager # Custom Dialogs # ============================================================================ -class NewProjectDialog(QDialog): - """Dialog for creating a new project.""" +class NewSessionTemplateDialog(QDialog): + """Dialog for creating a new session template.""" def __init__(self, parent=None): super().__init__(parent) - self.setWindowTitle("New Project") + self.setWindowTitle("New Session Template") self.setMinimumWidth(400) self.setup_ui() @@ -106,9 +147,14 @@ class NewProjectDialog(QDialog): form_layout = QFormLayout() self.name_input = QLineEdit() - self.name_input.setPlaceholderText("Enter project name...") + self.name_input.setPlaceholderText("Enter template name...") form_layout.addRow("Name:", self.name_input) + self.activity_combo = QComboBox() + for activity in ActivityType: + self.activity_combo.addItem(activity.display_name, activity) + form_layout.addRow("Activity Type:", self.activity_combo) + self.desc_input = QLineEdit() self.desc_input.setPlaceholderText("Enter description (optional)...") form_layout.addRow("Description:", self.desc_input) @@ -124,26 +170,31 @@ class NewProjectDialog(QDialog): 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 get_template_data(self) -> tuple: + """Get the entered template data.""" + activity = self.activity_combo.currentData() + return ( + self.name_input.text().strip(), + activity, + 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.") + QMessageBox.warning(self, "Validation Error", "Template name is required.") return super().accept() -class ProjectStatsDialog(QDialog): - """Dialog for displaying project statistics.""" +class TemplateStatsDialog(QDialog): + """Dialog for displaying session template statistics.""" - def __init__(self, project: Project, parent=None): + def __init__(self, template: SessionTemplate, parent=None): super().__init__(parent) - self.project = project - self.setWindowTitle(f"Project Statistics - {project.name}") + self.template = template + self.setWindowTitle(f"Template Statistics - {template.name}") self.setMinimumWidth(350) self.setup_ui() @@ -151,21 +202,24 @@ class ProjectStatsDialog(QDialog): layout = QVBoxLayout(self) # Stats display - stats_group = QGroupBox("Project Information") + stats_group = QGroupBox("Template 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("ID:", QLabel(str(self.template.id))) + stats_layout.addRow("Name:", QLabel(self.template.name)) + stats_layout.addRow("Activity Type:", QLabel(self.template.activity_type.display_name)) + + created = self.template.created_at.strftime("%Y-%m-%d %H:%M") if self.template.created_at else "N/A" stats_layout.addRow("Created:", QLabel(created)) + stats_layout.addRow("Total Sessions:", QLabel(str(self.template.session_count))) + + last = self.template.last_session.strftime("%Y-%m-%d %H:%M") if self.template.last_session else "Never" + stats_layout.addRow("Last Session:", QLabel(last)) + + description = self.template.description or "N/A" + stats_layout.addRow("Description:", QLabel(description)) + layout.addWidget(stats_group) # Close button @@ -180,27 +234,53 @@ class SettingsDialog(QDialog): def __init__(self, parent=None, current_player_name: str = ""): super().__init__(parent) self.setWindowTitle("Settings") - self.setMinimumWidth(400) + self.setMinimumWidth(450) self.player_name = current_player_name self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) - + # Player Settings Group player_group = QGroupBox("Player Settings") player_layout = QFormLayout(player_group) - + self.player_name_edit = QLineEdit() self.player_name_edit.setText(self.player_name) self.player_name_edit.setPlaceholderText("Your avatar name in Entropia Universe") player_layout.addRow("Avatar Name:", self.player_name_edit) - + help_label = QLabel("Set your avatar name to track your globals correctly.") help_label.setStyleSheet("color: #888; font-size: 11px;") player_layout.addRow(help_label) - + layout.addWidget(player_group) + + # Log Settings Group + log_group = QGroupBox("Log File Settings") + log_layout = QFormLayout(log_group) + + self.log_path_edit = QLineEdit() + self.log_path_edit.setPlaceholderText("Path to chat.log") + log_layout.addRow("Log Path:", self.log_path_edit) + + self.auto_detect_check = QCheckBox("Auto-detect log path on startup") + self.auto_detect_check.setChecked(True) + log_layout.addRow(self.auto_detect_check) + + layout.addWidget(log_group) + + # Default Activity Group + activity_group = QGroupBox("Default Activity") + activity_layout = QFormLayout(activity_group) + + self.default_activity_combo = QComboBox() + for activity in ActivityType: + self.default_activity_combo.addItem(activity.display_name, activity) + activity_layout.addRow("Default:", self.default_activity_combo) + + layout.addWidget(activity_group) + layout.addStretch() button_box = QDialogButtonBox( @@ -209,11 +289,24 @@ class SettingsDialog(QDialog): button_box.accepted.connect(self.accept) button_box.rejected.connect(self.reject) layout.addWidget(button_box) - + def get_player_name(self) -> str: """Get the configured player name.""" return self.player_name_edit.text().strip() + def get_log_path(self) -> str: + """Get the configured log path.""" + return self.log_path_edit.text().strip() + + def get_auto_detect(self) -> bool: + """Get auto-detect setting.""" + return self.auto_detect_check.isChecked() + + def get_default_activity(self) -> str: + """Get default activity type.""" + activity = self.default_activity_combo.currentData() + return activity.value if activity else "hunting" + # ============================================================================ # Main Window @@ -222,12 +315,15 @@ class SettingsDialog(QDialog): class MainWindow(QMainWindow): """ Main application window for Lemontropia Suite. - - Provides project management, session control, and log viewing capabilities. + + Session-focused UI with: + - Top: Activity Type selector + Loadout selector + - Middle: Session Control (start/stop/pause) with Loadout Manager button + - Bottom: Recent Sessions list """ # Signals - session_started = pyqtSignal(int) # project_id + session_started = pyqtSignal(int) session_stopped = pyqtSignal() session_paused = pyqtSignal() session_resumed = pyqtSignal() @@ -240,14 +336,12 @@ class MainWindow(QMainWindow): self.setMinimumSize(1200, 800) self.resize(1400, 900) - # Initialize database and real project manager + # Initialize database 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() @@ -262,29 +356,25 @@ class MainWindow(QMainWindow): # 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 + self._queue_timer.start(100) # State - self.current_project: Optional[Project] = None + self.current_template: Optional[SessionTemplate] = None + self.current_activity: ActivityType = ActivityType.HUNTING self.session_state = SessionState.IDLE self.current_session_id: Optional[int] = None self._current_db_session_id: Optional[int] = None - - # Player settings - self.player_name: str = "" # Set via Settings dialog - # 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 + # Player settings + self.player_name: str = "" + self.log_path: str = "" + self.auto_detect_log: bool = True + + # Selected gear/loadout self._selected_loadout: Optional[Any] = None - - # Session cost tracking (initialized empty, populated by loadout selection) + self._selected_loadout_name: str = "No Loadout" + + # Session cost tracking self._session_costs: dict = { 'cost_per_shot': Decimal('0'), 'cost_per_hit': Decimal('0'), @@ -296,28 +386,32 @@ class MainWindow(QMainWindow): 'healing_name': 'None', } + # Screenshot capture + self._screenshot_capture = ScreenshotCapture(self.db) + # Setup UI self.setup_ui() self.apply_dark_theme() self.create_menu_bar() self.create_status_bar() - # Load persistent settings (after UI setup so log_info works) + # Load persistent settings self._load_settings() # Load initial data - self.refresh_project_list() + self.refresh_session_templates() + self.refresh_recent_sessions() # Welcome message self.log_info("Application", "Lemontropia Suite initialized") self.log_info("Database", f"Database ready: {self.db.db_path}") # ======================================================================== - # UI Setup + # UI Setup - New Session-Focused Layout # ======================================================================== def setup_ui(self): - """Setup the main UI layout.""" + """Setup the main UI layout with session-focused design.""" # Central widget central_widget = QWidget() self.setCentralWidget(central_widget) @@ -331,99 +425,109 @@ class MainWindow(QMainWindow): self.main_splitter = QSplitter(Qt.Orientation.Horizontal) main_layout.addWidget(self.main_splitter) - # Left side container + # Left side container - Session Control Focus 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) + # === TOP: Activity Type Selector + Loadout Selector === + self.activity_panel = self.create_activity_panel() + left_layout.addWidget(self.activity_panel) - # Project panel - self.project_panel = self.create_project_panel() - left_splitter.addWidget(self.project_panel) - - # Session control panel + # === MIDDLE: Session Control (with Loadout Manager button) === self.session_panel = self.create_session_panel() - left_splitter.addWidget(self.session_panel) + left_layout.addWidget(self.session_panel) - # Set splitter proportions - left_splitter.setSizes([400, 300]) + # === BOTTOM: Recent Sessions List === + self.recent_sessions_panel = self.create_recent_sessions_panel() + left_layout.addWidget(self.recent_sessions_panel, 1) # Stretch factor # Add left container to main splitter self.main_splitter.addWidget(left_container) - # Log output panel + # Log output panel (right side) 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]) + # Set main splitter proportions (35% left, 65% log) + self.main_splitter.setSizes([450, 850]) - def create_project_panel(self) -> QGroupBox: - """Create the project management panel.""" - panel = QGroupBox("Project Management") + def create_activity_panel(self) -> QGroupBox: + """Create the activity type and loadout selection panel (TOP).""" + panel = QGroupBox("Activity Setup") layout = QVBoxLayout(panel) - layout.setSpacing(8) + layout.setSpacing(10) - # 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) + # Activity Type selector + activity_layout = QHBoxLayout() + activity_layout.addWidget(QLabel("π― Activity Type:")) + + self.activity_combo = QComboBox() + for activity in ActivityType: + self.activity_combo.addItem(activity.display_name, activity) + self.activity_combo.currentIndexChanged.connect(self.on_activity_changed) + activity_layout.addWidget(self.activity_combo, 1) + + layout.addLayout(activity_layout) - # 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) + # Session Template selector + template_layout = QHBoxLayout() + template_layout.addWidget(QLabel("π Template:")) + + self.template_combo = QComboBox() + self.template_combo.setPlaceholderText("Select a template...") + self.template_combo.currentIndexChanged.connect(self.on_template_changed) + template_layout.addWidget(self.template_combo, 1) + + # New template button + self.new_template_btn = QPushButton("+") + self.new_template_btn.setMaximumWidth(40) + self.new_template_btn.setToolTip("Create new template") + self.new_template_btn.clicked.connect(self.on_new_template) + template_layout.addWidget(self.new_template_btn) + + layout.addLayout(template_layout) - 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) + # Loadout selector (prominent) + loadout_layout = QHBoxLayout() + loadout_layout.addWidget(QLabel("ποΈ Loadout:")) + + self.loadout_display = QLabel("No Loadout Selected") + self.loadout_display.setStyleSheet("font-weight: bold; color: #ff9800;") + loadout_layout.addWidget(self.loadout_display, 1) + + # Prominent Loadout Manager button + self.loadout_manager_btn = QPushButton("π§ Open Loadout Manager") + self.loadout_manager_btn.setToolTip("Configure your gear loadout") + self.loadout_manager_btn.setMinimumHeight(32) + self.loadout_manager_btn.clicked.connect(self.on_loadout_manager) + loadout_layout.addWidget(self.loadout_manager_btn) + + layout.addLayout(loadout_layout) return panel def create_session_panel(self) -> QGroupBox: - """Create the session control panel.""" + """Create the session control panel (MIDDLE).""" panel = QGroupBox("Session Control") layout = QVBoxLayout(panel) - layout.setSpacing(10) + layout.setSpacing(12) - # 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) + # Current session info + info_layout = QGridLayout() + + self.current_activity_label = QLabel("No Activity") + self.current_activity_label.setStyleSheet("font-weight: bold; color: #888;") + info_layout.addWidget(QLabel("Activity:"), 0, 0) + info_layout.addWidget(self.current_activity_label, 0, 1) + + self.current_template_label = QLabel("No Template") + self.current_template_label.setStyleSheet("font-weight: bold; color: #888;") + info_layout.addWidget(QLabel("Template:"), 1, 0) + info_layout.addWidget(self.current_template_label, 1, 1) + + layout.addLayout(info_layout) # Separator line separator = QFrame() @@ -449,31 +553,76 @@ class MainWindow(QMainWindow): status_layout.addStretch() layout.addLayout(status_layout) - # Control buttons + # Control buttons - Large and prominent button_layout = QHBoxLayout() + button_layout.setSpacing(10) - self.start_session_btn = QPushButton("βΆοΈ Start Session") - self.start_session_btn.setToolTip("Start a new session with selected project") + self.start_session_btn = QPushButton("βΆοΈ START SESSION") + self.start_session_btn.setToolTip("Start a new session") + self.start_session_btn.setMinimumHeight(50) + self.start_session_btn.setStyleSheet(""" + QPushButton { + background-color: #1b5e20; + border: 2px solid #2e7d32; + border-radius: 6px; + padding: 10px 20px; + font-weight: bold; + font-size: 12pt; + } + QPushButton:hover { + background-color: #2e7d32; + } + QPushButton:disabled { + background-color: #1a3a1a; + color: #666; + border-color: #333; + } + """) self.start_session_btn.clicked.connect(self.on_start_session) - self.start_session_btn.setEnabled(False) - button_layout.addWidget(self.start_session_btn) + button_layout.addWidget(self.start_session_btn, 2) - self.stop_session_btn = QPushButton("βΉοΈ Stop") + 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.setMinimumHeight(50) self.stop_session_btn.setEnabled(False) - button_layout.addWidget(self.stop_session_btn) + self.stop_session_btn.setStyleSheet(""" + QPushButton { + background-color: #b71c1c; + border: 2px solid #c62828; + border-radius: 6px; + padding: 10px 20px; + font-weight: bold; + } + QPushButton:hover { + background-color: #c62828; + } + """) + self.stop_session_btn.clicked.connect(self.on_stop_session) + button_layout.addWidget(self.stop_session_btn, 1) - 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 = QPushButton("βΈοΈ PAUSE") + self.pause_session_btn.setToolTip("Pause/Resume session") + self.pause_session_btn.setMinimumHeight(50) self.pause_session_btn.setEnabled(False) - button_layout.addWidget(self.pause_session_btn) + self.pause_session_btn.setStyleSheet(""" + QPushButton { + background-color: #e65100; + border: 2px solid #f57c00; + border-radius: 6px; + padding: 10px 20px; + font-weight: bold; + } + QPushButton:hover { + background-color: #f57c00; + } + """) + self.pause_session_btn.clicked.connect(self.on_pause_session) + button_layout.addWidget(self.pause_session_btn, 1) layout.addLayout(button_layout) - # Session info - self.session_info_label = QLabel("Ready to start") + # Session stats summary + self.session_info_label = QLabel("Ready to start - Select activity and loadout first") self.session_info_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.session_info_label.setStyleSheet("color: #666; padding: 10px;") layout.addWidget(self.session_info_label) @@ -482,6 +631,57 @@ class MainWindow(QMainWindow): return panel + def create_recent_sessions_panel(self) -> QGroupBox: + """Create the recent sessions panel (BOTTOM).""" + panel = QGroupBox("Recent Sessions") + layout = QVBoxLayout(panel) + layout.setSpacing(8) + + # Recent sessions list + self.recent_sessions_list = QTreeWidget() + self.recent_sessions_list.setHeaderLabels([ + "Activity", "Template", "Started", "Duration", "Cost", "Return", "Status" + ]) + self.recent_sessions_list.setAlternatingRowColors(True) + self.recent_sessions_list.setSelectionMode(QTreeWidget.SelectionMode.SingleSelection) + self.recent_sessions_list.setRootIsDecorated(False) + self.recent_sessions_list.itemDoubleClicked.connect(self.on_session_double_clicked) + + # Adjust column widths + header = self.recent_sessions_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.setSectionResizeMode(4, QHeaderView.ResizeMode.Fixed) + header.setSectionResizeMode(5, QHeaderView.ResizeMode.Fixed) + header.setSectionResizeMode(6, QHeaderView.ResizeMode.Fixed) + header.resizeSection(0, 100) + header.resizeSection(2, 130) + header.resizeSection(3, 70) + header.resizeSection(4, 70) + header.resizeSection(5, 70) + header.resizeSection(6, 80) + + layout.addWidget(self.recent_sessions_list) + + # Button row + button_layout = QHBoxLayout() + + self.view_history_btn = QPushButton("π View Full History") + self.view_history_btn.setToolTip("View complete session history") + self.view_history_btn.clicked.connect(self.on_view_full_history) + button_layout.addWidget(self.view_history_btn) + + self.refresh_sessions_btn = QPushButton("π Refresh") + self.refresh_sessions_btn.setToolTip("Refresh recent sessions") + self.refresh_sessions_btn.clicked.connect(self.refresh_recent_sessions) + button_layout.addWidget(self.refresh_sessions_btn) + + layout.addLayout(button_layout) + + return panel + def create_log_panel(self) -> QGroupBox: """Create the log output panel.""" panel = QGroupBox("Event Log") @@ -520,15 +720,10 @@ class MainWindow(QMainWindow): # 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) + new_template_action = QAction("&New Session Template", self) + new_template_action.setShortcut("Ctrl+N") + new_template_action.triggered.connect(self.on_new_template) + file_menu.addAction(new_template_action) file_menu.addSeparator() @@ -558,30 +753,16 @@ class MainWindow(QMainWindow): 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 + loadout_action = QAction("&Loadout Manager", self) + loadout_action.setShortcut("Ctrl+L") + loadout_action.triggered.connect(self.on_loadout_manager) + tools_menu.addAction(loadout_action) + + tools_menu.addSeparator() + select_gear_menu = tools_menu.addMenu("Select &Gear") select_weapon_action = QAction("&Weapon", self) @@ -604,16 +785,50 @@ class MainWindow(QMainWindow): select_medical_action.triggered.connect(lambda: self.on_select_gear("medical_tool")) select_gear_menu.addAction(select_medical_action) - tools_menu.addSeparator() + # View menu + view_menu = menubar.addMenu("&View") - loadout_action = QAction("&Loadout Manager", self) - loadout_action.setShortcut("Ctrl+L") - loadout_action.triggered.connect(self.on_loadout_manager) - tools_menu.addAction(loadout_action) + 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) + + view_menu.addSeparator() + + # Session History + session_history_action = QAction("π Session &History", self) + session_history_action.setShortcut("Ctrl+H") + session_history_action.triggered.connect(self.on_session_history) + view_menu.addAction(session_history_action) + + # Gallery + gallery_action = QAction("πΌοΈ Screenshot &Gallery", self) + gallery_action.setShortcut("Ctrl+G") + gallery_action.triggered.connect(self.on_gallery) + view_menu.addAction(gallery_action) # Help menu help_menu = menubar.addMenu("&Help") + run_wizard_action = QAction("&Run Setup Wizard Again", self) + run_wizard_action.setShortcut("Ctrl+Shift+W") + run_wizard_action.triggered.connect(self.on_run_setup_wizard) + help_menu.addAction(run_wizard_action) + + help_menu.addSeparator() + about_action = QAction("&About", self) about_action.triggered.connect(self.on_about) help_menu.addAction(about_action) @@ -628,9 +843,9 @@ class MainWindow(QMainWindow): 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) + self.status_activity_label = QLabel("No Activity") + self.status_activity_label.setStyleSheet("color: #888; padding: 0 10px;") + self.status_bar.addPermanentWidget(self.status_activity_label) # Message area self.status_bar.showMessage("Ready") @@ -692,24 +907,6 @@ class MainWindow(QMainWindow): 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; @@ -759,6 +956,24 @@ class MainWindow(QMainWindow): border-color: #0d47a1; } + QComboBox { + background-color: #252525; + border: 1px solid #444; + border-radius: 4px; + padding: 6px; + color: #e0e0e0; + min-width: 120px; + } + + QComboBox:focus { + border-color: #0d47a1; + } + + QComboBox::drop-down { + border: none; + padding-right: 10px; + } + QMenuBar { background-color: #1e1e1e; border-bottom: 1px solid #444; @@ -826,129 +1041,284 @@ class MainWindow(QMainWindow): self.setStyleSheet(dark_stylesheet) # ======================================================================== - # Project Management + # Session Template Management # ======================================================================== - def refresh_project_list(self): - """Refresh the project list display.""" - self.project_list.clear() - projects = self.project_manager.list_projects() + def refresh_session_templates(self): + """Refresh the session template list.""" + self.template_combo.clear() + + # Load templates from database + templates = self._load_templates_from_db() + + for template in templates: + self.template_combo.addItem( + f"{template.activity_type.display_name} - {template.name}", + template + ) + + self.log_debug("Templates", f"Loaded {len(templates)} session templates") - 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) + def _load_templates_from_db(self) -> List[SessionTemplate]: + """Load session templates from database.""" + templates = [] + try: + # Query database for projects (using existing project table) + projects = self.db.fetchall( + "SELECT id, name, type, created_at, description FROM projects ORDER BY name" + ) + + for proj in projects: + activity = ActivityType.from_string(proj.get('type', 'hunting')) + # Get session count + count_result = self.db.fetchone( + "SELECT COUNT(*) as count FROM sessions WHERE project_id = ?", + (proj['id'],) + ) + session_count = count_result['count'] if count_result else 0 + + # Get last session + last_result = self.db.fetchone( + "SELECT MAX(started_at) as last FROM sessions WHERE project_id = ?", + (proj['id'],) + ) + last_session = None + if last_result and last_result['last']: + last_session = datetime.fromisoformat(last_result['last']) + + template = SessionTemplate( + id=proj['id'], + name=proj['name'], + activity_type=activity, + description=proj.get('description', ''), + created_at=datetime.fromisoformat(proj['created_at']) if proj.get('created_at') else None, + session_count=session_count, + last_session=last_session + ) + templates.append(template) + except Exception as e: + self.log_error("Templates", f"Failed to load templates: {e}") + + return templates - 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) + def on_new_template(self): + """Handle new session template creation.""" + dialog = NewSessionTemplateDialog(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) + name, activity_type, description = dialog.get_template_data() + + try: + # Save to database + result = self.db.execute( + """INSERT INTO projects (name, type, description, created_at) + VALUES (?, ?, ?, ?)""", + (name, activity_type.value, description, datetime.now().isoformat()) + ) + self.db.commit() + + self.refresh_session_templates() + self.log_info("Templates", f"Created template: {name} ({activity_type.display_name})") + self.status_bar.showMessage(f"Template '{name}' created", 3000) + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to create template: {e}") - 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_template_changed(self, index: int): + """Handle template selection change.""" + if index >= 0: + self.current_template = self.template_combo.currentData() + if self.current_template: + self.current_template_label.setText(self.current_template.name) + self.current_template_label.setStyleSheet(f"font-weight: bold; color: {self.current_template.activity_type.color};") + self.log_debug("Templates", f"Selected template: {self.current_template.name}") + self._update_start_button() + else: + self.current_template = None + self.current_template_label.setText("No Template") + self.current_template_label.setStyleSheet("font-weight: bold; color: #888;") + self._update_start_button() - def on_view_stats(self): - """Handle view stats button.""" - if self.current_project: - self.show_project_stats(self.current_project) + def on_activity_changed(self, index: int): + """Handle activity type change.""" + self.current_activity = self.activity_combo.currentData() + if self.current_activity: + self.current_activity_label.setText(self.current_activity.display_name) + self.current_activity_label.setStyleSheet(f"font-weight: bold; color: {self.current_activity.color};") + self.status_activity_label.setText(self.current_activity.display_name) + self.log_debug("Activity", f"Changed to: {self.current_activity.display_name}") - def show_project_stats(self, project: Project): - """Show project statistics dialog.""" - dialog = ProjectStatsDialog(project, self) + # ======================================================================== + # Recent Sessions + # ======================================================================== + + def refresh_recent_sessions(self): + """Refresh the recent sessions list.""" + self.recent_sessions_list.clear() + + sessions = self._load_recent_sessions_from_db() + + for session in sessions: + item = QTreeWidgetItem([ + session.activity_type.display_name, + session.template_name, + session.started_at.strftime("%m-%d %H:%M"), + f"{session.duration_minutes}m", + f"{session.total_cost:.2f}", + f"{session.total_return:.2f}", + session.status + ]) + item.setData(0, Qt.ItemDataRole.UserRole, session.id) + + # Color code status + if session.status == "completed": + item.setForeground(6, QColor("#4caf50")) + elif session.status == "running": + item.setForeground(6, QColor("#ff9800")) + + self.recent_sessions_list.addTopLevelItem(item) + + self.log_debug("Sessions", f"Loaded {len(sessions)} recent sessions") + + def _load_recent_sessions_from_db(self) -> List[RecentSession]: + """Load recent sessions from database.""" + sessions = [] + try: + rows = self.db.fetchall(""" + SELECT s.id, p.name as template_name, p.type, + s.started_at, s.ended_at, s.status + FROM sessions s + JOIN projects p ON s.project_id = p.id + ORDER BY s.started_at DESC + LIMIT 20 + """) + + for row in rows: + activity = ActivityType.from_string(row.get('type', 'hunting')) + + # Calculate duration + started = datetime.fromisoformat(row['started_at']) + duration = 0 + if row.get('ended_at'): + ended = datetime.fromisoformat(row['ended_at']) + duration = int((ended - started).total_seconds() / 60) + elif row['status'] == 'running': + duration = int((datetime.now() - started).total_seconds() / 60) + + # Get costs (placeholder - would need actual cost tracking) + total_cost = 0.0 + total_return = 0.0 + + session = RecentSession( + id=row['id'], + template_name=row['template_name'], + activity_type=activity, + started_at=started, + duration_minutes=duration, + total_cost=total_cost, + total_return=total_return, + status=row.get('status', 'unknown') + ) + sessions.append(session) + except Exception as e: + self.log_error("Sessions", f"Failed to load sessions: {e}") + + return sessions + + def on_session_double_clicked(self, item: QTreeWidgetItem, column: int): + """Handle double-click on session.""" + session_id = item.data(0, Qt.ItemDataRole.UserRole) + self.log_info("Sessions", f"Viewing session details: {session_id}") + # TODO: Open session detail dialog + + def on_view_full_history(self): + """Open full session history view.""" + self.log_info("Sessions", "Opening full session history") + dialog = SessionHistoryDialog(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 + def _update_start_button(self): + """Update start button state based on selections.""" + can_start = ( + self.session_state == SessionState.IDLE and + self.current_template is not None and + self._selected_loadout is not None + ) + self.start_session_btn.setEnabled(can_start) + self.start_action.setEnabled(can_start) + def on_start_session(self): + """Handle start session button.""" if self.session_state != SessionState.IDLE: self.log_warning("Session", "Cannot start: session already active") return + + if not self.current_template: + QMessageBox.warning(self, "No Template", "Please select a session template first.") + return + + if not self._selected_loadout: + QMessageBox.warning(self, "No Loadout", "Please configure a loadout before starting.") + return + + # Start the session + self.start_session_with_template(self.current_template) + + def start_session_with_template(self, template: SessionTemplate): + """Start a new session with the given template.""" + if self.session_state != SessionState.IDLE: + return # Update state self.set_session_state(SessionState.RUNNING) - self.current_session_id = project_id + self.current_session_id = template.id # Emit signal - self.session_started.emit(project_id) + self.session_started.emit(template.id) # Log - self.log_info("Session", f"Started session for project: {project.name}") - self.session_info_label.setText(f"Session active: {project.name}") + self.log_info("Session", f"Started {template.activity_type.display_name} session: {template.name}") + self.session_info_label.setText(f"Session active: {template.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 + # Start session in database + try: + result = self.db.execute( + "INSERT INTO sessions (project_id, started_at, status) VALUES (?, ?, ?)", + (template.id, datetime.now().isoformat(), 'running') + ) + self.db.commit() + self._current_db_session_id = result.lastrowid + except Exception as e: + self.log_error("Session", f"Failed to record session: {e}") # Setup LogWatcher + self._setup_log_watcher() + + # Show HUD + self.hud.show() + + # Start HUD session + session_display = getattr(self, '_session_display', {}) + session_costs = getattr(self, '_session_costs', {}) + + self.hud.start_session( + weapon=session_display.get('weapon_name', 'Unknown'), + armor=session_display.get('armor_name', 'None'), + fap=session_display.get('healing_name', 'None'), + loadout=self._selected_loadout_name, + weapon_dpp=Decimal('0'), + weapon_cost_per_hour=Decimal('0'), + cost_per_shot=session_costs.get('cost_per_shot', Decimal('0')), + cost_per_hit=session_costs.get('cost_per_hit', Decimal('0')), + cost_per_heal=session_costs.get('cost_per_heal', Decimal('0')) + ) + + def _setup_log_watcher(self): + """Setup and start the log watcher.""" use_mock = os.getenv('USE_MOCK_DATA', 'false').lower() in ('true', '1', 'yes') - log_path = os.getenv('EU_CHAT_LOG_PATH', '') + log_path = self.log_path or os.getenv('EU_CHAT_LOG_PATH', '') if use_mock or not log_path: # Use mock log for testing @@ -965,275 +1335,103 @@ class MainWindow(QMainWindow): self.log_info("LogWatcher", f"Using REAL log: {log_path}") # Subscribe to events - self._setup_log_watcher_callbacks() + self._subscribe_to_log_events() - # Start LogWatcher in background + # Start LogWatcher self._start_log_watcher() - # Show HUD and start session tracking - self.hud.show() - - # Get gear names and costs from simplified loadout structure - session_display = getattr(self, '_session_display', {}) - session_costs = getattr(self, '_session_costs', {}) - - weapon_name = session_display.get('weapon_name', self._selected_weapon or "Unknown") - armor_name = session_display.get('armor_name', "None") - healing_name = session_display.get('healing_name', "None") - loadout_name = "Loadout" if session_costs else "Default" - - 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 cost data from simplified structure - cost_per_shot = session_costs.get('cost_per_shot', Decimal('0')) - cost_per_hit = session_costs.get('cost_per_hit', Decimal('0')) - cost_per_heal = session_costs.get('cost_per_heal', Decimal('0')) - - self.hud.start_session( - weapon=weapon_name, - armor=armor_name, - fap=healing_name, - loadout=loadout_name, - weapon_dpp=weapon_dpp, - weapon_cost_per_hour=weapon_cost_per_hour, - cost_per_shot=cost_per_shot, - cost_per_hit=cost_per_hit, - cost_per_heal=cost_per_heal - ) - - # Simple cost tracking - no database required - self.log_info("CostTracker", "Cost tracking enabled with pre-calculated values") - - self.log_info("HUD", f"HUD shown - Weapon: {weapon_name}, Armor: {armor_name}, Healing: {healing_name}, Loadout: {loadout_name}") - - def _setup_log_watcher_callbacks(self): - """Setup LogWatcher event callbacks.""" + def _subscribe_to_log_events(self): + """Subscribe to log watcher events.""" 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')) - logger.debug(f"[EVENT] on_heal: heal_amount={heal_amount}") - - # 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 - try: - logger.debug(f"[DEBUG] _session_costs={self._session_costs}") - # Track healing cost - if self._session_costs: - cost_per_heal = self._session_costs.get('cost_per_heal', Decimal('0')) - logger.debug(f"[DEBUG] cost_per_heal={cost_per_heal}") - if cost_per_heal > 0: - logger.debug(f"[HUD] update_healing_cost({cost_per_heal})") - self.hud.update_healing_cost(cost_per_heal) - - # Track heal amount (as event dict for new HUD) - logger.debug(f"[HUD] on_heal_event({{'heal_amount': {heal_amount}}})") - self.hud.on_heal_event({'heal_amount': heal_amount}) - logger.debug(f"[HUD] Heal update successful") - except Exception as e: - logger.error(f"[ERROR] Error updating HUD heal: {e}") - import traceback - logger.error(traceback.format_exc()) - - # Log to UI - self.log_info("Heal", f"Healed {heal_amount} HP (Cost: {decay_cost:.4f} PED)") - def on_loot(event): - """Handle loot events.""" from decimal import Decimal - item_name = event.data.get('item_name', 'Unknown') value_ped = event.data.get('value_ped', Decimal('0.0')) - quantity = event.data.get('quantity', 1) - logger.debug(f"[EVENT] on_loot: item={item_name}, value={value_ped}, qty={quantity}") - - # Skip Universal Ammo if item_name == 'Universal Ammo': - logger.debug("[EVENT] on_loot: skipped Universal Ammo") return - try: - # Update loot value - this also handles kill counting internally - is_shrapnel = 'shrapnel' in item_name.lower() - logger.debug(f"[HUD] update_loot({value_ped}, is_shrapnel={is_shrapnel})") - self.hud.update_loot(value_ped, is_shrapnel=is_shrapnel) - logger.debug(f"[HUD] Loot update successful") - except Exception as e: - logger.error(f"[ERROR] Error updating HUD loot: {e}") - import traceback - logger.error(traceback.format_exc()) - - # Queue database write for main thread (SQLite thread safety) + is_shrapnel = 'shrapnel' in item_name.lower() + self.hud.update_loot(value_ped, is_shrapnel=is_shrapnel) + + # Queue for database 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 }) - # 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 (other players).""" - value_ped = event.data.get('value_ped', Decimal('0.0')) - player = event.data.get('player_name', 'Unknown') - # Don't count other players' globals in HUD - self.log_info("Global", f"{player} found {value_ped} PED!") - - def on_personal_global(event): - """Handle personal global events (only your globals).""" - value_ped = event.data.get('value_ped', Decimal('0.0')) - creature = event.data.get('creature', 'Unknown') - player = event.data.get('player_name', 'Unknown') - - # Only count if it matches our configured player name - if self.player_name and player.lower() == self.player_name.lower(): - # Only count personal globals in HUD stats - self.hud.on_personal_global(value_ped) - self.log_info("Global", f"π YOUR GLOBAL: {creature} for {value_ped} PED!!!") - else: - # Log but don't count - might be another player if names are similar - self.log_info("Global", f"{player} got global: {creature} for {value_ped} PED") - - # If no player name set, warn user - if not self.player_name: - self.log_info("Config", "β οΈ Set your avatar name in Settings to track your globals correctly") - - 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.""" - from decimal import Decimal - skill_name = event.data.get('skill_name', 'Unknown') - gained = event.data.get('gained', 0) - self.log_info("Skill", f"{skill_name} +{gained}") - - # Update HUD skill tracking - try: - gained_decimal = Decimal(str(gained)) - self.hud.update_skill(skill_name, gained_decimal) - except Exception as e: - logger.error(f"[ERROR] Error updating HUD skill: {e}") - def on_damage_dealt(event): - """Handle damage dealt - track damage stats and weapon cost.""" from decimal import Decimal - try: - damage = event.data.get('damage', 0) - logger.debug(f"[EVENT] on_damage_dealt: damage={damage}") - if damage: - # Track damage amount - logger.debug(f"[HUD] on_damage_dealt({damage})") - self.hud.on_damage_dealt(Decimal(str(damage))) - - # Track weapon cost per shot - logger.debug(f"[DEBUG] _session_costs={self._session_costs}") - if self._session_costs: - cost_per_shot = self._session_costs.get('cost_per_shot', Decimal('0')) - logger.debug(f"[DEBUG] cost_per_shot={cost_per_shot}") - if cost_per_shot > 0: - logger.debug(f"[HUD] update_weapon_cost({cost_per_shot})") - self.hud.update_weapon_cost(cost_per_shot) - except Exception as e: - logger.error(f"[ERROR] Error in on_damage_dealt: {e}") - import traceback - logger.error(traceback.format_exc()) - - def on_critical_hit(event): - """Handle critical hit - same as damage dealt.""" - try: - on_damage_dealt(event) - except Exception as e: - logger.error(f"Error in on_critical_hit: {e}") + damage = event.data.get('damage', 0) + if damage: + self.hud.on_damage_dealt(Decimal(str(damage))) + if self._session_costs.get('cost_per_shot'): + self.hud.update_weapon_cost(self._session_costs['cost_per_shot']) def on_damage_taken(event): - """Handle damage taken - track damage stats and armor cost.""" from decimal import Decimal - try: - damage = event.data.get('damage', 0) - logger.debug(f"[EVENT] on_damage_taken: damage={damage}") - if damage: - # Track damage amount - logger.debug(f"[HUD] on_damage_taken({damage})") - self.hud.on_damage_taken(Decimal(str(damage))) - - # Calculate armor decay based on actual damage absorbed - # EU Formula: Decay (PEC) = damage_absorbed * 0.05 * (1 - durability/100000) - # For (L) armor at full TT: ~0.05 PEC per damage point - # For adjusted armor: decay varies by piece - # Based on user feedback: ~0.00025 PED per damage (0.025 PEC) - decay_per_damage = Decimal('0.00025') # ~0.025 PEC per damage - cost_ped = Decimal(str(damage)) * decay_per_damage - if cost_ped > 0: - logger.debug(f"[HUD] update_armor_cost({cost_ped:.4f}) based on damage {damage}") - self.hud.update_armor_cost(cost_ped) - except Exception as e: - logger.error(f"[ERROR] Error in on_damage_taken: {e}") - import traceback - logger.error(traceback.format_exc()) + damage = event.data.get('damage', 0) + if damage: + self.hud.on_damage_taken(Decimal(str(damage))) + decay_per_damage = Decimal('0.00025') + cost_ped = Decimal(str(damage)) * decay_per_damage + if cost_ped > 0: + self.hud.update_armor_cost(cost_ped) - def on_evade(event): - """Handle evade - count weapon cost when target dodges your attack.""" - evade_type = event.data.get('type', 'Evade') - self.log_info("Evade", evade_type) + def on_heal(event): + from decimal import Decimal + heal_amount = event.data.get('heal_amount', Decimal('0')) + if self._session_costs.get('cost_per_heal'): + self.hud.update_healing_cost(self._session_costs['cost_per_heal']) + self.hud.on_heal_event({'heal_amount': heal_amount}) + + def on_personal_global(event): + value_ped = event.data.get('value_ped', Decimal('0.0')) + player = event.data.get('player_name', 'Unknown') + creature = event.data.get('creature', 'Unknown') + if self.player_name and player.lower() == self.player_name.lower(): + self.hud.on_personal_global(value_ped) + self.log_info("Global", f"π YOUR GLOBAL: {value_ped} PED!!!") + + # Capture screenshot + if self._current_db_session_id: + screenshot_path = self._screenshot_capture.capture_global( + self._current_db_session_id, + float(value_ped), + creature + ) + if screenshot_path: + self.log_info("Screenshot", f"π· Global captured: {screenshot_path}") + + def on_hof(event): + value_ped = event.data.get('value_ped', Decimal('0.0')) + creature = event.data.get('creature', 'Unknown') + self.hud.on_hof(value_ped) + self.log_info("HoF", f"π HALL OF FAME: {value_ped} PED!") - # If target evaded/dodged YOUR attack, you still pay weapon cost - target_evaded = 'target Evaded' in evade_type or 'target Dodged' in evade_type - if target_evaded: - try: - from decimal import Decimal - if self._session_costs: - cost_per_shot = self._session_costs.get('cost_per_shot', Decimal('0')) - if cost_per_shot > 0: - logger.debug(f"[HUD] update_weapon_cost({cost_per_shot}) [target evaded]") - self.hud.update_weapon_cost(cost_per_shot) - except Exception as e: - logger.error(f"[ERROR] Error tracking weapon cost on evade: {e}") + # Capture screenshot + if self._current_db_session_id: + screenshot_path = self._screenshot_capture.capture_hof( + self._current_db_session_id, + float(value_ped), + creature + ) + if screenshot_path: + self.log_info("Screenshot", f"π· HoF captured: {screenshot_path}") - # Subscribe to all event types + # Subscribe self.log_watcher.subscribe('loot', on_loot) - self.log_watcher.subscribe('global', on_global) + self.log_watcher.subscribe('damage_dealt', on_damage_dealt) + self.log_watcher.subscribe('damage_taken', on_damage_taken) + self.log_watcher.subscribe('heal', on_heal) 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.""" @@ -1270,227 +1468,79 @@ class MainWindow(QMainWindow): """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.wait(2000) self._log_watcher_thread = None self.log_info("LogWatcher", "Stopped") - def _setup_session_cost_tracker(self, loadout_info: dict): - """Set up session cost tracker with selected loadout.""" - from core.session_cost_tracker import SessionCostTracker - from core.database import DatabaseManager - - loadout_id = loadout_info.get('id') - loadout_source = loadout_info.get('source') - - # If JSON-based loadout, we need to save it to database first or handle differently - if loadout_source == 'json': - # For now, skip cost tracking for JSON loadouts - # TODO: Save JSON loadout to database first, then use its ID - self.log_warning("CostTracker", "Cost tracking not available for file-based loadouts. Save to database first.") - return - - 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 - + """Process events from the queue in the main thread.""" processed = 0 - while not self._event_queue.empty() and processed < 10: # Process max 10 per tick + while not self._event_queue.empty() and processed < 10: 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)") - + self.log_info("Loot", f"{event['item_name']} ({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: - # Show loadout selection dialog - from ui.loadout_selection_dialog_simple import LoadoutSelectionDialog - dialog = LoadoutSelectionDialog(self) - dialog.loadout_selected.connect(self._on_loadout_selected_for_session) - dialog.rejected.connect(lambda: self.log_info("Session", "Session start cancelled - no loadout selected")) - dialog.exec() - - def _on_loadout_selected_for_session(self, loadout_info: dict): - """Handle loadout selection and start session - simplified cost-focused version.""" - loadout_name = loadout_info.get('name', 'No Loadout') - costs = loadout_info.get('costs', {}) - display = loadout_info.get('display', {}) - - # Store cost data for session tracking - from decimal import Decimal - self._session_costs = { - 'cost_per_shot': costs.get('cost_per_shot', Decimal('0')), - 'cost_per_hit': costs.get('cost_per_hit', Decimal('0')), - 'cost_per_heal': costs.get('cost_per_heal', Decimal('0')), - } - - # Store display data for HUD - self._session_display = { - 'weapon_name': display.get('weapon_name', 'None'), - 'armor_name': display.get('armor_name', 'None'), - 'healing_name': display.get('healing_name', 'None'), - } - - if any(self._session_costs.values()): - self.log_info("Session", f"Starting with loadout: {loadout_name}") - self.log_info("SessionCosts", - f"Shot: {self._session_costs['cost_per_shot']:.4f} PED, " - f"Hit: {self._session_costs['cost_per_hit']:.4f} PED, " - f"Heal: {self._session_costs['cost_per_heal']:.4f} PED") - else: - self.log_info("Session", f"Starting with loadout: {loadout_name} (no costs configured)") - - # 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() - # Save HUD costs to database before ending session - if self._current_db_session_id and self.hud: - from decimal import Decimal - from datetime import datetime - - # Save weapon decay - if self.hud._stats.weapon_cost_total > 0: - self.db.execute( - """INSERT INTO decay_events - (session_id, item_name, decay_amount_ped, timestamp) - VALUES (?, ?, ?, ?)""", - (self._current_db_session_id, 'Weapon', - float(self.hud._stats.weapon_cost_total), datetime.now()) - ) - - # Save armor decay - if self.hud._stats.armor_cost_total > 0: - self.db.execute( - """INSERT INTO decay_events - (session_id, item_name, decay_amount_ped, timestamp) - VALUES (?, ?, ?, ?)""", - (self._current_db_session_id, 'Armor', - float(self.hud._stats.armor_cost_total), datetime.now()) - ) - - # Save healing decay - if self.hud._stats.healing_cost_total > 0: - self.db.execute( - """INSERT INTO decay_events - (session_id, item_name, decay_amount_ped, timestamp) - VALUES (?, ?, ?, ?)""", - (self._current_db_session_id, 'Healing', - float(self.hud._stats.healing_cost_total), datetime.now()) - ) - - self.db.commit() - self.log_info("Session", - f"Saved costs to DB: Weapon={self.hud._stats.weapon_cost_total:.2f}, " - f"Armor={self.hud._stats.armor_cost_total:.2f}, " - f"Healing={self.hud._stats.healing_cost_total:.2f}") - # End session in database if self._current_db_session_id: - self.project_manager.end_session(self._current_db_session_id) + try: + self.db.execute( + "UPDATE sessions SET ended_at = ?, status = ? WHERE id = ?", + (datetime.now().isoformat(), 'completed', self._current_db_session_id) + ) + self.db.commit() + except Exception as e: + self.log_error("Session", f"Failed to end session: {e}") 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() + + # Refresh recent sessions + self.refresh_recent_sessions() 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() - # Pause HUD tracking if self.hud: self.hud.session_active = False self.hud.status_label.setText("β Paused") self.hud.status_label.setStyleSheet("color: #FF9800; font-weight: bold;") - self.log_info("Session", "Session paused - tracking stopped") + self.log_info("Session", "Session paused") self.session_info_label.setText("Session paused") - self.pause_session_btn.setText("βΆοΈ Resume") + self.pause_session_btn.setText("βΆοΈ RESUME") elif self.session_state == SessionState.PAUSED: self.set_session_state(SessionState.RUNNING) self.session_resumed.emit() - # Resume HUD tracking if self.hud: self.hud.session_active = True self.hud.status_label.setText("β Live") self.hud.status_label.setStyleSheet("color: #7FFF7F; font-weight: bold;") - self.log_info("Session", "Session resumed - tracking started") + self.log_info("Session", "Session resumed") self.session_info_label.setText("Session resumed") - self.pause_session_btn.setText("βΈοΈ Pause") + self.pause_session_btn.setText("βΈοΈ PAUSE") def set_session_state(self, state: SessionState): - """ - Update the session state and UI. - - Args: - state: New session state - """ + """Update the session state and UI.""" self.session_state = state - # Update status label colors = { SessionState.IDLE: "#888", SessionState.RUNNING: "#4caf50", @@ -1511,54 +1561,121 @@ class MainWindow(QMainWindow): }} """) - # 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._update_start_button() 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") + self.pause_session_btn.setText("βΈοΈ PAUSE") + + # ======================================================================== + # Loadout Manager + # ======================================================================== + + def on_loadout_manager(self): + """Open Loadout Manager dialog.""" + from ui.loadout_manager_simple import LoadoutManagerDialog + dialog = LoadoutManagerDialog(self) + dialog.loadout_saved.connect(self._on_loadout_selected_for_session) + dialog.exec() + + def _on_loadout_selected_for_session(self, loadout_config): + """Handle loadout selection from LoadoutManagerDialog. + + Args: + loadout_config: LoadoutConfig object with full gear support + """ + from ui.loadout_manager_simple import LoadoutConfig + + if isinstance(loadout_config, LoadoutConfig): + # New LoadoutConfig format with full gear support + loadout_name = loadout_config.name + + # Get total costs including amplifiers, platings, and mindforce implants + self._session_costs = { + 'cost_per_shot': loadout_config.get_total_weapon_cost_per_shot(), + 'cost_per_hit': loadout_config.get_total_armor_cost_per_hit(), + 'cost_per_heal': loadout_config.get_total_healing_cost_per_heal(), + } + + # Display includes all gear types + self._session_display = { + 'weapon_name': loadout_config.weapon_name, + 'weapon_amp_name': loadout_config.weapon_amp_name if loadout_config.weapon_amp_id else None, + 'armor_name': loadout_config.armor_name, + 'plating_name': loadout_config.plating_name if loadout_config.plating_id else None, + 'healing_name': loadout_config.healing_name, + 'mindforce_name': loadout_config.mindforce_implant_name if loadout_config.mindforce_implant_id else None, + } + + # Log full gear details + gear_details = f"Weapon: {loadout_config.weapon_name}" + if loadout_config.weapon_amp_id: + gear_details += f" + {loadout_config.weapon_amp_name}" + gear_details += f" | Armor: {loadout_config.armor_name}" + if loadout_config.plating_id: + gear_details += f" + {loadout_config.plating_name}" + gear_details += f" | Healing: {loadout_config.healing_name}" + if loadout_config.mindforce_implant_id: + gear_details += f" + {loadout_config.mindforce_implant_name}" + + self.log_info("Loadout", f"Selected: {loadout_name}") + self.log_info("Loadout", f"Gear: {gear_details}") + self.log_info("Loadout", + f"Costs - Shot: {self._session_costs['cost_per_shot']:.4f} PED, " + f"Hit: {self._session_costs['cost_per_hit']:.4f} PED, " + f"Heal: {self._session_costs['cost_per_heal']:.4f} PED") + else: + # Legacy dict format (fallback) + loadout_name = loadout_config.get('name', 'No Loadout') if isinstance(loadout_config, dict) else 'No Loadout' + costs = loadout_config.get('costs', {}) if isinstance(loadout_config, dict) else {} + display = loadout_config.get('display', {}) if isinstance(loadout_config, dict) else {} + + from decimal import Decimal + self._session_costs = { + 'cost_per_shot': costs.get('cost_per_shot', Decimal('0')), + 'cost_per_hit': costs.get('cost_per_hit', Decimal('0')), + 'cost_per_heal': costs.get('cost_per_heal', Decimal('0')), + } + + self._session_display = { + 'weapon_name': display.get('weapon_name', 'None'), + 'armor_name': display.get('armor_name', 'None'), + 'healing_name': display.get('healing_name', 'None'), + } + + self.log_info("Loadout", f"Selected (legacy): {loadout_name}") + + self._selected_loadout = loadout_config + self._selected_loadout_name = loadout_name + self.loadout_display.setText(loadout_name) + self.loadout_display.setStyleSheet("font-weight: bold; color: #4caf50;") + + self._update_start_button() + + 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}") # ======================================================================== # 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) @@ -1588,7 +1705,6 @@ class MainWindow(QMainWindow): 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()) @@ -1618,88 +1734,24 @@ class MainWindow(QMainWindow): dialog = SettingsDialog(self, self.player_name) if dialog.exec() == QDialog.DialogCode.Accepted: self.player_name = dialog.get_player_name() - self._save_settings() # Save to persistent storage - if self.player_name: - self.log_info("Settings", f"Avatar name set to: {self.player_name}") - else: - self.log_info("Settings", "Warning: No avatar name set. Global tracking may not work correctly.") + self.log_path = dialog.get_log_path() + self.auto_detect_log = dialog.get_auto_detect() + self._save_settings() + self.log_info("Settings", f"Avatar name: {self.player_name}") - def _load_settings(self): - """Load persistent settings from QSettings.""" - settings = QSettings("Lemontropia", "Suite") - self.player_name = settings.value("player/name", "", type=str) - if self.player_name: - self.log_info("Settings", f"Loaded avatar name: {self.player_name}") - - def _save_settings(self): - """Save persistent settings to QSettings.""" - settings = QSettings("Lemontropia", "Suite") - settings.setValue("player/name", self.player_name) - settings.sync() # Ensure settings are written immediately - - def on_loadout_manager(self): - """Open Loadout Manager dialog.""" - from ui.loadout_manager_simple 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 - simplified version.""" - self._selected_loadout = loadout - self.log_info("Loadout", f"Selected loadout: {loadout.name}") - - # Update selected gear from loadout (simplified structure) - if hasattr(loadout, 'weapon_name'): - self._selected_weapon = loadout.weapon_name - if hasattr(loadout, 'healing_name'): - self._selected_medical_tool = loadout.healing_name - self._selected_medical_tool_stats = { - 'decay': float(loadout.healing_decay_pec), - 'cost_per_heal': float(loadout.healing_cost_per_heal), - } - # Store simplified costs for session - if hasattr(loadout, 'weapon_cost_per_shot'): - self._session_costs = { - 'cost_per_shot': loadout.weapon_cost_per_shot, - 'cost_per_hit': loadout.armor_cost_per_hit, - 'cost_per_heal': loadout.healing_cost_per_heal, - } - if hasattr(loadout, 'weapon_name'): - self._session_display = { - 'weapon_name': loadout.weapon_name, - 'armor_name': loadout.armor_name, - 'healing_name': loadout.healing_name, - } - - 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_run_setup_wizard(self): + """Run the setup wizard again.""" + from ui.setup_wizard import SetupWizard + wizard = SetupWizard(self, first_run=False) + if wizard.exec() == QDialog.DialogCode.Accepted: + settings = wizard.get_settings() + self.player_name = settings.get('avatar_name', '') + self.log_path = settings.get('log_path', '') + self.auto_detect_log = settings.get('auto_detect_log', True) + self.current_activity = ActivityType.from_string(settings.get('default_activity', 'hunting')) + self._save_settings() + self._load_settings() # Refresh UI + self.log_info("Setup", "Settings updated from wizard") def on_about(self): """Show about dialog.""" @@ -1708,17 +1760,72 @@ class MainWindow(QMainWindow): "About Lemontropia Suite", """
Version 1.0.0
-A PyQt6-based GUI for game automation and session management.
+A PyQt6-based GUI for Entropia Universe session tracking.
Features:
Lemontropia Suite helps you track your hunting, mining, and crafting sessions " + "in Entropia Universe. This wizard will guide you through the initial setup.
" + "You'll need:
" + "