From 3cd0613e10c5e1fb20948007692cf370dc9cfe6d Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Sun, 8 Feb 2026 23:11:07 +0000 Subject: [PATCH] feat(gui): full gear selection - weapons, armors, and finders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Gear Selector now supports: Weapons, Armors, Finders - Weapons: 3,099 items with DPP, cost/hour, damage - Armors: 1,985 items with protection values - Finders: 106 items with depth/radius - Menu: Tools → Select Gear → Weapon/Armor/Finder - Shortcuts: Ctrl+W (weapon), Ctrl+Shift+A (armor), Ctrl+Shift+F (finder) - Sync API for simpler code --- ui/gear_selector.py | 292 ++++++++++++++++++++++++++++++++++++++++++-- ui/main_window.py | 48 +++++--- 2 files changed, 315 insertions(+), 25 deletions(-) diff --git a/ui/gear_selector.py b/ui/gear_selector.py index adf4b2c..ba48b2f 100644 --- a/ui/gear_selector.py +++ b/ui/gear_selector.py @@ -13,7 +13,7 @@ from PyQt6.QtCore import Qt, pyqtSignal, QThread from decimal import Decimal from typing import Optional, List -from core.nexus_api import EntropiaNexusAPI, WeaponStats +from core.nexus_api import EntropiaNexusAPI, WeaponStats, ArmorStats, FinderStats class WeaponLoaderThread(QThread): @@ -22,18 +22,41 @@ class WeaponLoaderThread(QThread): weapons_loaded = pyqtSignal(list) error_occurred = pyqtSignal(str) - def __init__(self, api: EntropiaNexusAPI): - super().__init__() - self.api = api + def run(self): + try: + api = EntropiaNexusAPI() + weapons = api.get_all_weapons() + self.weapons_loaded.emit(weapons) + except Exception as e: + self.error_occurred.emit(str(e)) + + +class ArmorLoaderThread(QThread): + """Thread to load armors from API.""" + + armors_loaded = pyqtSignal(list) + error_occurred = pyqtSignal(str) def run(self): try: - import asyncio - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - weapons = loop.run_until_complete(self.api.get_all_weapons()) - self.weapons_loaded.emit(weapons) - loop.close() + api = EntropiaNexusAPI() + armors = api.get_all_armors() + self.armors_loaded.emit(armors) + except Exception as e: + self.error_occurred.emit(str(e)) + + +class FinderLoaderThread(QThread): + """Thread to load finders from API.""" + + finders_loaded = pyqtSignal(list) + error_occurred = pyqtSignal(str) + + def run(self): + try: + api = EntropiaNexusAPI() + finders = api.get_all_finders() + self.finders_loaded.emit(finders) except Exception as e: self.error_occurred.emit(str(e)) @@ -47,7 +70,254 @@ class GearSelectorDialog(QDialog): super().__init__(parent) self.gear_type = gear_type self.selected_gear = None - self.weapons: List[WeaponStats] = [] + self.items: List = [] + self.api = EntropiaNexusAPI() + + self.setWindowTitle(f"Select {gear_type.title()} - Entropia Nexus") + self.setMinimumSize(800, 600) + self.setup_ui() + self.load_data_async() + + def setup_ui(self): + layout = QVBoxLayout(self) + layout.setSpacing(10) + + # Status label + self.status_label = QLabel(f"Loading {self.gear_type}s from Entropia Nexus...") + layout.addWidget(self.status_label) + + # Search box + search_layout = QHBoxLayout() + search_layout.addWidget(QLabel("Search:")) + + self.search_input = QLineEdit() + self.search_input.setPlaceholderText(f"Search {self.gear_type}s...") + self.search_input.returnPressed.connect(self.on_search) + self.search_input.setEnabled(False) + search_layout.addWidget(self.search_input) + + self.search_btn = QPushButton("Search") + self.search_btn.clicked.connect(self.on_search) + self.search_btn.setEnabled(False) + search_layout.addWidget(self.search_btn) + + layout.addLayout(search_layout) + + # Results list + self.results_tree = QTreeWidget() + self.setup_columns() + self.results_tree.setAlternatingRowColors(True) + self.results_tree.setSelectionMode(QTreeWidget.SelectionMode.SingleSelection) + self.results_tree.itemSelectionChanged.connect(self.on_selection_changed) + self.results_tree.itemDoubleClicked.connect(self.on_item_double_clicked) + layout.addWidget(self.results_tree) + + # Stats preview + self.stats_group = QGroupBox("Stats Preview") + self.stats_layout = QFormLayout(self.stats_group) + self.stats_layout.addRow("Select an item to view stats", QLabel("")) + layout.addWidget(self.stats_group) + + # Buttons + button_box = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + ) + button_box.accepted.connect(self.on_accept) + button_box.rejected.connect(self.reject) + self.ok_btn = button_box.button(QDialogButtonBox.StandardButton.Ok) + self.ok_btn.setEnabled(False) + self.ok_btn.setText(f"Select {self.gear_type.title()}") + layout.addWidget(button_box) + + def setup_columns(self): + """Setup tree columns based on gear type.""" + if self.gear_type == "weapon": + self.results_tree.setHeaderLabels(["Name", "Type", "Damage", "DPP", "Cost/h", "Eff%"]) + header = self.results_tree.header() + header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) + header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed) + header.setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed) + header.setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed) + header.setSectionResizeMode(4, QHeaderView.ResizeMode.Fixed) + header.setSectionResizeMode(5, QHeaderView.ResizeMode.Fixed) + header.resizeSection(1, 100) + header.resizeSection(2, 60) + header.resizeSection(3, 60) + header.resizeSection(4, 70) + header.resizeSection(5, 50) + elif self.gear_type == "armor": + self.results_tree.setHeaderLabels(["Name", "Protection", "Durability"]) + header = self.results_tree.header() + header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) + header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed) + header.setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed) + header.resizeSection(1, 100) + header.resizeSection(2, 100) + elif self.gear_type == "finder": + self.results_tree.setHeaderLabels(["Name", "Type", "Depth", "Radius", "Decay"]) + header = self.results_tree.header() + header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) + for i in range(1, 5): + header.setSectionResizeMode(i, QHeaderView.ResizeMode.Fixed) + header.resizeSection(1, 100) + header.resizeSection(2, 80) + header.resizeSection(3, 80) + header.resizeSection(4, 80) + + def load_data_async(self): + """Load data in background thread.""" + if self.gear_type == "weapon": + self.loader_thread = WeaponLoaderThread() + self.loader_thread.weapons_loaded.connect(self.on_data_loaded) + self.loader_thread.error_occurred.connect(self.on_load_error) + elif self.gear_type == "armor": + self.loader_thread = ArmorLoaderThread() + self.loader_thread.armors_loaded.connect(self.on_data_loaded) + self.loader_thread.error_occurred.connect(self.on_load_error) + elif self.gear_type == "finder": + self.loader_thread = FinderLoaderThread() + self.loader_thread.finders_loaded.connect(self.on_data_loaded) + self.loader_thread.error_occurred.connect(self.on_load_error) + else: + return + + self.loader_thread.start() + + def on_data_loaded(self, items): + """Handle loaded data.""" + self.items = items + self.status_label.setText(f"Loaded {len(items)} {self.gear_type}s from Entropia Nexus") + self.search_input.setEnabled(True) + self.search_btn.setEnabled(True) + self.populate_tree(items[:100]) + + def on_load_error(self, error): + """Handle load error.""" + self.status_label.setText(f"Error: {error}") + + def populate_tree(self, items): + """Populate tree with items.""" + self.results_tree.clear() + + if self.gear_type == "weapon": + for w in items: + item = QTreeWidgetItem([ + w.name, + f"{w.type} {w.category}", + str(w.total_damage), + f"{w.dpp:.2f}", + f"{w.cost_per_hour:.0f}", + f"{w.efficiency:.0f}" if w.efficiency else "-" + ]) + item.setData(0, Qt.ItemDataRole.UserRole, w) + self.results_tree.addTopLevelItem(item) + elif self.gear_type == "armor": + for a in items: + item = QTreeWidgetItem([ + a.name, + str(a.total_protection), + str(a.durability) + ]) + item.setData(0, Qt.ItemDataRole.UserRole, a) + self.results_tree.addTopLevelItem(item) + elif self.gear_type == "finder": + for f in items: + item = QTreeWidgetItem([ + f.name, + f.type, + str(f.depth), + str(f.radius), + str(f.decay) + ]) + item.setData(0, Qt.ItemDataRole.UserRole, f) + self.results_tree.addTopLevelItem(item) + + def on_search(self): + """Search items.""" + query = self.search_input.text().strip().lower() + if not query: + self.populate_tree(self.items[:100]) + return + + if self.gear_type == "weapon": + results = self.api.search_weapons(query) + else: + results = [i for i in self.items if query in i.name.lower()] + + self.populate_tree(results) + self.status_label.setText(f"Found {len(results)} {self.gear_type}s matching '{query}'") + + def on_selection_changed(self): + """Handle selection change.""" + selected = self.results_tree.selectedItems() + if selected: + item = selected[0].data(0, Qt.ItemDataRole.UserRole) + self.selected_gear = item + self.ok_btn.setEnabled(True) + self.update_stats_preview(item) + else: + self.selected_gear = None + self.ok_btn.setEnabled(False) + + def update_stats_preview(self, item): + """Update stats preview.""" + while self.stats_layout.rowCount() > 0: + self.stats_layout.removeRow(0) + + if self.gear_type == "weapon" and isinstance(item, WeaponStats): + self.stats_layout.addRow("Name:", QLabel(item.name)) + self.stats_layout.addRow("Type:", QLabel(f"{item.type} {item.category}")) + self.stats_layout.addRow("Damage:", QLabel(str(item.total_damage))) + self.stats_layout.addRow("DPP:", QLabel(f"{item.dpp:.3f}")) + self.stats_layout.addRow("Decay:", QLabel(f"{item.decay} PEC" if item.decay else "-")) + self.stats_layout.addRow("Ammo:", QLabel(f"{item.ammo_burn} per shot" if item.ammo_burn else "-")) + self.stats_layout.addRow("Cost/hour:", QLabel(f"{item.cost_per_hour:.2f} PED")) + elif self.gear_type == "armor" and isinstance(item, ArmorStats): + self.stats_layout.addRow("Name:", QLabel(item.name)) + self.stats_layout.addRow("Total Protection:", QLabel(str(item.total_protection))) + self.stats_layout.addRow("Durability:", QLabel(str(item.durability))) + elif self.gear_type == "finder" and isinstance(item, FinderStats): + self.stats_layout.addRow("Name:", QLabel(item.name)) + self.stats_layout.addRow("Depth:", QLabel(f"{item.depth}m")) + self.stats_layout.addRow("Radius:", QLabel(f"{item.radius}m")) + self.stats_layout.addRow("Decay:", QLabel(f"{item.decay} PEC")) + + def on_item_double_clicked(self, item, column): + """Handle double click.""" + self.on_accept() + + def on_accept(self): + """Handle OK button.""" + if self.selected_gear: + if self.gear_type == "weapon": + w = self.selected_gear + self.gear_selected.emit("weapon", w.name, { + 'id': w.id, 'item_id': w.item_id, 'dpp': float(w.dpp), + 'cost_per_hour': float(w.cost_per_hour), 'total_damage': float(w.total_damage), + }) + elif self.gear_type == "armor": + a = self.selected_gear + self.gear_selected.emit("armor", a.name, { + 'id': a.id, 'item_id': a.item_id, 'protection': float(a.total_protection), + }) + elif self.gear_type == "finder": + f = self.selected_gear + self.gear_selected.emit("finder", f.name, { + 'id': f.id, 'item_id': f.item_id, 'depth': float(f.depth), 'radius': float(f.radius), + }) + self.accept() + + +if __name__ == "__main__": + import sys + from PyQt6.QtWidgets import QApplication + + app = QApplication(sys.argv) + dialog = GearSelectorDialog("weapon") + if dialog.exec() == QDialog.DialogCode.Accepted: + print("Selected:", dialog.selected_gear.name if dialog.selected_gear else None) + sys.exit(0) + self.api = EntropiaNexusAPI() self.setWindowTitle(f"Select {gear_type.title()} - Entropia Nexus") diff --git a/ui/main_window.py b/ui/main_window.py index 6770e7b..6572697 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -254,6 +254,10 @@ class MainWindow(QMainWindow): # Selected gear self._selected_weapon: Optional[str] = None self._selected_weapon_stats: Optional[dict] = None + self._selected_armor: Optional[str] = None + self._selected_armor_stats: Optional[dict] = None + self._selected_finder: Optional[str] = None + self._selected_finder_stats: Optional[dict] = None # Setup UI self.setup_ui() @@ -537,10 +541,23 @@ class MainWindow(QMainWindow): # Tools menu tools_menu = menubar.addMenu("&Tools") - gear_action = QAction("&Select Gear", self) - gear_action.setShortcut("Ctrl+G") - gear_action.triggered.connect(self.on_select_gear) - tools_menu.addAction(gear_action) + # Select Gear submenu + select_gear_menu = tools_menu.addMenu("Select &Gear") + + select_weapon_action = QAction("&Weapon", self) + select_weapon_action.setShortcut("Ctrl+W") + select_weapon_action.triggered.connect(lambda: self.on_select_gear("weapon")) + select_gear_menu.addAction(select_weapon_action) + + select_armor_action = QAction("&Armor", self) + select_armor_action.setShortcut("Ctrl+Shift+A") + select_armor_action.triggered.connect(lambda: self.on_select_gear("armor")) + select_gear_menu.addAction(select_armor_action) + + select_finder_action = QAction("&Finder", self) + select_finder_action.setShortcut("Ctrl+Shift+F") + select_finder_action.triggered.connect(lambda: self.on_select_gear("finder")) + select_gear_menu.addAction(select_finder_action) tools_menu.addSeparator() @@ -1256,26 +1273,29 @@ class MainWindow(QMainWindow): dialog = LoadoutManagerDialog(self) dialog.exec() - def on_select_gear(self): + def on_select_gear(self, gear_type: str = "weapon"): """Open Gear Selector dialog.""" from ui.gear_selector import GearSelectorDialog - # For now, default to weapon selection - dialog = GearSelectorDialog("weapon", self) + dialog = GearSelectorDialog(gear_type, self) dialog.gear_selected.connect(self.on_gear_selected) dialog.exec() def on_gear_selected(self, gear_type: str, name: str, stats: dict): """Handle gear selection.""" - self._selected_weapon = name - self._selected_weapon_stats = stats self.log_info("Gear", f"Selected {gear_type}: {name}") - # If session is active, update HUD - if self.session_state == SessionState.RUNNING: - self.hud.update_stats({ - 'weapon': name - }) + if gear_type == "weapon": + self._selected_weapon = name + self._selected_weapon_stats = stats + if self.session_state == SessionState.RUNNING: + self.hud.update_stats({'weapon': name}) + elif gear_type == "armor": + self._selected_armor = name + self._selected_armor_stats = stats + elif gear_type == "finder": + self._selected_finder = name + self._selected_finder_stats = stats def on_about(self): """Show about dialog."""