feat(api): full Entropia Nexus API integration + searchable selectors
- NEW: core/nexus_full_api.py - Complete API for all gear types - Weapons, Armors, Plates, Attachments, Enhancers - Healing Tools, Rings, Clothing, Pets - Search across all categories - NEW: ui/armor_selector.py - Searchable armor browser - Real-time search, filter by protection type - Preview panel with economy info - NEW: ui/healing_selector.py - Searchable healing tool browser - Filter by type (FAP/Chip/Limited) - Filter by heal amount - Color-coded economy ratings All gear now searchable like weapons!
This commit is contained in:
parent
6a6d50e5f8
commit
dceafbc69f
|
|
@ -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
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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()
|
||||||
Loading…
Reference in New Issue