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."""