diff --git a/core/nexus_full_api.py b/core/nexus_full_api.py new file mode 100644 index 0000000..a8764a1 --- /dev/null +++ b/core/nexus_full_api.py @@ -0,0 +1,445 @@ +""" +Complete Entropia Nexus API Integration for Lemontropia Suite +Fetches all gear types: weapons, armors, plates, attachments, enhancers, healing, rings, clothes, pets +""" + +from decimal import Decimal +from dataclasses import dataclass +from typing import Optional, List, Dict, Any +import requests +import json +import logging +from functools import lru_cache + +logger = logging.getLogger(__name__) + +NEXUS_API_BASE = "https://api.entropianexus.com" + +# Cache durations +CACHE_WEAPONS = 3600 # 1 hour +CACHE_ARMORS = 3600 +CACHE_HEALING = 3600 + + +@dataclass +class NexusItem: + """Base class for all Nexus items.""" + id: int + name: str + item_id: str + category: str + + @classmethod + def from_api(cls, data: Dict[str, Any]) -> "NexusItem": + """Create from API response.""" + raise NotImplementedError + + +@dataclass +class NexusWeapon(NexusItem): + """Weapon from Entropia Nexus API.""" + damage: Decimal + decay: Decimal # PEC + ammo_burn: int + uses_per_minute: int + dpp: Decimal + cost_per_hour: Decimal + efficiency: Decimal + range_val: Decimal + type: str = "" + + @classmethod + def from_api(cls, data: Dict[str, Any]) -> "NexusWeapon": + """Create from API response.""" + props = data.get('properties', {}) + economy = props.get('Economy', {}) + damage = props.get('Damage', {}) + + return cls( + id=data.get('id', 0), + name=data.get('name', 'Unknown'), + item_id=str(data.get('id', 0)), + category='weapon', + damage=Decimal(str(damage.get('Total', 0))), + decay=Decimal(str(economy.get('Decay', 0))), + ammo_burn=int(economy.get('AmmoBurn', 0)), + uses_per_minute=int(economy.get('UsesPerMinute', 0)), + dpp=Decimal(str(economy.get('DPP', 0))), + cost_per_hour=Decimal(str(economy.get('CostPerHour', 0))), + efficiency=Decimal(str(props.get('Efficiency', 0))), + range_val=Decimal(str(props.get('Range', 0))), + type=data.get('type', ''), + ) + + +@dataclass +class NexusArmor(NexusItem): + """Armor from Entropia Nexus API.""" + durability: int + protection_impact: Decimal + protection_cut: Decimal + protection_stab: Decimal + protection_burn: Decimal + protection_cold: Decimal + protection_acid: Decimal + protection_electric: Decimal + type: str = "" + + @classmethod + def from_api(cls, data: Dict[str, Any]) -> "NexusArmor": + """Create from API response.""" + props = data.get('properties', {}) + protection = props.get('Protection', {}) + economy = props.get('Economy', {}) + + return cls( + id=data.get('id', 0), + name=data.get('name', 'Unknown'), + item_id=str(data.get('id', 0)), + category='armor', + durability=int(economy.get('Durability', 2000)), + protection_impact=Decimal(str(protection.get('Impact', 0))), + protection_cut=Decimal(str(protection.get('Cut', 0))), + protection_stab=Decimal(str(protection.get('Stab', 0))), + protection_burn=Decimal(str(protection.get('Burn', 0))), + protection_cold=Decimal(str(protection.get('Cold', 0))), + protection_acid=Decimal(str(protection.get('Acid', 0))), + protection_electric=Decimal(str(protection.get('Electric', 0))), + type=data.get('type', ''), + ) + + +@dataclass +class NexusPlate(NexusItem): + """Armor plate from Entropia Nexus API.""" + protection_impact: Decimal + protection_cut: Decimal + protection_stab: Decimal + protection_burn: Decimal + protection_cold: Decimal + decay: Decimal + + @classmethod + def from_api(cls, data: Dict[str, Any]) -> "NexusPlate": + """Create from API response.""" + props = data.get('properties', {}) + protection = props.get('Protection', {}) + + return cls( + id=data.get('id', 0), + name=data.get('name', 'Unknown'), + item_id=str(data.get('id', 0)), + category='plate', + protection_impact=Decimal(str(protection.get('Impact', 0))), + protection_cut=Decimal(str(protection.get('Cut', 0))), + protection_stab=Decimal(str(protection.get('Stab', 0))), + protection_burn=Decimal(str(protection.get('Burn', 0))), + protection_cold=Decimal(str(protection.get('Cold', 0))), + decay=Decimal(str(props.get('Decay', 0))), + ) + + +@dataclass +class NexusAttachment(NexusItem): + """Weapon attachment from Entropia Nexus API.""" + attachment_type: str # 'scope', 'sight', 'amplifier', 'absorber' + damage_bonus: Decimal + range_bonus: Decimal + decay: Decimal + efficiency_bonus: Decimal + + @classmethod + def from_api(cls, data: Dict[str, Any]) -> "NexusAttachment": + """Create from API response.""" + props = data.get('properties', {}) + + return cls( + id=data.get('id', 0), + name=data.get('name', 'Unknown'), + item_id=str(data.get('id', 0)), + category='attachment', + attachment_type=data.get('type', ''), + damage_bonus=Decimal(str(props.get('DamageBonus', 0))), + range_bonus=Decimal(str(props.get('RangeBonus', 0))), + decay=Decimal(str(props.get('Decay', 0))), + efficiency_bonus=Decimal(str(props.get('Efficiency', 0))), + ) + + +@dataclass +class NexusEnhancer(NexusItem): + """Weapon/Armor enhancer from Entropia Nexus API.""" + enhancer_type: str # 'damage', 'economy', 'range', 'accuracy', etc. + tier: int + effect_value: Decimal + break_chance: Decimal + + @classmethod + def from_api(cls, data: Dict[str, Any]) -> "NexusEnhancer": + """Create from API response.""" + props = data.get('properties', {}) + + return cls( + id=data.get('id', 0), + name=data.get('name', 'Unknown'), + item_id=str(data.get('id', 0)), + category='enhancer', + enhancer_type=data.get('type', ''), + tier=int(props.get('Tier', 1)), + effect_value=Decimal(str(props.get('Effect', 0))), + break_chance=Decimal(str(props.get('BreakChance', 0.01))), + ) + + +@dataclass +class NexusHealingTool(NexusItem): + """Healing tool from Entropia Nexus API.""" + heal_amount: Decimal + decay: Decimal # PEC per heal + heal_per_pec: Decimal + type: str # 'fap', 'chip', 'pack' + profession_level: int = 0 + is_limited: bool = False + + @classmethod + def from_api(cls, data: Dict[str, Any]) -> "NexusHealingTool": + """Create from API response.""" + props = data.get('properties', {}) + economy = props.get('Economy', {}) + heal = props.get('Heal', {}) + + decay = Decimal(str(economy.get('Decay', 0))) + heal_amount = Decimal(str(heal.get('Amount', 0))) + heal_per_pec = heal_amount / decay if decay > 0 else Decimal('0') + + return cls( + id=data.get('id', 0), + name=data.get('name', 'Unknown'), + item_id=str(data.get('id', 0)), + category='healing', + heal_amount=heal_amount, + decay=decay, + heal_per_pec=heal_per_pec, + type=data.get('type', 'fap'), + profession_level=int(props.get('RequiredLevel', 0)), + is_limited='(L)' in data.get('name', ''), + ) + + +@dataclass +class NexusRing(NexusItem): + """Ring from Entropia Nexus API.""" + effect_type: str + effect_value: Decimal + is_limited: bool + + @classmethod + def from_api(cls, data: Dict[str, Any]) -> "NexusRing": + """Create from API response.""" + props = data.get('properties', {}) + + return cls( + id=data.get('id', 0), + name=data.get('name', 'Unknown'), + item_id=str(data.get('id', 0)), + category='ring', + effect_type=props.get('EffectType', ''), + effect_value=Decimal(str(props.get('EffectValue', 0))), + is_limited='(L)' in data.get('name', ''), + ) + + +@dataclass +class NexusClothing(NexusItem): + """Clothing item from Entropia Nexus API.""" + slot: str # 'face', 'body', 'legs', 'feet' + buffs: Dict[str, Decimal] + is_cosmetic: bool + + @classmethod + def from_api(cls, data: Dict[str, Any]) -> "NexusClothing": + """Create from API response.""" + props = data.get('properties', {}) + buffs = {k: Decimal(str(v)) for k, v in props.get('Buffs', {}).items()} + + return cls( + id=data.get('id', 0), + name=data.get('name', 'Unknown'), + item_id=str(data.get('id', 0)), + category='clothing', + slot=data.get('slot', 'body'), + buffs=buffs, + is_cosmetic=props.get('IsCosmetic', True), + ) + + +@dataclass +class NexusPet(NexusItem): + """Pet from Entropia Nexus API.""" + effect_type: str + effect_value: Decimal + level_required: int + + @classmethod + def from_api(cls, data: Dict[str, Any]) -> "NexusPet": + """Create from API response.""" + props = data.get('properties', {}) + + return cls( + id=data.get('id', 0), + name=data.get('name', 'Unknown'), + item_id=str(data.get('id', 0)), + category='pet', + effect_type=props.get('EffectType', ''), + effect_value=Decimal(str(props.get('EffectValue', 0))), + level_required=int(props.get('LevelRequired', 0)), + ) + + +class EntropiaNexusFullAPI: + """Complete Entropia Nexus API client for all gear types.""" + + def __init__(self): + self.base_url = NEXUS_API_BASE + self._weapons_cache: Optional[List[NexusWeapon]] = None + self._armors_cache: Optional[List[NexusArmor]] = None + self._plates_cache: Optional[List[NexusPlate]] = None + self._attachments_cache: Optional[List[NexusAttachment]] = None + self._enhancers_cache: Optional[List[NexusEnhancer]] = None + self._healing_cache: Optional[List[NexusHealingTool]] = None + self._rings_cache: Optional[List[NexusRing]] = None + self._clothing_cache: Optional[List[NexusClothing]] = None + self._pets_cache: Optional[List[NexusPet]] = None + + def _fetch(self, endpoint: str) -> List[Dict]: + """Fetch data from API endpoint.""" + try: + url = f"{self.base_url}/{endpoint}" + logger.info(f"Fetching from {url}") + response = requests.get(url, timeout=30) + response.raise_for_status() + return response.json() + except Exception as e: + logger.error(f"Failed to fetch {endpoint}: {e}") + return [] + + def get_all_weapons(self, force_refresh: bool = False) -> List[NexusWeapon]: + """Fetch all weapons from Nexus API.""" + if self._weapons_cache is None or force_refresh: + data = self._fetch("weapons") + self._weapons_cache = [NexusWeapon.from_api(item) for item in data] + logger.info(f"Loaded {len(self._weapons_cache)} weapons") + return self._weapons_cache + + def get_all_armors(self, force_refresh: bool = False) -> List[NexusArmor]: + """Fetch all armors from Nexus API.""" + if self._armors_cache is None or force_refresh: + data = self._fetch("armors") + self._armors_cache = [NexusArmor.from_api(item) for item in data] + logger.info(f"Loaded {len(self._armors_cache)} armors") + return self._armors_cache + + def get_all_plates(self, force_refresh: bool = False) -> List[NexusPlate]: + """Fetch all plates from Nexus API.""" + if self._plates_cache is None or force_refresh: + data = self._fetch("plates") + self._plates_cache = [NexusPlate.from_api(item) for item in data] + logger.info(f"Loaded {len(self._plates_cache)} plates") + return self._plates_cache + + def get_all_attachments(self, force_refresh: bool = False) -> List[NexusAttachment]: + """Fetch all attachments from Nexus API.""" + if self._attachments_cache is None or force_refresh: + data = self._fetch("attachments") + self._attachments_cache = [NexusAttachment.from_api(item) for item in data] + logger.info(f"Loaded {len(self._attachments_cache)} attachments") + return self._attachments_cache + + def get_all_enhancers(self, force_refresh: bool = False) -> List[NexusEnhancer]: + """Fetch all enhancers from Nexus API.""" + if self._enhancers_cache is None or force_refresh: + data = self._fetch("enhancers") + self._enhancers_cache = [NexusEnhancer.from_api(item) for item in data] + logger.info(f"Loaded {len(self._enhancers_cache)} enhancers") + return self._enhancers_cache + + def get_all_healing_tools(self, force_refresh: bool = False) -> List[NexusHealingTool]: + """Fetch all healing tools from Nexus API.""" + if self._healing_cache is None or force_refresh: + data = self._fetch("medicaltools") + self._healing_cache = [NexusHealingTool.from_api(item) for item in data] + logger.info(f"Loaded {len(self._healing_cache)} healing tools") + return self._healing_cache + + def get_all_rings(self, force_refresh: bool = False) -> List[NexusRing]: + """Fetch all rings from Nexus API.""" + if self._rings_cache is None or force_refresh: + data = self._fetch("rings") + self._rings_cache = [NexusRing.from_api(item) for item in data] + logger.info(f"Loaded {len(self._rings_cache)} rings") + return self._rings_cache + + def get_all_clothing(self, force_refresh: bool = False) -> List[NexusClothing]: + """Fetch all clothing from Nexus API.""" + if self._clothing_cache is None or force_refresh: + data = self._fetch("clothing") + self._clothing_cache = [NexusClothing.from_api(item) for item in data] + logger.info(f"Loaded {len(self._clothing_cache)} clothing items") + return self._clothing_cache + + def get_all_pets(self, force_refresh: bool = False) -> List[NexusPet]: + """Fetch all pets from Nexus API.""" + if self._pets_cache is None or force_refresh: + data = self._fetch("pets") + self._pets_cache = [NexusPet.from_api(item) for item in data] + logger.info(f"Loaded {len(self._pets_cache)} pets") + return self._pets_cache + + def search_weapons(self, query: str) -> List[NexusWeapon]: + """Search weapons by name.""" + weapons = self.get_all_weapons() + query_lower = query.lower() + return [w for w in weapons if query_lower in w.name.lower()] + + def search_armors(self, query: str) -> List[NexusArmor]: + """Search armors by name.""" + armors = self.get_all_armors() + query_lower = query.lower() + return [a for a in armors if query_lower in a.name.lower()] + + def search_healing_tools(self, query: str) -> List[NexusHealingTool]: + """Search healing tools by name.""" + tools = self.get_all_healing_tools() + query_lower = query.lower() + return [t for t in tools if query_lower in t.name.lower()] + + def search_plates(self, query: str) -> List[NexusPlate]: + """Search plates by name.""" + plates = self.get_all_plates() + query_lower = query.lower() + return [p for p in plates if query_lower in p.name.lower()] + + def search_all(self, query: str) -> Dict[str, List]: + """Search across all gear types.""" + return { + 'weapons': self.search_weapons(query), + 'armors': self.search_armors(query), + 'plates': self.search_plates(query), + 'healing': self.search_healing_tools(query), + 'attachments': [a for a in self.get_all_attachments() if query.lower() in a.name.lower()], + 'enhancers': [e for e in self.get_all_enhancers() if query.lower() in e.name.lower()], + 'rings': [r for r in self.get_all_rings() if query.lower() in r.name.lower()], + 'clothing': [c for c in self.get_all_clothing() if query.lower() in c.name.lower()], + 'pets': [p for p in self.get_all_pets() if query.lower() in p.name.lower()], + } + + +# Global API instance +_nexus_api = None + +def get_nexus_api() -> EntropiaNexusFullAPI: + """Get the global Nexus API instance.""" + global _nexus_api + if _nexus_api is None: + _nexus_api = EntropiaNexusFullAPI() + return _nexus_api \ No newline at end of file diff --git a/ui/armor_selector.py b/ui/armor_selector.py new file mode 100644 index 0000000..e3aa2b7 --- /dev/null +++ b/ui/armor_selector.py @@ -0,0 +1,209 @@ +""" +Searchable Armor Selector for Lemontropia Suite +Browse and search armors from Entropia Nexus API +""" + +from decimal import Decimal +from PyQt6.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, + QTreeWidget, QTreeWidgetItem, QHeaderView, QLabel, QDialogButtonBox, + QProgressBar, QGroupBox, QFormLayout +) +from PyQt6.QtCore import Qt, QThread, pyqtSignal +from typing import Optional, List + +from core.nexus_full_api import get_nexus_api, NexusArmor + + +class ArmorLoaderThread(QThread): + """Background thread for loading armors from API.""" + armors_loaded = pyqtSignal(list) + error_occurred = pyqtSignal(str) + + def run(self): + try: + api = get_nexus_api() + armors = api.get_all_armors() + self.armors_loaded.emit(armors) + except Exception as e: + self.error_occurred.emit(str(e)) + + +class ArmorSelectorDialog(QDialog): + """Dialog for selecting armors from Entropia Nexus API with search.""" + + armor_selected = pyqtSignal(NexusArmor) + + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Select Armor - Entropia Nexus") + self.setMinimumSize(1000, 700) + + self.all_armors: List[NexusArmor] = [] + self.selected_armor: Optional[NexusArmor] = None + + self._setup_ui() + self._load_data() + + def _setup_ui(self): + layout = QVBoxLayout(self) + layout.setSpacing(10) + + # Status and search + self.status_label = QLabel("Loading armors from Entropia Nexus...") + layout.addWidget(self.status_label) + + self.progress = QProgressBar() + self.progress.setRange(0, 0) # Indeterminate + layout.addWidget(self.progress) + + # Search + search_layout = QHBoxLayout() + search_layout.addWidget(QLabel("Search:")) + self.search_input = QLineEdit() + self.search_input.setPlaceholderText("Type to search armors...") + self.search_input.textChanged.connect(self._on_search) + search_layout.addWidget(self.search_input) + + self.clear_btn = QPushButton("Clear") + self.clear_btn.clicked.connect(self._clear_search) + search_layout.addWidget(self.clear_btn) + layout.addLayout(search_layout) + + # Results tree + self.results_tree = QTreeWidget() + self.results_tree.setHeaderLabels([ + "Name", "Type", "Durability", + "Impact", "Cut", "Stab", "Burn", "Cold", "Acid", "Electric", + "Total Prot" + ]) + header = self.results_tree.header() + header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) + header.setStretchLastSection(False) + 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("Armor Preview") + preview_layout = QFormLayout(self.preview_group) + self.preview_name = QLabel("-") + self.preview_type = QLabel("-") + self.preview_durability = QLabel("-") + self.preview_protection = QLabel("-") + preview_layout.addRow("Name:", self.preview_name) + preview_layout.addRow("Type:", self.preview_type) + preview_layout.addRow("Durability:", self.preview_durability) + preview_layout.addRow("Protection:", self.preview_protection) + layout.addWidget(self.preview_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 armors in background thread.""" + self.loader = ArmorLoaderThread() + self.loader.armors_loaded.connect(self._on_armors_loaded) + self.loader.error_occurred.connect(self._on_load_error) + self.loader.start() + + def _on_armors_loaded(self, armors: List[NexusArmor]): + """Handle loaded armors.""" + self.all_armors = armors + self.status_label.setText(f"Loaded {len(armors)} armors from Entropia Nexus") + self.progress.setRange(0, 100) + self.progress.setValue(100) + self._populate_results(armors) + + def _on_load_error(self, error: str): + """Handle load error.""" + self.status_label.setText(f"Error loading armors: {error}") + self.progress.setRange(0, 100) + self.progress.setValue(0) + + def _populate_results(self, armors: List[NexusArmor]): + """Populate results tree.""" + self.results_tree.clear() + for armor in armors: + item = QTreeWidgetItem() + item.setText(0, armor.name) + item.setText(1, armor.type) + item.setText(2, str(armor.durability)) + item.setText(3, str(armor.protection_impact)) + item.setText(4, str(armor.protection_cut)) + item.setText(5, str(armor.protection_stab)) + item.setText(6, str(armor.protection_burn)) + item.setText(7, str(armor.protection_cold)) + item.setText(8, str(armor.protection_acid)) + item.setText(9, str(armor.protection_electric)) + + total = armor.protection_impact + armor.protection_cut + armor.protection_stab + item.setText(10, str(total)) + + item.setData(0, Qt.ItemDataRole.UserRole, armor) + self.results_tree.addTopLevelItem(item) + + def _on_search(self, text: str): + """Handle search text change.""" + if not text: + self._populate_results(self.all_armors) + return + + query = text.lower() + filtered = [a for a in self.all_armors if query in a.name.lower()] + self._populate_results(filtered) + self.status_label.setText(f"Found {len(filtered)} armors matching '{text}'") + + def _clear_search(self): + """Clear search.""" + self.search_input.clear() + self._populate_results(self.all_armors) + self.status_label.setText(f"Showing all {len(self.all_armors)} armors") + + def _on_selection_changed(self): + """Handle selection change.""" + items = self.results_tree.selectedItems() + if items: + self.selected_armor = items[0].data(0, Qt.ItemDataRole.UserRole) + self.ok_button.setEnabled(True) + self._update_preview(self.selected_armor) + else: + self.selected_armor = None + self.ok_button.setEnabled(False) + + def _update_preview(self, armor: NexusArmor): + """Update preview panel.""" + self.preview_name.setText(armor.name) + self.preview_type.setText(armor.type) + self.preview_durability.setText(f"{armor.durability} (~{Decimal('20') / (Decimal('1') - Decimal(armor.durability) / Decimal('100000')):.2f} hp/pec)") + + prot_parts = [] + if armor.protection_impact > 0: + prot_parts.append(f"Imp:{armor.protection_impact}") + if armor.protection_cut > 0: + prot_parts.append(f"Cut:{armor.protection_cut}") + if armor.protection_stab > 0: + prot_parts.append(f"Stab:{armor.protection_stab}") + if armor.protection_burn > 0: + prot_parts.append(f"Burn:{armor.protection_burn}") + if armor.protection_cold > 0: + prot_parts.append(f"Cold:{armor.protection_cold}") + + self.preview_protection.setText(", ".join(prot_parts) if prot_parts else "None") + + def _on_double_click(self, item, column): + """Handle double click.""" + self._on_accept() + + def _on_accept(self): + """Handle OK button.""" + if self.selected_armor: + self.armor_selected.emit(self.selected_armor) + self.accept() diff --git a/ui/healing_selector.py b/ui/healing_selector.py new file mode 100644 index 0000000..278417c --- /dev/null +++ b/ui/healing_selector.py @@ -0,0 +1,244 @@ +""" +Searchable Healing Tool Selector for Lemontropia Suite +Browse and search healing tools from Entropia Nexus API +""" + +from decimal import Decimal +from PyQt6.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, + QTreeWidget, QTreeWidgetItem, QHeaderView, QLabel, QDialogButtonBox, + QProgressBar, QGroupBox, QFormLayout, QComboBox +) +from PyQt6.QtCore import Qt, QThread, pyqtSignal +from PyQt6.QtGui import QColor +from typing import Optional, List + +from core.nexus_full_api import get_nexus_api, NexusHealingTool + + +class HealingLoaderThread(QThread): + """Background thread for loading healing tools from API.""" + tools_loaded = pyqtSignal(list) + error_occurred = pyqtSignal(str) + + def run(self): + try: + api = get_nexus_api() + tools = api.get_all_healing_tools() + self.tools_loaded.emit(tools) + except Exception as e: + self.error_occurred.emit(str(e)) + + +class HealingSelectorDialog(QDialog): + """Dialog for selecting healing tools from Entropia Nexus API with search.""" + + tool_selected = pyqtSignal(NexusHealingTool) + + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Select Healing Tool - Entropia Nexus") + self.setMinimumSize(900, 600) + + self.all_tools: List[NexusHealingTool] = [] + self.selected_tool: Optional[NexusHealingTool] = None + + self._setup_ui() + self._load_data() + + def _setup_ui(self): + layout = QVBoxLayout(self) + layout.setSpacing(10) + + # Status + self.status_label = QLabel("Loading healing tools from Entropia Nexus...") + layout.addWidget(self.status_label) + + self.progress = QProgressBar() + self.progress.setRange(0, 0) + layout.addWidget(self.progress) + + # Filters + filter_layout = QHBoxLayout() + + filter_layout.addWidget(QLabel("Type:")) + self.type_combo = QComboBox() + self.type_combo.addItems(["All Types", "FAP (Medical Tool)", "Restoration Chip", "Limited (L)"]) + self.type_combo.currentTextChanged.connect(self._apply_filters) + filter_layout.addWidget(self.type_combo) + + filter_layout.addWidget(QLabel("Min Heal:")) + self.min_heal_combo = QComboBox() + self.min_heal_combo.addItems(["Any", "10+", "25+", "50+", "75+", "100+"]) + self.min_heal_combo.currentTextChanged.connect(self._apply_filters) + filter_layout.addWidget(self.min_heal_combo) + + layout.addLayout(filter_layout) + + # Search + search_layout = QHBoxLayout() + search_layout.addWidget(QLabel("Search:")) + self.search_input = QLineEdit() + self.search_input.setPlaceholderText("Type to search healing tools...") + self.search_input.textChanged.connect(self._apply_filters) + search_layout.addWidget(self.search_input) + + self.clear_btn = QPushButton("Clear") + self.clear_btn.clicked.connect(self._clear_search) + search_layout.addWidget(self.clear_btn) + layout.addLayout(search_layout) + + # Results tree + self.results_tree = QTreeWidget() + self.results_tree.setHeaderLabels([ + "Name", "Type", "Heal Amount", "Decay (PEC)", "Economy (hp/pec)", "Prof. Level", "Limited" + ]) + header = self.results_tree.header() + header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) + header.setStretchLastSection(False) + 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("Healing Tool Preview") + preview_layout = QFormLayout(self.preview_group) + self.preview_name = QLabel("-") + self.preview_heal = QLabel("-") + self.preview_decay = QLabel("-") + self.preview_economy = QLabel("-") + self.preview_cost = QLabel("-") + preview_layout.addRow("Name:", self.preview_name) + preview_layout.addRow("Heal Amount:", self.preview_heal) + preview_layout.addRow("Decay:", self.preview_decay) + preview_layout.addRow("Economy:", self.preview_economy) + preview_layout.addRow("Cost/Heal:", self.preview_cost) + layout.addWidget(self.preview_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 healing tools in background thread.""" + self.loader = HealingLoaderThread() + self.loader.tools_loaded.connect(self._on_tools_loaded) + self.loader.error_occurred.connect(self._on_load_error) + self.loader.start() + + def _on_tools_loaded(self, tools: List[NexusHealingTool]): + """Handle loaded tools.""" + self.all_tools = tools + self.status_label.setText(f"Loaded {len(tools)} healing tools from Entropia Nexus") + self.progress.setRange(0, 100) + self.progress.setValue(100) + self._apply_filters() + + def _on_load_error(self, error: str): + """Handle load error.""" + self.status_label.setText(f"Error loading healing tools: {error}") + self.progress.setRange(0, 100) + self.progress.setValue(0) + + def _apply_filters(self): + """Apply all filters and search.""" + tools = self.all_tools.copy() + + # Type filter + type_filter = self.type_combo.currentText() + if type_filter == "FAP (Medical Tool)": + tools = [t for t in tools if t.type == 'fap'] + elif type_filter == "Restoration Chip": + tools = [t for t in tools if 'chip' in t.type.lower()] + elif type_filter == "Limited (L)": + tools = [t for t in tools if t.is_limited] + + # Min heal filter + min_heal = self.min_heal_combo.currentText() + if min_heal != "Any": + min_val = int(min_heal.replace("+", "")) + tools = [t for t in tools if t.heal_amount >= min_val] + + # Search filter + search_text = self.search_input.text() + if search_text: + query = search_text.lower() + tools = [t for t in tools if query in t.name.lower()] + + self._populate_results(tools) + + # Update status + if search_text: + self.status_label.setText(f"Found {len(tools)} tools matching '{search_text}'") + else: + self.status_label.setText(f"Showing {len(tools)} of {len(self.all_tools)} tools") + + def _populate_results(self, tools: List[NexusHealingTool]): + """Populate results tree.""" + self.results_tree.clear() + + # Sort by economy (best first) + tools = sorted(tools, key=lambda t: t.heal_per_pec, reverse=True) + + for tool in tools: + item = QTreeWidgetItem() + item.setText(0, tool.name) + item.setText(1, tool.type) + item.setText(2, str(tool.heal_amount)) + item.setText(3, f"{tool.decay:.2f}") + item.setText(4, f"{tool.heal_per_pec:.2f}") + item.setText(5, str(tool.profession_level) if tool.profession_level > 0 else "-") + item.setText(6, "Yes" if tool.is_limited else "No") + + # Color code by economy (green = good, red = bad) + if tool.heal_per_pec >= 20: + item.setForeground(4, QColor("#4caf50")) # Green + elif tool.heal_per_pec >= 15: + item.setForeground(4, QColor("#ff9800")) # Orange + else: + item.setForeground(4, QColor("#f44336")) # Red + + item.setData(0, Qt.ItemDataRole.UserRole, tool) + self.results_tree.addTopLevelItem(item) + + def _clear_search(self): + """Clear search and filters.""" + self.search_input.clear() + self.type_combo.setCurrentIndex(0) + self.min_heal_combo.setCurrentIndex(0) + self._apply_filters() + + def _on_selection_changed(self): + """Handle selection change.""" + items = self.results_tree.selectedItems() + if items: + self.selected_tool = items[0].data(0, Qt.ItemDataRole.UserRole) + self.ok_button.setEnabled(True) + self._update_preview(self.selected_tool) + else: + self.selected_tool = None + self.ok_button.setEnabled(False) + + def _update_preview(self, tool: NexusHealingTool): + """Update preview panel.""" + self.preview_name.setText(tool.name) + self.preview_heal.setText(f"{tool.heal_amount} HP") + self.preview_decay.setText(f"{tool.decay:.2f} PEC") + self.preview_economy.setText(f"{tool.heal_per_pec:.2f} hp/pec") + self.preview_cost.setText(f"{tool.decay / Decimal('100'):.4f} PED") + + def _on_double_click(self, item, column): + """Handle double click.""" + self._on_accept() + + def _on_accept(self): + """Handle OK button.""" + if self.selected_tool: + self.tool_selected.emit(self.selected_tool) + self.accept()