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.QtCore import Qt
|
||||
from ui.main_window import MainWindow
|
||||
from ui.setup_wizard import SetupWizard
|
||||
|
||||
|
||||
def main():
|
||||
|
|
@ -28,6 +29,14 @@ def main():
|
|||
app.setApplicationName("Lemontropia Suite")
|
||||
app.setApplicationVersion("0.2.0")
|
||||
|
||||
# Check if first run - show setup wizard
|
||||
if SetupWizard.is_first_run():
|
||||
wizard = SetupWizard(first_run=True)
|
||||
if wizard.exec() != SetupWizard.DialogCode.Accepted:
|
||||
# User cancelled wizard, exit
|
||||
print("Setup cancelled by user")
|
||||
sys.exit(0)
|
||||
|
||||
# Create and show main window
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
|
|
@ -36,4 +45,4 @@ def main():
|
|||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
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
|
||||
Simplified cost-focused loadout system.
|
||||
Lemontropia Suite - Loadout Manager UI v5.0
|
||||
Full gear support with weapon amplifiers, mindforce implants, and armor platings.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
|
@ -15,7 +15,7 @@ from PyQt6.QtWidgets import (
|
|||
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout,
|
||||
QLineEdit, QLabel, QPushButton, QGroupBox,
|
||||
QMessageBox, QListWidget, QListWidgetItem,
|
||||
QSplitter, QWidget, QFrame, QGridLayout,
|
||||
QSplitter, QWidget, QFrame, QGridLayout, QScrollArea,
|
||||
)
|
||||
from PyQt6.QtCore import Qt, pyqtSignal
|
||||
|
||||
|
|
@ -25,62 +25,94 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
# ============================================================================
|
||||
# Simple Cost-Focused Loadout Config
|
||||
# Full Gear Loadout Config
|
||||
# ============================================================================
|
||||
|
||||
@dataclass
|
||||
class LoadoutConfig:
|
||||
"""Simple loadout configuration focused on cost tracking.
|
||||
"""Complete loadout configuration with full gear support.
|
||||
|
||||
Core principle: Only store what's needed for cost calculations.
|
||||
Everything else is display metadata.
|
||||
Core principle: Store all gear types needed for comprehensive cost tracking.
|
||||
"""
|
||||
# Identity
|
||||
name: str = "Unnamed"
|
||||
version: int = 2 # Version 2 = simplified format
|
||||
version: int = 3 # Version 3 = full gear support
|
||||
|
||||
# === COST DATA (Required for tracking) ===
|
||||
# All values in PED (not PEC)
|
||||
# === WEAPON & ATTACHMENTS ===
|
||||
weapon_cost_per_shot: Decimal = Decimal("0")
|
||||
armor_cost_per_hit: Decimal = Decimal("0")
|
||||
healing_cost_per_heal: Decimal = Decimal("0")
|
||||
|
||||
# === DISPLAY METADATA (For UI only) ===
|
||||
weapon_name: str = "None"
|
||||
weapon_damage: Decimal = Decimal("0")
|
||||
weapon_decay_pec: Decimal = Decimal("0") # Raw for reference
|
||||
weapon_ammo_pec: Decimal = Decimal("0") # Raw for reference
|
||||
|
||||
armor_name: str = "None"
|
||||
armor_decay_pec: Decimal = Decimal("0") # Raw for reference
|
||||
|
||||
healing_name: str = "None"
|
||||
healing_decay_pec: Decimal = Decimal("0") # Raw for reference
|
||||
|
||||
# === API REFERENCES (For re-loading from API) ===
|
||||
weapon_decay_pec: Decimal = Decimal("0")
|
||||
weapon_ammo_pec: Decimal = Decimal("0")
|
||||
weapon_api_id: Optional[int] = None
|
||||
|
||||
# Weapon Amplifier
|
||||
weapon_amp_id: Optional[int] = None
|
||||
weapon_amp_name: str = "None"
|
||||
weapon_amp_decay: Decimal = Decimal("0")
|
||||
weapon_amp_damage_bonus: Decimal = Decimal("0")
|
||||
|
||||
# === ARMOR & PLATINGS ===
|
||||
armor_cost_per_hit: Decimal = Decimal("0")
|
||||
armor_name: str = "None"
|
||||
armor_decay_pec: Decimal = Decimal("0")
|
||||
armor_api_id: Optional[int] = None
|
||||
|
||||
# Armor Plating
|
||||
plating_id: Optional[int] = None
|
||||
plating_name: str = "None"
|
||||
plating_decay: Decimal = Decimal("0")
|
||||
plating_protection_summary: str = ""
|
||||
|
||||
# === HEALING & MINDFORCE ===
|
||||
healing_cost_per_heal: Decimal = Decimal("0")
|
||||
healing_name: str = "None"
|
||||
healing_decay_pec: Decimal = Decimal("0")
|
||||
healing_api_id: Optional[int] = None
|
||||
|
||||
# Mindforce Implant (for healing chips)
|
||||
mindforce_implant_id: Optional[int] = None
|
||||
mindforce_implant_name: str = "None"
|
||||
mindforce_implant_decay: Decimal = Decimal("0")
|
||||
mindforce_implant_heal_amount: Decimal = Decimal("0")
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Serialize to simple dictionary."""
|
||||
"""Serialize to dictionary."""
|
||||
return {
|
||||
'name': self.name,
|
||||
'version': self.version,
|
||||
# Weapon
|
||||
'weapon_cost_per_shot': str(self.weapon_cost_per_shot),
|
||||
'armor_cost_per_hit': str(self.armor_cost_per_hit),
|
||||
'healing_cost_per_heal': str(self.healing_cost_per_heal),
|
||||
'weapon_name': self.weapon_name,
|
||||
'weapon_damage': str(self.weapon_damage),
|
||||
'weapon_decay_pec': str(self.weapon_decay_pec),
|
||||
'weapon_ammo_pec': str(self.weapon_ammo_pec),
|
||||
'weapon_api_id': self.weapon_api_id,
|
||||
# Weapon Amplifier
|
||||
'weapon_amp_id': self.weapon_amp_id,
|
||||
'weapon_amp_name': self.weapon_amp_name,
|
||||
'weapon_amp_decay': str(self.weapon_amp_decay),
|
||||
'weapon_amp_damage_bonus': str(self.weapon_amp_damage_bonus),
|
||||
# Armor
|
||||
'armor_cost_per_hit': str(self.armor_cost_per_hit),
|
||||
'armor_name': self.armor_name,
|
||||
'armor_decay_pec': str(self.armor_decay_pec),
|
||||
'armor_api_id': self.armor_api_id,
|
||||
# Plating
|
||||
'plating_id': self.plating_id,
|
||||
'plating_name': self.plating_name,
|
||||
'plating_decay': str(self.plating_decay),
|
||||
'plating_protection_summary': self.plating_protection_summary,
|
||||
# Healing
|
||||
'healing_cost_per_heal': str(self.healing_cost_per_heal),
|
||||
'healing_name': self.healing_name,
|
||||
'healing_decay_pec': str(self.healing_decay_pec),
|
||||
'weapon_api_id': self.weapon_api_id,
|
||||
'armor_api_id': self.armor_api_id,
|
||||
'healing_api_id': self.healing_api_id,
|
||||
# Mindforce
|
||||
'mindforce_implant_id': self.mindforce_implant_id,
|
||||
'mindforce_implant_name': self.mindforce_implant_name,
|
||||
'mindforce_implant_decay': str(self.mindforce_implant_decay),
|
||||
'mindforce_implant_heal_amount': str(self.mindforce_implant_heal_amount),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
|
@ -89,13 +121,15 @@ class LoadoutConfig:
|
|||
version = data.get('version', 1)
|
||||
|
||||
if version == 1:
|
||||
return cls._from_legacy(data)
|
||||
else:
|
||||
return cls._from_legacy_v1(data)
|
||||
elif version == 2:
|
||||
return cls._from_v2(data)
|
||||
else:
|
||||
return cls._from_v3(data)
|
||||
|
||||
@classmethod
|
||||
def _from_v2(cls, data: dict) -> "LoadoutConfig":
|
||||
"""Parse version 2 (current) format."""
|
||||
def _from_v3(cls, data: dict) -> "LoadoutConfig":
|
||||
"""Parse version 3 (current) format."""
|
||||
def get_decimal(key: str, default: str = "0") -> Decimal:
|
||||
try:
|
||||
return Decimal(str(data.get(key, default)))
|
||||
|
|
@ -104,26 +138,72 @@ class LoadoutConfig:
|
|||
|
||||
return cls(
|
||||
name=data.get('name', 'Unnamed'),
|
||||
version=2,
|
||||
version=3,
|
||||
# Weapon
|
||||
weapon_cost_per_shot=get_decimal('weapon_cost_per_shot'),
|
||||
armor_cost_per_hit=get_decimal('armor_cost_per_hit'),
|
||||
healing_cost_per_heal=get_decimal('healing_cost_per_heal'),
|
||||
weapon_name=data.get('weapon_name', 'None'),
|
||||
weapon_damage=get_decimal('weapon_damage'),
|
||||
weapon_decay_pec=get_decimal('weapon_decay_pec'),
|
||||
weapon_ammo_pec=get_decimal('weapon_ammo_pec'),
|
||||
weapon_api_id=data.get('weapon_api_id'),
|
||||
# Weapon Amplifier
|
||||
weapon_amp_id=data.get('weapon_amp_id'),
|
||||
weapon_amp_name=data.get('weapon_amp_name', 'None'),
|
||||
weapon_amp_decay=get_decimal('weapon_amp_decay'),
|
||||
weapon_amp_damage_bonus=get_decimal('weapon_amp_damage_bonus'),
|
||||
# Armor
|
||||
armor_cost_per_hit=get_decimal('armor_cost_per_hit'),
|
||||
armor_name=data.get('armor_name', 'None'),
|
||||
armor_decay_pec=get_decimal('armor_decay_pec'),
|
||||
armor_api_id=data.get('armor_api_id'),
|
||||
# Plating
|
||||
plating_id=data.get('plating_id'),
|
||||
plating_name=data.get('plating_name', 'None'),
|
||||
plating_decay=get_decimal('plating_decay'),
|
||||
plating_protection_summary=data.get('plating_protection_summary', ''),
|
||||
# Healing
|
||||
healing_cost_per_heal=get_decimal('healing_cost_per_heal'),
|
||||
healing_name=data.get('healing_name', 'None'),
|
||||
healing_decay_pec=get_decimal('healing_decay_pec'),
|
||||
healing_api_id=data.get('healing_api_id'),
|
||||
# Mindforce
|
||||
mindforce_implant_id=data.get('mindforce_implant_id'),
|
||||
mindforce_implant_name=data.get('mindforce_implant_name', 'None'),
|
||||
mindforce_implant_decay=get_decimal('mindforce_implant_decay'),
|
||||
mindforce_implant_heal_amount=get_decimal('mindforce_implant_heal_amount'),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_v2(cls, data: dict) -> "LoadoutConfig":
|
||||
"""Convert version 2 to version 3."""
|
||||
def get_decimal(key: str, default: str = "0") -> Decimal:
|
||||
try:
|
||||
return Decimal(str(data.get(key, default)))
|
||||
except Exception:
|
||||
return Decimal(default)
|
||||
|
||||
return cls(
|
||||
name=data.get('name', 'Unnamed'),
|
||||
version=3,
|
||||
weapon_cost_per_shot=get_decimal('weapon_cost_per_shot'),
|
||||
weapon_name=data.get('weapon_name', 'None'),
|
||||
weapon_damage=get_decimal('weapon_damage'),
|
||||
weapon_decay_pec=get_decimal('weapon_decay_pec'),
|
||||
weapon_ammo_pec=get_decimal('weapon_ammo_pec'),
|
||||
weapon_api_id=data.get('weapon_api_id'),
|
||||
armor_cost_per_hit=get_decimal('armor_cost_per_hit'),
|
||||
armor_name=data.get('armor_name', 'None'),
|
||||
armor_decay_pec=get_decimal('armor_decay_pec'),
|
||||
armor_api_id=data.get('armor_api_id'),
|
||||
healing_cost_per_heal=get_decimal('healing_cost_per_heal'),
|
||||
healing_name=data.get('healing_name', 'None'),
|
||||
healing_decay_pec=get_decimal('healing_decay_pec'),
|
||||
healing_api_id=data.get('healing_api_id'),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_legacy(cls, data: dict) -> "LoadoutConfig":
|
||||
"""Convert legacy format to new simple format."""
|
||||
def _from_legacy_v1(cls, data: dict) -> "LoadoutConfig":
|
||||
"""Convert legacy format to new format."""
|
||||
def get_decimal(key: str, default: str = "0") -> Decimal:
|
||||
try:
|
||||
return Decimal(str(data.get(key, default)))
|
||||
|
|
@ -143,62 +223,78 @@ class LoadoutConfig:
|
|||
|
||||
return cls(
|
||||
name=data.get('name', 'Unnamed'),
|
||||
version=2,
|
||||
version=3,
|
||||
weapon_cost_per_shot=weapon_cost_per_shot,
|
||||
armor_cost_per_hit=armor_cost_per_hit,
|
||||
healing_cost_per_heal=healing_cost_per_heal,
|
||||
weapon_name=data.get('weapon_name', data.get('weapon', 'None')),
|
||||
weapon_damage=get_decimal('weapon_damage'),
|
||||
weapon_decay_pec=weapon_decay,
|
||||
weapon_ammo_pec=weapon_ammo,
|
||||
armor_cost_per_hit=armor_cost_per_hit,
|
||||
armor_name=data.get('armor_set_name', data.get('armor_name', 'None')),
|
||||
armor_decay_pec=armor_decay,
|
||||
healing_cost_per_heal=healing_cost_per_heal,
|
||||
healing_name=data.get('heal_name', 'None'),
|
||||
healing_decay_pec=heal_decay,
|
||||
)
|
||||
|
||||
def get_total_weapon_cost_per_shot(self) -> Decimal:
|
||||
"""Calculate total weapon cost including amplifier."""
|
||||
base_cost = self.weapon_cost_per_shot
|
||||
amp_cost = self.weapon_amp_decay / Decimal("100") # Convert PEC to PED
|
||||
return base_cost + amp_cost
|
||||
|
||||
def get_total_healing_cost_per_heal(self) -> Decimal:
|
||||
"""Calculate total healing cost including mindforce implant."""
|
||||
base_cost = self.healing_cost_per_heal
|
||||
implant_cost = self.mindforce_implant_decay / Decimal("100") # Convert PEC to PED
|
||||
return base_cost + implant_cost
|
||||
|
||||
def get_total_armor_cost_per_hit(self) -> Decimal:
|
||||
"""Calculate total armor cost including plating."""
|
||||
base_cost = self.armor_cost_per_hit
|
||||
plating_cost = self.plating_decay / Decimal("100") # Convert PEC to PED
|
||||
return base_cost + plating_cost
|
||||
|
||||
def get_summary(self) -> Dict[str, Any]:
|
||||
"""Get cost summary for display."""
|
||||
return {
|
||||
'name': self.name,
|
||||
'weapon': self.weapon_name,
|
||||
'weapon_amp': self.weapon_amp_name if self.weapon_amp_id else "None",
|
||||
'armor': self.armor_name,
|
||||
'plating': self.plating_name if self.plating_id else "None",
|
||||
'healing': self.healing_name,
|
||||
'cost_per_shot': self.weapon_cost_per_shot,
|
||||
'cost_per_hit': self.armor_cost_per_hit,
|
||||
'cost_per_heal': self.healing_cost_per_heal,
|
||||
'mindforce': self.mindforce_implant_name if self.mindforce_implant_id else "None",
|
||||
'cost_per_shot': self.get_total_weapon_cost_per_shot(),
|
||||
'cost_per_hit': self.get_total_armor_cost_per_hit(),
|
||||
'cost_per_heal': self.get_total_healing_cost_per_heal(),
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Simple Loadout Manager Dialog
|
||||
# Full Gear Loadout Manager Dialog
|
||||
# ============================================================================
|
||||
|
||||
class LoadoutManagerDialog(QDialog):
|
||||
"""Simplified loadout manager focused on cost configuration."""
|
||||
"""Full-featured loadout manager with all gear types."""
|
||||
|
||||
loadout_saved = pyqtSignal(LoadoutConfig)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Loadout Manager")
|
||||
self.setMinimumSize(600, 500)
|
||||
self.setWindowTitle("Loadout Manager - Full Gear Configuration")
|
||||
self.setMinimumSize(800, 700)
|
||||
|
||||
# State
|
||||
self.config_dir = Path.home() / ".lemontropia" / "loadouts"
|
||||
self.config_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.current_config: Optional[LoadoutConfig] = None
|
||||
|
||||
# Cached API data
|
||||
self._cached_weapons: Optional[list] = None
|
||||
self._cached_armors: Optional[list] = None
|
||||
self._cached_healing: Optional[list] = None
|
||||
|
||||
self._setup_ui()
|
||||
self._load_saved_loadouts()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Setup simplified UI."""
|
||||
"""Setup full gear UI."""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(10)
|
||||
|
||||
|
|
@ -206,7 +302,7 @@ class LoadoutManagerDialog(QDialog):
|
|||
name_layout = QHBoxLayout()
|
||||
name_layout.addWidget(QLabel("Loadout Name:"))
|
||||
self.name_edit = QLineEdit()
|
||||
self.name_edit.setPlaceholderText("e.g., ArMatrix Ghost Hunt")
|
||||
self.name_edit.setPlaceholderText("e.g., ArMatrix Ghost Hunt with Dante")
|
||||
name_layout.addWidget(self.name_edit)
|
||||
layout.addLayout(name_layout)
|
||||
|
||||
|
|
@ -235,13 +331,16 @@ class LoadoutManagerDialog(QDialog):
|
|||
|
||||
splitter.addWidget(left_widget)
|
||||
|
||||
# Right: Configuration
|
||||
# Right: Configuration (in a scroll area)
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
right_widget = QWidget()
|
||||
right_layout = QVBoxLayout(right_widget)
|
||||
right_layout.setContentsMargins(0, 0, 0, 0)
|
||||
right_layout.setSpacing(10)
|
||||
|
||||
# -- Weapon Section --
|
||||
weapon_group = QGroupBox("⚔️ Weapon")
|
||||
weapon_group = QGroupBox("⚔️ Weapon & Amplifier")
|
||||
weapon_layout = QFormLayout(weapon_group)
|
||||
|
||||
self.weapon_btn = QPushButton("Select Weapon...")
|
||||
|
|
@ -252,20 +351,31 @@ class LoadoutManagerDialog(QDialog):
|
|||
self.weapon_info.setStyleSheet("color: #888;")
|
||||
weapon_layout.addRow(self.weapon_info)
|
||||
|
||||
self.weapon_decay_label = QLabel("0 PEC")
|
||||
weapon_layout.addRow("Decay:", self.weapon_decay_label)
|
||||
|
||||
self.weapon_ammo_label = QLabel("0")
|
||||
weapon_layout.addRow("Ammo:", self.weapon_ammo_label)
|
||||
|
||||
self.weapon_cost_label = QLabel("0.0000 PED")
|
||||
self.weapon_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F;")
|
||||
weapon_layout.addRow("Cost/Shot:", self.weapon_cost_label)
|
||||
weapon_layout.addRow("Base Cost/Shot:", self.weapon_cost_label)
|
||||
|
||||
# Amplifier
|
||||
self.amp_btn = QPushButton("Select Amplifier...")
|
||||
self.amp_btn.clicked.connect(self._select_amplifier)
|
||||
weapon_layout.addRow("Amplifier:", self.amp_btn)
|
||||
|
||||
self.amp_info = QLabel("None selected")
|
||||
self.amp_info.setStyleSheet("color: #888;")
|
||||
weapon_layout.addRow(self.amp_info)
|
||||
|
||||
self.amp_cost_label = QLabel("0.0000 PED")
|
||||
self.amp_cost_label.setStyleSheet("color: #FFA07A;")
|
||||
weapon_layout.addRow("Amp Cost/Shot:", self.amp_cost_label)
|
||||
|
||||
self.total_weapon_cost_label = QLabel("0.0000 PED")
|
||||
self.total_weapon_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F; font-size: 12px;")
|
||||
weapon_layout.addRow("Total Cost/Shot:", self.total_weapon_cost_label)
|
||||
|
||||
right_layout.addWidget(weapon_group)
|
||||
|
||||
# -- Armor Section --
|
||||
armor_group = QGroupBox("🛡️ Armor")
|
||||
armor_group = QGroupBox("🛡️ Armor & Plating")
|
||||
armor_layout = QFormLayout(armor_group)
|
||||
|
||||
self.armor_btn = QPushButton("Select Armor...")
|
||||
|
|
@ -278,17 +388,34 @@ class LoadoutManagerDialog(QDialog):
|
|||
|
||||
self.armor_cost_label = QLabel("0.0000 PED")
|
||||
self.armor_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F;")
|
||||
armor_layout.addRow("Cost/Hit:", self.armor_cost_label)
|
||||
armor_layout.addRow("Base Cost/Hit:", self.armor_cost_label)
|
||||
|
||||
# Plating
|
||||
self.plating_btn = QPushButton("Select Plating...")
|
||||
self.plating_btn.clicked.connect(self._select_plating)
|
||||
armor_layout.addRow("Plating:", self.plating_btn)
|
||||
|
||||
self.plating_info = QLabel("None selected")
|
||||
self.plating_info.setStyleSheet("color: #888;")
|
||||
armor_layout.addRow(self.plating_info)
|
||||
|
||||
self.plating_cost_label = QLabel("0.0000 PED")
|
||||
self.plating_cost_label.setStyleSheet("color: #FFA07A;")
|
||||
armor_layout.addRow("Plating Cost/Hit:", self.plating_cost_label)
|
||||
|
||||
self.total_armor_cost_label = QLabel("0.0000 PED")
|
||||
self.total_armor_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F; font-size: 12px;")
|
||||
armor_layout.addRow("Total Cost/Hit:", self.total_armor_cost_label)
|
||||
|
||||
right_layout.addWidget(armor_group)
|
||||
|
||||
# -- Healing Section --
|
||||
healing_group = QGroupBox("💚 Healing")
|
||||
healing_group = QGroupBox("💚 Healing & Mindforce")
|
||||
healing_layout = QFormLayout(healing_group)
|
||||
|
||||
self.healing_btn = QPushButton("Select Healing...")
|
||||
self.healing_btn = QPushButton("Select Healing Tool...")
|
||||
self.healing_btn.clicked.connect(self._select_healing)
|
||||
healing_layout.addRow("Healing:", self.healing_btn)
|
||||
healing_layout.addRow("Healing Tool:", self.healing_btn)
|
||||
|
||||
self.healing_info = QLabel("None selected")
|
||||
self.healing_info.setStyleSheet("color: #888;")
|
||||
|
|
@ -296,24 +423,44 @@ class LoadoutManagerDialog(QDialog):
|
|||
|
||||
self.healing_cost_label = QLabel("0.0000 PED")
|
||||
self.healing_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F;")
|
||||
healing_layout.addRow("Cost/Heal:", self.healing_cost_label)
|
||||
healing_layout.addRow("Base Cost/Heal:", self.healing_cost_label)
|
||||
|
||||
# Mindforce Implant
|
||||
self.mindforce_btn = QPushButton("Select Mindforce Chip...")
|
||||
self.mindforce_btn.clicked.connect(self._select_mindforce)
|
||||
healing_layout.addRow("Mindforce Chip:", self.mindforce_btn)
|
||||
|
||||
self.mindforce_info = QLabel("None selected")
|
||||
self.mindforce_info.setStyleSheet("color: #888;")
|
||||
healing_layout.addRow(self.mindforce_info)
|
||||
|
||||
self.mindforce_cost_label = QLabel("0.0000 PED")
|
||||
self.mindforce_cost_label.setStyleSheet("color: #FFA07A;")
|
||||
healing_layout.addRow("Chip Cost/Heal:", self.mindforce_cost_label)
|
||||
|
||||
self.total_healing_cost_label = QLabel("0.0000 PED")
|
||||
self.total_healing_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F; font-size: 12px;")
|
||||
healing_layout.addRow("Total Cost/Heal:", self.total_healing_cost_label)
|
||||
|
||||
right_layout.addWidget(healing_group)
|
||||
|
||||
# -- Summary Section --
|
||||
summary_group = QGroupBox("💰 Session Cost Summary")
|
||||
summary_group = QGroupBox("💰 Total Session Cost Summary")
|
||||
summary_layout = QGridLayout(summary_group)
|
||||
|
||||
summary_layout.addWidget(QLabel("Cost per Shot:"), 0, 0)
|
||||
self.summary_shot = QLabel("0.0000 PED")
|
||||
self.summary_shot.setStyleSheet("font-weight: bold; color: #7FFF7F;")
|
||||
summary_layout.addWidget(self.summary_shot, 0, 1)
|
||||
|
||||
summary_layout.addWidget(QLabel("Cost per Hit:"), 1, 0)
|
||||
self.summary_hit = QLabel("0.0000 PED")
|
||||
self.summary_hit.setStyleSheet("font-weight: bold; color: #7FFF7F;")
|
||||
summary_layout.addWidget(self.summary_hit, 1, 1)
|
||||
|
||||
summary_layout.addWidget(QLabel("Cost per Heal:"), 2, 0)
|
||||
self.summary_heal = QLabel("0.0000 PED")
|
||||
self.summary_heal.setStyleSheet("font-weight: bold; color: #7FFF7F;")
|
||||
summary_layout.addWidget(self.summary_heal, 2, 1)
|
||||
|
||||
summary_layout.setColumnStretch(1, 1)
|
||||
|
|
@ -321,8 +468,10 @@ class LoadoutManagerDialog(QDialog):
|
|||
|
||||
right_layout.addStretch()
|
||||
|
||||
splitter.addWidget(right_widget)
|
||||
splitter.setSizes([200, 400])
|
||||
scroll.setWidget(right_widget)
|
||||
splitter.addWidget(scroll)
|
||||
|
||||
splitter.setSizes([250, 550])
|
||||
|
||||
layout.addWidget(splitter)
|
||||
|
||||
|
|
@ -371,8 +520,6 @@ class LoadoutManagerDialog(QDialog):
|
|||
# Update UI
|
||||
self.weapon_btn.setText(weapon.name[:30])
|
||||
self.weapon_info.setText(f"Damage: {weapon.damage} | Range: {weapon.range_val}")
|
||||
self.weapon_decay_label.setText(f"{decay_pec} PEC")
|
||||
self.weapon_ammo_label.setText(f"{ammo}")
|
||||
self.weapon_cost_label.setText(f"{cost_per_shot:.4f} PED")
|
||||
|
||||
# Store for saving
|
||||
|
|
@ -385,8 +532,54 @@ class LoadoutManagerDialog(QDialog):
|
|||
'cost_per_shot': cost_per_shot,
|
||||
}
|
||||
|
||||
self._update_weapon_total()
|
||||
self._update_summary()
|
||||
|
||||
def _select_amplifier(self):
|
||||
"""Open weapon amplifier selector."""
|
||||
from ui.amplifier_selector import AmplifierSelectorDialog
|
||||
|
||||
dialog = AmplifierSelectorDialog(self)
|
||||
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||
amp = dialog.get_selected_amplifier()
|
||||
if amp:
|
||||
self._set_amplifier(amp)
|
||||
|
||||
def _set_amplifier(self, amp_data: dict):
|
||||
"""Set weapon amplifier."""
|
||||
name = amp_data.get('name', 'Unknown')
|
||||
decay_pec = Decimal(str(amp_data.get('decay_pec', 0)))
|
||||
damage_bonus = Decimal(str(amp_data.get('damage_bonus', 0)))
|
||||
cost_per_shot = decay_pec / Decimal("100")
|
||||
|
||||
# Update UI
|
||||
self.amp_btn.setText(name[:30])
|
||||
self.amp_info.setText(f"+{damage_bonus} Damage")
|
||||
self.amp_cost_label.setText(f"{cost_per_shot:.4f} PED")
|
||||
|
||||
# Store for saving
|
||||
self._pending_amplifier = {
|
||||
'name': name,
|
||||
'api_id': amp_data.get('api_id'),
|
||||
'decay_pec': decay_pec,
|
||||
'damage_bonus': damage_bonus,
|
||||
'cost_per_shot': cost_per_shot,
|
||||
}
|
||||
|
||||
self._update_weapon_total()
|
||||
self._update_summary()
|
||||
|
||||
def _update_weapon_total(self):
|
||||
"""Update total weapon cost display."""
|
||||
weapon = getattr(self, '_pending_weapon', {})
|
||||
amp = getattr(self, '_pending_amplifier', {})
|
||||
|
||||
weapon_cost = weapon.get('cost_per_shot', Decimal("0"))
|
||||
amp_cost = amp.get('cost_per_shot', Decimal("0"))
|
||||
total = weapon_cost + amp_cost
|
||||
|
||||
self.total_weapon_cost_label.setText(f"{total:.4f} PED")
|
||||
|
||||
def _select_armor(self):
|
||||
"""Open simplified armor selector."""
|
||||
from ui.armor_selector import ArmorSelectorDialog
|
||||
|
|
@ -399,7 +592,6 @@ class LoadoutManagerDialog(QDialog):
|
|||
|
||||
def _set_armor(self, armor_data: dict):
|
||||
"""Set armor and calculate cost."""
|
||||
# armor_data has: name, decay_pec, protection_summary
|
||||
name = armor_data.get('name', 'Unknown')
|
||||
decay_pec = Decimal(str(armor_data.get('decay_pec', 0)))
|
||||
cost_per_hit = decay_pec / Decimal("100")
|
||||
|
|
@ -418,8 +610,54 @@ class LoadoutManagerDialog(QDialog):
|
|||
'cost_per_hit': cost_per_hit,
|
||||
}
|
||||
|
||||
self._update_armor_total()
|
||||
self._update_summary()
|
||||
|
||||
def _select_plating(self):
|
||||
"""Open armor plating selector."""
|
||||
from ui.plate_selector import PlateSelectorDialog
|
||||
|
||||
dialog = PlateSelectorDialog(self)
|
||||
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||
plate = dialog.get_selected_plate()
|
||||
if plate:
|
||||
self._set_plating(plate)
|
||||
|
||||
def _set_plating(self, plate_data: dict):
|
||||
"""Set armor plating."""
|
||||
name = plate_data.get('name', 'Unknown')
|
||||
decay_pec = Decimal(str(plate_data.get('decay_pec', 0)))
|
||||
cost_per_hit = decay_pec / Decimal("100")
|
||||
|
||||
# Update UI
|
||||
self.plating_btn.setText(name[:30])
|
||||
prot_summary = plate_data.get('protection_summary', '')
|
||||
self.plating_info.setText(prot_summary[:40] if prot_summary else "No data")
|
||||
self.plating_cost_label.setText(f"{cost_per_hit:.4f} PED")
|
||||
|
||||
# Store for saving
|
||||
self._pending_plating = {
|
||||
'name': name,
|
||||
'api_id': plate_data.get('api_id'),
|
||||
'decay_pec': decay_pec,
|
||||
'protection_summary': prot_summary,
|
||||
'cost_per_hit': cost_per_hit,
|
||||
}
|
||||
|
||||
self._update_armor_total()
|
||||
self._update_summary()
|
||||
|
||||
def _update_armor_total(self):
|
||||
"""Update total armor cost display."""
|
||||
armor = getattr(self, '_pending_armor', {})
|
||||
plating = getattr(self, '_pending_plating', {})
|
||||
|
||||
armor_cost = armor.get('cost_per_hit', Decimal("0"))
|
||||
plating_cost = plating.get('cost_per_hit', Decimal("0"))
|
||||
total = armor_cost + plating_cost
|
||||
|
||||
self.total_armor_cost_label.setText(f"{total:.4f} PED")
|
||||
|
||||
def _select_healing(self):
|
||||
"""Open healing selector."""
|
||||
from ui.healing_selector import HealingSelectorDialog
|
||||
|
|
@ -451,13 +689,66 @@ class LoadoutManagerDialog(QDialog):
|
|||
'cost_per_heal': cost_per_heal,
|
||||
}
|
||||
|
||||
self._update_healing_total()
|
||||
self._update_summary()
|
||||
|
||||
def _select_mindforce(self):
|
||||
"""Open mindforce implant selector."""
|
||||
from ui.mindforce_selector import MindforceSelectorDialog
|
||||
|
||||
dialog = MindforceSelectorDialog(self)
|
||||
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||
chip = dialog.get_selected_chip()
|
||||
if chip:
|
||||
self._set_mindforce(chip)
|
||||
|
||||
def _set_mindforce(self, chip_data: dict):
|
||||
"""Set mindforce implant."""
|
||||
name = chip_data.get('name', 'Unknown')
|
||||
decay_pec = Decimal(str(chip_data.get('decay_pec', 0)))
|
||||
heal_amount = Decimal(str(chip_data.get('heal_amount', 0)))
|
||||
cost_per_heal = decay_pec / Decimal("100")
|
||||
|
||||
# Update UI
|
||||
self.mindforce_btn.setText(name[:30])
|
||||
self.mindforce_info.setText(f"Heal: {heal_amount} HP")
|
||||
self.mindforce_cost_label.setText(f"{cost_per_heal:.4f} PED")
|
||||
|
||||
# Store for saving
|
||||
self._pending_mindforce = {
|
||||
'name': name,
|
||||
'api_id': chip_data.get('api_id'),
|
||||
'decay_pec': decay_pec,
|
||||
'heal_amount': heal_amount,
|
||||
'cost_per_heal': cost_per_heal,
|
||||
}
|
||||
|
||||
self._update_healing_total()
|
||||
self._update_summary()
|
||||
|
||||
def _update_healing_total(self):
|
||||
"""Update total healing cost display."""
|
||||
healing = getattr(self, '_pending_healing', {})
|
||||
mindforce = getattr(self, '_pending_mindforce', {})
|
||||
|
||||
healing_cost = healing.get('cost_per_heal', Decimal("0"))
|
||||
mindforce_cost = mindforce.get('cost_per_heal', Decimal("0"))
|
||||
total = healing_cost + mindforce_cost
|
||||
|
||||
self.total_healing_cost_label.setText(f"{total:.4f} PED")
|
||||
|
||||
def _update_summary(self):
|
||||
"""Update cost summary display."""
|
||||
shot = getattr(self, '_pending_weapon', {}).get('cost_per_shot', Decimal("0"))
|
||||
hit = getattr(self, '_pending_armor', {}).get('cost_per_hit', Decimal("0"))
|
||||
heal = getattr(self, '_pending_healing', {}).get('cost_per_heal', Decimal("0"))
|
||||
weapon = getattr(self, '_pending_weapon', {})
|
||||
amp = getattr(self, '_pending_amplifier', {})
|
||||
armor = getattr(self, '_pending_armor', {})
|
||||
plating = getattr(self, '_pending_plating', {})
|
||||
healing = getattr(self, '_pending_healing', {})
|
||||
mindforce = getattr(self, '_pending_mindforce', {})
|
||||
|
||||
shot = weapon.get('cost_per_shot', Decimal("0")) + amp.get('cost_per_shot', Decimal("0"))
|
||||
hit = armor.get('cost_per_hit', Decimal("0")) + plating.get('cost_per_hit', Decimal("0"))
|
||||
heal = healing.get('cost_per_heal', Decimal("0")) + mindforce.get('cost_per_heal', Decimal("0"))
|
||||
|
||||
self.summary_shot.setText(f"{shot:.4f} PED")
|
||||
self.summary_hit.setText(f"{hit:.4f} PED")
|
||||
|
|
@ -472,25 +763,46 @@ class LoadoutManagerDialog(QDialog):
|
|||
|
||||
# Build config from pending data
|
||||
weapon = getattr(self, '_pending_weapon', {})
|
||||
amp = getattr(self, '_pending_amplifier', {})
|
||||
armor = getattr(self, '_pending_armor', {})
|
||||
plating = getattr(self, '_pending_plating', {})
|
||||
healing = getattr(self, '_pending_healing', {})
|
||||
mindforce = getattr(self, '_pending_mindforce', {})
|
||||
|
||||
config = LoadoutConfig(
|
||||
name=name,
|
||||
# Weapon
|
||||
weapon_cost_per_shot=weapon.get('cost_per_shot', Decimal("0")),
|
||||
armor_cost_per_hit=armor.get('cost_per_hit', Decimal("0")),
|
||||
healing_cost_per_heal=healing.get('cost_per_heal', Decimal("0")),
|
||||
weapon_name=weapon.get('name', 'None'),
|
||||
weapon_damage=weapon.get('damage', Decimal("0")),
|
||||
weapon_decay_pec=weapon.get('decay_pec', Decimal("0")),
|
||||
weapon_ammo_pec=weapon.get('ammo_pec', Decimal("0")),
|
||||
weapon_api_id=weapon.get('api_id'),
|
||||
# Weapon Amplifier
|
||||
weapon_amp_id=amp.get('api_id'),
|
||||
weapon_amp_name=amp.get('name', 'None'),
|
||||
weapon_amp_decay=amp.get('decay_pec', Decimal("0")),
|
||||
weapon_amp_damage_bonus=amp.get('damage_bonus', Decimal("0")),
|
||||
# Armor
|
||||
armor_cost_per_hit=armor.get('cost_per_hit', Decimal("0")),
|
||||
armor_name=armor.get('name', 'None'),
|
||||
armor_decay_pec=armor.get('decay_pec', Decimal("0")),
|
||||
armor_api_id=armor.get('api_id'),
|
||||
# Plating
|
||||
plating_id=plating.get('api_id'),
|
||||
plating_name=plating.get('name', 'None'),
|
||||
plating_decay=plating.get('decay_pec', Decimal("0")),
|
||||
plating_protection_summary=plating.get('protection_summary', ''),
|
||||
# Healing
|
||||
healing_cost_per_heal=healing.get('cost_per_heal', Decimal("0")),
|
||||
healing_name=healing.get('name', 'None'),
|
||||
healing_decay_pec=healing.get('decay_pec', Decimal("0")),
|
||||
weapon_api_id=weapon.get('api_id'),
|
||||
armor_api_id=armor.get('api_id'),
|
||||
healing_api_id=healing.get('api_id'),
|
||||
# Mindforce
|
||||
mindforce_implant_id=mindforce.get('api_id'),
|
||||
mindforce_implant_name=mindforce.get('name', 'None'),
|
||||
mindforce_implant_decay=mindforce.get('decay_pec', Decimal("0")),
|
||||
mindforce_implant_heal_amount=mindforce.get('heal_amount', Decimal("0")),
|
||||
)
|
||||
|
||||
# Save to file
|
||||
|
|
@ -529,10 +841,14 @@ class LoadoutManagerDialog(QDialog):
|
|||
# Tooltip with costs
|
||||
tooltip = (
|
||||
f"Weapon: {config.weapon_name}\n"
|
||||
f" + Amp: {config.weapon_amp_name if config.weapon_amp_id else 'None'}\n"
|
||||
f"Armor: {config.armor_name}\n"
|
||||
f"Cost/Shot: {config.weapon_cost_per_shot:.4f} PED\n"
|
||||
f"Cost/Hit: {config.armor_cost_per_hit:.4f} PED\n"
|
||||
f"Cost/Heal: {config.healing_cost_per_heal:.4f} PED"
|
||||
f" + Plating: {config.plating_name if config.plating_id else 'None'}\n"
|
||||
f"Healing: {config.healing_name}\n"
|
||||
f" + Chip: {config.mindforce_implant_name if config.mindforce_implant_id else 'None'}\n"
|
||||
f"Cost/Shot: {config.get_total_weapon_cost_per_shot():.4f} PED\n"
|
||||
f"Cost/Hit: {config.get_total_armor_cost_per_hit():.4f} PED\n"
|
||||
f"Cost/Heal: {config.get_total_healing_cost_per_heal():.4f} PED"
|
||||
)
|
||||
item.setToolTip(tooltip)
|
||||
self.saved_list.addItem(item)
|
||||
|
|
@ -569,8 +885,6 @@ class LoadoutManagerDialog(QDialog):
|
|||
if config.weapon_name != "None":
|
||||
self.weapon_btn.setText(config.weapon_name[:30])
|
||||
self.weapon_info.setText(f"Damage: {config.weapon_damage}")
|
||||
self.weapon_decay_label.setText(f"{config.weapon_decay_pec} PEC")
|
||||
self.weapon_ammo_label.setText(f"{config.weapon_ammo_pec}")
|
||||
self.weapon_cost_label.setText(f"{config.weapon_cost_per_shot:.4f} PED")
|
||||
self._pending_weapon = {
|
||||
'name': config.weapon_name,
|
||||
|
|
@ -581,6 +895,20 @@ class LoadoutManagerDialog(QDialog):
|
|||
'cost_per_shot': config.weapon_cost_per_shot,
|
||||
}
|
||||
|
||||
# Weapon Amplifier
|
||||
if config.weapon_amp_id:
|
||||
self.amp_btn.setText(config.weapon_amp_name[:30])
|
||||
self.amp_info.setText(f"+{config.weapon_amp_damage_bonus} Damage")
|
||||
amp_cost = config.weapon_amp_decay / Decimal("100")
|
||||
self.amp_cost_label.setText(f"{amp_cost:.4f} PED")
|
||||
self._pending_amplifier = {
|
||||
'name': config.weapon_amp_name,
|
||||
'api_id': config.weapon_amp_id,
|
||||
'decay_pec': config.weapon_amp_decay,
|
||||
'damage_bonus': config.weapon_amp_damage_bonus,
|
||||
'cost_per_shot': amp_cost,
|
||||
}
|
||||
|
||||
# Armor
|
||||
if config.armor_name != "None":
|
||||
self.armor_btn.setText(config.armor_name[:30])
|
||||
|
|
@ -593,6 +921,20 @@ class LoadoutManagerDialog(QDialog):
|
|||
'cost_per_hit': config.armor_cost_per_hit,
|
||||
}
|
||||
|
||||
# Plating
|
||||
if config.plating_id:
|
||||
self.plating_btn.setText(config.plating_name[:30])
|
||||
self.plating_info.setText(config.plating_protection_summary[:40] if config.plating_protection_summary else "No data")
|
||||
plating_cost = config.plating_decay / Decimal("100")
|
||||
self.plating_cost_label.setText(f"{plating_cost:.4f} PED")
|
||||
self._pending_plating = {
|
||||
'name': config.plating_name,
|
||||
'api_id': config.plating_id,
|
||||
'decay_pec': config.plating_decay,
|
||||
'protection_summary': config.plating_protection_summary,
|
||||
'cost_per_hit': plating_cost,
|
||||
}
|
||||
|
||||
# Healing
|
||||
if config.healing_name != "None":
|
||||
self.healing_btn.setText(config.healing_name[:30])
|
||||
|
|
@ -604,29 +946,64 @@ class LoadoutManagerDialog(QDialog):
|
|||
'cost_per_heal': config.healing_cost_per_heal,
|
||||
}
|
||||
|
||||
# Mindforce
|
||||
if config.mindforce_implant_id:
|
||||
self.mindforce_btn.setText(config.mindforce_implant_name[:30])
|
||||
mindforce_cost = config.mindforce_implant_decay / Decimal("100")
|
||||
self.mindforce_cost_label.setText(f"{mindforce_cost:.4f} PED")
|
||||
self._pending_mindforce = {
|
||||
'name': config.mindforce_implant_name,
|
||||
'api_id': config.mindforce_implant_id,
|
||||
'decay_pec': config.mindforce_implant_decay,
|
||||
'heal_amount': config.mindforce_implant_heal_amount,
|
||||
'cost_per_heal': mindforce_cost,
|
||||
}
|
||||
|
||||
self._update_weapon_total()
|
||||
self._update_armor_total()
|
||||
self._update_healing_total()
|
||||
self._update_summary()
|
||||
self.current_config = config
|
||||
|
||||
def _new_loadout(self):
|
||||
"""Clear all fields for new loadout."""
|
||||
self.name_edit.clear()
|
||||
|
||||
# Reset weapon section
|
||||
self.weapon_btn.setText("Select Weapon...")
|
||||
self.weapon_info.setText("None selected")
|
||||
self.weapon_decay_label.setText("0 PEC")
|
||||
self.weapon_ammo_label.setText("0")
|
||||
self.weapon_cost_label.setText("0.0000 PED")
|
||||
self.amp_btn.setText("Select Amplifier...")
|
||||
self.amp_info.setText("None selected")
|
||||
self.amp_cost_label.setText("0.0000 PED")
|
||||
self.total_weapon_cost_label.setText("0.0000 PED")
|
||||
|
||||
# Reset armor section
|
||||
self.armor_btn.setText("Select Armor...")
|
||||
self.armor_info.setText("None selected")
|
||||
self.armor_cost_label.setText("0.0000 PED")
|
||||
self.plating_btn.setText("Select Plating...")
|
||||
self.plating_info.setText("None selected")
|
||||
self.plating_cost_label.setText("0.0000 PED")
|
||||
self.total_armor_cost_label.setText("0.0000 PED")
|
||||
|
||||
# Reset healing section
|
||||
self.healing_btn.setText("Select Healing...")
|
||||
self.healing_info.setText("None selected")
|
||||
self.healing_cost_label.setText("0.0000 PED")
|
||||
self.mindforce_btn.setText("Select Mindforce Chip...")
|
||||
self.mindforce_info.setText("None selected")
|
||||
self.mindforce_cost_label.setText("0.0000 PED")
|
||||
self.total_healing_cost_label.setText("0.0000 PED")
|
||||
|
||||
# Clear pending data
|
||||
self._pending_weapon = None
|
||||
self._pending_amplifier = None
|
||||
self._pending_armor = None
|
||||
self._pending_plating = None
|
||||
self._pending_healing = None
|
||||
self._pending_mindforce = None
|
||||
|
||||
self._update_summary()
|
||||
self.current_config = None
|
||||
|
||||
|
|
|
|||
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
|
||||
Browse and select mindforce implants from Entropia Nexus API
|
||||
Mindforce Implant/Chip Selector Dialog
|
||||
Uses /medicalchips API endpoint for healing chips
|
||||
"""
|
||||
|
||||
from decimal import Decimal
|
||||
from PyQt6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton,
|
||||
QTreeWidget, QTreeWidgetItem, QHeaderView, QLabel, QDialogButtonBox,
|
||||
QProgressBar, QGroupBox, QFormLayout, QComboBox
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QListWidget, QListWidgetItem, QLineEdit, QGroupBox,
|
||||
QFormLayout, QMessageBox
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QThread, pyqtSignal
|
||||
from PyQt6.QtGui import QColor
|
||||
from typing import Optional, List
|
||||
from PyQt6.QtCore import Qt
|
||||
from decimal import Decimal
|
||||
|
||||
from core.nexus_full_api import get_nexus_api, NexusMindforceImplant
|
||||
from core.nexus_full_api import get_nexus_api
|
||||
|
||||
|
||||
class MindforceImplantLoaderThread(QThread):
|
||||
"""Background thread for loading mindforce implants from API."""
|
||||
implants_loaded = pyqtSignal(list)
|
||||
error_occurred = pyqtSignal(str)
|
||||
class MindforceSelectorDialog(QDialog):
|
||||
"""Dialog for selecting mindforce implants (healing chips)."""
|
||||
|
||||
def run(self):
|
||||
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 = ""):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.preferred_type = implant_type.lower()
|
||||
self.setWindowTitle("Select Mindforce Healing Chip")
|
||||
self.setMinimumSize(600, 500)
|
||||
|
||||
type_names = {
|
||||
"healing": "Healing Chip",
|
||||
"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._selected_chip = None
|
||||
self._chips_cache = []
|
||||
|
||||
self._setup_ui()
|
||||
self._load_data()
|
||||
self._load_chips()
|
||||
|
||||
def _setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(10)
|
||||
|
||||
# Status
|
||||
self.status_label = QLabel("Loading mindforce implants from Entropia Nexus...")
|
||||
layout.addWidget(self.status_label)
|
||||
# Search bar
|
||||
search_layout = QHBoxLayout()
|
||||
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()
|
||||
self.progress.setRange(0, 0)
|
||||
layout.addWidget(self.progress)
|
||||
# Chip 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)
|
||||
|
||||
# Filters
|
||||
filter_layout = QHBoxLayout()
|
||||
# Info panel
|
||||
info_group = QGroupBox("Chip Info")
|
||||
info_layout = QFormLayout(info_group)
|
||||
|
||||
filter_layout.addWidget(QLabel("Type:"))
|
||||
self.type_combo = QComboBox()
|
||||
self.type_combo.addItems(["All", "Healing", "Damage", "Utility"])
|
||||
if self.preferred_type:
|
||||
type_map = {"healing": 1, "damage": 2, "utility": 3}
|
||||
self.type_combo.setCurrentIndex(type_map.get(self.preferred_type, 0))
|
||||
self.type_combo.currentTextChanged.connect(self._filter_implants)
|
||||
filter_layout.addWidget(self.type_combo)
|
||||
self.info_name = QLabel("Select a chip")
|
||||
self.info_heal = QLabel("-")
|
||||
self.info_decay = QLabel("-")
|
||||
self.info_uses = QLabel("-")
|
||||
self.info_mindforce_level = QLabel("-")
|
||||
self.info_cost = QLabel("-")
|
||||
|
||||
filter_layout.addWidget(QLabel("Search:"))
|
||||
self.search_input = QLineEdit()
|
||||
self.search_input.setPlaceholderText("Search implants...")
|
||||
self.search_input.textChanged.connect(self._filter_implants)
|
||||
filter_layout.addWidget(self.search_input)
|
||||
info_layout.addRow("Name:", self.info_name)
|
||||
info_layout.addRow("Heal Amount:", self.info_heal)
|
||||
info_layout.addRow("Decay (PEC):", self.info_decay)
|
||||
info_layout.addRow("Uses/Min:", self.info_uses)
|
||||
info_layout.addRow("Mindforce Level:", self.info_mindforce_level)
|
||||
info_layout.addRow("Cost/Heal:", self.info_cost)
|
||||
|
||||
clear_btn = QPushButton("Clear")
|
||||
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)
|
||||
layout.addWidget(info_group)
|
||||
|
||||
# Buttons
|
||||
buttons = QDialogButtonBox(
|
||||
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
)
|
||||
buttons.accepted.connect(self._on_accept)
|
||||
buttons.rejected.connect(self.reject)
|
||||
self.ok_button = buttons.button(QDialogButtonBox.StandardButton.Ok)
|
||||
self.ok_button.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()
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.addStretch()
|
||||
|
||||
if not implants:
|
||||
item = QTreeWidgetItem()
|
||||
item.setText(0, "No implants available")
|
||||
item.setForeground(0, QColor("#888888"))
|
||||
self.results_tree.addTopLevelItem(item)
|
||||
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_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
|
||||
|
||||
# Sort by decay (lower is better economy)
|
||||
implants = sorted(implants, key=lambda i: i.decay)
|
||||
filtered = [chip for chip in self._chips_cache
|
||||
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:
|
||||
item = QTreeWidgetItem()
|
||||
item.setText(0, implant.name)
|
||||
item.setText(1, implant.implant_type.title())
|
||||
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()
|
||||
# Extract chip data
|
||||
heal_amount = getattr(chip, 'heal_amount', Decimal("0"))
|
||||
decay = Decimal(str(getattr(chip, 'decay', 0)))
|
||||
uses_per_min = getattr(chip, 'uses_per_minute', None)
|
||||
|
||||
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
|
||||
if type_filter != "all":
|
||||
filtered = [i for i in filtered if i.implant_type == type_filter]
|
||||
# Update info panel
|
||||
self.info_name.setText(chip.name)
|
||||
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
|
||||
if search:
|
||||
filtered = [
|
||||
i for i in filtered
|
||||
if search in i.name.lower()
|
||||
or search in i.chip_type.lower()
|
||||
]
|
||||
# Mindforce level if available
|
||||
mindforce_level = "N/A"
|
||||
if hasattr(chip, 'profession_level') and chip.profession_level:
|
||||
mindforce_level = str(chip.profession_level)
|
||||
self.info_mindforce_level.setText(mindforce_level)
|
||||
|
||||
self._populate_results(filtered)
|
||||
|
||||
def _on_selection_changed(self):
|
||||
"""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")
|
||||
# Calculate cost per heal
|
||||
cost_per_heal = decay / Decimal("100")
|
||||
self.info_cost.setText(f"{cost_per_heal:.4f} PED")
|
||||
|
||||
# Color by type
|
||||
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;")
|
||||
self.select_btn.setEnabled(True)
|
||||
|
||||
def _on_double_click(self, item: QTreeWidgetItem, column: int):
|
||||
"""Handle double click."""
|
||||
if item.data(0, Qt.ItemDataRole.UserRole):
|
||||
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)
|
||||
def get_selected_chip(self):
|
||||
"""Get the selected chip data."""
|
||||
return self._selected_chip
|
||||
|
|
|
|||
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