diff --git a/core/nexus_api.py b/core/nexus_api.py
index 9826c21..5c3ca61 100644
--- a/core/nexus_api.py
+++ b/core/nexus_api.py
@@ -132,6 +132,7 @@ class ArmorStats:
name: str
weight: Decimal
durability: int
+ decay: Optional[Decimal] # Decay in PEC per hit taken
protection_stab: Decimal
protection_cut: Decimal
protection_impact: Decimal
@@ -154,6 +155,7 @@ class ArmorStats:
def from_api_data(cls, data: Dict[str, Any]) -> 'ArmorStats':
props = data.get('Properties', {})
protection = props.get('Protection', {})
+ economy = props.get('Economy', {})
return cls(
id=data.get('Id', 0),
@@ -161,6 +163,7 @@ class ArmorStats:
name=data.get('Name', 'Unknown'),
weight=Decimal(str(props.get('Weight', 0))) if props.get('Weight') else Decimal('0'),
durability=props.get('Durability', 0) or 0,
+ decay=Decimal(str(economy.get('Decay'))) if economy.get('Decay') else None,
protection_stab=Decimal(str(protection.get('Stab', 0))) if protection.get('Stab') else Decimal('0'),
protection_cut=Decimal(str(protection.get('Cut', 0))) if protection.get('Cut') else Decimal('0'),
protection_impact=Decimal(str(protection.get('Impact', 0))) if protection.get('Impact') else Decimal('0'),
@@ -255,6 +258,66 @@ class MobStats:
)
+@dataclass
+class MedicalTool:
+ """Medical tool (FAP) statistics from Entropia Nexus API."""
+ id: int
+ item_id: int
+ name: str
+ weight: Optional[Decimal]
+ max_heal: Optional[Decimal]
+ min_heal: Optional[Decimal]
+ uses_per_minute: Optional[int]
+ max_tt: Optional[Decimal]
+ min_tt: Optional[Decimal]
+ decay: Optional[Decimal] # Decay per use in PEC
+ sib: bool = False
+ skill_start: Optional[Decimal] = None
+ skill_end: Optional[Decimal] = None
+
+ # Computed fields
+ cost_per_heal: Decimal = field(default_factory=lambda: Decimal("0"))
+ cost_per_hour: Decimal = field(default_factory=lambda: Decimal("0"))
+
+ def __post_init__(self):
+ """Calculate derived statistics."""
+ # Calculate cost per heal (decay in PEC converted to PED)
+ if self.decay and self.decay > 0:
+ self.cost_per_heal = self.decay / Decimal("100")
+
+ # Calculate cost per hour
+ if self.uses_per_minute and self.uses_per_minute > 0 and self.decay:
+ uses_per_hour = self.uses_per_minute * 60
+ self.cost_per_hour = (self.decay * uses_per_hour) / 100
+
+ @classmethod
+ def from_api_data(cls, data: Dict[str, Any]) -> 'MedicalTool':
+ """Create MedicalTool from API response data."""
+ props = data.get('Properties', {})
+ economy = props.get('Economy', {})
+ skill = props.get('Skill', {})
+
+ # Parse heal values
+ max_heal = props.get('MaxHeal')
+ min_heal = props.get('MinHeal')
+
+ return cls(
+ id=data.get('Id', 0),
+ item_id=data.get('ItemId', 0),
+ name=data.get('Name', 'Unknown'),
+ weight=Decimal(str(props.get('Weight'))) if props.get('Weight') else None,
+ max_heal=Decimal(str(max_heal)) if max_heal else None,
+ min_heal=Decimal(str(min_heal)) if min_heal else None,
+ uses_per_minute=props.get('UsesPerMinute'),
+ max_tt=Decimal(str(economy.get('MaxTT'))) if economy.get('MaxTT') else None,
+ min_tt=Decimal(str(economy.get('MinTT'))) if economy.get('MinTT') else None,
+ decay=Decimal(str(economy.get('Decay'))) if economy.get('Decay') else None,
+ sib=skill.get('IsSiB', False),
+ skill_start=Decimal(str(skill.get('LearningIntervalStart'))) if skill.get('LearningIntervalStart') else None,
+ skill_end=Decimal(str(skill.get('LearningIntervalEnd'))) if skill.get('LearningIntervalEnd') else None,
+ )
+
+
# =============================================================================
# API Client
# =============================================================================
@@ -411,6 +474,30 @@ class EntropiaNexusAPI:
query_lower = query.lower()
return [m for m in mobs if query_lower in m.name.lower()]
+ # ========================================================================
+ # Medical Tools (FAPs)
+ # ========================================================================
+
+ def get_all_medical_tools(self) -> List[MedicalTool]:
+ """Get all medical tools (FAPs)."""
+ data = self._make_request('medicaltools')
+ if data:
+ return [MedicalTool.from_api_data(m) for m in data]
+ return []
+
+ def get_medical_tool(self, tool_id: int) -> Optional[MedicalTool]:
+ """Get specific medical tool by ID."""
+ data = self._get_item('medicaltools', tool_id)
+ if data:
+ return MedicalTool.from_api_data(data)
+ return None
+
+ def search_medical_tools(self, query: str) -> List[MedicalTool]:
+ """Search medical tools by name."""
+ tools = self.get_all_medical_tools()
+ query_lower = query.lower()
+ return [t for t in tools if query_lower in t.name.lower()]
+
# ========================================================================
# Cache Management
# ========================================================================
@@ -424,5 +511,5 @@ class EntropiaNexusAPI:
__all__ = [
'WeaponStats', 'ArmorStats', 'FinderStats', 'ExcavatorStats', 'MobStats',
- 'EntropiaNexusAPI'
+ 'MedicalTool', 'EntropiaNexusAPI'
]
diff --git a/ui/__init__.py b/ui/__init__.py
index 5b08b0c..02f2d76 100644
--- a/ui/__init__.py
+++ b/ui/__init__.py
@@ -3,7 +3,11 @@
from .main_window import MainWindow
from .hud_overlay import HUDOverlay, HUDStats
-from .loadout_manager import LoadoutManagerDialog, LoadoutConfig
+from .loadout_manager import (
+ LoadoutManagerDialog, LoadoutConfig, AttachmentConfig,
+ WeaponSelectorDialog, ArmorSelectorDialog
+)
+from .attachment_selector import AttachmentSelectorDialog, AttachmentManagerWidget
__all__ = [
'MainWindow',
@@ -11,4 +15,9 @@ __all__ = [
'HUDStats',
'LoadoutManagerDialog',
'LoadoutConfig',
+ 'AttachmentConfig',
+ 'WeaponSelectorDialog',
+ 'ArmorSelectorDialog',
+ 'AttachmentSelectorDialog',
+ 'AttachmentManagerWidget',
]
diff --git a/ui/attachment_selector.py b/ui/attachment_selector.py
new file mode 100644
index 0000000..afb022a
--- /dev/null
+++ b/ui/attachment_selector.py
@@ -0,0 +1,678 @@
+"""
+Attachment Selector Dialog for Lemontropia Suite
+UI for selecting and managing gear attachments.
+"""
+
+from PyQt6.QtWidgets import (
+ QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
+ QListWidget, QListWidgetItem, QDialogButtonBox, QFormLayout,
+ QTabWidget, QWidget
+)
+from PyQt6.QtCore import Qt, pyqtSignal
+from PyQt6.QtGui import QFont
+from decimal import Decimal
+from typing import Optional, List, Dict
+
+from core.attachments import (
+ Attachment, WeaponAmplifier, WeaponScope, WeaponAbsorber,
+ ArmorPlating, FinderAmplifier, Enhancer, MindforceImplant,
+ get_mock_attachments
+)
+
+
+# ============================================================================
+# Data Class for Attachment Configuration
+# ============================================================================
+
+class AttachmentConfig:
+ """Configuration for an equipped attachment."""
+
+ def __init__(
+ self,
+ name: str,
+ item_id: str,
+ attachment_type: str,
+ decay_pec: Decimal,
+ damage_bonus: Decimal = Decimal("0"),
+ range_bonus: Decimal = Decimal("0"),
+ efficiency_bonus: Decimal = Decimal("0"),
+ protection_bonus: Optional[Dict[str, Decimal]] = None,
+ extra_data: Optional[Dict] = None
+ ):
+ self.name = name
+ self.item_id = item_id
+ self.attachment_type = attachment_type
+ self.decay_pec = decay_pec
+ self.damage_bonus = damage_bonus
+ self.range_bonus = range_bonus
+ self.efficiency_bonus = efficiency_bonus
+ self.protection_bonus = protection_bonus or {}
+ self.extra_data = extra_data or {}
+
+ def to_dict(self) -> dict:
+ """Convert to dictionary for JSON serialization."""
+ return {
+ 'name': self.name,
+ 'item_id': self.item_id,
+ 'attachment_type': self.attachment_type,
+ 'decay_pec': str(self.decay_pec),
+ 'damage_bonus': str(self.damage_bonus),
+ 'range_bonus': str(self.range_bonus),
+ 'efficiency_bonus': str(self.efficiency_bonus),
+ 'protection_bonus': {k: str(v) for k, v in self.protection_bonus.items()},
+ 'extra_data': self.extra_data,
+ }
+
+ @classmethod
+ def from_dict(cls, data: dict) -> 'AttachmentConfig':
+ """Create from dictionary."""
+ return cls(
+ name=data['name'],
+ item_id=data['item_id'],
+ attachment_type=data['attachment_type'],
+ decay_pec=Decimal(data['decay_pec']),
+ damage_bonus=Decimal(data.get('damage_bonus', '0')),
+ range_bonus=Decimal(data.get('range_bonus', '0')),
+ efficiency_bonus=Decimal(data.get('efficiency_bonus', '0')),
+ protection_bonus={k: Decimal(v) for k, v in data.get('protection_bonus', {}).items()},
+ extra_data=data.get('extra_data', {}),
+ )
+
+ def get_cost_per_hour(self, uses_per_hour: int = 3600) -> Decimal:
+ """Calculate cost per hour in PED."""
+ return (self.decay_pec * Decimal(uses_per_hour)) / Decimal("100")
+
+ def get_tooltip_text(self) -> str:
+ """Get tooltip text describing the attachment."""
+ lines = [f"Decay: {self.decay_pec} PEC/shot"]
+
+ if self.damage_bonus > 0:
+ lines.append(f"Damage: +{self.damage_bonus}")
+ if self.range_bonus > 0:
+ lines.append(f"Range: +{self.range_bonus}m")
+ if self.efficiency_bonus > 0:
+ lines.append(f"Efficiency: +{self.efficiency_bonus}%")
+
+ if self.protection_bonus:
+ prots = [f"{k}: +{v}" for k, v in self.protection_bonus.items() if v > 0]
+ if prots:
+ lines.append("Protection: " + ", ".join(prots))
+
+ return "\n".join(lines)
+
+
+# ============================================================================
+# Attachment Selector Dialog
+# ============================================================================
+
+class AttachmentSelectorDialog(QDialog):
+ """
+ Dialog for selecting attachments.
+
+ Supports all attachment types:
+ - amplifier: Weapon amplifiers (damage boost)
+ - scope: Weapon scopes (range boost)
+ - absorber: Weapon absorbers (damage reduction)
+ - plating: Armor plating (protection boost)
+ - finder_amp: Mining finder amplifiers
+ - enhancer: Gear enhancers
+ - implant: Mindforce implants
+ """
+
+ attachment_selected = pyqtSignal(object) # AttachmentConfig or None
+
+ # Friendly names for attachment types
+ TYPE_NAMES = {
+ 'amplifier': 'Weapon Amplifier',
+ 'scope': 'Weapon Scope',
+ 'absorber': 'Weapon Absorber',
+ 'plating': 'Armor Plating',
+ 'finder_amp': 'Finder Amplifier',
+ 'enhancer': 'Enhancer',
+ 'implant': 'Mindforce Implant',
+ }
+
+ # Icons for attachment types
+ TYPE_ICONS = {
+ 'amplifier': '⚡',
+ 'scope': '🔭',
+ 'absorber': '🛡️',
+ 'plating': '🔩',
+ 'finder_amp': '⛏️',
+ 'enhancer': '✨',
+ 'implant': '🧠',
+ }
+
+ def __init__(self, attachment_type: str, parent=None, allow_none: bool = True):
+ """
+ Initialize attachment selector.
+
+ Args:
+ attachment_type: Type of attachment to select
+ parent: Parent widget
+ allow_none: Whether to allow selecting "None"
+ """
+ super().__init__(parent)
+ self.attachment_type = attachment_type
+ self.selected_attachment = None
+ self.allow_none = allow_none
+
+ type_name = self.TYPE_NAMES.get(attachment_type, attachment_type.title())
+ self.setWindowTitle(f"Select {type_name}")
+ self.setMinimumSize(500, 450)
+
+ self._setup_ui()
+ self._load_attachments()
+
+ def _setup_ui(self):
+ """Setup the UI."""
+ layout = QVBoxLayout(self)
+ layout.setSpacing(10)
+
+ # Header
+ type_name = self.TYPE_NAMES.get(self.attachment_type, self.attachment_type.title())
+ type_icon = self.TYPE_ICONS.get(self.attachment_type, '📎')
+
+ header = QLabel(f"{type_icon} Select {type_name}")
+ header.setFont(QFont("Arial", 14, QFont.Weight.Bold))
+ header.setStyleSheet("color: #4a90d9; padding: 5px;")
+ layout.addWidget(header)
+
+ # Description
+ desc = self._get_type_description()
+ if desc:
+ desc_label = QLabel(desc)
+ desc_label.setStyleSheet("color: #888888; padding-bottom: 10px;")
+ desc_label.setWordWrap(True)
+ layout.addWidget(desc_label)
+
+ # Attachment list
+ self.list_widget = QListWidget()
+ self.list_widget.setAlternatingRowColors(True)
+ self.list_widget.setStyleSheet("""
+ QListWidget {
+ background-color: #2d2d2d;
+ color: #e0e0e0;
+ border: 1px solid #3d3d3d;
+ border-radius: 4px;
+ }
+ QListWidget::item:selected {
+ background-color: #4a90d9;
+ }
+ QListWidget::item:hover {
+ background-color: #3d3d3d;
+ }
+ """)
+ self.list_widget.itemSelectionChanged.connect(self._on_selection_changed)
+ self.list_widget.itemDoubleClicked.connect(self._on_double_click)
+ layout.addWidget(self.list_widget)
+
+ # Stats preview
+ self.preview_group = QWidget()
+ preview_layout = QFormLayout(self.preview_group)
+ preview_layout.setContentsMargins(10, 10, 10, 10)
+ self.preview_group.setStyleSheet("""
+ QWidget {
+ background-color: #2d2d2d;
+ border-radius: 4px;
+ }
+ QLabel {
+ color: #e0e0e0;
+ }
+ """)
+
+ self.preview_name = QLabel("No attachment selected")
+ self.preview_name.setStyleSheet("font-weight: bold; color: #4caf50;")
+ self.preview_stats = QLabel("")
+ self.preview_stats.setStyleSheet("color: #888888;")
+
+ preview_layout.addRow(self.preview_name)
+ preview_layout.addRow(self.preview_stats)
+ layout.addWidget(self.preview_group)
+
+ # Buttons
+ button_layout = QHBoxLayout()
+
+ if self.allow_none:
+ self.none_btn = QPushButton("❌ Remove Attachment")
+ self.none_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #7d2e2e;
+ color: #e0e0e0;
+ border: 1px solid #f44336;
+ border-radius: 4px;
+ padding: 8px 16px;
+ }
+ QPushButton:hover {
+ background-color: #f44336;
+ }
+ """)
+ self.none_btn.clicked.connect(self._on_none)
+ button_layout.addWidget(self.none_btn)
+
+ button_layout.addStretch()
+
+ self.ok_btn = QPushButton("✓ Select")
+ self.ok_btn.setEnabled(False)
+ self.ok_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #2e7d32;
+ color: #e0e0e0;
+ border: 1px solid #4caf50;
+ border-radius: 4px;
+ padding: 8px 24px;
+ }
+ QPushButton:hover {
+ background-color: #4caf50;
+ }
+ QPushButton:disabled {
+ background-color: #3d3d3d;
+ border-color: #4d4d4d;
+ }
+ """)
+ self.ok_btn.clicked.connect(self._on_accept)
+
+ cancel_btn = QPushButton("Cancel")
+ cancel_btn.clicked.connect(self.reject)
+
+ button_layout.addWidget(self.ok_btn)
+ button_layout.addWidget(cancel_btn)
+ layout.addLayout(button_layout)
+
+ def _get_type_description(self) -> str:
+ """Get description for attachment type."""
+ descriptions = {
+ 'amplifier': 'Amplifiers increase weapon damage at the cost of additional decay and ammo.',
+ 'scope': 'Scopes increase weapon range for better accuracy at distance.',
+ 'absorber': 'Absorbers reduce incoming damage when attached to weapons.',
+ 'plating': 'Armor plating adds protection values to your armor set.',
+ 'finder_amp': 'Finder amplifiers increase mining depth and radius.',
+ 'enhancer': 'Enhancers add special effects to gear.',
+ 'implant': 'Mindforce implants boost mindforce chip performance.',
+ }
+ return descriptions.get(self.attachment_type, "")
+
+ def _load_attachments(self):
+ """Load attachments from data source."""
+ attachments = get_mock_attachments(self.attachment_type)
+ self.attachments = attachments
+
+ self.list_widget.clear()
+
+ for att in attachments:
+ icon = self.TYPE_ICONS.get(self.attachment_type, '📎')
+ item = QListWidgetItem(f"{icon} {att.name}")
+ item.setData(Qt.ItemDataRole.UserRole, att)
+
+ # Build tooltip
+ tooltip_lines = [f"Decay: {att.decay_pec} PEC/shot"]
+
+ if isinstance(att, WeaponAmplifier):
+ tooltip_lines.append(f"Damage: +{att.damage_increase}")
+ tooltip_lines.append(f"Ammo: +{att.ammo_increase}")
+ elif isinstance(att, WeaponScope):
+ tooltip_lines.append(f"Range: +{att.range_increase}m")
+ if att.accuracy_bonus > 0:
+ tooltip_lines.append(f"Accuracy: +{att.accuracy_bonus}")
+ elif isinstance(att, WeaponAbsorber):
+ tooltip_lines.append(f"Damage Reduction: {att.damage_reduction}")
+ elif isinstance(att, ArmorPlating):
+ tooltip_lines.append(f"Total Protection: +{att.get_total_protection()}")
+ # Add individual protections
+ prots = []
+ if att.protection_impact > 0:
+ prots.append(f"Impact +{att.protection_impact}")
+ if att.protection_cut > 0:
+ prots.append(f"Cut +{att.protection_cut}")
+ if att.protection_stab > 0:
+ prots.append(f"Stab +{att.protection_stab}")
+ if att.protection_burn > 0:
+ prots.append(f"Burn +{att.protection_burn}")
+ if prots:
+ tooltip_lines.append(", ".join(prots))
+ elif isinstance(att, FinderAmplifier):
+ tooltip_lines.append(f"Depth: +{att.depth_increase}m")
+ tooltip_lines.append(f"Radius: +{att.radius_increase}m")
+ elif isinstance(att, Enhancer):
+ tooltip_lines.append(f"Tier: {att.tier}")
+ tooltip_lines.append(f"Effect: {att.effect_name} +{att.effect_value}")
+ elif isinstance(att, MindforceImplant):
+ tooltip_lines.append(f"Mindforce Bonus: +{att.mindforce_bonus}")
+
+ item.setToolTip("\n".join(tooltip_lines))
+ self.list_widget.addItem(item)
+
+ def _on_selection_changed(self):
+ """Handle selection change."""
+ selected = self.list_widget.selectedItems()
+ if selected:
+ attachment = selected[0].data(Qt.ItemDataRole.UserRole)
+ self.selected_attachment = attachment
+ self.ok_btn.setEnabled(True)
+ self._update_preview(attachment)
+ else:
+ self.selected_attachment = None
+ self.ok_btn.setEnabled(False)
+ self.preview_name.setText("No attachment selected")
+ self.preview_stats.setText("")
+
+ def _update_preview(self, attachment):
+ """Update preview panel."""
+ self.preview_name.setText(attachment.name)
+
+ stats_lines = []
+ stats_lines.append(f"Decay: {attachment.decay_pec} PEC/shot")
+
+ if isinstance(attachment, WeaponAmplifier):
+ stats_lines.append(f"Damage Increase: +{attachment.damage_increase}")
+ stats_lines.append(f"Ammo Increase: +{attachment.ammo_increase}")
+
+ # Calculate DPP impact
+ if attachment.damage_increase > 0:
+ # Rough DPP calculation for the amp
+ amp_cost = attachment.decay_pec + (Decimal(attachment.ammo_increase) * Decimal("0.01"))
+ amp_dpp = attachment.damage_increase / amp_cost if amp_cost > 0 else Decimal("0")
+ stats_lines.append(f"Amp DPP: {amp_dpp:.2f}")
+
+ elif isinstance(attachment, WeaponScope):
+ stats_lines.append(f"Range Increase: +{attachment.range_increase}m")
+ if attachment.accuracy_bonus > 0:
+ stats_lines.append(f"Accuracy Bonus: +{attachment.accuracy_bonus}")
+
+ elif isinstance(attachment, WeaponAbsorber):
+ stats_lines.append(f"Damage Reduction: -{attachment.damage_reduction}")
+
+ elif isinstance(attachment, ArmorPlating):
+ total = attachment.get_total_protection()
+ stats_lines.append(f"Total Protection: +{total}")
+
+ # Individual protections
+ prots = []
+ if attachment.protection_impact > 0:
+ prots.append(f"Impact +{attachment.protection_impact}")
+ if attachment.protection_cut > 0:
+ prots.append(f"Cut +{attachment.protection_cut}")
+ if attachment.protection_stab > 0:
+ prots.append(f"Stab +{attachment.protection_stab}")
+ if attachment.protection_penetration > 0:
+ prots.append(f"Pen +{attachment.protection_penetration}")
+ if attachment.protection_burn > 0:
+ prots.append(f"Burn +{attachment.protection_burn}")
+ if attachment.protection_cold > 0:
+ prots.append(f"Cold +{attachment.protection_cold}")
+ if attachment.protection_acid > 0:
+ prots.append(f"Acid +{attachment.protection_acid}")
+ if attachment.protection_electric > 0:
+ prots.append(f"Elec +{attachment.protection_electric}")
+
+ if prots:
+ stats_lines.append(f"Protection: {', '.join(prots)}")
+
+ elif isinstance(attachment, FinderAmplifier):
+ stats_lines.append(f"Depth: +{attachment.depth_increase}m")
+ stats_lines.append(f"Radius: +{attachment.radius_increase}m")
+
+ elif isinstance(attachment, Enhancer):
+ stats_lines.append(f"Tier: {attachment.tier}")
+ stats_lines.append(f"Effect: {attachment.effect_name} +{attachment.effect_value}")
+
+ elif isinstance(attachment, MindforceImplant):
+ stats_lines.append(f"Mindforce Bonus: +{attachment.mindforce_bonus}")
+
+ # Calculate cost per hour (assuming 3600 uses/hour)
+ cost_per_hour = (attachment.decay_pec * Decimal("3600")) / Decimal("100")
+ stats_lines.append(f"Cost/Hour: {cost_per_hour:.2f} PED")
+
+ self.preview_stats.setText("
".join(stats_lines))
+
+ def _on_double_click(self, item):
+ """Handle double click."""
+ self._on_accept()
+
+ def _on_accept(self):
+ """Handle OK button."""
+ if self.selected_attachment:
+ att = self.selected_attachment
+
+ # Build protection bonus dict for armor plating
+ protection_bonus = {}
+ if isinstance(att, ArmorPlating):
+ protection_bonus = {
+ 'stab': att.protection_stab,
+ 'cut': att.protection_cut,
+ 'impact': att.protection_impact,
+ 'penetration': att.protection_penetration,
+ 'shrapnel': att.protection_shrapnel,
+ 'burn': att.protection_burn,
+ 'cold': att.protection_cold,
+ 'acid': att.protection_acid,
+ 'electric': att.protection_electric,
+ }
+
+ # Build extra data based on type
+ extra_data = {}
+ if isinstance(att, WeaponAmplifier):
+ extra_data['ammo_increase'] = att.ammo_increase
+ elif isinstance(att, Enhancer):
+ extra_data['tier'] = att.tier
+ extra_data['effect_name'] = att.effect_name
+ extra_data['effect_value'] = float(att.effect_value)
+
+ config = AttachmentConfig(
+ name=att.name,
+ item_id=att.item_id,
+ attachment_type=att.attachment_type,
+ decay_pec=att.decay_pec,
+ damage_bonus=getattr(att, 'damage_increase', Decimal("0")),
+ range_bonus=getattr(att, 'range_increase', Decimal("0")),
+ efficiency_bonus=Decimal("0"),
+ protection_bonus=protection_bonus,
+ extra_data=extra_data,
+ )
+
+ self.attachment_selected.emit(config)
+ self.accept()
+
+ def _on_none(self):
+ """Remove attachment."""
+ self.attachment_selected.emit(None)
+ self.accept()
+
+ @staticmethod
+ def select_attachment(attachment_type: str, parent=None, allow_none: bool = True) -> Optional[AttachmentConfig]:
+ """
+ Static method to open selector and return selected attachment.
+
+ Args:
+ attachment_type: Type of attachment to select
+ parent: Parent widget
+ allow_none: Whether to allow selecting "None"
+
+ Returns:
+ AttachmentConfig or None if cancelled or "None" selected
+ """
+ dialog = AttachmentSelectorDialog(attachment_type, parent, allow_none)
+ result = None
+
+ def on_selected(att):
+ nonlocal result
+ result = att
+
+ dialog.attachment_selected.connect(on_selected)
+
+ if dialog.exec() == QDialog.DialogCode.Accepted:
+ return result
+ return None
+
+
+# ============================================================================
+# Multi-Attachment Manager
+# ============================================================================
+
+class AttachmentManagerWidget(QWidget):
+ """
+ Widget for managing multiple attachments on a piece of gear.
+ Shows currently equipped attachments and allows adding/removing.
+ """
+
+ attachment_changed = pyqtSignal(str, object) # slot_name, AttachmentConfig or None
+
+ def __init__(self, gear_type: str = 'weapon', parent=None):
+ super().__init__(parent)
+ self.gear_type = gear_type
+ self.attachments: Dict[str, Optional[AttachmentConfig]] = {}
+
+ self._setup_ui()
+
+ def _setup_ui(self):
+ """Setup the UI."""
+ from core.attachments import get_compatible_attachments
+
+ layout = QVBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(5)
+
+ # Get compatible attachment types
+ compatible_types = get_compatible_attachments(self.gear_type)
+
+ for att_type in compatible_types:
+ type_name = AttachmentSelectorDialog.TYPE_NAMES.get(att_type, att_type.title())
+ type_icon = AttachmentSelectorDialog.TYPE_ICONS.get(att_type, '📎')
+
+ row = QWidget()
+ row_layout = QHBoxLayout(row)
+ row_layout.setContentsMargins(0, 0, 0, 0)
+
+ label = QLabel(f"{type_icon} {type_name}:")
+ label.setFixedWidth(120)
+ row_layout.addWidget(label)
+
+ status_label = QLabel("None")
+ status_label.setStyleSheet("color: #888888;")
+ row_layout.addWidget(status_label, stretch=1)
+
+ add_btn = QPushButton("+")
+ add_btn.setFixedSize(30, 24)
+ add_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #2e7d32;
+ color: white;
+ border-radius: 4px;
+ }
+ QPushButton:hover {
+ background-color: #4caf50;
+ }
+ """)
+ add_btn.clicked.connect(lambda checked, t=att_type: self._on_add(t))
+ row_layout.addWidget(add_btn)
+
+ remove_btn = QPushButton("✕")
+ remove_btn.setFixedSize(30, 24)
+ remove_btn.setEnabled(False)
+ remove_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #7d2e2e;
+ color: white;
+ border-radius: 4px;
+ }
+ QPushButton:hover {
+ background-color: #f44336;
+ }
+ QPushButton:disabled {
+ background-color: #3d3d3d;
+ }
+ """)
+ remove_btn.clicked.connect(lambda checked, t=att_type: self._on_remove(t))
+ row_layout.addWidget(remove_btn)
+
+ layout.addWidget(row)
+
+ # Store references
+ self.attachments[att_type] = None
+ setattr(self, f"{att_type}_status", status_label)
+ setattr(self, f"{att_type}_add_btn", add_btn)
+ setattr(self, f"{att_type}_remove_btn", remove_btn)
+
+ def _on_add(self, attachment_type: str):
+ """Open selector for attachment type."""
+ dialog = AttachmentSelectorDialog(attachment_type, self, allow_none=False)
+
+ def on_selected(att):
+ if att:
+ self.attachments[attachment_type] = att
+ self._update_display(attachment_type, att)
+ self.attachment_changed.emit(attachment_type, att)
+
+ dialog.attachment_selected.connect(on_selected)
+ dialog.exec()
+
+ def _on_remove(self, attachment_type: str):
+ """Remove attachment."""
+ self.attachments[attachment_type] = None
+ self._update_display(attachment_type, None)
+ self.attachment_changed.emit(attachment_type, None)
+
+ def _update_display(self, attachment_type: str, att: Optional[AttachmentConfig]):
+ """Update display for attachment type."""
+ status_label = getattr(self, f"{attachment_type}_status")
+ remove_btn = getattr(self, f"{attachment_type}_remove_btn")
+
+ if att:
+ # Build status text
+ status_parts = [att.name]
+ if att.damage_bonus > 0:
+ status_parts.append(f"(+{att.damage_bonus} dmg)")
+ elif att.range_bonus > 0:
+ status_parts.append(f"(+{att.range_bonus}m)")
+
+ status_label.setText(" ".join(status_parts))
+ status_label.setStyleSheet("color: #4caf50;")
+ remove_btn.setEnabled(True)
+ else:
+ status_label.setText("None")
+ status_label.setStyleSheet("color: #888888;")
+ remove_btn.setEnabled(False)
+
+ def get_attachment(self, attachment_type: str) -> Optional[AttachmentConfig]:
+ """Get attachment of given type."""
+ return self.attachments.get(attachment_type)
+
+ def set_attachment(self, attachment_type: str, att: Optional[AttachmentConfig]):
+ """Set attachment of given type."""
+ if attachment_type in self.attachments:
+ self.attachments[attachment_type] = att
+ self._update_display(attachment_type, att)
+
+ def get_all_attachments(self) -> Dict[str, Optional[AttachmentConfig]]:
+ """Get all attachments."""
+ return self.attachments.copy()
+
+
+# ============================================================================
+# Main entry point for testing
+# ============================================================================
+
+def main():
+ """Test the attachment selector."""
+ import sys
+ from PyQt6.QtWidgets import QApplication
+
+ app = QApplication(sys.argv)
+ app.setStyle('Fusion')
+
+ # Test single selector
+ print("Testing single attachment selector...")
+ result = AttachmentSelectorDialog.select_attachment('amplifier')
+
+ if result:
+ print(f"Selected: {result.name}")
+ print(f" Decay: {result.decay_pec} PEC")
+ print(f" Damage Bonus: {result.damage_bonus}")
+ else:
+ print("No attachment selected")
+
+ sys.exit(0)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ui/hud_overlay.py b/ui/hud_overlay.py
index 5730ee4..736bc78 100644
--- a/ui/hud_overlay.py
+++ b/ui/hud_overlay.py
@@ -56,21 +56,28 @@ class HUDStats:
# Financial tracking
loot_total: Decimal = Decimal('0.0')
cost_total: Decimal = Decimal('0.0') # Weapon decay + ammo
- profit_loss: Decimal = Decimal('0.0') # loot - cost
+ healing_cost_total: Decimal = Decimal('0.0') # NEW: Healing/FAP decay cost
+ profit_loss: Decimal = Decimal('0.0') # loot - cost - healing_cost
# Combat stats
damage_dealt: int = 0
damage_taken: int = 0
- shots_fired: int = 0 # NEW: Track shots fired
+ shots_fired: int = 0
kills: int = 0
globals_count: int = 0
hofs_count: int = 0
+ # Healing stats (NEW)
+ healing_done: Decimal = Decimal('0.0') # Total HP healed
+ heals_count: int = 0 # Number of heal actions
+
# Current gear
current_weapon: str = "None"
current_loadout: str = "None"
+ current_medical_tool: str = "None" # NEW: Current FAP
weapon_dpp: Decimal = Decimal('0.0')
weapon_cost_per_hour: Decimal = Decimal('0.0')
+ medical_tool_decay: Decimal = Decimal('0.0') # NEW: FAP decay per use in PEC
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for serialization."""
@@ -78,6 +85,7 @@ class HUDStats:
'session_time_seconds': self.session_time.total_seconds(),
'loot_total': str(self.loot_total),
'cost_total': str(self.cost_total),
+ 'healing_cost_total': str(self.healing_cost_total),
'profit_loss': str(self.profit_loss),
'damage_dealt': self.damage_dealt,
'damage_taken': self.damage_taken,
@@ -85,10 +93,14 @@ class HUDStats:
'kills': self.kills,
'globals_count': self.globals_count,
'hofs_count': self.hofs_count,
+ 'healing_done': str(self.healing_done),
+ 'heals_count': self.heals_count,
'current_weapon': self.current_weapon,
'current_loadout': self.current_loadout,
+ 'current_medical_tool': self.current_medical_tool,
'weapon_dpp': str(self.weapon_dpp),
'weapon_cost_per_hour': str(self.weapon_cost_per_hour),
+ 'medical_tool_decay': str(self.medical_tool_decay),
}
@classmethod
@@ -98,6 +110,7 @@ class HUDStats:
session_time=timedelta(seconds=data.get('session_time_seconds', 0)),
loot_total=Decimal(data.get('loot_total', '0.0')),
cost_total=Decimal(data.get('cost_total', '0.0')),
+ healing_cost_total=Decimal(data.get('healing_cost_total', '0.0')),
profit_loss=Decimal(data.get('profit_loss', '0.0')),
damage_dealt=data.get('damage_dealt', 0),
damage_taken=data.get('damage_taken', 0),
@@ -105,10 +118,14 @@ class HUDStats:
kills=data.get('kills', 0),
globals_count=data.get('globals_count', 0),
hofs_count=data.get('hofs_count', 0),
+ healing_done=Decimal(data.get('healing_done', '0.0')),
+ heals_count=data.get('heals_count', 0),
current_weapon=data.get('current_weapon', 'None'),
current_loadout=data.get('current_loadout', 'None'),
+ current_medical_tool=data.get('current_medical_tool', 'None'),
weapon_dpp=Decimal(data.get('weapon_dpp', '0.0')),
weapon_cost_per_hour=Decimal(data.get('weapon_cost_per_hour', '0.0')),
+ medical_tool_decay=Decimal(data.get('medical_tool_decay', '0.0')),
)
@@ -202,17 +219,17 @@ class HUDOverlay(QWidget):
# Enable mouse tracking for hover detection
self.setMouseTracking(True)
- # Size
- self.setFixedSize(320, 220)
+ # Size - increased to accommodate healing stats row
+ self.setFixedSize(320, 260)
# Accept focus for keyboard events (needed for modifier detection)
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
def _setup_ui(self) -> None:
"""Build the HUD UI components."""
- # Main container with semi-transparent background
+ # Main container with semi-transparent background - increased height for healing row
self.container = QFrame(self)
- self.container.setFixedSize(320, 220)
+ self.container.setFixedSize(320, 260)
self.container.setObjectName("hudContainer")
# Style the container - semi-transparent dark background
@@ -405,6 +422,50 @@ class HUDOverlay(QWidget):
layout.addLayout(row2)
+ # Row 3: Healing Stats (NEW)
+ row3 = QHBoxLayout()
+
+ # Healing Done (HP)
+ healing_layout = QVBoxLayout()
+ healing_label = QLabel("❤️ HEALING")
+ healing_label.setStyleSheet("font-size: 10px; color: #888888;")
+ healing_layout.addWidget(healing_label)
+
+ self.healing_value_label = QLabel("0 HP")
+ self.healing_value_label.setStyleSheet("font-size: 13px; font-weight: bold; color: #7FFF7F;")
+ healing_layout.addWidget(self.healing_value_label)
+
+ row3.addLayout(healing_layout)
+ row3.addStretch()
+
+ # Number of Heals
+ heals_layout = QVBoxLayout()
+ heals_label = QLabel("💉 HEALS")
+ heals_label.setStyleSheet("font-size: 10px; color: #888888;")
+ heals_layout.addWidget(heals_label)
+
+ self.heals_value_label = QLabel("0")
+ self.heals_value_label.setStyleSheet("font-size: 13px; font-weight: bold; color: #FFFFFF;")
+ heals_layout.addWidget(self.heals_value_label)
+
+ row3.addLayout(heals_layout)
+ row3.addStretch()
+
+ # Healing Cost
+ heal_cost_layout = QVBoxLayout()
+ heal_cost_label = QLabel("💊 HEAL COST")
+ heal_cost_label.setStyleSheet("font-size: 10px; color: #888888;")
+ heal_cost_layout.addWidget(heal_cost_label)
+
+ self.healing_cost_value_label = QLabel("0.00 PED")
+ self.healing_cost_value_label.setStyleSheet("font-size: 13px; font-weight: bold; color: #FF7F7F;")
+ heal_cost_layout.addWidget(self.healing_cost_value_label)
+
+ row3.addLayout(heal_cost_layout)
+ row3.addStretch()
+
+ layout.addLayout(row3)
+
# === WEAPON INFO ===
weapon_separator = QFrame()
weapon_separator.setFrameShape(QFrame.Shape.HLine)
@@ -736,6 +797,32 @@ class HUDOverlay(QWidget):
self._refresh_display()
self.stats_updated.emit(self._stats.to_dict())
+ def on_heal_event(self, heal_amount: Decimal, decay_cost: Decimal = Decimal('0')) -> None:
+ """Called when healing is done from LogWatcher.
+
+ Args:
+ heal_amount: Amount of HP healed
+ decay_cost: Cost of the heal in PED (based on FAP decay)
+ """
+ if not self.session_active:
+ return
+
+ self._stats.healing_done += heal_amount
+ self._stats.heals_count += 1
+
+ # Add healing cost to total healing cost and update profit/loss
+ if decay_cost > 0:
+ self._stats.healing_cost_total += decay_cost
+ # Recalculate profit/loss including healing costs
+ self._stats.profit_loss = (
+ self._stats.loot_total -
+ self._stats.cost_total -
+ self._stats.healing_cost_total
+ )
+
+ self._refresh_display()
+ self.stats_updated.emit(self._stats.to_dict())
+
def update_display(self) -> None:
"""Public method to refresh display (alias for _refresh_display)."""
self._refresh_display()
@@ -850,6 +937,12 @@ class HUDOverlay(QWidget):
- 'damage_dealt_add': int - Add to damage dealt
- 'damage_taken': int - Total damage taken (or add)
- 'damage_taken_add': int - Add to damage taken
+ - 'healing_done': Decimal - Total HP healed (or add)
+ - 'healing_add': Decimal - Add to healing done
+ - 'heals_count': int - Total number of heals (or add)
+ - 'heals_add': int - Add to heal count
+ - 'healing_cost': Decimal - Total healing cost in PED (or add)
+ - 'healing_cost_add': Decimal - Add to healing cost
- 'kills': int - Total kills (or add)
- 'kills_add': int - Add to kills
- 'globals': int - Total globals (or add)
@@ -858,6 +951,7 @@ class HUDOverlay(QWidget):
- 'hofs_add': int - Add to HoFs
- 'weapon': str - Current weapon name
- 'loadout': str - Current loadout name
+ - 'medical_tool': str - Current medical tool (FAP) name
"""
# Loot (Decimal precision)
if 'loot' in stats:
@@ -877,6 +971,24 @@ class HUDOverlay(QWidget):
elif 'damage_taken_add' in stats:
self._stats.damage_taken += int(stats['damage_taken_add'])
+ # Healing done (NEW)
+ if 'healing_done' in stats:
+ self._stats.healing_done = Decimal(str(stats['healing_done']))
+ elif 'healing_add' in stats:
+ self._stats.healing_done += Decimal(str(stats['healing_add']))
+
+ # Healing count (NEW)
+ if 'heals_count' in stats:
+ self._stats.heals_count = int(stats['heals_count'])
+ elif 'heals_add' in stats:
+ self._stats.heals_count += int(stats['heals_add'])
+
+ # Healing cost (NEW)
+ if 'healing_cost' in stats:
+ self._stats.healing_cost_total = Decimal(str(stats['healing_cost']))
+ elif 'healing_cost_add' in stats:
+ self._stats.healing_cost_total += Decimal(str(stats['healing_cost_add']))
+
# Shots fired
if 'shots_fired' in stats:
self._stats.shots_fired = int(stats['shots_fired'])
@@ -909,6 +1021,18 @@ class HUDOverlay(QWidget):
if 'loadout' in stats:
self._stats.current_loadout = str(stats['loadout'])
+ # Medical Tool (NEW)
+ if 'medical_tool' in stats:
+ self._stats.current_medical_tool = str(stats['medical_tool'])
+
+ # Recalculate profit/loss if costs changed
+ if any(k in stats for k in ['loot', 'loot_delta', 'healing_cost', 'healing_cost_add']):
+ self._stats.profit_loss = (
+ self._stats.loot_total -
+ self._stats.cost_total -
+ self._stats.healing_cost_total
+ )
+
# Refresh display
self._refresh_display()
@@ -920,10 +1044,10 @@ class HUDOverlay(QWidget):
# Loot with 2 decimal places (PED format)
self.loot_value_label.setText(f"{self._stats.loot_total:.2f} PED")
- # Cost with 2 decimal places
+ # Cost with 2 decimal places (weapon cost only)
self.cost_value_label.setText(f"{self._stats.cost_total:.2f} PED")
- # Profit/Loss with color coding
+ # Profit/Loss with color coding (includes healing costs)
profit = self._stats.profit_loss
self.profit_value_label.setText(f"{profit:+.2f} PED")
if profit > 0:
@@ -948,6 +1072,11 @@ class HUDOverlay(QWidget):
self.dealt_value_label.setText(str(self._stats.damage_dealt))
self.taken_value_label.setText(str(self._stats.damage_taken))
+ # Healing Stats (NEW)
+ self.healing_value_label.setText(f"{self._stats.healing_done:.0f} HP")
+ self.heals_value_label.setText(str(self._stats.heals_count))
+ self.healing_cost_value_label.setText(f"{self._stats.healing_cost_total:.2f} PED")
+
# Weapon/Loadout/DPP
self.weapon_label.setText(self._stats.current_weapon[:20])
self.loadout_label.setText(self._stats.current_loadout[:15])
diff --git a/ui/loadout_manager.py b/ui/loadout_manager.py
index 1486247..699925f 100644
--- a/ui/loadout_manager.py
+++ b/ui/loadout_manager.py
@@ -1,45 +1,102 @@
"""
-Lemontropia Suite - Loadout Manager UI
-A PyQt6 dialog for configuring hunting gear loadouts with cost calculations.
+Lemontropia Suite - Loadout Manager UI v2.0
+Full API integration with Entropia Nexus and complete attachment support.
"""
import json
import os
-from dataclasses import dataclass, asdict
+import logging
+from dataclasses import dataclass, asdict, field
from decimal import Decimal, InvalidOperation
from pathlib import Path
-from typing import Optional, List
+from typing import Optional, List, Dict, Any
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout,
QLineEdit, QComboBox, QLabel, QPushButton,
- QGroupBox, QSpinBox, QDoubleSpinBox, QMessageBox,
+ QGroupBox, QSpinBox, QMessageBox,
QListWidget, QListWidgetItem, QSplitter, QWidget,
- QFrame, QScrollArea, QGridLayout
+ QFrame, QScrollArea, QGridLayout, QCheckBox,
+ QDialogButtonBox, QTreeWidget, QTreeWidgetItem,
+ QHeaderView, QTabWidget, QProgressDialog
)
-from PyQt6.QtCore import Qt, pyqtSignal
+from PyQt6.QtCore import Qt, pyqtSignal, QThread
from PyQt6.QtGui import QFont
+from core.nexus_api import EntropiaNexusAPI, WeaponStats, ArmorStats, FinderStats
+from core.attachments import (
+ Attachment, WeaponAmplifier, WeaponScope, WeaponAbsorber,
+ ArmorPlating, Enhancer, can_attach, get_mock_attachments
+)
+
+logger = logging.getLogger(__name__)
+
# ============================================================================
# Data Structures
# ============================================================================
@dataclass
-class LoadoutConfig:
- """Configuration for a hunting loadout."""
+class AttachmentConfig:
+ """Configuration for an equipped attachment."""
name: str
- weapon_name: str
- weapon_damage: Decimal
- weapon_decay_pec: Decimal
- weapon_ammo_pec: Decimal
- armor_name: str
- armor_decay_pec: Decimal
- heal_name: str
- heal_cost_pec: Decimal
+ item_id: str
+ attachment_type: str
+ decay_pec: Decimal
+ # Effect values
+ damage_bonus: Decimal = Decimal("0")
+ range_bonus: Decimal = Decimal("0")
+ efficiency_bonus: Decimal = Decimal("0")
+ protection_bonus: Dict[str, Decimal] = field(default_factory=dict)
- # Optional fields for extended calculations
- shots_per_hour: int = 3600 # Default: 1 shot per second
+ def to_dict(self) -> dict:
+ return {
+ 'name': self.name,
+ 'item_id': self.item_id,
+ 'attachment_type': self.attachment_type,
+ 'decay_pec': str(self.decay_pec),
+ 'damage_bonus': str(self.damage_bonus),
+ 'range_bonus': str(self.range_bonus),
+ 'efficiency_bonus': str(self.efficiency_bonus),
+ 'protection_bonus': {k: str(v) for k, v in self.protection_bonus.items()},
+ }
+
+ @classmethod
+ def from_dict(cls, data: dict) -> "AttachmentConfig":
+ return cls(
+ name=data['name'],
+ item_id=data['item_id'],
+ attachment_type=data['attachment_type'],
+ decay_pec=Decimal(data['decay_pec']),
+ damage_bonus=Decimal(data.get('damage_bonus', '0')),
+ range_bonus=Decimal(data.get('range_bonus', '0')),
+ efficiency_bonus=Decimal(data.get('efficiency_bonus', '0')),
+ protection_bonus={k: Decimal(v) for k, v in data.get('protection_bonus', {}).items()},
+ )
+
+
+@dataclass
+class LoadoutConfig:
+ """Configuration for a hunting loadout with full gear and attachments."""
+ name: str
+
+ # Weapon
+ weapon_name: str
+ weapon_id: int = 0
+ weapon_damage: Decimal = Decimal("0")
+ weapon_decay_pec: Decimal = Decimal("0")
+ weapon_ammo_pec: Decimal = Decimal("0")
+ weapon_dpp: Decimal = Decimal("0")
+
+ # Weapon Attachments
+ weapon_amplifier: Optional[AttachmentConfig] = None
+ weapon_scope: Optional[AttachmentConfig] = None
+ weapon_absorber: Optional[AttachmentConfig] = None
+
+ # Armor
+ armor_name: str = "-- None --"
+ armor_id: int = 0
+ armor_decay_pec: Decimal = Decimal("0")
protection_stab: Decimal = Decimal("0")
protection_cut: Decimal = Decimal("0")
protection_impact: Decimal = Decimal("0")
@@ -50,50 +107,134 @@ class LoadoutConfig:
protection_acid: Decimal = Decimal("0")
protection_electric: Decimal = Decimal("0")
+ # Armor Plating
+ armor_plating: Optional[AttachmentConfig] = None
+
+ # Healing
+ heal_name: str = "-- Custom --"
+ heal_cost_pec: Decimal = Decimal("2.0")
+ heal_amount: Decimal = Decimal("20")
+
+ # Settings
+ shots_per_hour: int = 3600
+ hits_per_hour: int = 720 # Estimate: 1 hit per 5 shots
+ heals_per_hour: int = 60 # Estimate: 1 heal per minute
+
+ def get_total_damage(self) -> Decimal:
+ """Calculate total damage including amplifier."""
+ base = self.weapon_damage
+ if self.weapon_amplifier:
+ base += self.weapon_amplifier.damage_bonus
+ return base
+
+ def get_total_decay_per_shot(self) -> Decimal:
+ """Calculate total decay per shot including attachments."""
+ total = self.weapon_decay_pec
+ if self.weapon_amplifier:
+ total += self.weapon_amplifier.decay_pec
+ if self.weapon_scope:
+ total += self.weapon_scope.decay_pec
+ if self.weapon_absorber:
+ total += self.weapon_absorber.decay_pec
+ return total
+
+ def get_total_ammo_per_shot(self) -> Decimal:
+ """Calculate total ammo per shot including amplifier."""
+ total = self.weapon_ammo_pec
+ if self.weapon_amplifier:
+ # Amplifiers typically add ammo cost based on damage increase
+ # Rough estimate: 0.2 PEC per damage point for amp
+ total += self.weapon_amplifier.damage_bonus * Decimal("0.2")
+ return total
+
def calculate_dpp(self) -> Decimal:
- """Calculate Damage Per Pec (DPP) = Damage / (Decay + Ammo)."""
- total_cost = self.weapon_decay_pec + self.weapon_ammo_pec
+ """Calculate Damage Per Pec (DPP) with all attachments."""
+ total_damage = self.get_total_damage()
+ total_cost = self.get_total_decay_per_shot() + self.get_total_ammo_per_shot()
if total_cost == 0:
return Decimal("0")
- return self.weapon_damage / total_cost
+ return total_damage / total_cost
- def calculate_cost_per_hour(self) -> Decimal:
- """Calculate total PED cost per hour based on shots per hour."""
- cost_per_shot = self.weapon_decay_pec + self.weapon_ammo_pec
- total_weapon_cost = cost_per_shot * Decimal(self.shots_per_hour)
+ def calculate_weapon_cost_per_hour(self) -> Decimal:
+ """Calculate weapon cost per hour."""
+ cost_per_shot = self.get_total_decay_per_shot() + self.get_total_ammo_per_shot()
+ return cost_per_shot * Decimal(self.shots_per_hour)
+
+ def calculate_armor_cost_per_hour(self) -> Decimal:
+ """Calculate armor cost per hour including plating."""
+ total_decay = self.armor_decay_pec
+ if self.armor_plating:
+ total_decay += self.armor_plating.decay_pec
+ return total_decay * Decimal(self.hits_per_hour)
+
+ def calculate_heal_cost_per_hour(self) -> Decimal:
+ """Calculate healing cost per hour."""
+ return self.heal_cost_pec * Decimal(self.heals_per_hour)
+
+ def calculate_total_cost_per_hour(self) -> Decimal:
+ """Calculate total PED cost per hour including all gear and attachments."""
+ weapon_cost = self.calculate_weapon_cost_per_hour()
+ armor_cost = self.calculate_armor_cost_per_hour()
+ heal_cost = self.calculate_heal_cost_per_hour()
- # Estimate armor decay (assume 1 hit per 5 shots on average)
- armor_cost_per_hour = self.armor_decay_pec * Decimal(self.shots_per_hour // 5)
-
- # Estimate healing cost (assume 1 heal per 10 shots)
- heal_cost_per_hour = self.heal_cost_pec * Decimal(self.shots_per_hour // 10)
-
- total_pec = total_weapon_cost + armor_cost_per_hour + heal_cost_per_hour
+ total_pec = weapon_cost + armor_cost + heal_cost
return total_pec / Decimal("100") # Convert PEC to PED
def calculate_break_even(self, mob_health: Decimal) -> Decimal:
"""Calculate break-even loot value for a mob with given health."""
- shots_to_kill = mob_health / self.weapon_damage
+ total_damage = self.get_total_damage()
+ shots_to_kill = mob_health / total_damage if total_damage > 0 else Decimal("1")
if shots_to_kill < 1:
shots_to_kill = Decimal("1")
- cost_per_shot = self.weapon_decay_pec + self.weapon_ammo_pec
+ cost_per_shot = self.get_total_decay_per_shot() + self.get_total_ammo_per_shot()
total_cost_pec = shots_to_kill * cost_per_shot
return total_cost_pec / Decimal("100") # Convert to PED
+ def get_total_protection(self) -> Dict[str, Decimal]:
+ """Get total protection values including plating."""
+ protections = {
+ 'stab': self.protection_stab,
+ 'cut': self.protection_cut,
+ 'impact': self.protection_impact,
+ 'penetration': self.protection_penetration,
+ 'shrapnel': self.protection_shrapnel,
+ 'burn': self.protection_burn,
+ 'cold': self.protection_cold,
+ 'acid': self.protection_acid,
+ 'electric': self.protection_electric,
+ }
+
+ if self.armor_plating and self.armor_plating.protection_bonus:
+ for dmg_type, value in self.armor_plating.protection_bonus.items():
+ if dmg_type in protections:
+ protections[dmg_type] += value
+
+ return protections
+
def to_dict(self) -> dict:
"""Convert to dictionary for JSON serialization."""
- return {
+ data = {
k: str(v) if isinstance(v, Decimal) else v
for k, v in asdict(self).items()
}
+ # Handle attachment configs
+ if self.weapon_amplifier:
+ data['weapon_amplifier'] = self.weapon_amplifier.to_dict()
+ if self.weapon_scope:
+ data['weapon_scope'] = self.weapon_scope.to_dict()
+ if self.weapon_absorber:
+ data['weapon_absorber'] = self.weapon_absorber.to_dict()
+ if self.armor_plating:
+ data['armor_plating'] = self.armor_plating.to_dict()
+ return data
@classmethod
def from_dict(cls, data: dict) -> "LoadoutConfig":
"""Create LoadoutConfig from dictionary."""
decimal_fields = [
- 'weapon_damage', 'weapon_decay_pec', 'weapon_ammo_pec',
- 'armor_decay_pec', 'heal_cost_pec', 'protection_stab',
+ 'weapon_damage', 'weapon_decay_pec', 'weapon_ammo_pec', 'weapon_dpp',
+ 'armor_decay_pec', 'heal_cost_pec', 'heal_amount', 'protection_stab',
'protection_cut', 'protection_impact', 'protection_penetration',
'protection_shrapnel', 'protection_burn', 'protection_cold',
'protection_acid', 'protection_electric'
@@ -103,10 +244,34 @@ class LoadoutConfig:
if field in data:
data[field] = Decimal(data[field])
- if 'shots_per_hour' in data:
- data['shots_per_hour'] = int(data['shots_per_hour'])
+ # Handle integer fields
+ int_fields = ['weapon_id', 'armor_id', 'shots_per_hour', 'hits_per_hour', 'heals_per_hour']
+ for field in int_fields:
+ if field in data:
+ data[field] = int(data[field])
- # Handle legacy configs without heal_name
+ # Handle attachment configs
+ if 'weapon_amplifier' in data and data['weapon_amplifier']:
+ data['weapon_amplifier'] = AttachmentConfig.from_dict(data['weapon_amplifier'])
+ else:
+ data['weapon_amplifier'] = None
+
+ if 'weapon_scope' in data and data['weapon_scope']:
+ data['weapon_scope'] = AttachmentConfig.from_dict(data['weapon_scope'])
+ else:
+ data['weapon_scope'] = None
+
+ if 'weapon_absorber' in data and data['weapon_absorber']:
+ data['weapon_absorber'] = AttachmentConfig.from_dict(data['weapon_absorber'])
+ else:
+ data['weapon_absorber'] = None
+
+ if 'armor_plating' in data and data['armor_plating']:
+ data['armor_plating'] = AttachmentConfig.from_dict(data['armor_plating'])
+ else:
+ data['armor_plating'] = None
+
+ # Handle legacy configs
if 'heal_name' not in data:
data['heal_name'] = '-- Custom --'
@@ -114,33 +279,18 @@ class LoadoutConfig:
# ============================================================================
-# Mock Data
+# Mock Data for Healing
# ============================================================================
-MOCK_WEAPONS = [
- {"name": "Sollomate Opalo", "damage": Decimal("9.0"), "decay": Decimal("0.09"), "ammo": Decimal("1.80")},
- {"name": "Omegaton M2100", "damage": Decimal("10.5"), "decay": Decimal("0.10"), "ammo": Decimal("2.10")},
- {"name": "Breer M1a", "damage": Decimal("12.0"), "decay": Decimal("0.12"), "ammo": Decimal("2.40")},
- {"name": "Justifier Mk.II", "damage": Decimal("25.0"), "decay": Decimal("0.25"), "ammo": Decimal("5.00")},
- {"name": "Marber Bravo-Type", "damage": Decimal("45.0"), "decay": Decimal("0.45"), "ammo": Decimal("9.00")},
- {"name": " Maddox IV", "damage": Decimal("80.0"), "decay": Decimal("0.80"), "ammo": Decimal("16.00")},
-]
-
-MOCK_ARMOR = [
- {"name": "Pixie Harness", "decay": Decimal("0.05"), "impact": Decimal("4"), "cut": Decimal("3"), "stab": Decimal("2")},
- {"name": "Shogun Harness", "decay": Decimal("0.12"), "impact": Decimal("8"), "cut": Decimal("6"), "stab": Decimal("5"), "burn": Decimal("4")},
- {"name": "Ghost Harness", "decay": Decimal("0.20"), "impact": Decimal("12"), "cut": Decimal("10"), "stab": Decimal("9"), "burn": Decimal("8"), "cold": Decimal("7")},
- {"name": "Vigilante Harness", "decay": Decimal("0.08"), "impact": Decimal("6"), "cut": Decimal("5"), "stab": Decimal("4")},
- {"name": "Hermes Harness", "decay": Decimal("0.15"), "impact": Decimal("10"), "cut": Decimal("8"), "stab": Decimal("7"), "penetration": Decimal("5")},
-]
-
MOCK_HEALING = [
- {"name": "Vivo T10", "cost": Decimal("2.0")},
- {"name": "Vivo T15", "cost": Decimal("3.5")},
- {"name": "Vivo S10", "cost": Decimal("4.0")},
- {"name": "Refurbished H.E.A.R.T.", "cost": Decimal("1.5")},
- {"name": "Restoration Chip I", "cost": Decimal("5.0")},
- {"name": "Restoration Chip II", "cost": Decimal("8.0")},
+ {"name": "Vivo T10", "cost": Decimal("2.0"), "amount": Decimal("12")},
+ {"name": "Vivo T15", "cost": Decimal("3.5"), "amount": Decimal("18")},
+ {"name": "Vivo S10", "cost": Decimal("4.0"), "amount": Decimal("25")},
+ {"name": "Refurbished H.E.A.R.T.", "cost": Decimal("1.5"), "amount": Decimal("8")},
+ {"name": "Restoration Chip I", "cost": Decimal("5.0"), "amount": Decimal("30")},
+ {"name": "Restoration Chip II", "cost": Decimal("8.0"), "amount": Decimal("50")},
+ {"name": "Restoration Chip III", "cost": Decimal("12.0"), "amount": Decimal("80")},
+ {"name": "Mod 2350", "cost": Decimal("15.0"), "amount": Decimal("100")},
]
@@ -193,18 +343,521 @@ class DarkGroupBox(QGroupBox):
# ============================================================================
-# Main Dialog
+# Attachment Selector Dialog
+# ============================================================================
+
+class AttachmentSelectorDialog(QDialog):
+ """Dialog for selecting attachments."""
+
+ attachment_selected = pyqtSignal(object) # AttachmentConfig
+
+ def __init__(self, attachment_type: str, parent=None):
+ super().__init__(parent)
+ self.attachment_type = attachment_type
+ self.selected_attachment = None
+
+ self.setWindowTitle(f"Select {attachment_type.title()}")
+ self.setMinimumSize(500, 400)
+ self._setup_ui()
+ self._load_attachments()
+
+ def _setup_ui(self):
+ layout = QVBoxLayout(self)
+
+ # Header
+ header = QLabel(f"Select a {self.attachment_type.title()}")
+ header.setFont(QFont("Arial", 12, QFont.Weight.Bold))
+ layout.addWidget(header)
+
+ # Attachment list
+ self.list_widget = QListWidget()
+ self.list_widget.itemSelectionChanged.connect(self._on_selection_changed)
+ self.list_widget.itemDoubleClicked.connect(self._on_double_click)
+ layout.addWidget(self.list_widget)
+
+ # Stats preview
+ self.preview_label = QLabel("Select an attachment to view stats")
+ self.preview_label.setStyleSheet("color: #888888; padding: 10px;")
+ layout.addWidget(self.preview_label)
+
+ # 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)
+ layout.addWidget(button_box)
+
+ # None option
+ self.none_btn = QPushButton("Remove Attachment")
+ self.none_btn.clicked.connect(self._on_none)
+ layout.addWidget(self.none_btn)
+
+ def _load_attachments(self):
+ """Load attachments from mock data."""
+ attachments = get_mock_attachments(self.attachment_type)
+ self.attachments = attachments
+
+ self.list_widget.clear()
+
+ for att in attachments:
+ item = QListWidgetItem(f"📎 {att.name}")
+ item.setData(Qt.ItemDataRole.UserRole, att)
+
+ # Add tooltip with stats
+ tooltip = f"Decay: {att.decay_pec} PEC\n"
+ if hasattr(att, 'damage_increase'):
+ tooltip += f"Damage: +{att.damage_increase}"
+ elif hasattr(att, 'range_increase'):
+ tooltip += f"Range: +{att.range_increase}m"
+ elif hasattr(att, 'damage_reduction'):
+ tooltip += f"Protection: +{att.damage_reduction}"
+ elif hasattr(att, 'protection_impact'):
+ tooltip += f"Impact: +{att.protection_impact}"
+
+ item.setToolTip(tooltip)
+ self.list_widget.addItem(item)
+
+ def _on_selection_changed(self):
+ """Handle selection change."""
+ selected = self.list_widget.selectedItems()
+ if selected:
+ attachment = selected[0].data(Qt.ItemDataRole.UserRole)
+ self.selected_attachment = attachment
+ self.ok_btn.setEnabled(True)
+ self._update_preview(attachment)
+
+ def _update_preview(self, attachment):
+ """Update preview label."""
+ text = f"{attachment.name}
"
+ text += f"Decay: {attachment.decay_pec} PEC
"
+
+ if isinstance(attachment, WeaponAmplifier):
+ text += f"Damage Increase: +{attachment.damage_increase}
"
+ text += f"Ammo Increase: +{attachment.ammo_increase}"
+ elif isinstance(attachment, WeaponScope):
+ text += f"Range Increase: +{attachment.range_increase}m"
+ elif isinstance(attachment, WeaponAbsorber):
+ text += f"Damage Reduction: -{attachment.damage_reduction}"
+ elif isinstance(attachment, ArmorPlating):
+ text += f"Total Protection: +{attachment.get_total_protection()}"
+
+ self.preview_label.setText(text)
+ self.preview_label.setStyleSheet("color: #e0e0e0; padding: 10px; background: #2d2d2d; border-radius: 4px;")
+
+ def _on_double_click(self, item):
+ """Handle double click."""
+ self._on_accept()
+
+ def _on_accept(self):
+ """Handle OK button."""
+ if self.selected_attachment:
+ att = self.selected_attachment
+
+ # Build protection bonus dict for armor plating
+ protection_bonus = {}
+ if isinstance(att, ArmorPlating):
+ protection_bonus = {
+ 'stab': att.protection_stab,
+ 'cut': att.protection_cut,
+ 'impact': att.protection_impact,
+ 'penetration': att.protection_penetration,
+ 'shrapnel': att.protection_shrapnel,
+ 'burn': att.protection_burn,
+ 'cold': att.protection_cold,
+ 'acid': att.protection_acid,
+ 'electric': att.protection_electric,
+ }
+
+ config = AttachmentConfig(
+ name=att.name,
+ item_id=att.item_id,
+ attachment_type=att.attachment_type,
+ decay_pec=att.decay_pec,
+ damage_bonus=getattr(att, 'damage_increase', Decimal("0")),
+ range_bonus=getattr(att, 'range_increase', Decimal("0")),
+ efficiency_bonus=Decimal("0"),
+ protection_bonus=protection_bonus,
+ )
+
+ self.attachment_selected.emit(config)
+ self.accept()
+
+ def _on_none(self):
+ """Remove attachment."""
+ self.attachment_selected.emit(None)
+ self.accept()
+
+
+# ============================================================================
+# Gear Loader Threads
+# ============================================================================
+
+class WeaponLoaderThread(QThread):
+ """Thread to load weapons from API."""
+ weapons_loaded = pyqtSignal(list)
+ error_occurred = pyqtSignal(str)
+
+ def run(self):
+ try:
+ api = EntropiaNexusAPI()
+ weapons = api.get_all_weapons()
+ self.weapons_loaded.emit(weapons)
+ except Exception as e:
+ logger.error(f"Failed to load weapons: {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:
+ api = EntropiaNexusAPI()
+ armors = api.get_all_armors()
+ self.armors_loaded.emit(armors)
+ except Exception as e:
+ logger.error(f"Failed to load armors: {e}")
+ self.error_occurred.emit(str(e))
+
+
+# ============================================================================
+# Weapon Selector Dialog
+# ============================================================================
+
+class WeaponSelectorDialog(QDialog):
+ """Dialog for selecting weapons from Entropia Nexus API."""
+
+ weapon_selected = pyqtSignal(object) # WeaponStats
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setWindowTitle("Select Weapon - Entropia Nexus")
+ self.setMinimumSize(900, 600)
+ self.weapons = []
+ self.selected_weapon = None
+ self.api = EntropiaNexusAPI()
+
+ self._setup_ui()
+ self._load_data()
+
+ def _setup_ui(self):
+ layout = QVBoxLayout(self)
+ layout.setSpacing(10)
+
+ # Status
+ self.status_label = QLabel("Loading weapons from Entropia Nexus...")
+ layout.addWidget(self.status_label)
+
+ # Search
+ search_layout = QHBoxLayout()
+ search_layout.addWidget(QLabel("Search:"))
+ self.search_input = QLineEdit()
+ self.search_input.setPlaceholderText("Search weapons by name...")
+ 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 tree
+ self.results_tree = QTreeWidget()
+ self.results_tree.setHeaderLabels([
+ "Name", "Type", "Category", "Damage", "DPP", "Decay", "Ammo", "Cost/h"
+ ])
+ header = self.results_tree.header()
+ header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
+ for i in range(1, 8):
+ header.setSectionResizeMode(i, QHeaderView.ResizeMode.Fixed)
+ header.resizeSection(1, 80)
+ header.resizeSection(2, 80)
+ header.resizeSection(3, 60)
+ header.resizeSection(4, 60)
+ header.resizeSection(5, 70)
+ header.resizeSection(6, 60)
+ header.resizeSection(7, 70)
+
+ self.results_tree.setAlternatingRowColors(True)
+ self.results_tree.itemSelectionChanged.connect(self._on_selection_changed)
+ self.results_tree.itemDoubleClicked.connect(self._on_double_click)
+ layout.addWidget(self.results_tree)
+
+ # Stats preview
+ self.preview_group = DarkGroupBox("Weapon Stats")
+ self.preview_layout = QFormLayout(self.preview_group)
+ self.preview_layout.addRow("Select a weapon to view stats", QLabel(""))
+ layout.addWidget(self.preview_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 Weapon")
+ layout.addWidget(button_box)
+
+ def _load_data(self):
+ """Load weapons asynchronously."""
+ self.loader = WeaponLoaderThread()
+ self.loader.weapons_loaded.connect(self._on_data_loaded)
+ self.loader.error_occurred.connect(self._on_load_error)
+ self.loader.start()
+
+ def _on_data_loaded(self, weapons):
+ """Handle loaded weapons."""
+ self.weapons = weapons
+ self.status_label.setText(f"Loaded {len(weapons):,} weapons from Entropia Nexus")
+ self._populate_tree(weapons[:200]) # Show first 200
+
+ def _on_load_error(self, error):
+ """Handle load error."""
+ self.status_label.setText(f"Error loading weapons: {error}")
+ QMessageBox.critical(self, "Error", f"Failed to load weapons: {error}")
+
+ def _populate_tree(self, weapons):
+ """Populate tree with weapons."""
+ self.results_tree.clear()
+
+ for w in weapons:
+ item = QTreeWidgetItem([
+ w.name,
+ w.type,
+ w.category,
+ str(w.total_damage),
+ f"{w.dpp:.2f}",
+ f"{w.decay:.2f}" if w.decay else "-",
+ str(w.ammo_burn) if w.ammo_burn else "-",
+ f"{w.cost_per_hour:.0f}"
+ ])
+ item.setData(0, Qt.ItemDataRole.UserRole, w)
+ self.results_tree.addTopLevelItem(item)
+
+ def _on_search(self):
+ """Search weapons."""
+ query = self.search_input.text().strip().lower()
+ if not query:
+ self._populate_tree(self.weapons[:200])
+ return
+
+ results = [w for w in self.weapons if query in w.name.lower()]
+ self._populate_tree(results)
+ self.status_label.setText(f"Found {len(results)} weapons matching '{query}'")
+
+ def _on_selection_changed(self):
+ """Handle selection change."""
+ selected = self.results_tree.selectedItems()
+ if selected:
+ weapon = selected[0].data(0, Qt.ItemDataRole.UserRole)
+ self.selected_weapon = weapon
+ self.ok_btn.setEnabled(True)
+ self._update_preview(weapon)
+ else:
+ self.selected_weapon = None
+ self.ok_btn.setEnabled(False)
+
+ def _update_preview(self, w):
+ """Update stats preview."""
+ while self.preview_layout.rowCount() > 0:
+ self.preview_layout.removeRow(0)
+
+ self.preview_layout.addRow("Name:", QLabel(w.name))
+ self.preview_layout.addRow("Type:", QLabel(f"{w.type} {w.category}"))
+ self.preview_layout.addRow("Damage:", QLabel(str(w.total_damage)))
+ self.preview_layout.addRow("DPP:", QLabel(f"{w.dpp:.3f}"))
+ self.preview_layout.addRow("Decay:", QLabel(f"{w.decay:.3f} PEC/shot" if w.decay else "-"))
+ self.preview_layout.addRow("Ammo:", QLabel(f"{w.ammo_burn} units/shot" if w.ammo_burn else "-"))
+ self.preview_layout.addRow("Cost/Hour:", QLabel(f"{w.cost_per_hour:.2f} PED"))
+ if w.efficiency:
+ self.preview_layout.addRow("Efficiency:", QLabel(f"{w.efficiency:.1f}%"))
+
+ def _on_double_click(self, item, column):
+ """Handle double click."""
+ self._on_accept()
+
+ def _on_accept(self):
+ """Handle OK button."""
+ if self.selected_weapon:
+ self.weapon_selected.emit(self.selected_weapon)
+ self.accept()
+
+
+# ============================================================================
+# Armor Selector Dialog
+# ============================================================================
+
+class ArmorSelectorDialog(QDialog):
+ """Dialog for selecting armors from Entropia Nexus API."""
+
+ armor_selected = pyqtSignal(object) # ArmorStats
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setWindowTitle("Select Armor - Entropia Nexus")
+ self.setMinimumSize(800, 500)
+ self.armors = []
+ self.selected_armor = None
+ self.api = EntropiaNexusAPI()
+
+ self._setup_ui()
+ self._load_data()
+
+ def _setup_ui(self):
+ layout = QVBoxLayout(self)
+ layout.setSpacing(10)
+
+ # Status
+ self.status_label = QLabel("Loading armors from Entropia Nexus...")
+ layout.addWidget(self.status_label)
+
+ # Search
+ search_layout = QHBoxLayout()
+ search_layout.addWidget(QLabel("Search:"))
+ self.search_input = QLineEdit()
+ self.search_input.setPlaceholderText("Search armors by name...")
+ 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 tree
+ self.results_tree = QTreeWidget()
+ self.results_tree.setHeaderLabels([
+ "Name", "Impact", "Cut", "Stab", "Pen", "Burn", "Cold", "Total"
+ ])
+ header = self.results_tree.header()
+ header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
+ for i in range(1, 8):
+ header.setSectionResizeMode(i, QHeaderView.ResizeMode.Fixed)
+ header.resizeSection(i, 60)
+
+ self.results_tree.setAlternatingRowColors(True)
+ self.results_tree.itemSelectionChanged.connect(self._on_selection_changed)
+ self.results_tree.itemDoubleClicked.connect(self._on_double_click)
+ layout.addWidget(self.results_tree)
+
+ # Stats preview
+ self.preview_group = DarkGroupBox("Armor Stats")
+ self.preview_layout = QFormLayout(self.preview_group)
+ self.preview_layout.addRow("Select an armor to view stats", QLabel(""))
+ layout.addWidget(self.preview_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 Armor")
+ layout.addWidget(button_box)
+
+ def _load_data(self):
+ """Load armors asynchronously."""
+ self.loader = ArmorLoaderThread()
+ self.loader.armors_loaded.connect(self._on_data_loaded)
+ self.loader.error_occurred.connect(self._on_load_error)
+ self.loader.start()
+
+ def _on_data_loaded(self, armors):
+ """Handle loaded armors."""
+ self.armors = armors
+ self.status_label.setText(f"Loaded {len(armors):,} armors from Entropia Nexus")
+ self._populate_tree(armors[:200])
+
+ def _on_load_error(self, error):
+ """Handle load error."""
+ self.status_label.setText(f"Error loading armors: {error}")
+ QMessageBox.critical(self, "Error", f"Failed to load armors: {error}")
+
+ def _populate_tree(self, armors):
+ """Populate tree with armors."""
+ self.results_tree.clear()
+
+ for a in armors:
+ item = QTreeWidgetItem([
+ a.name,
+ str(a.protection_impact),
+ str(a.protection_cut),
+ str(a.protection_stab),
+ str(a.protection_penetration),
+ str(a.protection_burn),
+ str(a.protection_cold),
+ str(a.total_protection),
+ ])
+ item.setData(0, Qt.ItemDataRole.UserRole, a)
+ self.results_tree.addTopLevelItem(item)
+
+ def _on_search(self):
+ """Search armors."""
+ query = self.search_input.text().strip().lower()
+ if not query:
+ self._populate_tree(self.armors[:200])
+ return
+
+ results = [a for a in self.armors if query in a.name.lower()]
+ self._populate_tree(results)
+ self.status_label.setText(f"Found {len(results)} armors matching '{query}'")
+
+ def _on_selection_changed(self):
+ """Handle selection change."""
+ selected = self.results_tree.selectedItems()
+ if selected:
+ armor = selected[0].data(0, Qt.ItemDataRole.UserRole)
+ self.selected_armor = armor
+ self.ok_btn.setEnabled(True)
+ self._update_preview(armor)
+ else:
+ self.selected_armor = None
+ self.ok_btn.setEnabled(False)
+
+ def _update_preview(self, a):
+ """Update stats preview."""
+ while self.preview_layout.rowCount() > 0:
+ self.preview_layout.removeRow(0)
+
+ self.preview_layout.addRow("Name:", QLabel(a.name))
+ self.preview_layout.addRow("Total Protection:", QLabel(str(a.total_protection)))
+ self.preview_layout.addRow("Durability:", QLabel(str(a.durability)))
+
+ # Protection breakdown
+ prot_text = f"Impact: {a.protection_impact}, Cut: {a.protection_cut}, Stab: {a.protection_stab}"
+ self.preview_layout.addRow("Protection:", QLabel(prot_text))
+
+ 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()
+
+
+# ============================================================================
+# Main Loadout Manager Dialog
# ============================================================================
class LoadoutManagerDialog(QDialog):
- """Main dialog for managing hunting loadouts."""
+ """Main dialog for managing hunting loadouts with full API integration."""
loadout_saved = pyqtSignal(str) # Emitted when loadout is saved
def __init__(self, parent=None, config_dir: Optional[str] = None):
super().__init__(parent)
- self.setWindowTitle("Lemontropia Suite - Loadout Manager")
- self.setMinimumSize(900, 700)
+ self.setWindowTitle("Lemontropia Suite - Loadout Manager v2.0")
+ self.setMinimumSize(1000, 800)
# Configuration directory
if config_dir is None:
@@ -214,13 +867,15 @@ class LoadoutManagerDialog(QDialog):
self.config_dir.mkdir(parents=True, exist_ok=True)
self.current_loadout: Optional[LoadoutConfig] = None
+ self.current_weapon: Optional[WeaponStats] = None
+ self.current_armor: Optional[ArmorStats] = None
self._apply_dark_theme()
self._create_widgets()
self._create_layout()
self._connect_signals()
self._load_saved_loadouts()
- self._populate_mock_data()
+ self._populate_healing_data()
def _apply_dark_theme(self):
"""Apply dark theme styling."""
@@ -289,6 +944,13 @@ class LoadoutManagerDialog(QDialog):
QPushButton#deleteButton:hover {
background-color: #f44336;
}
+ QPushButton#selectButton {
+ background-color: #1565c0;
+ border-color: #2196f3;
+ }
+ QPushButton#selectButton:hover {
+ background-color: #2196f3;
+ }
QListWidget {
background-color: #2d2d2d;
color: #e0e0e0;
@@ -301,6 +963,19 @@ class LoadoutManagerDialog(QDialog):
QScrollArea {
border: none;
}
+ QTabWidget::pane {
+ border: 1px solid #3d3d3d;
+ background-color: #1e1e1e;
+ }
+ QTabBar::tab {
+ background-color: #2d2d2d;
+ color: #e0e0e0;
+ padding: 8px 16px;
+ border: 1px solid #3d3d3d;
+ }
+ QTabBar::tab:selected {
+ background-color: #4a90d9;
+ }
""")
def _create_widgets(self):
@@ -309,26 +984,56 @@ class LoadoutManagerDialog(QDialog):
self.loadout_name_edit = QLineEdit()
self.loadout_name_edit.setPlaceholderText("Enter loadout name...")
- # Shots per hour
+ # Activity settings
self.shots_per_hour_spin = QSpinBox()
- self.shots_per_hour_spin.setRange(1, 10000)
+ self.shots_per_hour_spin.setRange(1, 20000)
self.shots_per_hour_spin.setValue(3600)
- self.shots_per_hour_spin.setSuffix(" shots/hr")
+ self.shots_per_hour_spin.setSuffix(" /hr")
+
+ self.hits_per_hour_spin = QSpinBox()
+ self.hits_per_hour_spin.setRange(0, 5000)
+ self.hits_per_hour_spin.setValue(720)
+ self.hits_per_hour_spin.setSuffix(" /hr")
+
+ self.heals_per_hour_spin = QSpinBox()
+ self.heals_per_hour_spin.setRange(0, 500)
+ self.heals_per_hour_spin.setValue(60)
+ self.heals_per_hour_spin.setSuffix(" /hr")
# Weapon section
self.weapon_group = DarkGroupBox("🔫 Weapon Configuration")
- self.weapon_combo = QComboBox()
- self.weapon_combo.setEditable(False) # Show dropdown list on click
+ self.select_weapon_btn = QPushButton("🔍 Select from Entropia Nexus")
+ self.select_weapon_btn.setObjectName("selectButton")
+ self.weapon_name_label = QLabel("No weapon selected")
+ self.weapon_name_label.setStyleSheet("font-weight: bold; color: #4a90d9;")
+
self.weapon_damage_edit = DecimalLineEdit()
self.weapon_decay_edit = DecimalLineEdit()
self.weapon_ammo_edit = DecimalLineEdit()
- self.dpp_label = QLabel("0.00")
- self.dpp_label.setStyleSheet("color: #4caf50; font-weight: bold; font-size: 14px;")
+ self.dpp_label = QLabel("0.0000")
+ self.dpp_label.setStyleSheet("color: #4caf50; font-weight: bold; font-size: 16px;")
+
+ # Weapon attachments
+ self.attach_amp_btn = QPushButton("⚡ Add Amplifier")
+ self.attach_scope_btn = QPushButton("🔭 Add Scope")
+ self.attach_absorber_btn = QPushButton("🛡️ Add Absorber")
+ self.amp_label = QLabel("None")
+ self.scope_label = QLabel("None")
+ self.absorber_label = QLabel("None")
+ self.remove_amp_btn = QPushButton("✕")
+ self.remove_scope_btn = QPushButton("✕")
+ self.remove_absorber_btn = QPushButton("✕")
+ self.remove_amp_btn.setFixedWidth(30)
+ self.remove_scope_btn.setFixedWidth(30)
+ self.remove_absorber_btn.setFixedWidth(30)
# Armor section
self.armor_group = DarkGroupBox("🛡️ Armor Configuration")
- self.armor_combo = QComboBox()
- self.armor_combo.setEditable(False) # Show dropdown list on click
+ self.select_armor_btn = QPushButton("🔍 Select from Entropia Nexus")
+ self.select_armor_btn.setObjectName("selectButton")
+ self.armor_name_label = QLabel("No armor selected")
+ self.armor_name_label.setStyleSheet("font-weight: bold; color: #4a90d9;")
+
self.armor_decay_edit = DecimalLineEdit()
# Protection values
@@ -342,23 +1047,34 @@ class LoadoutManagerDialog(QDialog):
self.protection_acid_edit = DecimalLineEdit()
self.protection_elec_edit = DecimalLineEdit()
+ # Armor plating
+ self.attach_plating_btn = QPushButton("🔩 Add Plating")
+ self.plating_label = QLabel("None")
+ self.remove_plating_btn = QPushButton("✕")
+ self.remove_plating_btn.setFixedWidth(30)
+
# Healing section
self.heal_group = DarkGroupBox("💊 Healing Configuration")
self.heal_combo = QComboBox()
- self.heal_combo.setEditable(False) # Show dropdown list on click
self.heal_cost_edit = DecimalLineEdit()
+ self.heal_amount_edit = DecimalLineEdit()
# Cost summary
self.summary_group = DarkGroupBox("📊 Cost Summary")
- self.cost_per_hour_label = QLabel("0.00 PED/hr")
- self.cost_per_hour_label.setStyleSheet("color: #ff9800; font-weight: bold; font-size: 16px;")
- self.break_even_label = QLabel("Break-even: 0.00 PED (mob health: 100)")
- self.break_even_label.setStyleSheet("color: #4caf50;")
+ self.weapon_cost_label = QLabel("0.00 PEC/hr")
+ self.armor_cost_label = QLabel("0.00 PEC/hr")
+ self.heal_cost_label = QLabel("0.00 PEC/hr")
+ self.total_cost_label = QLabel("0.00 PED/hr")
+ self.total_cost_label.setStyleSheet("color: #ff9800; font-weight: bold; font-size: 18px;")
+ self.total_dpp_label = QLabel("0.0000")
+ self.total_dpp_label.setStyleSheet("color: #4caf50; font-weight: bold; font-size: 18px;")
# Break-even calculator
self.mob_health_edit = DecimalLineEdit()
self.mob_health_edit.set_decimal(Decimal("100"))
- self.calc_break_even_btn = QPushButton("Calculate Break-Even")
+ self.calc_break_even_btn = QPushButton("Calculate")
+ self.break_even_label = QLabel("Break-even: 0.00 PED")
+ self.break_even_label.setStyleSheet("color: #4caf50;")
# Saved loadouts list
self.saved_list = QListWidget()
@@ -371,8 +1087,6 @@ class LoadoutManagerDialog(QDialog):
self.delete_btn.setObjectName("deleteButton")
self.new_btn = QPushButton("🆕 New Loadout")
self.close_btn = QPushButton("❌ Close")
-
- # Refresh button for saved list
self.refresh_btn = QPushButton("🔄 Refresh")
def _create_layout(self):
@@ -417,22 +1131,61 @@ class LoadoutManagerDialog(QDialog):
name_label.setFont(QFont("Arial", 10, QFont.Weight.Bold))
name_layout.addWidget(name_label)
name_layout.addWidget(self.loadout_name_edit, stretch=1)
- name_layout.addWidget(QLabel("Shots/Hour:"))
- name_layout.addWidget(self.shots_per_hour_spin)
right_layout.addLayout(name_layout)
+ # Activity settings
+ activity_group = DarkGroupBox("⚙️ Activity Settings")
+ activity_layout = QGridLayout(activity_group)
+ activity_layout.addWidget(QLabel("Shots/Hour:"), 0, 0)
+ activity_layout.addWidget(self.shots_per_hour_spin, 0, 1)
+ activity_layout.addWidget(QLabel("Hits Taken/Hour:"), 0, 2)
+ activity_layout.addWidget(self.hits_per_hour_spin, 0, 3)
+ activity_layout.addWidget(QLabel("Heals/Hour:"), 0, 4)
+ activity_layout.addWidget(self.heals_per_hour_spin, 0, 5)
+ right_layout.addWidget(activity_group)
+
# Weapon configuration
weapon_layout = QFormLayout(self.weapon_group)
- weapon_layout.addRow("Weapon:", self.weapon_combo)
+
+ weapon_select_layout = QHBoxLayout()
+ weapon_select_layout.addWidget(self.select_weapon_btn)
+ weapon_select_layout.addWidget(self.weapon_name_label, stretch=1)
+ weapon_layout.addRow("Weapon:", weapon_select_layout)
+
weapon_layout.addRow("Damage:", self.weapon_damage_edit)
weapon_layout.addRow("Decay/shot (PEC):", self.weapon_decay_edit)
weapon_layout.addRow("Ammo/shot (PEC):", self.weapon_ammo_edit)
- weapon_layout.addRow("DPP (Damage Per Pec):", self.dpp_label)
+ weapon_layout.addRow("Total DPP:", self.dpp_label)
+
+ # Attachments
+ attachments_frame = QFrame()
+ attachments_layout = QGridLayout(attachments_frame)
+ attachments_layout.addWidget(QLabel("Amplifier:"), 0, 0)
+ attachments_layout.addWidget(self.amp_label, 0, 1)
+ attachments_layout.addWidget(self.attach_amp_btn, 0, 2)
+ attachments_layout.addWidget(self.remove_amp_btn, 0, 3)
+
+ attachments_layout.addWidget(QLabel("Scope:"), 1, 0)
+ attachments_layout.addWidget(self.scope_label, 1, 1)
+ attachments_layout.addWidget(self.attach_scope_btn, 1, 2)
+ attachments_layout.addWidget(self.remove_scope_btn, 1, 3)
+
+ attachments_layout.addWidget(QLabel("Absorber:"), 2, 0)
+ attachments_layout.addWidget(self.absorber_label, 2, 1)
+ attachments_layout.addWidget(self.attach_absorber_btn, 2, 2)
+ attachments_layout.addWidget(self.remove_absorber_btn, 2, 3)
+
+ weapon_layout.addRow("Attachments:", attachments_frame)
right_layout.addWidget(self.weapon_group)
# Armor configuration
armor_layout = QFormLayout(self.armor_group)
- armor_layout.addRow("Armor Set:", self.armor_combo)
+
+ armor_select_layout = QHBoxLayout()
+ armor_select_layout.addWidget(self.select_armor_btn)
+ armor_select_layout.addWidget(self.armor_name_label, stretch=1)
+ armor_layout.addRow("Armor:", armor_select_layout)
+
armor_layout.addRow("Decay/hit (PEC):", self.armor_decay_edit)
# Protection grid
@@ -459,23 +1212,37 @@ class LoadoutManagerDialog(QDialog):
protection_layout.addWidget(edit, row, col + 1)
armor_layout.addRow("Protection Values:", protection_frame)
+
+ # Plating
+ plating_layout = QHBoxLayout()
+ plating_layout.addWidget(QLabel("Armor Plating:"))
+ plating_layout.addWidget(self.plating_label)
+ plating_layout.addWidget(self.attach_plating_btn)
+ plating_layout.addWidget(self.remove_plating_btn)
+ armor_layout.addRow("", plating_layout)
+
right_layout.addWidget(self.armor_group)
# Healing configuration
heal_layout = QFormLayout(self.heal_group)
heal_layout.addRow("Healing Tool:", self.heal_combo)
heal_layout.addRow("Cost/heal (PEC):", self.heal_cost_edit)
+ heal_layout.addRow("Heal amount:", self.heal_amount_edit)
right_layout.addWidget(self.heal_group)
# Cost summary
summary_layout = QFormLayout(self.summary_group)
- summary_layout.addRow("Estimated Cost:", self.cost_per_hour_label)
+ summary_layout.addRow("Weapon Cost:", self.weapon_cost_label)
+ summary_layout.addRow("Armor Cost:", self.armor_cost_label)
+ summary_layout.addRow("Healing Cost:", self.heal_cost_label)
+ summary_layout.addRow("Total DPP:", self.total_dpp_label)
+ summary_layout.addRow("Total Cost:", self.total_cost_label)
break_even_layout = QHBoxLayout()
break_even_layout.addWidget(QLabel("Mob Health:"))
break_even_layout.addWidget(self.mob_health_edit)
break_even_layout.addWidget(self.calc_break_even_btn)
- summary_layout.addRow(break_even_layout)
+ summary_layout.addRow("Break-Even:", break_even_layout)
summary_layout.addRow("", self.break_even_label)
right_layout.addWidget(self.summary_group)
@@ -490,24 +1257,39 @@ class LoadoutManagerDialog(QDialog):
splitter = QSplitter(Qt.Orientation.Horizontal)
splitter.addWidget(left_panel)
splitter.addWidget(right_scroll)
- splitter.setSizes([250, 650])
+ splitter.setSizes([250, 750])
main_layout.addWidget(splitter)
def _connect_signals(self):
"""Connect all signal handlers."""
- # Weapon changes
- self.weapon_combo.currentTextChanged.connect(self._on_weapon_changed)
+ # Weapon selection
+ self.select_weapon_btn.clicked.connect(self._on_select_weapon)
self.weapon_damage_edit.textChanged.connect(self._update_calculations)
self.weapon_decay_edit.textChanged.connect(self._update_calculations)
self.weapon_ammo_edit.textChanged.connect(self._update_calculations)
- # Armor changes
- self.armor_combo.currentTextChanged.connect(self._on_armor_changed)
+ # Armor selection
+ self.select_armor_btn.clicked.connect(self._on_select_armor)
- # Healing changes
+ # Attachments
+ self.attach_amp_btn.clicked.connect(lambda: self._on_attach("amplifier"))
+ self.attach_scope_btn.clicked.connect(lambda: self._on_attach("scope"))
+ self.attach_absorber_btn.clicked.connect(lambda: self._on_attach("absorber"))
+ self.attach_plating_btn.clicked.connect(lambda: self._on_attach("plating"))
+ self.remove_amp_btn.clicked.connect(self._on_remove_amp)
+ self.remove_scope_btn.clicked.connect(self._on_remove_scope)
+ self.remove_absorber_btn.clicked.connect(self._on_remove_absorber)
+ self.remove_plating_btn.clicked.connect(self._on_remove_plating)
+
+ # Healing
self.heal_combo.currentTextChanged.connect(self._on_heal_changed)
+ # Activity settings
+ self.shots_per_hour_spin.valueChanged.connect(self._update_calculations)
+ self.hits_per_hour_spin.valueChanged.connect(self._update_calculations)
+ self.heals_per_hour_spin.valueChanged.connect(self._update_calculations)
+
# Buttons
self.save_btn.clicked.connect(self._save_loadout)
self.load_btn.clicked.connect(self._load_selected)
@@ -516,146 +1298,188 @@ class LoadoutManagerDialog(QDialog):
self.refresh_btn.clicked.connect(self._load_saved_loadouts)
self.close_btn.clicked.connect(self.reject)
self.calc_break_even_btn.clicked.connect(self._calculate_break_even)
- self.shots_per_hour_spin.valueChanged.connect(self._update_calculations)
# Double click on list
self.saved_list.itemDoubleClicked.connect(self._load_from_item)
- def _populate_mock_data(self):
- """Populate combos with mock data."""
- # Weapons
- self.weapon_combo.addItem("-- Custom --")
- for weapon in MOCK_WEAPONS:
- self.weapon_combo.addItem(weapon["name"])
-
- # Armor
- self.armor_combo.addItem("-- Custom --")
- for armor in MOCK_ARMOR:
- self.armor_combo.addItem(armor["name"])
-
- # Healing
+ def _populate_healing_data(self):
+ """Populate healing combo with data."""
self.heal_combo.addItem("-- Custom --")
for heal in MOCK_HEALING:
self.heal_combo.addItem(heal["name"])
-
- # Set initial enabled state (all fields enabled for custom entry)
- self.weapon_damage_edit.setEnabled(True)
- self.weapon_decay_edit.setEnabled(True)
- self.weapon_ammo_edit.setEnabled(True)
- self.armor_decay_edit.setEnabled(True)
- self.protection_stab_edit.setEnabled(True)
- self.protection_cut_edit.setEnabled(True)
- self.protection_impact_edit.setEnabled(True)
- self.protection_pen_edit.setEnabled(True)
- self.protection_shrap_edit.setEnabled(True)
- self.protection_burn_edit.setEnabled(True)
- self.protection_cold_edit.setEnabled(True)
- self.protection_acid_edit.setEnabled(True)
- self.protection_elec_edit.setEnabled(True)
- self.heal_cost_edit.setEnabled(True)
- def _on_weapon_changed(self, name: str):
- """Handle weapon selection change."""
- if name == "-- Custom --":
- # Enable manual entry for custom weapon
- self.weapon_damage_edit.setEnabled(True)
- self.weapon_decay_edit.setEnabled(True)
- self.weapon_ammo_edit.setEnabled(True)
- # Clear fields for user to enter custom values
- self.weapon_damage_edit.clear()
- self.weapon_decay_edit.clear()
- self.weapon_ammo_edit.clear()
- else:
- # Auto-fill stats for predefined weapon and disable fields
- for weapon in MOCK_WEAPONS:
- if weapon["name"] == name:
- self.weapon_damage_edit.set_decimal(weapon["damage"])
- self.weapon_decay_edit.set_decimal(weapon["decay"])
- self.weapon_ammo_edit.set_decimal(weapon["ammo"])
- break
- self.weapon_damage_edit.setEnabled(False)
- self.weapon_decay_edit.setEnabled(False)
- self.weapon_ammo_edit.setEnabled(False)
+ def _on_select_weapon(self):
+ """Open weapon selector dialog."""
+ dialog = WeaponSelectorDialog(self)
+ dialog.weapon_selected.connect(self._on_weapon_selected)
+ dialog.exec()
+
+ def _on_weapon_selected(self, weapon: WeaponStats):
+ """Handle weapon selection."""
+ self.current_weapon = weapon
+ self.weapon_name_label.setText(weapon.name)
+ self.weapon_damage_edit.set_decimal(weapon.total_damage)
+ self.weapon_decay_edit.set_decimal(weapon.decay or Decimal("0"))
+ self.weapon_ammo_edit.set_decimal(Decimal(weapon.ammo_burn or 0))
self._update_calculations()
- def _on_armor_changed(self, name: str):
- """Handle armor selection change."""
- if name == "-- Custom --":
- # Enable manual entry for custom armor
- self.armor_decay_edit.setEnabled(True)
- self.protection_stab_edit.setEnabled(True)
- self.protection_cut_edit.setEnabled(True)
- self.protection_impact_edit.setEnabled(True)
- self.protection_pen_edit.setEnabled(True)
- self.protection_shrap_edit.setEnabled(True)
- self.protection_burn_edit.setEnabled(True)
- self.protection_cold_edit.setEnabled(True)
- self.protection_acid_edit.setEnabled(True)
- self.protection_elec_edit.setEnabled(True)
- # Clear fields for user to enter custom values
- self.armor_decay_edit.clear()
- self.protection_stab_edit.clear()
- self.protection_cut_edit.clear()
- self.protection_impact_edit.clear()
- self.protection_pen_edit.clear()
- self.protection_shrap_edit.clear()
- self.protection_burn_edit.clear()
- self.protection_cold_edit.clear()
- self.protection_acid_edit.clear()
- self.protection_elec_edit.clear()
- else:
- # Auto-fill stats for predefined armor and disable fields
- for armor in MOCK_ARMOR:
- if armor["name"] == name:
- self.armor_decay_edit.set_decimal(armor["decay"])
- self.protection_impact_edit.set_decimal(Decimal(armor.get("impact", "0")))
- self.protection_cut_edit.set_decimal(Decimal(armor.get("cut", "0")))
- self.protection_stab_edit.set_decimal(Decimal(armor.get("stab", "0")))
- self.protection_burn_edit.set_decimal(Decimal(armor.get("burn", "0")))
- self.protection_cold_edit.set_decimal(Decimal(armor.get("cold", "0")))
- self.protection_pen_edit.set_decimal(Decimal(armor.get("penetration", "0")))
- break
- self.armor_decay_edit.setEnabled(False)
- self.protection_stab_edit.setEnabled(False)
- self.protection_cut_edit.setEnabled(False)
- self.protection_impact_edit.setEnabled(False)
- self.protection_pen_edit.setEnabled(False)
- self.protection_shrap_edit.setEnabled(False)
- self.protection_burn_edit.setEnabled(False)
- self.protection_cold_edit.setEnabled(False)
- self.protection_acid_edit.setEnabled(False)
- self.protection_elec_edit.setEnabled(False)
+ def _on_select_armor(self):
+ """Open armor selector dialog."""
+ dialog = ArmorSelectorDialog(self)
+ dialog.armor_selected.connect(self._on_armor_selected)
+ dialog.exec()
+
+ def _on_armor_selected(self, armor: ArmorStats):
+ """Handle armor selection."""
+ self.current_armor = armor
+ self.armor_name_label.setText(armor.name)
+
+ # Calculate decay estimate (rough approximation)
+ decay_estimate = Decimal("0.05") # Default estimate
+ self.armor_decay_edit.set_decimal(decay_estimate)
+
+ # Set protection values
+ self.protection_stab_edit.set_decimal(armor.protection_stab)
+ self.protection_cut_edit.set_decimal(armor.protection_cut)
+ self.protection_impact_edit.set_decimal(armor.protection_impact)
+ self.protection_pen_edit.set_decimal(armor.protection_penetration)
+ self.protection_shrap_edit.set_decimal(armor.protection_shrapnel)
+ self.protection_burn_edit.set_decimal(armor.protection_burn)
+ self.protection_cold_edit.set_decimal(armor.protection_cold)
+ self.protection_acid_edit.set_decimal(armor.protection_acid)
+ self.protection_elec_edit.set_decimal(armor.protection_electric)
+
+ self._update_calculations()
+
+ def _on_attach(self, attachment_type: str):
+ """Open attachment selector."""
+ dialog = AttachmentSelectorDialog(attachment_type, self)
+ dialog.attachment_selected.connect(lambda att: self._on_attachment_selected(attachment_type, att))
+ dialog.exec()
+
+ def _on_attachment_selected(self, attachment_type: str, config: Optional[AttachmentConfig]):
+ """Handle attachment selection."""
+ if attachment_type == "amplifier":
+ if config:
+ self.current_loadout = self._get_current_config()
+ self.current_loadout.weapon_amplifier = config
+ self.amp_label.setText(f"{config.name} (+{config.damage_bonus} dmg)")
+ else:
+ if self.current_loadout:
+ self.current_loadout.weapon_amplifier = None
+ self.amp_label.setText("None")
+
+ elif attachment_type == "scope":
+ if config:
+ self.current_loadout = self._get_current_config()
+ self.current_loadout.weapon_scope = config
+ self.scope_label.setText(f"{config.name} (+{config.range_bonus}m)")
+ else:
+ if self.current_loadout:
+ self.current_loadout.weapon_scope = None
+ self.scope_label.setText("None")
+
+ elif attachment_type == "absorber":
+ if config:
+ self.current_loadout = self._get_current_config()
+ self.current_loadout.weapon_absorber = config
+ # Get damage reduction from attachment
+ attachments = get_mock_attachments("absorber")
+ absorber = next((a for a in attachments if a.name == config.name), None)
+ reduction = absorber.damage_reduction if absorber else Decimal("0")
+ self.absorber_label.setText(f"{config.name} (-{reduction} dmg)")
+ else:
+ if self.current_loadout:
+ self.current_loadout.weapon_absorber = None
+ self.absorber_label.setText("None")
+
+ elif attachment_type == "plating":
+ if config:
+ self.current_loadout = self._get_current_config()
+ self.current_loadout.armor_plating = config
+ # Calculate total protection from plating
+ attachments = get_mock_attachments("plating")
+ plating = next((p for p in attachments if p.name == config.name), None)
+ if plating:
+ total_prot = plating.get_total_protection()
+ self.plating_label.setText(f"{config.name} (+{total_prot} prot)")
+ else:
+ self.plating_label.setText(config.name)
+ else:
+ if self.current_loadout:
+ self.current_loadout.armor_plating = None
+ self.plating_label.setText("None")
+
+ self._update_calculations()
+
+ def _on_remove_amp(self):
+ """Remove amplifier."""
+ if self.current_loadout:
+ self.current_loadout.weapon_amplifier = None
+ self.amp_label.setText("None")
+ self._update_calculations()
+
+ def _on_remove_scope(self):
+ """Remove scope."""
+ if self.current_loadout:
+ self.current_loadout.weapon_scope = None
+ self.scope_label.setText("None")
+ self._update_calculations()
+
+ def _on_remove_absorber(self):
+ """Remove absorber."""
+ if self.current_loadout:
+ self.current_loadout.weapon_absorber = None
+ self.absorber_label.setText("None")
+ self._update_calculations()
+
+ def _on_remove_plating(self):
+ """Remove plating."""
+ if self.current_loadout:
+ self.current_loadout.armor_plating = None
+ self.plating_label.setText("None")
+ self._update_calculations()
def _on_heal_changed(self, name: str):
"""Handle healing selection change."""
if name == "-- Custom --":
- # Enable manual entry for custom healing
self.heal_cost_edit.setEnabled(True)
- # Clear field for user to enter custom value
+ self.heal_amount_edit.setEnabled(True)
self.heal_cost_edit.clear()
+ self.heal_amount_edit.clear()
else:
- # Auto-fill stats for predefined healing and disable field
for heal in MOCK_HEALING:
if heal["name"] == name:
self.heal_cost_edit.set_decimal(heal["cost"])
+ self.heal_amount_edit.set_decimal(heal["amount"])
break
self.heal_cost_edit.setEnabled(False)
+ self.heal_amount_edit.setEnabled(False)
+ self._update_calculations()
def _update_calculations(self):
- """Update DPP and cost calculations."""
+ """Update all cost and DPP calculations."""
try:
config = self._get_current_config()
# Update DPP
dpp = config.calculate_dpp()
self.dpp_label.setText(f"{dpp:.4f}")
+ self.total_dpp_label.setText(f"{dpp:.4f}")
- # Update cost per hour
- cost_per_hour = config.calculate_cost_per_hour()
- self.cost_per_hour_label.setText(f"{cost_per_hour:.2f} PED/hr")
+ # Update cost breakdown
+ weapon_cost = config.calculate_weapon_cost_per_hour()
+ armor_cost = config.calculate_armor_cost_per_hour()
+ heal_cost = config.calculate_heal_cost_per_hour()
+ total_cost = config.calculate_total_cost_per_hour()
+
+ self.weapon_cost_label.setText(f"{weapon_cost:.0f} PEC/hr")
+ self.armor_cost_label.setText(f"{armor_cost:.0f} PEC/hr")
+ self.heal_cost_label.setText(f"{heal_cost:.0f} PEC/hr")
+ self.total_cost_label.setText(f"{total_cost:.2f} PED/hr")
except Exception as e:
- pass # Ignore calculation errors during typing
+ logger.error(f"Calculation error: {e}")
def _calculate_break_even(self):
"""Calculate and display break-even loot value."""
@@ -669,24 +1493,29 @@ class LoadoutManagerDialog(QDialog):
break_even = config.calculate_break_even(mob_health)
self.break_even_label.setText(
- f"Break-even: {break_even:.2f} PED (mob health: {mob_health})"
+ f"Break-even: {break_even:.2f} PED (mob HP: {mob_health})"
)
except Exception as e:
QMessageBox.critical(self, "Error", f"Calculation failed: {str(e)}")
def _get_current_config(self) -> LoadoutConfig:
"""Get current configuration from UI fields."""
- return LoadoutConfig(
+ config = LoadoutConfig(
name=self.loadout_name_edit.text().strip() or "Unnamed",
- weapon_name=self.weapon_combo.currentText(),
+ weapon_name=self.current_weapon.name if self.current_weapon else (self.weapon_name_label.text() if self.weapon_name_label.text() != "No weapon selected" else "-- Custom --"),
+ weapon_id=self.current_weapon.id if self.current_weapon else 0,
weapon_damage=self.weapon_damage_edit.get_decimal(),
weapon_decay_pec=self.weapon_decay_edit.get_decimal(),
weapon_ammo_pec=self.weapon_ammo_edit.get_decimal(),
- armor_name=self.armor_combo.currentText(),
+ armor_name=self.current_armor.name if self.current_armor else (self.armor_name_label.text() if self.armor_name_label.text() != "No armor selected" else "-- None --"),
+ armor_id=self.current_armor.id if self.current_armor else 0,
armor_decay_pec=self.armor_decay_edit.get_decimal(),
heal_name=self.heal_combo.currentText(),
heal_cost_pec=self.heal_cost_edit.get_decimal(),
+ heal_amount=self.heal_amount_edit.get_decimal(),
shots_per_hour=self.shots_per_hour_spin.value(),
+ hits_per_hour=self.hits_per_hour_spin.value(),
+ heals_per_hour=self.heals_per_hour_spin.value(),
protection_stab=self.protection_stab_edit.get_decimal(),
protection_cut=self.protection_cut_edit.get_decimal(),
protection_impact=self.protection_impact_edit.get_decimal(),
@@ -697,23 +1526,47 @@ class LoadoutManagerDialog(QDialog):
protection_acid=self.protection_acid_edit.get_decimal(),
protection_electric=self.protection_elec_edit.get_decimal(),
)
+
+ # Preserve existing attachments
+ if self.current_loadout:
+ config.weapon_amplifier = self.current_loadout.weapon_amplifier
+ config.weapon_scope = self.current_loadout.weapon_scope
+ config.weapon_absorber = self.current_loadout.weapon_absorber
+ config.armor_plating = self.current_loadout.armor_plating
+
+ return config
def _set_config(self, config: LoadoutConfig):
"""Set UI fields from configuration."""
self.loadout_name_edit.setText(config.name)
self.shots_per_hour_spin.setValue(config.shots_per_hour)
+ self.hits_per_hour_spin.setValue(config.hits_per_hour)
+ self.heals_per_hour_spin.setValue(config.heals_per_hour)
- self.weapon_combo.setCurrentText(config.weapon_name)
+ # Weapon
+ self.weapon_name_label.setText(config.weapon_name)
self.weapon_damage_edit.set_decimal(config.weapon_damage)
self.weapon_decay_edit.set_decimal(config.weapon_decay_pec)
self.weapon_ammo_edit.set_decimal(config.weapon_ammo_pec)
- # Enable/disable based on whether it's a custom weapon
- is_custom_weapon = config.weapon_name == "-- Custom --"
- self.weapon_damage_edit.setEnabled(is_custom_weapon)
- self.weapon_decay_edit.setEnabled(is_custom_weapon)
- self.weapon_ammo_edit.setEnabled(is_custom_weapon)
- self.armor_combo.setCurrentText(config.armor_name)
+ # Weapon attachments
+ if config.weapon_amplifier:
+ self.amp_label.setText(f"{config.weapon_amplifier.name} (+{config.weapon_amplifier.damage_bonus} dmg)")
+ else:
+ self.amp_label.setText("None")
+
+ if config.weapon_scope:
+ self.scope_label.setText(f"{config.weapon_scope.name} (+{config.weapon_scope.range_bonus}m)")
+ else:
+ self.scope_label.setText("None")
+
+ if config.weapon_absorber:
+ self.absorber_label.setText(config.weapon_absorber.name)
+ else:
+ self.absorber_label.setText("None")
+
+ # Armor
+ self.armor_name_label.setText(config.armor_name)
self.armor_decay_edit.set_decimal(config.armor_decay_pec)
self.protection_stab_edit.set_decimal(config.protection_stab)
@@ -725,24 +1578,20 @@ class LoadoutManagerDialog(QDialog):
self.protection_cold_edit.set_decimal(config.protection_cold)
self.protection_acid_edit.set_decimal(config.protection_acid)
self.protection_elec_edit.set_decimal(config.protection_electric)
- # Enable/disable based on whether it's custom armor
- is_custom_armor = config.armor_name == "-- Custom --"
- self.armor_decay_edit.setEnabled(is_custom_armor)
- self.protection_stab_edit.setEnabled(is_custom_armor)
- self.protection_cut_edit.setEnabled(is_custom_armor)
- self.protection_impact_edit.setEnabled(is_custom_armor)
- self.protection_pen_edit.setEnabled(is_custom_armor)
- self.protection_shrap_edit.setEnabled(is_custom_armor)
- self.protection_burn_edit.setEnabled(is_custom_armor)
- self.protection_cold_edit.setEnabled(is_custom_armor)
- self.protection_acid_edit.setEnabled(is_custom_armor)
- self.protection_elec_edit.setEnabled(is_custom_armor)
- self.heal_combo.setCurrentText(config.heal_name if hasattr(config, 'heal_name') else "-- Custom --")
+ # Armor plating
+ if config.armor_plating:
+ self.plating_label.setText(config.armor_plating.name)
+ else:
+ self.plating_label.setText("None")
+
+ # Healing
+ self.heal_combo.setCurrentText(config.heal_name)
self.heal_cost_edit.set_decimal(config.heal_cost_pec)
- # Enable/disable based on whether it's custom healing
- is_custom_heal = (config.heal_name if hasattr(config, 'heal_name') else "-- Custom --") == "-- Custom --"
- self.heal_cost_edit.setEnabled(is_custom_heal)
+ self.heal_amount_edit.set_decimal(config.heal_amount)
+
+ # Store config
+ self.current_loadout = config
self._update_calculations()
@@ -788,16 +1637,26 @@ class LoadoutManagerDialog(QDialog):
item = QListWidgetItem(f"📋 {config.name}")
item.setData(Qt.ItemDataRole.UserRole, str(filepath))
- item.setToolTip(
+
+ # Build tooltip
+ dpp = config.calculate_dpp()
+ cost = config.calculate_total_cost_per_hour()
+ tooltip = (
f"Weapon: {config.weapon_name}\n"
f"Armor: {config.armor_name}\n"
- f"DPP: {config.calculate_dpp():.2f}"
+ f"Total DPP: {dpp:.3f}\n"
+ f"Cost/hr: {cost:.2f} PED"
)
+ if config.weapon_amplifier:
+ tooltip += f"\nAmplifier: {config.weapon_amplifier.name}"
+
+ item.setToolTip(tooltip)
self.saved_list.addItem(item)
- except Exception:
+ except Exception as e:
+ logger.error(f"Failed to load {filepath}: {e}")
continue
- except Exception:
- pass
+ except Exception as e:
+ logger.error(f"Failed to list loadouts: {e}")
def _load_selected(self):
"""Load the selected loadout from the list."""
@@ -819,7 +1678,6 @@ class LoadoutManagerDialog(QDialog):
config = LoadoutConfig.from_dict(data)
self._set_config(config)
- self.current_loadout = config
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to load: {str(e)}")
@@ -851,11 +1709,10 @@ class LoadoutManagerDialog(QDialog):
def _new_loadout(self):
"""Clear all fields for a new loadout."""
self.loadout_name_edit.clear()
- self.weapon_combo.setCurrentIndex(0) # "-- Custom --"
- self.armor_combo.setCurrentIndex(0) # "-- Custom --"
- self.heal_combo.setCurrentIndex(0) # "-- Custom --"
+ self.weapon_name_label.setText("No weapon selected")
+ self.armor_name_label.setText("No armor selected")
- # Clear all fields
+ # Clear fields
self.weapon_damage_edit.clear()
self.weapon_decay_edit.clear()
self.weapon_ammo_edit.clear()
@@ -870,25 +1727,28 @@ class LoadoutManagerDialog(QDialog):
self.protection_acid_edit.clear()
self.protection_elec_edit.clear()
self.heal_cost_edit.clear()
+ self.heal_amount_edit.clear()
- # Enable all fields for custom entry (since "-- Custom --" is selected)
- self.weapon_damage_edit.setEnabled(True)
- self.weapon_decay_edit.setEnabled(True)
- self.weapon_ammo_edit.setEnabled(True)
- self.armor_decay_edit.setEnabled(True)
- self.protection_stab_edit.setEnabled(True)
- self.protection_cut_edit.setEnabled(True)
- self.protection_impact_edit.setEnabled(True)
- self.protection_pen_edit.setEnabled(True)
- self.protection_shrap_edit.setEnabled(True)
- self.protection_burn_edit.setEnabled(True)
- self.protection_cold_edit.setEnabled(True)
- self.protection_acid_edit.setEnabled(True)
- self.protection_elec_edit.setEnabled(True)
- self.heal_cost_edit.setEnabled(True)
+ # Reset attachment labels
+ self.amp_label.setText("None")
+ self.scope_label.setText("None")
+ self.absorber_label.setText("None")
+ self.plating_label.setText("None")
+ # Reset values
+ self.shots_per_hour_spin.setValue(3600)
+ self.hits_per_hour_spin.setValue(720)
+ self.heals_per_hour_spin.setValue(60)
self.mob_health_edit.set_decimal(Decimal("100"))
+
+ # Reset combos
+ self.heal_combo.setCurrentIndex(0)
+
+ # Clear stored objects
+ self.current_weapon = None
+ self.current_armor = None
self.current_loadout = None
+
self._update_calculations()
def get_current_loadout(self) -> Optional[LoadoutConfig]:
@@ -905,6 +1765,9 @@ def main():
import sys
from PyQt6.QtWidgets import QApplication
+ # Setup logging
+ logging.basicConfig(level=logging.INFO)
+
app = QApplication(sys.argv)
app.setStyle('Fusion')
@@ -921,8 +1784,11 @@ def main():
config = dialog.get_current_loadout()
if config:
print(f"\nFinal Loadout: {config.name}")
- print(f" Weapon: {config.weapon_name} (DPP: {config.calculate_dpp():.2f})")
- print(f" Cost/hour: {config.calculate_cost_per_hour():.2f} PED")
+ print(f" Weapon: {config.weapon_name}")
+ if config.weapon_amplifier:
+ print(f" Amplifier: {config.weapon_amplifier.name}")
+ print(f" Total DPP: {config.calculate_dpp():.4f}")
+ print(f" Total Cost: {config.calculate_total_cost_per_hour():.2f} PED/hr")
sys.exit(0)
diff --git a/ui/main_window.py b/ui/main_window.py
index ca5f7d6..8bea2d8 100644
--- a/ui/main_window.py
+++ b/ui/main_window.py
@@ -1028,9 +1028,21 @@ class MainWindow(QMainWindow):
on_damage_dealt(event)
def on_damage_taken(event):
- """Handle damage taken."""
+ """Handle damage taken - track armor decay cost."""
+ from decimal import Decimal
+
damage = event.data.get('damage', 0)
self.hud.on_damage_taken(float(damage))
+
+ # Calculate armor decay cost per hit
+ # Formula: cost_per_hit = armor_decay_pec / 100 (PED)
+ if self._selected_armor_stats and self._selected_armor_stats.get('decay'):
+ armor_decay_pec = Decimal(str(self._selected_armor_stats.get('decay', 0)))
+ if armor_decay_pec > 0:
+ # Convert PEC to PED (1 PED = 100 PEC)
+ cost_ped = armor_decay_pec / Decimal('100')
+ self.hud.update_cost(cost_ped)
+ self.log_debug("Armor", f"Armor decay: {cost_ped:.4f} PED (decay: {armor_decay_pec} PEC)")
def on_evade(event):
"""Handle evade."""