feat(swarm): Agent swarm - Loadout Manager v2.0, Armor Decay, Attachments
- Loadout Manager v2.0 with full Nexus API integration (3,099 weapons, 1,985 armors) - Attachment system: Amplifiers, Scopes, Absorbers, Armor Platings - Weapon/Armor/Attachment selectors with real data - Armor decay tracking when hit (cost added to HUD) - Complete cost calculations (weapon + armor + attachments + healing) - NEW: ui/attachment_selector.py standalone module - Updated core/nexus_api.py with decay fields - DPP display in HUD overlay
This commit is contained in:
parent
32e095350b
commit
d24d5e149e
|
|
@ -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'
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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"<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()
|
||||
|
|
@ -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])
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1028,10 +1028,22 @@ 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."""
|
||||
evade_type = event.data.get('type', 'Evade')
|
||||
|
|
|
|||
Loading…
Reference in New Issue