feat: major update - UI redesign, Session History, Gallery, Enhanced Loadout Manager
=== UI REDESIGN === - Rename Project Management to Activity History - Add Setup Wizard for first-run configuration - Reorganize layout: Activity Setup | Session Control | Recent Sessions - Add prominent Loadout Manager button - Add Activity Types (Hunting/Mining/Crafting) === SESSION HISTORY & GALLERY === - New SessionHistoryDialog (Ctrl+H) - view/export past sessions - New GalleryDialog (Ctrl+G) - browse screenshots - Auto-screenshot on globals and HoFs - Screenshots saved to data/screenshots/ === ENHANCED LOADOUT MANAGER === - Add Weapon Amplifier support - Add Armor Plating support - Add Mindforce Implant support - New AmplifierSelector dialog - Full cost calculations for all gear types === NEW FILES === - ui/setup_wizard.py - ui/session_history.py - ui/gallery_dialog.py - ui/amplifier_selector.py - docs/CODEBASE_AUDIT_REPORT.md === MODIFIED FILES === - ui/main_window.py - major restructuring - ui/loadout_manager_simple.py - enhanced gear support - gui_main.py - first-run wizard check
This commit is contained in:
parent
c93b57aec4
commit
c347b5d28e
|
|
@ -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*
|
||||||
11
gui_main.py
11
gui_main.py
|
|
@ -16,6 +16,7 @@ sys.path.insert(0, str(Path(__file__).parent))
|
||||||
from PyQt6.QtWidgets import QApplication
|
from PyQt6.QtWidgets import QApplication
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
from ui.main_window import MainWindow
|
from ui.main_window import MainWindow
|
||||||
|
from ui.setup_wizard import SetupWizard
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
@ -28,6 +29,14 @@ def main():
|
||||||
app.setApplicationName("Lemontropia Suite")
|
app.setApplicationName("Lemontropia Suite")
|
||||||
app.setApplicationVersion("0.2.0")
|
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
|
# Create and show main window
|
||||||
window = MainWindow()
|
window = MainWindow()
|
||||||
window.show()
|
window.show()
|
||||||
|
|
@ -36,4 +45,4 @@ def main():
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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, "")
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"""
|
"""
|
||||||
Lemontropia Suite - Loadout Manager UI v4.0
|
Lemontropia Suite - Loadout Manager UI v5.0
|
||||||
Simplified cost-focused loadout system.
|
Full gear support with weapon amplifiers, mindforce implants, and armor platings.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
@ -15,7 +15,7 @@ from PyQt6.QtWidgets import (
|
||||||
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout,
|
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout,
|
||||||
QLineEdit, QLabel, QPushButton, QGroupBox,
|
QLineEdit, QLabel, QPushButton, QGroupBox,
|
||||||
QMessageBox, QListWidget, QListWidgetItem,
|
QMessageBox, QListWidget, QListWidgetItem,
|
||||||
QSplitter, QWidget, QFrame, QGridLayout,
|
QSplitter, QWidget, QFrame, QGridLayout, QScrollArea,
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt, pyqtSignal
|
from PyQt6.QtCore import Qt, pyqtSignal
|
||||||
|
|
||||||
|
|
@ -25,62 +25,94 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Simple Cost-Focused Loadout Config
|
# Full Gear Loadout Config
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LoadoutConfig:
|
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.
|
Core principle: Store all gear types needed for comprehensive cost tracking.
|
||||||
Everything else is display metadata.
|
|
||||||
"""
|
"""
|
||||||
# Identity
|
# Identity
|
||||||
name: str = "Unnamed"
|
name: str = "Unnamed"
|
||||||
version: int = 2 # Version 2 = simplified format
|
version: int = 3 # Version 3 = full gear support
|
||||||
|
|
||||||
# === COST DATA (Required for tracking) ===
|
# === WEAPON & ATTACHMENTS ===
|
||||||
# All values in PED (not PEC)
|
|
||||||
weapon_cost_per_shot: Decimal = Decimal("0")
|
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_name: str = "None"
|
||||||
weapon_damage: Decimal = Decimal("0")
|
weapon_damage: Decimal = Decimal("0")
|
||||||
weapon_decay_pec: Decimal = Decimal("0") # Raw for reference
|
weapon_decay_pec: Decimal = Decimal("0")
|
||||||
weapon_ammo_pec: Decimal = Decimal("0") # Raw for reference
|
weapon_ammo_pec: Decimal = Decimal("0")
|
||||||
|
|
||||||
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_api_id: Optional[int] = None
|
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_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
|
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:
|
def to_dict(self) -> dict:
|
||||||
"""Serialize to simple dictionary."""
|
"""Serialize to dictionary."""
|
||||||
return {
|
return {
|
||||||
'name': self.name,
|
'name': self.name,
|
||||||
'version': self.version,
|
'version': self.version,
|
||||||
|
# Weapon
|
||||||
'weapon_cost_per_shot': str(self.weapon_cost_per_shot),
|
'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_name': self.weapon_name,
|
||||||
'weapon_damage': str(self.weapon_damage),
|
'weapon_damage': str(self.weapon_damage),
|
||||||
'weapon_decay_pec': str(self.weapon_decay_pec),
|
'weapon_decay_pec': str(self.weapon_decay_pec),
|
||||||
'weapon_ammo_pec': str(self.weapon_ammo_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_name': self.armor_name,
|
||||||
'armor_decay_pec': str(self.armor_decay_pec),
|
'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_name': self.healing_name,
|
||||||
'healing_decay_pec': str(self.healing_decay_pec),
|
'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,
|
'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
|
@classmethod
|
||||||
|
|
@ -89,13 +121,15 @@ class LoadoutConfig:
|
||||||
version = data.get('version', 1)
|
version = data.get('version', 1)
|
||||||
|
|
||||||
if version == 1:
|
if version == 1:
|
||||||
return cls._from_legacy(data)
|
return cls._from_legacy_v1(data)
|
||||||
else:
|
elif version == 2:
|
||||||
return cls._from_v2(data)
|
return cls._from_v2(data)
|
||||||
|
else:
|
||||||
|
return cls._from_v3(data)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_v2(cls, data: dict) -> "LoadoutConfig":
|
def _from_v3(cls, data: dict) -> "LoadoutConfig":
|
||||||
"""Parse version 2 (current) format."""
|
"""Parse version 3 (current) format."""
|
||||||
def get_decimal(key: str, default: str = "0") -> Decimal:
|
def get_decimal(key: str, default: str = "0") -> Decimal:
|
||||||
try:
|
try:
|
||||||
return Decimal(str(data.get(key, default)))
|
return Decimal(str(data.get(key, default)))
|
||||||
|
|
@ -104,26 +138,72 @@ class LoadoutConfig:
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
name=data.get('name', 'Unnamed'),
|
name=data.get('name', 'Unnamed'),
|
||||||
version=2,
|
version=3,
|
||||||
|
# Weapon
|
||||||
weapon_cost_per_shot=get_decimal('weapon_cost_per_shot'),
|
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_name=data.get('weapon_name', 'None'),
|
||||||
weapon_damage=get_decimal('weapon_damage'),
|
weapon_damage=get_decimal('weapon_damage'),
|
||||||
weapon_decay_pec=get_decimal('weapon_decay_pec'),
|
weapon_decay_pec=get_decimal('weapon_decay_pec'),
|
||||||
weapon_ammo_pec=get_decimal('weapon_ammo_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_name=data.get('armor_name', 'None'),
|
||||||
armor_decay_pec=get_decimal('armor_decay_pec'),
|
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_name=data.get('healing_name', 'None'),
|
||||||
healing_decay_pec=get_decimal('healing_decay_pec'),
|
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'),
|
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'),
|
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'),
|
healing_api_id=data.get('healing_api_id'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_legacy(cls, data: dict) -> "LoadoutConfig":
|
def _from_legacy_v1(cls, data: dict) -> "LoadoutConfig":
|
||||||
"""Convert legacy format to new simple format."""
|
"""Convert legacy format to new format."""
|
||||||
def get_decimal(key: str, default: str = "0") -> Decimal:
|
def get_decimal(key: str, default: str = "0") -> Decimal:
|
||||||
try:
|
try:
|
||||||
return Decimal(str(data.get(key, default)))
|
return Decimal(str(data.get(key, default)))
|
||||||
|
|
@ -143,62 +223,78 @@ class LoadoutConfig:
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
name=data.get('name', 'Unnamed'),
|
name=data.get('name', 'Unnamed'),
|
||||||
version=2,
|
version=3,
|
||||||
weapon_cost_per_shot=weapon_cost_per_shot,
|
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_name=data.get('weapon_name', data.get('weapon', 'None')),
|
||||||
weapon_damage=get_decimal('weapon_damage'),
|
weapon_damage=get_decimal('weapon_damage'),
|
||||||
weapon_decay_pec=weapon_decay,
|
weapon_decay_pec=weapon_decay,
|
||||||
weapon_ammo_pec=weapon_ammo,
|
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_name=data.get('armor_set_name', data.get('armor_name', 'None')),
|
||||||
armor_decay_pec=armor_decay,
|
armor_decay_pec=armor_decay,
|
||||||
|
healing_cost_per_heal=healing_cost_per_heal,
|
||||||
healing_name=data.get('heal_name', 'None'),
|
healing_name=data.get('heal_name', 'None'),
|
||||||
healing_decay_pec=heal_decay,
|
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]:
|
def get_summary(self) -> Dict[str, Any]:
|
||||||
"""Get cost summary for display."""
|
"""Get cost summary for display."""
|
||||||
return {
|
return {
|
||||||
'name': self.name,
|
'name': self.name,
|
||||||
'weapon': self.weapon_name,
|
'weapon': self.weapon_name,
|
||||||
|
'weapon_amp': self.weapon_amp_name if self.weapon_amp_id else "None",
|
||||||
'armor': self.armor_name,
|
'armor': self.armor_name,
|
||||||
|
'plating': self.plating_name if self.plating_id else "None",
|
||||||
'healing': self.healing_name,
|
'healing': self.healing_name,
|
||||||
'cost_per_shot': self.weapon_cost_per_shot,
|
'mindforce': self.mindforce_implant_name if self.mindforce_implant_id else "None",
|
||||||
'cost_per_hit': self.armor_cost_per_hit,
|
'cost_per_shot': self.get_total_weapon_cost_per_shot(),
|
||||||
'cost_per_heal': self.healing_cost_per_heal,
|
'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):
|
class LoadoutManagerDialog(QDialog):
|
||||||
"""Simplified loadout manager focused on cost configuration."""
|
"""Full-featured loadout manager with all gear types."""
|
||||||
|
|
||||||
loadout_saved = pyqtSignal(LoadoutConfig)
|
loadout_saved = pyqtSignal(LoadoutConfig)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle("Loadout Manager")
|
self.setWindowTitle("Loadout Manager - Full Gear Configuration")
|
||||||
self.setMinimumSize(600, 500)
|
self.setMinimumSize(800, 700)
|
||||||
|
|
||||||
# State
|
# State
|
||||||
self.config_dir = Path.home() / ".lemontropia" / "loadouts"
|
self.config_dir = Path.home() / ".lemontropia" / "loadouts"
|
||||||
self.config_dir.mkdir(parents=True, exist_ok=True)
|
self.config_dir.mkdir(parents=True, exist_ok=True)
|
||||||
self.current_config: Optional[LoadoutConfig] = None
|
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._setup_ui()
|
||||||
self._load_saved_loadouts()
|
self._load_saved_loadouts()
|
||||||
|
|
||||||
def _setup_ui(self):
|
def _setup_ui(self):
|
||||||
"""Setup simplified UI."""
|
"""Setup full gear UI."""
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
layout.setSpacing(10)
|
layout.setSpacing(10)
|
||||||
|
|
||||||
|
|
@ -206,7 +302,7 @@ class LoadoutManagerDialog(QDialog):
|
||||||
name_layout = QHBoxLayout()
|
name_layout = QHBoxLayout()
|
||||||
name_layout.addWidget(QLabel("Loadout Name:"))
|
name_layout.addWidget(QLabel("Loadout Name:"))
|
||||||
self.name_edit = QLineEdit()
|
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)
|
name_layout.addWidget(self.name_edit)
|
||||||
layout.addLayout(name_layout)
|
layout.addLayout(name_layout)
|
||||||
|
|
||||||
|
|
@ -235,13 +331,16 @@ class LoadoutManagerDialog(QDialog):
|
||||||
|
|
||||||
splitter.addWidget(left_widget)
|
splitter.addWidget(left_widget)
|
||||||
|
|
||||||
# Right: Configuration
|
# Right: Configuration (in a scroll area)
|
||||||
|
scroll = QScrollArea()
|
||||||
|
scroll.setWidgetResizable(True)
|
||||||
right_widget = QWidget()
|
right_widget = QWidget()
|
||||||
right_layout = QVBoxLayout(right_widget)
|
right_layout = QVBoxLayout(right_widget)
|
||||||
right_layout.setContentsMargins(0, 0, 0, 0)
|
right_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
right_layout.setSpacing(10)
|
||||||
|
|
||||||
# -- Weapon Section --
|
# -- Weapon Section --
|
||||||
weapon_group = QGroupBox("⚔️ Weapon")
|
weapon_group = QGroupBox("⚔️ Weapon & Amplifier")
|
||||||
weapon_layout = QFormLayout(weapon_group)
|
weapon_layout = QFormLayout(weapon_group)
|
||||||
|
|
||||||
self.weapon_btn = QPushButton("Select Weapon...")
|
self.weapon_btn = QPushButton("Select Weapon...")
|
||||||
|
|
@ -252,20 +351,31 @@ class LoadoutManagerDialog(QDialog):
|
||||||
self.weapon_info.setStyleSheet("color: #888;")
|
self.weapon_info.setStyleSheet("color: #888;")
|
||||||
weapon_layout.addRow(self.weapon_info)
|
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 = QLabel("0.0000 PED")
|
||||||
self.weapon_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F;")
|
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)
|
right_layout.addWidget(weapon_group)
|
||||||
|
|
||||||
# -- Armor Section --
|
# -- Armor Section --
|
||||||
armor_group = QGroupBox("🛡️ Armor")
|
armor_group = QGroupBox("🛡️ Armor & Plating")
|
||||||
armor_layout = QFormLayout(armor_group)
|
armor_layout = QFormLayout(armor_group)
|
||||||
|
|
||||||
self.armor_btn = QPushButton("Select Armor...")
|
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 = QLabel("0.0000 PED")
|
||||||
self.armor_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F;")
|
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)
|
right_layout.addWidget(armor_group)
|
||||||
|
|
||||||
# -- Healing Section --
|
# -- Healing Section --
|
||||||
healing_group = QGroupBox("💚 Healing")
|
healing_group = QGroupBox("💚 Healing & Mindforce")
|
||||||
healing_layout = QFormLayout(healing_group)
|
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)
|
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 = QLabel("None selected")
|
||||||
self.healing_info.setStyleSheet("color: #888;")
|
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 = QLabel("0.0000 PED")
|
||||||
self.healing_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F;")
|
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)
|
right_layout.addWidget(healing_group)
|
||||||
|
|
||||||
# -- Summary Section --
|
# -- Summary Section --
|
||||||
summary_group = QGroupBox("💰 Session Cost Summary")
|
summary_group = QGroupBox("💰 Total Session Cost Summary")
|
||||||
summary_layout = QGridLayout(summary_group)
|
summary_layout = QGridLayout(summary_group)
|
||||||
|
|
||||||
summary_layout.addWidget(QLabel("Cost per Shot:"), 0, 0)
|
summary_layout.addWidget(QLabel("Cost per Shot:"), 0, 0)
|
||||||
self.summary_shot = QLabel("0.0000 PED")
|
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(self.summary_shot, 0, 1)
|
||||||
|
|
||||||
summary_layout.addWidget(QLabel("Cost per Hit:"), 1, 0)
|
summary_layout.addWidget(QLabel("Cost per Hit:"), 1, 0)
|
||||||
self.summary_hit = QLabel("0.0000 PED")
|
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(self.summary_hit, 1, 1)
|
||||||
|
|
||||||
summary_layout.addWidget(QLabel("Cost per Heal:"), 2, 0)
|
summary_layout.addWidget(QLabel("Cost per Heal:"), 2, 0)
|
||||||
self.summary_heal = QLabel("0.0000 PED")
|
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.addWidget(self.summary_heal, 2, 1)
|
||||||
|
|
||||||
summary_layout.setColumnStretch(1, 1)
|
summary_layout.setColumnStretch(1, 1)
|
||||||
|
|
@ -321,8 +468,10 @@ class LoadoutManagerDialog(QDialog):
|
||||||
|
|
||||||
right_layout.addStretch()
|
right_layout.addStretch()
|
||||||
|
|
||||||
splitter.addWidget(right_widget)
|
scroll.setWidget(right_widget)
|
||||||
splitter.setSizes([200, 400])
|
splitter.addWidget(scroll)
|
||||||
|
|
||||||
|
splitter.setSizes([250, 550])
|
||||||
|
|
||||||
layout.addWidget(splitter)
|
layout.addWidget(splitter)
|
||||||
|
|
||||||
|
|
@ -371,8 +520,6 @@ class LoadoutManagerDialog(QDialog):
|
||||||
# Update UI
|
# Update UI
|
||||||
self.weapon_btn.setText(weapon.name[:30])
|
self.weapon_btn.setText(weapon.name[:30])
|
||||||
self.weapon_info.setText(f"Damage: {weapon.damage} | Range: {weapon.range_val}")
|
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")
|
self.weapon_cost_label.setText(f"{cost_per_shot:.4f} PED")
|
||||||
|
|
||||||
# Store for saving
|
# Store for saving
|
||||||
|
|
@ -385,8 +532,54 @@ class LoadoutManagerDialog(QDialog):
|
||||||
'cost_per_shot': cost_per_shot,
|
'cost_per_shot': cost_per_shot,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self._update_weapon_total()
|
||||||
self._update_summary()
|
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):
|
def _select_armor(self):
|
||||||
"""Open simplified armor selector."""
|
"""Open simplified armor selector."""
|
||||||
from ui.armor_selector import ArmorSelectorDialog
|
from ui.armor_selector import ArmorSelectorDialog
|
||||||
|
|
@ -399,7 +592,6 @@ class LoadoutManagerDialog(QDialog):
|
||||||
|
|
||||||
def _set_armor(self, armor_data: dict):
|
def _set_armor(self, armor_data: dict):
|
||||||
"""Set armor and calculate cost."""
|
"""Set armor and calculate cost."""
|
||||||
# armor_data has: name, decay_pec, protection_summary
|
|
||||||
name = armor_data.get('name', 'Unknown')
|
name = armor_data.get('name', 'Unknown')
|
||||||
decay_pec = Decimal(str(armor_data.get('decay_pec', 0)))
|
decay_pec = Decimal(str(armor_data.get('decay_pec', 0)))
|
||||||
cost_per_hit = decay_pec / Decimal("100")
|
cost_per_hit = decay_pec / Decimal("100")
|
||||||
|
|
@ -418,8 +610,54 @@ class LoadoutManagerDialog(QDialog):
|
||||||
'cost_per_hit': cost_per_hit,
|
'cost_per_hit': cost_per_hit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self._update_armor_total()
|
||||||
self._update_summary()
|
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):
|
def _select_healing(self):
|
||||||
"""Open healing selector."""
|
"""Open healing selector."""
|
||||||
from ui.healing_selector import HealingSelectorDialog
|
from ui.healing_selector import HealingSelectorDialog
|
||||||
|
|
@ -451,13 +689,66 @@ class LoadoutManagerDialog(QDialog):
|
||||||
'cost_per_heal': cost_per_heal,
|
'cost_per_heal': cost_per_heal,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self._update_healing_total()
|
||||||
self._update_summary()
|
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):
|
def _update_summary(self):
|
||||||
"""Update cost summary display."""
|
"""Update cost summary display."""
|
||||||
shot = getattr(self, '_pending_weapon', {}).get('cost_per_shot', Decimal("0"))
|
weapon = getattr(self, '_pending_weapon', {})
|
||||||
hit = getattr(self, '_pending_armor', {}).get('cost_per_hit', Decimal("0"))
|
amp = getattr(self, '_pending_amplifier', {})
|
||||||
heal = getattr(self, '_pending_healing', {}).get('cost_per_heal', Decimal("0"))
|
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_shot.setText(f"{shot:.4f} PED")
|
||||||
self.summary_hit.setText(f"{hit:.4f} PED")
|
self.summary_hit.setText(f"{hit:.4f} PED")
|
||||||
|
|
@ -472,25 +763,46 @@ class LoadoutManagerDialog(QDialog):
|
||||||
|
|
||||||
# Build config from pending data
|
# Build config from pending data
|
||||||
weapon = getattr(self, '_pending_weapon', {})
|
weapon = getattr(self, '_pending_weapon', {})
|
||||||
|
amp = getattr(self, '_pending_amplifier', {})
|
||||||
armor = getattr(self, '_pending_armor', {})
|
armor = getattr(self, '_pending_armor', {})
|
||||||
|
plating = getattr(self, '_pending_plating', {})
|
||||||
healing = getattr(self, '_pending_healing', {})
|
healing = getattr(self, '_pending_healing', {})
|
||||||
|
mindforce = getattr(self, '_pending_mindforce', {})
|
||||||
|
|
||||||
config = LoadoutConfig(
|
config = LoadoutConfig(
|
||||||
name=name,
|
name=name,
|
||||||
|
# Weapon
|
||||||
weapon_cost_per_shot=weapon.get('cost_per_shot', Decimal("0")),
|
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_name=weapon.get('name', 'None'),
|
||||||
weapon_damage=weapon.get('damage', Decimal("0")),
|
weapon_damage=weapon.get('damage', Decimal("0")),
|
||||||
weapon_decay_pec=weapon.get('decay_pec', Decimal("0")),
|
weapon_decay_pec=weapon.get('decay_pec', Decimal("0")),
|
||||||
weapon_ammo_pec=weapon.get('ammo_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_name=armor.get('name', 'None'),
|
||||||
armor_decay_pec=armor.get('decay_pec', Decimal("0")),
|
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_name=healing.get('name', 'None'),
|
||||||
healing_decay_pec=healing.get('decay_pec', Decimal("0")),
|
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'),
|
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
|
# Save to file
|
||||||
|
|
@ -529,10 +841,14 @@ class LoadoutManagerDialog(QDialog):
|
||||||
# Tooltip with costs
|
# Tooltip with costs
|
||||||
tooltip = (
|
tooltip = (
|
||||||
f"Weapon: {config.weapon_name}\n"
|
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"Armor: {config.armor_name}\n"
|
||||||
f"Cost/Shot: {config.weapon_cost_per_shot:.4f} PED\n"
|
f" + Plating: {config.plating_name if config.plating_id else 'None'}\n"
|
||||||
f"Cost/Hit: {config.armor_cost_per_hit:.4f} PED\n"
|
f"Healing: {config.healing_name}\n"
|
||||||
f"Cost/Heal: {config.healing_cost_per_heal:.4f} PED"
|
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)
|
item.setToolTip(tooltip)
|
||||||
self.saved_list.addItem(item)
|
self.saved_list.addItem(item)
|
||||||
|
|
@ -569,8 +885,6 @@ class LoadoutManagerDialog(QDialog):
|
||||||
if config.weapon_name != "None":
|
if config.weapon_name != "None":
|
||||||
self.weapon_btn.setText(config.weapon_name[:30])
|
self.weapon_btn.setText(config.weapon_name[:30])
|
||||||
self.weapon_info.setText(f"Damage: {config.weapon_damage}")
|
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.weapon_cost_label.setText(f"{config.weapon_cost_per_shot:.4f} PED")
|
||||||
self._pending_weapon = {
|
self._pending_weapon = {
|
||||||
'name': config.weapon_name,
|
'name': config.weapon_name,
|
||||||
|
|
@ -581,6 +895,20 @@ class LoadoutManagerDialog(QDialog):
|
||||||
'cost_per_shot': config.weapon_cost_per_shot,
|
'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
|
# Armor
|
||||||
if config.armor_name != "None":
|
if config.armor_name != "None":
|
||||||
self.armor_btn.setText(config.armor_name[:30])
|
self.armor_btn.setText(config.armor_name[:30])
|
||||||
|
|
@ -593,6 +921,20 @@ class LoadoutManagerDialog(QDialog):
|
||||||
'cost_per_hit': config.armor_cost_per_hit,
|
'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
|
# Healing
|
||||||
if config.healing_name != "None":
|
if config.healing_name != "None":
|
||||||
self.healing_btn.setText(config.healing_name[:30])
|
self.healing_btn.setText(config.healing_name[:30])
|
||||||
|
|
@ -604,29 +946,64 @@ class LoadoutManagerDialog(QDialog):
|
||||||
'cost_per_heal': config.healing_cost_per_heal,
|
'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._update_summary()
|
||||||
self.current_config = config
|
self.current_config = config
|
||||||
|
|
||||||
def _new_loadout(self):
|
def _new_loadout(self):
|
||||||
"""Clear all fields for new loadout."""
|
"""Clear all fields for new loadout."""
|
||||||
self.name_edit.clear()
|
self.name_edit.clear()
|
||||||
|
|
||||||
|
# Reset weapon section
|
||||||
self.weapon_btn.setText("Select Weapon...")
|
self.weapon_btn.setText("Select Weapon...")
|
||||||
self.weapon_info.setText("None selected")
|
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.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_btn.setText("Select Armor...")
|
||||||
self.armor_info.setText("None selected")
|
self.armor_info.setText("None selected")
|
||||||
self.armor_cost_label.setText("0.0000 PED")
|
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_btn.setText("Select Healing...")
|
||||||
self.healing_info.setText("None selected")
|
self.healing_info.setText("None selected")
|
||||||
self.healing_cost_label.setText("0.0000 PED")
|
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_weapon = None
|
||||||
|
self._pending_amplifier = None
|
||||||
self._pending_armor = None
|
self._pending_armor = None
|
||||||
|
self._pending_plating = None
|
||||||
self._pending_healing = None
|
self._pending_healing = None
|
||||||
|
self._pending_mindforce = None
|
||||||
|
|
||||||
self._update_summary()
|
self._update_summary()
|
||||||
self.current_config = None
|
self.current_config = None
|
||||||
|
|
||||||
|
|
|
||||||
1766
ui/main_window.py
1766
ui/main_window.py
File diff suppressed because it is too large
Load Diff
|
|
@ -1,260 +1,171 @@
|
||||||
"""
|
"""
|
||||||
Mindforce Implant Selector for Lemontropia Suite
|
Mindforce Implant/Chip Selector Dialog
|
||||||
Browse and select mindforce implants from Entropia Nexus API
|
Uses /medicalchips API endpoint for healing chips
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from decimal import Decimal
|
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QDialog, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton,
|
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||||
QTreeWidget, QTreeWidgetItem, QHeaderView, QLabel, QDialogButtonBox,
|
QListWidget, QListWidgetItem, QLineEdit, QGroupBox,
|
||||||
QProgressBar, QGroupBox, QFormLayout, QComboBox
|
QFormLayout, QMessageBox
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt, QThread, pyqtSignal
|
from PyQt6.QtCore import Qt
|
||||||
from PyQt6.QtGui import QColor
|
from decimal import Decimal
|
||||||
from typing import Optional, List
|
|
||||||
|
|
||||||
from core.nexus_full_api import get_nexus_api, NexusMindforceImplant
|
from core.nexus_full_api import get_nexus_api
|
||||||
|
|
||||||
|
|
||||||
class MindforceImplantLoaderThread(QThread):
|
class MindforceSelectorDialog(QDialog):
|
||||||
"""Background thread for loading mindforce implants from API."""
|
"""Dialog for selecting mindforce implants (healing chips)."""
|
||||||
implants_loaded = pyqtSignal(list)
|
|
||||||
error_occurred = pyqtSignal(str)
|
|
||||||
|
|
||||||
def run(self):
|
def __init__(self, parent=None):
|
||||||
try:
|
|
||||||
api = get_nexus_api()
|
|
||||||
implants = api.get_all_mindforce_implants()
|
|
||||||
self.implants_loaded.emit(implants)
|
|
||||||
except Exception as e:
|
|
||||||
self.error_occurred.emit(str(e))
|
|
||||||
|
|
||||||
|
|
||||||
class MindforceImplantSelectorDialog(QDialog):
|
|
||||||
"""Dialog for selecting mindforce implants from Entropia Nexus API."""
|
|
||||||
|
|
||||||
implant_selected = pyqtSignal(NexusMindforceImplant)
|
|
||||||
|
|
||||||
def __init__(self, parent=None, implant_type: str = ""):
|
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.preferred_type = implant_type.lower()
|
self.setWindowTitle("Select Mindforce Healing Chip")
|
||||||
|
self.setMinimumSize(600, 500)
|
||||||
|
|
||||||
type_names = {
|
self._selected_chip = None
|
||||||
"healing": "Healing Chip",
|
self._chips_cache = []
|
||||||
"damage": "Damage Chip",
|
|
||||||
"utility": "Utility Chip"
|
|
||||||
}
|
|
||||||
title_type = type_names.get(self.preferred_type, "Mindforce Implant")
|
|
||||||
|
|
||||||
self.setWindowTitle(f"Select {title_type} - Entropia Nexus")
|
|
||||||
self.setMinimumSize(800, 500)
|
|
||||||
|
|
||||||
self.all_implants: List[NexusMindforceImplant] = []
|
|
||||||
self.selected_implant: Optional[NexusMindforceImplant] = None
|
|
||||||
|
|
||||||
self._setup_ui()
|
self._setup_ui()
|
||||||
self._load_data()
|
self._load_chips()
|
||||||
|
|
||||||
def _setup_ui(self):
|
def _setup_ui(self):
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
layout.setSpacing(10)
|
|
||||||
|
|
||||||
# Status
|
# Search bar
|
||||||
self.status_label = QLabel("Loading mindforce implants from Entropia Nexus...")
|
search_layout = QHBoxLayout()
|
||||||
layout.addWidget(self.status_label)
|
search_layout.addWidget(QLabel("Search:"))
|
||||||
|
self.search_edit = QLineEdit()
|
||||||
|
self.search_edit.setPlaceholderText("Type to search chips...")
|
||||||
|
self.search_edit.textChanged.connect(self._on_search)
|
||||||
|
search_layout.addWidget(self.search_edit)
|
||||||
|
layout.addLayout(search_layout)
|
||||||
|
|
||||||
self.progress = QProgressBar()
|
# Chip list
|
||||||
self.progress.setRange(0, 0)
|
self.list_widget = QListWidget()
|
||||||
layout.addWidget(self.progress)
|
self.list_widget.itemClicked.connect(self._on_item_selected)
|
||||||
|
self.list_widget.itemDoubleClicked.connect(self.accept)
|
||||||
|
layout.addWidget(self.list_widget)
|
||||||
|
|
||||||
# Filters
|
# Info panel
|
||||||
filter_layout = QHBoxLayout()
|
info_group = QGroupBox("Chip Info")
|
||||||
|
info_layout = QFormLayout(info_group)
|
||||||
|
|
||||||
filter_layout.addWidget(QLabel("Type:"))
|
self.info_name = QLabel("Select a chip")
|
||||||
self.type_combo = QComboBox()
|
self.info_heal = QLabel("-")
|
||||||
self.type_combo.addItems(["All", "Healing", "Damage", "Utility"])
|
self.info_decay = QLabel("-")
|
||||||
if self.preferred_type:
|
self.info_uses = QLabel("-")
|
||||||
type_map = {"healing": 1, "damage": 2, "utility": 3}
|
self.info_mindforce_level = QLabel("-")
|
||||||
self.type_combo.setCurrentIndex(type_map.get(self.preferred_type, 0))
|
self.info_cost = QLabel("-")
|
||||||
self.type_combo.currentTextChanged.connect(self._filter_implants)
|
|
||||||
filter_layout.addWidget(self.type_combo)
|
|
||||||
|
|
||||||
filter_layout.addWidget(QLabel("Search:"))
|
info_layout.addRow("Name:", self.info_name)
|
||||||
self.search_input = QLineEdit()
|
info_layout.addRow("Heal Amount:", self.info_heal)
|
||||||
self.search_input.setPlaceholderText("Search implants...")
|
info_layout.addRow("Decay (PEC):", self.info_decay)
|
||||||
self.search_input.textChanged.connect(self._filter_implants)
|
info_layout.addRow("Uses/Min:", self.info_uses)
|
||||||
filter_layout.addWidget(self.search_input)
|
info_layout.addRow("Mindforce Level:", self.info_mindforce_level)
|
||||||
|
info_layout.addRow("Cost/Heal:", self.info_cost)
|
||||||
|
|
||||||
clear_btn = QPushButton("Clear")
|
layout.addWidget(info_group)
|
||||||
clear_btn.clicked.connect(self.search_input.clear)
|
|
||||||
filter_layout.addWidget(clear_btn)
|
|
||||||
|
|
||||||
layout.addLayout(filter_layout)
|
|
||||||
|
|
||||||
# Results tree
|
|
||||||
self.results_tree = QTreeWidget()
|
|
||||||
self.results_tree.setHeaderLabels([
|
|
||||||
"Name", "Type", "Chip Type", "Decay (PEC)", "Prof. Level", "Limited"
|
|
||||||
])
|
|
||||||
header = self.results_tree.header()
|
|
||||||
header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
|
||||||
self.results_tree.itemSelectionChanged.connect(self._on_selection_changed)
|
|
||||||
self.results_tree.itemDoubleClicked.connect(self._on_double_click)
|
|
||||||
layout.addWidget(self.results_tree)
|
|
||||||
|
|
||||||
# Preview panel
|
|
||||||
self.preview_group = QGroupBox("Implant Preview")
|
|
||||||
preview_layout = QFormLayout(self.preview_group)
|
|
||||||
self.preview_name = QLabel("-")
|
|
||||||
self.preview_type = QLabel("-")
|
|
||||||
self.preview_decay = QLabel("-")
|
|
||||||
preview_layout.addRow("Name:", self.preview_name)
|
|
||||||
preview_layout.addRow("Type:", self.preview_type)
|
|
||||||
preview_layout.addRow("Decay/Use:", self.preview_decay)
|
|
||||||
layout.addWidget(self.preview_group)
|
|
||||||
|
|
||||||
# Buttons
|
# Buttons
|
||||||
buttons = QDialogButtonBox(
|
button_layout = QHBoxLayout()
|
||||||
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
button_layout.addStretch()
|
||||||
)
|
|
||||||
buttons.accepted.connect(self._on_accept)
|
|
||||||
buttons.rejected.connect(self.reject)
|
|
||||||
self.ok_button = buttons.button(QDialogButtonBox.StandardButton.Ok)
|
|
||||||
self.ok_button.setEnabled(False)
|
|
||||||
layout.addWidget(buttons)
|
|
||||||
|
|
||||||
def _load_data(self):
|
|
||||||
"""Load implants in background thread."""
|
|
||||||
self.loader = MindforceImplantLoaderThread()
|
|
||||||
self.loader.implants_loaded.connect(self._on_implants_loaded)
|
|
||||||
self.loader.error_occurred.connect(self._on_load_error)
|
|
||||||
self.loader.start()
|
|
||||||
|
|
||||||
def _on_implants_loaded(self, implants: List[NexusMindforceImplant]):
|
|
||||||
"""Handle loaded implants."""
|
|
||||||
self.all_implants = implants
|
|
||||||
self._filter_implants()
|
|
||||||
self.status_label.setText(f"Loaded {len(implants)} mindforce implants")
|
|
||||||
self.progress.setRange(0, 100)
|
|
||||||
self.progress.setValue(100)
|
|
||||||
|
|
||||||
def _on_load_error(self, error: str):
|
|
||||||
"""Handle load error."""
|
|
||||||
self.status_label.setText(f"Error loading implants: {error}")
|
|
||||||
self.progress.setRange(0, 100)
|
|
||||||
self.progress.setValue(0)
|
|
||||||
|
|
||||||
def _populate_results(self, implants: List[NexusMindforceImplant]):
|
|
||||||
"""Populate results tree."""
|
|
||||||
self.results_tree.clear()
|
|
||||||
|
|
||||||
if not implants:
|
self.select_btn = QPushButton("Select")
|
||||||
item = QTreeWidgetItem()
|
self.select_btn.clicked.connect(self.accept)
|
||||||
item.setText(0, "No implants available")
|
self.select_btn.setEnabled(False)
|
||||||
item.setForeground(0, QColor("#888888"))
|
button_layout.addWidget(self.select_btn)
|
||||||
self.results_tree.addTopLevelItem(item)
|
|
||||||
|
cancel_btn = QPushButton("Cancel")
|
||||||
|
cancel_btn.clicked.connect(self.reject)
|
||||||
|
button_layout.addWidget(cancel_btn)
|
||||||
|
|
||||||
|
layout.addLayout(button_layout)
|
||||||
|
|
||||||
|
def _load_chips(self):
|
||||||
|
"""Load healing chips from API."""
|
||||||
|
try:
|
||||||
|
api = get_nexus_api()
|
||||||
|
# Medical chips are healing-focused mindforce implants
|
||||||
|
self._chips_cache = api.get_all_healing_chips()
|
||||||
|
self._populate_list(self._chips_cache)
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.warning(self, "Error", f"Failed to load chips: {e}")
|
||||||
|
|
||||||
|
def _populate_list(self, chips):
|
||||||
|
"""Populate the list widget."""
|
||||||
|
self.list_widget.clear()
|
||||||
|
|
||||||
|
for chip in chips:
|
||||||
|
# Get heal amount (average of min/max)
|
||||||
|
heal_amount = getattr(chip, 'heal_amount', Decimal("0"))
|
||||||
|
decay = getattr(chip, 'decay', Decimal("0"))
|
||||||
|
|
||||||
|
item_text = f"{chip.name} | Heal: {heal_amount}"
|
||||||
|
if decay > 0:
|
||||||
|
item_text += f" | Decay: {decay} PEC"
|
||||||
|
|
||||||
|
item = QListWidgetItem(item_text)
|
||||||
|
item.setData(Qt.ItemDataRole.UserRole, chip)
|
||||||
|
|
||||||
|
# Tooltip
|
||||||
|
tooltip = f"Name: {chip.name}"
|
||||||
|
if hasattr(chip, 'heal_amount'):
|
||||||
|
tooltip += f"\nHeal: {chip.heal_amount} HP"
|
||||||
|
if decay > 0:
|
||||||
|
tooltip += f"\nDecay: {decay} PEC"
|
||||||
|
if hasattr(chip, 'uses_per_minute') and chip.uses_per_minute:
|
||||||
|
tooltip += f"\nUses/Min: {chip.uses_per_minute}"
|
||||||
|
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._chips_cache)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Sort by decay (lower is better economy)
|
filtered = [chip for chip in self._chips_cache
|
||||||
implants = sorted(implants, key=lambda i: i.decay)
|
if text.lower() in chip.name.lower()]
|
||||||
|
self._populate_list(filtered)
|
||||||
|
|
||||||
|
def _on_item_selected(self, item):
|
||||||
|
"""Handle item selection."""
|
||||||
|
chip = item.data(Qt.ItemDataRole.UserRole)
|
||||||
|
if not chip:
|
||||||
|
return
|
||||||
|
|
||||||
for implant in implants:
|
# Extract chip data
|
||||||
item = QTreeWidgetItem()
|
heal_amount = getattr(chip, 'heal_amount', Decimal("0"))
|
||||||
item.setText(0, implant.name)
|
decay = Decimal(str(getattr(chip, 'decay', 0)))
|
||||||
item.setText(1, implant.implant_type.title())
|
uses_per_min = getattr(chip, 'uses_per_minute', None)
|
||||||
item.setText(2, implant.chip_type)
|
|
||||||
item.setText(3, f"{implant.decay:.2f}")
|
|
||||||
item.setText(4, str(implant.profession_level) if implant.profession_level > 0 else "-")
|
|
||||||
item.setText(5, "Yes" if implant.is_limited else "No")
|
|
||||||
|
|
||||||
# Color limited items
|
|
||||||
if implant.is_limited:
|
|
||||||
item.setForeground(5, QColor("#ff9800"))
|
|
||||||
|
|
||||||
# Color by type
|
|
||||||
if implant.implant_type == "healing":
|
|
||||||
item.setForeground(1, QColor("#4caf50"))
|
|
||||||
elif implant.implant_type == "damage":
|
|
||||||
item.setForeground(1, QColor("#f44336"))
|
|
||||||
|
|
||||||
item.setData(0, Qt.ItemDataRole.UserRole, implant)
|
|
||||||
self.results_tree.addTopLevelItem(item)
|
|
||||||
|
|
||||||
def _filter_implants(self):
|
|
||||||
"""Filter implants based on search and type."""
|
|
||||||
type_filter = self.type_combo.currentText().lower()
|
|
||||||
search = self.search_input.text().lower()
|
|
||||||
|
|
||||||
filtered = self.all_implants
|
self._selected_chip = {
|
||||||
|
'name': chip.name,
|
||||||
|
'api_id': chip.id,
|
||||||
|
'decay_pec': decay,
|
||||||
|
'heal_amount': heal_amount,
|
||||||
|
'uses_per_minute': uses_per_min,
|
||||||
|
}
|
||||||
|
|
||||||
# Filter by type
|
# Update info panel
|
||||||
if type_filter != "all":
|
self.info_name.setText(chip.name)
|
||||||
filtered = [i for i in filtered if i.implant_type == type_filter]
|
self.info_heal.setText(f"{heal_amount} HP")
|
||||||
|
self.info_decay.setText(f"{decay} PEC")
|
||||||
|
self.info_uses.setText(str(uses_per_min) if uses_per_min else "N/A")
|
||||||
|
|
||||||
# Filter by search
|
# Mindforce level if available
|
||||||
if search:
|
mindforce_level = "N/A"
|
||||||
filtered = [
|
if hasattr(chip, 'profession_level') and chip.profession_level:
|
||||||
i for i in filtered
|
mindforce_level = str(chip.profession_level)
|
||||||
if search in i.name.lower()
|
self.info_mindforce_level.setText(mindforce_level)
|
||||||
or search in i.chip_type.lower()
|
|
||||||
]
|
|
||||||
|
|
||||||
self._populate_results(filtered)
|
# Calculate cost per heal
|
||||||
|
cost_per_heal = decay / Decimal("100")
|
||||||
def _on_selection_changed(self):
|
self.info_cost.setText(f"{cost_per_heal:.4f} PED")
|
||||||
"""Handle selection change."""
|
|
||||||
items = self.results_tree.selectedItems()
|
|
||||||
if items:
|
|
||||||
self.selected_implant = items[0].data(0, Qt.ItemDataRole.UserRole)
|
|
||||||
self._update_preview(self.selected_implant)
|
|
||||||
self.ok_button.setEnabled(True)
|
|
||||||
else:
|
|
||||||
self.selected_implant = None
|
|
||||||
self.ok_button.setEnabled(False)
|
|
||||||
|
|
||||||
def _update_preview(self, implant: NexusMindforceImplant):
|
|
||||||
"""Update preview panel."""
|
|
||||||
self.preview_name.setText(implant.name)
|
|
||||||
self.preview_type.setText(f"{implant.implant_type.title()} ({implant.chip_type})")
|
|
||||||
self.preview_decay.setText(f"{implant.decay:.4f} PEC per use")
|
|
||||||
|
|
||||||
# Color by type
|
self.select_btn.setEnabled(True)
|
||||||
if implant.implant_type == "healing":
|
|
||||||
self.preview_type.setStyleSheet("color: #4caf50;")
|
|
||||||
elif implant.implant_type == "damage":
|
|
||||||
self.preview_type.setStyleSheet("color: #f44336;")
|
|
||||||
else:
|
|
||||||
self.preview_type.setStyleSheet("color: #4a90d9;")
|
|
||||||
|
|
||||||
def _on_double_click(self, item: QTreeWidgetItem, column: int):
|
def get_selected_chip(self):
|
||||||
"""Handle double click."""
|
"""Get the selected chip data."""
|
||||||
if item.data(0, Qt.ItemDataRole.UserRole):
|
return self._selected_chip
|
||||||
self._on_accept()
|
|
||||||
|
|
||||||
def _on_accept(self):
|
|
||||||
"""Handle OK button."""
|
|
||||||
if self.selected_implant:
|
|
||||||
self.implant_selected.emit(self.selected_implant)
|
|
||||||
self.accept()
|
|
||||||
|
|
||||||
|
|
||||||
# Main entry for testing
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
from PyQt6.QtWidgets import QApplication
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
|
|
||||||
app = QApplication(sys.argv)
|
|
||||||
app.setStyle('Fusion')
|
|
||||||
|
|
||||||
dialog = MindforceImplantSelectorDialog()
|
|
||||||
|
|
||||||
# Connect signal for testing
|
|
||||||
dialog.implant_selected.connect(lambda i: print(f"Selected implant: {i.name}"))
|
|
||||||
|
|
||||||
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
||||||
print("Implant selected!")
|
|
||||||
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,635 @@
|
||||||
|
"""
|
||||||
|
Lemontropia Suite - Setup Wizard
|
||||||
|
First-run wizard for configuring the application.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QWizard, QWizardPage, QVBoxLayout, QHBoxLayout,
|
||||||
|
QLabel, QLineEdit, QPushButton, QFileDialog,
|
||||||
|
QComboBox, QCheckBox, QProgressBar, QGroupBox,
|
||||||
|
QFormLayout, QTextEdit, QMessageBox, QWidget
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import Qt, QSettings, QThread, pyqtSignal
|
||||||
|
from PyQt6.QtGui import QFont, QPixmap
|
||||||
|
|
||||||
|
|
||||||
|
class GearDownloadWorker(QThread):
|
||||||
|
"""Background worker for downloading gear database."""
|
||||||
|
progress = pyqtSignal(int)
|
||||||
|
finished_signal = pyqtSignal(bool, str)
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._cancelled = False
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Download gear database in background."""
|
||||||
|
try:
|
||||||
|
# Simulate download progress
|
||||||
|
import time
|
||||||
|
for i in range(0, 101, 10):
|
||||||
|
if self._cancelled:
|
||||||
|
self.finished_signal.emit(False, "Download cancelled")
|
||||||
|
return
|
||||||
|
self.progress.emit(i)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# TODO: Implement actual gear database download
|
||||||
|
# For now, just create a placeholder
|
||||||
|
self.finished_signal.emit(True, "Gear database downloaded successfully")
|
||||||
|
except Exception as e:
|
||||||
|
self.finished_signal.emit(False, str(e))
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
self._cancelled = True
|
||||||
|
|
||||||
|
|
||||||
|
class WelcomePage(QWizardPage):
|
||||||
|
"""Welcome page of the setup wizard."""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setTitle("Welcome to Lemontropia Suite")
|
||||||
|
self.setSubTitle("Let's get you set up for tracking your Entropia Universe sessions.")
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# Welcome text
|
||||||
|
welcome_label = QLabel(
|
||||||
|
"<h2>Welcome!</h2>"
|
||||||
|
"<p>Lemontropia Suite helps you track your hunting, mining, and crafting sessions "
|
||||||
|
"in Entropia Universe. This wizard will guide you through the initial setup.</p>"
|
||||||
|
"<p>You'll need:</p>"
|
||||||
|
"<ul>"
|
||||||
|
"<li>Your avatar name (for tracking globals)</li>"
|
||||||
|
"<li>Path to your Entropia Universe chat log file</li>"
|
||||||
|
"<li>Preferred default activity type</li>"
|
||||||
|
"</ul>"
|
||||||
|
)
|
||||||
|
welcome_label.setWordWrap(True)
|
||||||
|
layout.addWidget(welcome_label)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
|
||||||
|
# Note about re-running wizard
|
||||||
|
note_label = QLabel(
|
||||||
|
"<i>You can run this wizard again anytime from the Settings menu.</i>"
|
||||||
|
)
|
||||||
|
note_label.setStyleSheet("color: #888;")
|
||||||
|
layout.addWidget(note_label)
|
||||||
|
|
||||||
|
|
||||||
|
class AvatarNamePage(QWizardPage):
|
||||||
|
"""Page for setting the avatar name."""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setTitle("Avatar Name")
|
||||||
|
self.setSubTitle("Enter your avatar name exactly as it appears in Entropia Universe.")
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# Explanation
|
||||||
|
info_label = QLabel(
|
||||||
|
"Your avatar name is used to identify your globals and HoFs in the chat log. "
|
||||||
|
"Make sure to enter it exactly as it appears in-game, including capitalization."
|
||||||
|
)
|
||||||
|
info_label.setWordWrap(True)
|
||||||
|
layout.addWidget(info_label)
|
||||||
|
|
||||||
|
layout.addSpacing(20)
|
||||||
|
|
||||||
|
# Name input
|
||||||
|
form_layout = QFormLayout()
|
||||||
|
self.name_input = QLineEdit()
|
||||||
|
self.name_input.setPlaceholderText("e.g., John Doe Mega")
|
||||||
|
self.name_input.textChanged.connect(self.completeChanged)
|
||||||
|
form_layout.addRow("Avatar Name:", self.name_input)
|
||||||
|
layout.addLayout(form_layout)
|
||||||
|
|
||||||
|
layout.addSpacing(10)
|
||||||
|
|
||||||
|
# Example
|
||||||
|
example_label = QLabel(
|
||||||
|
"<i>Example: If your avatar is named 'Roberth Noname Rajala', "
|
||||||
|
"enter it exactly like that.</i>"
|
||||||
|
)
|
||||||
|
example_label.setStyleSheet("color: #888;")
|
||||||
|
layout.addWidget(example_label)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
|
||||||
|
def isComplete(self) -> bool:
|
||||||
|
return len(self.name_input.text().strip()) > 0
|
||||||
|
|
||||||
|
def get_avatar_name(self) -> str:
|
||||||
|
return self.name_input.text().strip()
|
||||||
|
|
||||||
|
|
||||||
|
class LogPathPage(QWizardPage):
|
||||||
|
"""Page for configuring the log file path."""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setTitle("Chat Log Path")
|
||||||
|
self.setSubTitle("Configure the path to your Entropia Universe chat log file.")
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# Explanation
|
||||||
|
info_label = QLabel(
|
||||||
|
"Lemontropia Suite reads your chat log to track events like loot, globals, "
|
||||||
|
"damage dealt/taken, and healing. Please select your chat.log file."
|
||||||
|
)
|
||||||
|
info_label.setWordWrap(True)
|
||||||
|
layout.addWidget(info_label)
|
||||||
|
|
||||||
|
layout.addSpacing(20)
|
||||||
|
|
||||||
|
# Default path info
|
||||||
|
default_path = self._get_default_log_path()
|
||||||
|
default_label = QLabel(f"<b>Default location:</b><br>{default_path}")
|
||||||
|
default_label.setStyleSheet("color: #4caf50;")
|
||||||
|
default_label.setWordWrap(True)
|
||||||
|
layout.addWidget(default_label)
|
||||||
|
|
||||||
|
layout.addSpacing(10)
|
||||||
|
|
||||||
|
# Path selection
|
||||||
|
path_layout = QHBoxLayout()
|
||||||
|
self.path_input = QLineEdit()
|
||||||
|
self.path_input.setText(default_path)
|
||||||
|
self.path_input.textChanged.connect(self.completeChanged)
|
||||||
|
path_layout.addWidget(self.path_input)
|
||||||
|
|
||||||
|
browse_btn = QPushButton("Browse...")
|
||||||
|
browse_btn.clicked.connect(self.on_browse)
|
||||||
|
path_layout.addWidget(browse_btn)
|
||||||
|
|
||||||
|
layout.addLayout(path_layout)
|
||||||
|
|
||||||
|
layout.addSpacing(10)
|
||||||
|
|
||||||
|
# Auto-detect checkbox
|
||||||
|
self.auto_detect_check = QCheckBox("Automatically detect log path on startup")
|
||||||
|
self.auto_detect_check.setChecked(True)
|
||||||
|
layout.addWidget(self.auto_detect_check)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
|
||||||
|
# Help text
|
||||||
|
help_label = QLabel(
|
||||||
|
"<i>Tip: If you can't find your chat.log, make sure you've run Entropia Universe "
|
||||||
|
"at least once and enabled chat logging in-game.</i>"
|
||||||
|
)
|
||||||
|
help_label.setStyleSheet("color: #888;")
|
||||||
|
layout.addWidget(help_label)
|
||||||
|
|
||||||
|
def _get_default_log_path(self) -> str:
|
||||||
|
"""Get the default chat log path based on OS."""
|
||||||
|
# Default paths for different scenarios
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Check environment variable first
|
||||||
|
env_path = os.getenv('EU_CHAT_LOG_PATH', '')
|
||||||
|
if env_path:
|
||||||
|
return env_path
|
||||||
|
|
||||||
|
# Windows default path
|
||||||
|
appdata = os.getenv('LOCALAPPDATA', '')
|
||||||
|
if appdata:
|
||||||
|
return str(Path(appdata) / "Entropia Universe" / "chat.log")
|
||||||
|
|
||||||
|
# Fallback
|
||||||
|
return str(Path.home() / "Entropia Universe" / "chat.log")
|
||||||
|
|
||||||
|
def on_browse(self):
|
||||||
|
"""Open file browser to select chat log."""
|
||||||
|
file_path, _ = QFileDialog.getOpenFileName(
|
||||||
|
self,
|
||||||
|
"Select Chat Log File",
|
||||||
|
str(Path.home()),
|
||||||
|
"Log Files (*.log);;All Files (*.*)"
|
||||||
|
)
|
||||||
|
if file_path:
|
||||||
|
self.path_input.setText(file_path)
|
||||||
|
|
||||||
|
def isComplete(self) -> bool:
|
||||||
|
return len(self.path_input.text().strip()) > 0
|
||||||
|
|
||||||
|
def get_log_path(self) -> str:
|
||||||
|
return self.path_input.text().strip()
|
||||||
|
|
||||||
|
def get_auto_detect(self) -> bool:
|
||||||
|
return self.auto_detect_check.isChecked()
|
||||||
|
|
||||||
|
|
||||||
|
class ActivityTypePage(QWizardPage):
|
||||||
|
"""Page for selecting default activity type."""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setTitle("Default Activity Type")
|
||||||
|
self.setSubTitle("Choose your preferred default activity type.")
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# Explanation
|
||||||
|
info_label = QLabel(
|
||||||
|
"Select the activity type you do most often. This will be the default "
|
||||||
|
"when starting new sessions. You can always change this later."
|
||||||
|
)
|
||||||
|
info_label.setWordWrap(True)
|
||||||
|
layout.addWidget(info_label)
|
||||||
|
|
||||||
|
layout.addSpacing(20)
|
||||||
|
|
||||||
|
# Activity type selection
|
||||||
|
form_layout = QFormLayout()
|
||||||
|
self.activity_combo = QComboBox()
|
||||||
|
self.activity_combo.addItem("🎯 Hunting", "hunting")
|
||||||
|
self.activity_combo.addItem("⛏️ Mining", "mining")
|
||||||
|
self.activity_combo.addItem("⚒️ Crafting", "crafting")
|
||||||
|
form_layout.addRow("Default Activity:", self.activity_combo)
|
||||||
|
layout.addLayout(form_layout)
|
||||||
|
|
||||||
|
layout.addSpacing(20)
|
||||||
|
|
||||||
|
# Activity descriptions
|
||||||
|
descriptions = QGroupBox("Activity Types")
|
||||||
|
desc_layout = QVBoxLayout(descriptions)
|
||||||
|
|
||||||
|
hunting_desc = QLabel(
|
||||||
|
"<b>🎯 Hunting</b><br>"
|
||||||
|
"Track weapon decay, armor decay, healing costs, loot, globals, and HoFs."
|
||||||
|
)
|
||||||
|
hunting_desc.setWordWrap(True)
|
||||||
|
desc_layout.addWidget(hunting_desc)
|
||||||
|
|
||||||
|
mining_desc = QLabel(
|
||||||
|
"<b>⛏️ Mining</b><br>"
|
||||||
|
"Track finder decay, extractor decay, claim values, and mining runs."
|
||||||
|
)
|
||||||
|
mining_desc.setWordWrap(True)
|
||||||
|
desc_layout.addWidget(mining_desc)
|
||||||
|
|
||||||
|
crafting_desc = QLabel(
|
||||||
|
"<b>⚒️ Crafting</b><br>"
|
||||||
|
"Track blueprint runs, material costs, success rates, and near-successes."
|
||||||
|
)
|
||||||
|
crafting_desc.setWordWrap(True)
|
||||||
|
desc_layout.addWidget(crafting_desc)
|
||||||
|
|
||||||
|
layout.addWidget(descriptions)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
|
||||||
|
def get_activity_type(self) -> str:
|
||||||
|
return self.activity_combo.currentData()
|
||||||
|
|
||||||
|
|
||||||
|
class GearDatabasePage(QWizardPage):
|
||||||
|
"""Page for downloading initial gear database."""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setTitle("Gear Database (Optional)")
|
||||||
|
self.setSubTitle("Download initial gear database for cost tracking.")
|
||||||
|
self.setup_ui()
|
||||||
|
self.worker = None
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# Explanation
|
||||||
|
info_label = QLabel(
|
||||||
|
"You can download an initial gear database with common weapons, armor, and tools. "
|
||||||
|
"This makes it easier to set up your loadouts for cost tracking.<br><br>"
|
||||||
|
"This step is optional - you can always add gear manually later."
|
||||||
|
)
|
||||||
|
info_label.setWordWrap(True)
|
||||||
|
layout.addWidget(info_label)
|
||||||
|
|
||||||
|
layout.addSpacing(20)
|
||||||
|
|
||||||
|
# Download option
|
||||||
|
self.download_check = QCheckBox("Download initial gear database")
|
||||||
|
self.download_check.setChecked(True)
|
||||||
|
layout.addWidget(self.download_check)
|
||||||
|
|
||||||
|
layout.addSpacing(10)
|
||||||
|
|
||||||
|
# Progress bar (hidden initially)
|
||||||
|
self.progress_bar = QProgressBar()
|
||||||
|
self.progress_bar.setVisible(False)
|
||||||
|
layout.addWidget(self.progress_bar)
|
||||||
|
|
||||||
|
# Status label
|
||||||
|
self.status_label = QLabel("")
|
||||||
|
self.status_label.setStyleSheet("color: #4caf50;")
|
||||||
|
layout.addWidget(self.status_label)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
|
||||||
|
# Note
|
||||||
|
note_label = QLabel(
|
||||||
|
"<i>Note: The gear database requires internet connection. "
|
||||||
|
"If you skip this now, you can download it later from the Settings menu.</i>"
|
||||||
|
)
|
||||||
|
note_label.setStyleSheet("color: #888;")
|
||||||
|
note_label.setWordWrap(True)
|
||||||
|
layout.addWidget(note_label)
|
||||||
|
|
||||||
|
def initializePage(self):
|
||||||
|
"""Called when page is shown."""
|
||||||
|
self.progress_bar.setVisible(False)
|
||||||
|
self.status_label.setText("")
|
||||||
|
|
||||||
|
def download_gear_database(self):
|
||||||
|
"""Start downloading gear database."""
|
||||||
|
if not self.download_check.isChecked():
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.progress_bar.setVisible(True)
|
||||||
|
self.progress_bar.setValue(0)
|
||||||
|
self.status_label.setText("Downloading gear database...")
|
||||||
|
|
||||||
|
# Start worker thread
|
||||||
|
self.worker = GearDownloadWorker(self)
|
||||||
|
self.worker.progress.connect(self.progress_bar.setValue)
|
||||||
|
self.worker.finished_signal.connect(self.on_download_finished)
|
||||||
|
self.worker.start()
|
||||||
|
|
||||||
|
# Wait for completion
|
||||||
|
self.worker.wait()
|
||||||
|
|
||||||
|
return self._download_success
|
||||||
|
|
||||||
|
def on_download_finished(self, success: bool, message: str):
|
||||||
|
"""Handle download completion."""
|
||||||
|
self._download_success = success
|
||||||
|
if success:
|
||||||
|
self.status_label.setText(f"✓ {message}")
|
||||||
|
self.status_label.setStyleSheet("color: #4caf50;")
|
||||||
|
else:
|
||||||
|
self.status_label.setText(f"✗ {message}")
|
||||||
|
self.status_label.setStyleSheet("color: #f44336;")
|
||||||
|
|
||||||
|
def get_download_gear(self) -> bool:
|
||||||
|
return self.download_check.isChecked()
|
||||||
|
|
||||||
|
|
||||||
|
class SummaryPage(QWizardPage):
|
||||||
|
"""Final page showing summary of configuration."""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setTitle("Setup Complete")
|
||||||
|
self.setSubTitle("Review your configuration before finishing.")
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# Success message
|
||||||
|
success_label = QLabel("<h2>✓ Setup Complete!</h2>")
|
||||||
|
success_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
layout.addWidget(success_label)
|
||||||
|
|
||||||
|
layout.addSpacing(20)
|
||||||
|
|
||||||
|
# Summary text area
|
||||||
|
self.summary_text = QTextEdit()
|
||||||
|
self.summary_text.setReadOnly(True)
|
||||||
|
self.summary_text.setMaximumHeight(200)
|
||||||
|
layout.addWidget(self.summary_text)
|
||||||
|
|
||||||
|
layout.addSpacing(20)
|
||||||
|
|
||||||
|
# Next steps
|
||||||
|
next_label = QLabel(
|
||||||
|
"<b>Next Steps:</b><br>"
|
||||||
|
"1. Create or select a loadout for your gear<br>"
|
||||||
|
"2. Start a new session to begin tracking<br>"
|
||||||
|
"3. Check the HUD overlay for real-time stats<br><br>"
|
||||||
|
"Click <b>Finish</b> to start using Lemontropia Suite!"
|
||||||
|
)
|
||||||
|
next_label.setWordWrap(True)
|
||||||
|
layout.addWidget(next_label)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
|
||||||
|
def initializePage(self):
|
||||||
|
"""Update summary when page is shown."""
|
||||||
|
wizard = self.wizard()
|
||||||
|
|
||||||
|
# Get values from previous pages
|
||||||
|
avatar_page = wizard.page(1) # AvatarNamePage
|
||||||
|
log_page = wizard.page(2) # LogPathPage
|
||||||
|
activity_page = wizard.page(3) # ActivityTypePage
|
||||||
|
gear_page = wizard.page(4) # GearDatabasePage
|
||||||
|
|
||||||
|
summary = f"""
|
||||||
|
<b>Configuration Summary:</b><br>
|
||||||
|
<br>
|
||||||
|
<b>Avatar Name:</b> {avatar_page.get_avatar_name()}<br>
|
||||||
|
<b>Chat Log Path:</b> {log_page.get_log_path()}<br>
|
||||||
|
<b>Auto-detect Log:</b> {'Yes' if log_page.get_auto_detect() else 'No'}<br>
|
||||||
|
<b>Default Activity:</b> {activity_page.get_activity_type().capitalize()}<br>
|
||||||
|
<b>Download Gear DB:</b> {'Yes' if gear_page.get_download_gear() else 'No'}<br>
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.summary_text.setHtml(summary)
|
||||||
|
|
||||||
|
|
||||||
|
class SetupWizard(QWizard):
|
||||||
|
"""
|
||||||
|
First-run setup wizard for Lemontropia Suite.
|
||||||
|
|
||||||
|
Guides users through:
|
||||||
|
- Setting avatar name
|
||||||
|
- Configuring log file path
|
||||||
|
- Choosing default activity type
|
||||||
|
- Downloading initial gear database (optional)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None, first_run: bool = True):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self.setWindowTitle("Lemontropia Suite - Setup Wizard")
|
||||||
|
self.setMinimumSize(600, 500)
|
||||||
|
|
||||||
|
# Set wizard style
|
||||||
|
self.setWizardStyle(QWizard.WizardStyle.ModernStyle)
|
||||||
|
|
||||||
|
# Add pages
|
||||||
|
self.addPage(WelcomePage())
|
||||||
|
self.addPage(AvatarNamePage())
|
||||||
|
self.addPage(LogPathPage())
|
||||||
|
self.addPage(ActivityTypePage())
|
||||||
|
self.addPage(GearDatabasePage())
|
||||||
|
self.addPage(SummaryPage())
|
||||||
|
|
||||||
|
# Set button text
|
||||||
|
self.setButtonText(QWizard.WizardButton.FinishButton, "Start Using Lemontropia Suite")
|
||||||
|
|
||||||
|
self._first_run = first_run
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
"""Handle wizard completion."""
|
||||||
|
# Collect all settings
|
||||||
|
settings = {
|
||||||
|
'avatar_name': self.field("avatar_name") or self.page(1).get_avatar_name(),
|
||||||
|
'log_path': self.page(2).get_log_path(),
|
||||||
|
'auto_detect_log': self.page(2).get_auto_detect(),
|
||||||
|
'default_activity': self.page(3).get_activity_type(),
|
||||||
|
'gear_db_downloaded': self.page(4).get_download_gear(),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save to QSettings
|
||||||
|
qsettings = QSettings("Lemontropia", "Suite")
|
||||||
|
qsettings.setValue("player/name", settings['avatar_name'])
|
||||||
|
qsettings.setValue("log/path", settings['log_path'])
|
||||||
|
qsettings.setValue("log/auto_detect", settings['auto_detect_log'])
|
||||||
|
qsettings.setValue("activity/default", settings['default_activity'])
|
||||||
|
qsettings.setValue("setup/first_run_complete", True)
|
||||||
|
qsettings.setValue("setup/gear_db_downloaded", settings['gear_db_downloaded'])
|
||||||
|
qsettings.sync()
|
||||||
|
|
||||||
|
# Store settings for access after wizard closes
|
||||||
|
self._settings = settings
|
||||||
|
|
||||||
|
super().accept()
|
||||||
|
|
||||||
|
def get_settings(self) -> dict:
|
||||||
|
"""Get the configured settings after wizard completion."""
|
||||||
|
return getattr(self, '_settings', {})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_first_run() -> bool:
|
||||||
|
"""Check if this is the first run of the application."""
|
||||||
|
settings = QSettings("Lemontropia", "Suite")
|
||||||
|
return not settings.value("setup/first_run_complete", False, type=bool)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def reset_first_run():
|
||||||
|
"""Reset the first-run flag (for testing)."""
|
||||||
|
settings = QSettings("Lemontropia", "Suite")
|
||||||
|
settings.setValue("setup/first_run_complete", False)
|
||||||
|
settings.sync()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Test entry point for the setup wizard."""
|
||||||
|
from PyQt6.QtWidgets import QApplication
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
# Apply dark theme
|
||||||
|
dark_stylesheet = """
|
||||||
|
QWizard {
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWizardPage {
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QLabel {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QLineEdit {
|
||||||
|
background-color: #252525;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: #3d3d3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
QComboBox {
|
||||||
|
background-color: #252525;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QGroupBox {
|
||||||
|
font-weight: bold;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextEdit {
|
||||||
|
background-color: #252525;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QCheckBox {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QCheckBox::indicator {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QProgressBar {
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: center;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QProgressBar::chunk {
|
||||||
|
background-color: #4caf50;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
app.setStyleSheet(dark_stylesheet)
|
||||||
|
|
||||||
|
wizard = SetupWizard()
|
||||||
|
if wizard.exec() == QWizard.DialogCode.Accepted:
|
||||||
|
print("Settings saved:", wizard.get_settings())
|
||||||
|
else:
|
||||||
|
print("Wizard cancelled")
|
||||||
|
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue