Lemontropia-Suite/ui/attachment_selector.py

679 lines
26 KiB
Python

"""
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"<b>Decay:</b> {attachment.decay_pec} PEC/shot")
if isinstance(attachment, WeaponAmplifier):
stats_lines.append(f"<b>Damage Increase:</b> +{attachment.damage_increase}")
stats_lines.append(f"<b>Ammo Increase:</b> +{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"<b>Amp DPP:</b> {amp_dpp:.2f}")
elif isinstance(attachment, WeaponScope):
stats_lines.append(f"<b>Range Increase:</b> +{attachment.range_increase}m")
if attachment.accuracy_bonus > 0:
stats_lines.append(f"<b>Accuracy Bonus:</b> +{attachment.accuracy_bonus}")
elif isinstance(attachment, WeaponAbsorber):
stats_lines.append(f"<b>Damage Reduction:</b> -{attachment.damage_reduction}")
elif isinstance(attachment, ArmorPlating):
total = attachment.get_total_protection()
stats_lines.append(f"<b>Total Protection:</b> +{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"<b>Protection:</b> {', '.join(prots)}")
elif isinstance(attachment, FinderAmplifier):
stats_lines.append(f"<b>Depth:</b> +{attachment.depth_increase}m")
stats_lines.append(f"<b>Radius:</b> +{attachment.radius_increase}m")
elif isinstance(attachment, Enhancer):
stats_lines.append(f"<b>Tier:</b> {attachment.tier}")
stats_lines.append(f"<b>Effect:</b> {attachment.effect_name} +{attachment.effect_value}")
elif isinstance(attachment, MindforceImplant):
stats_lines.append(f"<b>Mindforce Bonus:</b> +{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"<b>Cost/Hour:</b> {cost_per_hour:.2f} PED")
self.preview_stats.setText("<br>".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()