From b8e7a892ffd2ec91208bd9a59d4fdbc4103965a1 Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Sun, 8 Feb 2026 22:42:55 +0000 Subject: [PATCH] feat(gui): add Gear Selector and fix kill counting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove auto-kill counting on loot (was overcounting) - Add GearSelectorDialog to select weapons/armor/tools from Nexus API - Add Tools → Select Gear menu (Ctrl+G) - Selected weapon now shows in HUD during session - Uses mock data from nexus_api.py for now --- ui/gear_selector.py | 230 ++++++++++++++++++++++++++++++++++++++++++++ ui/hud_overlay.py | 6 +- ui/main_window.py | 37 ++++++- 3 files changed, 268 insertions(+), 5 deletions(-) create mode 100644 ui/gear_selector.py diff --git a/ui/gear_selector.py b/ui/gear_selector.py new file mode 100644 index 0000000..b2b8f2e --- /dev/null +++ b/ui/gear_selector.py @@ -0,0 +1,230 @@ +""" +Gear Selector Dialog for Lemontropia Suite +Uses Entropia Nexus API to search and select real gear. +""" + +from PyQt6.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, + QPushButton, QComboBox, QTreeWidget, QTreeWidgetItem, + QDialogButtonBox, QTabWidget, QGroupBox, QFormLayout, + QMessageBox, QHeaderView +) +from PyQt6.QtCore import Qt, pyqtSignal +from decimal import Decimal + +import asyncio +from typing import Optional, List + + +class GearSelectorDialog(QDialog): + """Dialog for selecting gear from Entropia Nexus.""" + + gear_selected = pyqtSignal(str, str, dict) # type, name, stats + + def __init__(self, gear_type: str = "weapon", parent=None): + super().__init__(parent) + self.gear_type = gear_type # weapon, armor, tool + self.selected_gear = None + + self.setWindowTitle(f"Select {gear_type.title()} - Entropia Nexus") + self.setMinimumSize(700, 500) + self.setup_ui() + + # Load initial data + self.load_mock_data() # Use mock data for now + + def setup_ui(self): + layout = QVBoxLayout(self) + layout.setSpacing(10) + + # Search box + search_layout = QHBoxLayout() + search_layout.addWidget(QLabel("Search:")) + + self.search_input = QLineEdit() + self.search_input.setPlaceholderText(f"Search for {self.gear_type}...") + self.search_input.returnPressed.connect(self.on_search) + search_layout.addWidget(self.search_input) + + self.search_btn = QPushButton("Search") + self.search_btn.clicked.connect(self.on_search) + search_layout.addWidget(self.search_btn) + + layout.addLayout(search_layout) + + # Results list + self.results_tree = QTreeWidget() + self.results_tree.setHeaderLabels(["Name", "Damage/Protection", "DPP/Cost", "Range/Depth"]) + 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) + + # Adjust columns + 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.resizeSection(1, 120) + header.resizeSection(2, 100) + header.resizeSection(3, 100) + + 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("Select Gear") + layout.addWidget(button_box) + + def load_mock_data(self): + """Load mock gear data for testing.""" + from core.nexus_api import MOCK_WEAPONS, MOCK_ARMORS, MOCK_TOOLS + + self.results_tree.clear() + + if self.gear_type == "weapon": + for weapon_id, weapon in MOCK_WEAPONS.items(): + item = QTreeWidgetItem([ + weapon.name, + f"{weapon.damage}", + f"{weapon.dpp}", + f"{weapon.range}m" + ]) + item.setData(0, Qt.ItemDataRole.UserRole, { + 'id': weapon_id, + 'type': 'weapon', + 'data': weapon + }) + self.results_tree.addTopLevelItem(item) + + elif self.gear_type == "armor": + for armor_id, armor in MOCK_ARMORS.items(): + total_prot = armor.get_total_protection() + item = QTreeWidgetItem([ + armor.name, + f"{total_prot}", + f"{armor.decay_pec} PEC", + "-" + ]) + item.setData(0, Qt.ItemDataRole.UserRole, { + 'id': armor_id, + 'type': 'armor', + 'data': armor + }) + self.results_tree.addTopLevelItem(item) + + elif self.gear_type == "tool": + for tool_id, tool in MOCK_TOOLS.items(): + item = QTreeWidgetItem([ + tool.name, + f"{tool.depth}m", + f"{tool.decay_pec} PEC", + f"{tool.radius}m" + ]) + item.setData(0, Qt.ItemDataRole.UserRole, { + 'id': tool_id, + 'type': 'tool', + 'data': tool + }) + self.results_tree.addTopLevelItem(item) + + def on_search(self): + """Search for gear.""" + query = self.search_input.text().strip().lower() + if not query: + self.load_mock_data() + return + + # Filter results + for i in range(self.results_tree.topLevelItemCount()): + item = self.results_tree.topLevelItem(i) + name = item.text(0).lower() + item.setHidden(query not in name) + + def on_selection_changed(self): + """Handle selection change.""" + selected = self.results_tree.selectedItems() + if selected: + item_data = selected[0].data(0, Qt.ItemDataRole.UserRole) + self.selected_gear = item_data + self.ok_btn.setEnabled(True) + self.update_stats_preview(item_data) + else: + self.selected_gear = None + self.ok_btn.setEnabled(False) + + def update_stats_preview(self, item_data): + """Update stats preview.""" + # Clear previous + while self.stats_layout.rowCount() > 0: + self.stats_layout.removeRow(0) + + gear = item_data['data'] + + if item_data['type'] == 'weapon': + self.stats_layout.addRow("Damage:", QLabel(f"{gear.damage}")) + self.stats_layout.addRow("DPP:", QLabel(f"{gear.dpp}")) + self.stats_layout.addRow("Decay:", QLabel(f"{gear.decay_pec} PEC")) + self.stats_layout.addRow("Ammo:", QLabel(f"{gear.ammo_pec} PEC")) + self.stats_layout.addRow("Range:", QLabel(f"{gear.range}m")) + self.stats_layout.addRow("Attacks/min:", QLabel(f"{gear.attacks_per_min}")) + self.stats_layout.addRow("Cost/hour:", QLabel(f"{gear.calculate_cost_per_hour():.2f} PED")) + + elif item_data['type'] == 'armor': + self.stats_layout.addRow("Decay:", QLabel(f"{gear.decay_pec} PEC")) + self.stats_layout.addRow("Total Protection:", QLabel(f"{gear.get_total_protection()}")) + for dmg_type, value in gear.protection.items(): + self.stats_layout.addRow(f" {dmg_type.title()}:", QLabel(f"{value}")) + + elif item_data['type'] == 'tool': + self.stats_layout.addRow("Type:", QLabel(f"{gear.tool_type}")) + self.stats_layout.addRow("Depth:", QLabel(f"{gear.depth}m")) + self.stats_layout.addRow("Radius:", QLabel(f"{gear.radius}m")) + self.stats_layout.addRow("Decay:", QLabel(f"{gear.decay_pec} PEC")) + if gear.probe_cost: + self.stats_layout.addRow("Cost/drop:", QLabel(f"{gear.calculate_cost_per_drop():.3f} PED")) + + 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: + gear = self.selected_gear['data'] + self.gear_selected.emit( + self.selected_gear['type'], + gear.name, + { + 'id': self.selected_gear['id'], + 'dpp': getattr(gear, 'dpp', None), + 'cost_per_hour': getattr(gear, 'calculate_cost_per_hour', lambda: 0)() + } + ) + 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 gear:", dialog.selected_gear) + + sys.exit(0) \ No newline at end of file diff --git a/ui/hud_overlay.py b/ui/hud_overlay.py index 40150a1..476b162 100644 --- a/ui/hud_overlay.py +++ b/ui/hud_overlay.py @@ -587,9 +587,9 @@ class HUDOverlay(QWidget): return self._stats.loot_total += value_ped - # Count actual loot as kills (exclude Shrapnel and Universal Ammo) - if item_name not in ('Shrapnel', 'Universal Ammo'): - self._stats.kills += 1 + # Note: Kill counting should be based on actual kill messages from log + # For now, we don't auto-increment kills on loot to avoid overcounting + # (Multiple items from one kill would count as multiple kills) self._refresh_display() self.stats_updated.emit(self._stats.to_dict()) diff --git a/ui/main_window.py b/ui/main_window.py index 3966f35..6770e7b 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -251,6 +251,10 @@ class MainWindow(QMainWindow): self.current_session_id: Optional[int] = None self._current_db_session_id: Optional[int] = None + # Selected gear + self._selected_weapon: Optional[str] = None + self._selected_weapon_stats: Optional[dict] = None + # Setup UI self.setup_ui() self.apply_dark_theme() @@ -533,6 +537,13 @@ 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) + + tools_menu.addSeparator() + loadout_action = QAction("&Loadout Manager", self) loadout_action.setShortcut("Ctrl+L") loadout_action.triggered.connect(self.on_loadout_manager) @@ -899,8 +910,9 @@ class MainWindow(QMainWindow): # Show HUD and start session tracking self.hud.show() - self.hud.start_session(weapon="Unknown", loadout="Default") - self.log_info("HUD", "HUD overlay shown and session tracking started") + weapon_name = self._selected_weapon or "Unknown" + self.hud.start_session(weapon=weapon_name, loadout="Default") + self.log_info("HUD", f"HUD shown - Weapon: {weapon_name}") def _setup_log_watcher_callbacks(self): """Setup LogWatcher event callbacks.""" @@ -1244,6 +1256,27 @@ class MainWindow(QMainWindow): dialog = LoadoutManagerDialog(self) dialog.exec() + def on_select_gear(self): + """Open Gear Selector dialog.""" + from ui.gear_selector import GearSelectorDialog + + # For now, default to weapon selection + dialog = GearSelectorDialog("weapon", 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 + }) + def on_about(self): """Show about dialog.""" QMessageBox.about(