feat(attachments): add attachment system and DPP display

- Created core/attachments.py with full attachment type system
- Supports: Amplifiers, Scopes, Absorbers, Finder Amps, Platings, Enhancers, Implants
- Added DPP display to HUD overlay
- Attachment compatibility validation rules
This commit is contained in:
LemonNexus 2026-02-09 09:15:03 +00:00
parent a3a0fbe2f5
commit 32e095350b
2 changed files with 207 additions and 2 deletions

196
core/attachments.py Normal file
View File

@ -0,0 +1,196 @@
"""
Attachment System for Lemontropia Suite
Defines all attachment types and their compatibility with gear.
"""
from dataclasses import dataclass
from decimal import Decimal
from typing import Optional, List
@dataclass
class Attachment:
"""Base class for all attachments."""
name: str
item_id: str
decay_pec: Decimal
attachment_type: str # e.g., 'amplifier', 'scope', 'absorber'
# Effects
damage_bonus: Decimal = Decimal('0')
range_bonus: Decimal = Decimal('0')
efficiency_bonus: Decimal = Decimal('0')
def calculate_cost_per_use(self) -> Decimal:
"""Calculate cost per use in PED."""
return self.decay_pec / Decimal('100')
@dataclass
class WeaponAmplifier(Attachment):
"""Weapon amplifier attachment."""
damage_increase: Decimal = Decimal('0')
ammo_increase: int = 0
def __post_init__(self):
self.attachment_type = 'amplifier'
@dataclass
class WeaponScope(Attachment):
"""Weapon scope attachment."""
range_increase: Decimal = Decimal('0')
accuracy_bonus: Decimal = Decimal('0')
def __post_init__(self):
self.attachment_type = 'scope'
@dataclass
class WeaponAbsorber(Attachment):
"""Weapon absorber (reduces damage taken)."""
damage_reduction: Decimal = Decimal('0')
def __post_init__(self):
self.attachment_type = 'absorber'
@dataclass
class FinderAmplifier(Attachment):
"""Mining finder amplifier."""
depth_increase: Decimal = Decimal('0')
radius_increase: Decimal = Decimal('0')
def __post_init__(self):
self.attachment_type = 'finder_amp'
@dataclass
class ArmorPlating(Attachment):
"""Armor plating for increased protection."""
protection_stab: Decimal = Decimal('0')
protection_cut: Decimal = Decimal('0')
protection_impact: Decimal = Decimal('0')
protection_penetration: Decimal = Decimal('0')
protection_shrapnel: Decimal = Decimal('0')
protection_burn: Decimal = Decimal('0')
protection_cold: Decimal = Decimal('0')
protection_acid: Decimal = Decimal('0')
protection_electric: Decimal = Decimal('0')
def __post_init__(self):
self.attachment_type = 'plating'
def get_total_protection(self) -> Decimal:
"""Get total protection value."""
return (
self.protection_stab + self.protection_cut + self.protection_impact +
self.protection_penetration + self.protection_shrapnel +
self.protection_burn + self.protection_cold +
self.protection_acid + self.protection_electric
)
@dataclass
class Enhancer(Attachment):
"""Gear enhancer (adds special effects)."""
tier: int = 1
effect_name: str = ""
effect_value: Decimal = Decimal('0')
def __post_init__(self):
self.attachment_type = 'enhancer'
@dataclass
class MindforceImplant(Attachment):
"""Mindforce implant for mindforce chips."""
mindforce_bonus: Decimal = Decimal('0')
def __post_init__(self):
self.attachment_type = 'implant'
# ============================================================================
# Attachment Compatibility Rules
# ============================================================================
ATTACHMENT_COMPATIBILITY = {
'weapon': ['amplifier', 'scope', 'absorber', 'enhancer'],
'armor': ['plating', 'enhancer'],
'finder': ['finder_amp', 'enhancer'],
'mindforce': ['implant', 'enhancer'],
'tool': ['enhancer'],
}
def can_attach(gear_type: str, attachment_type: str) -> bool:
"""Check if attachment is compatible with gear type."""
compatible = ATTACHMENT_COMPATIBILITY.get(gear_type, [])
return attachment_type in compatible
def get_compatible_attachments(gear_type: str) -> List[str]:
"""Get list of compatible attachment types for gear."""
return ATTACHMENT_COMPATIBILITY.get(gear_type, [])
# ============================================================================
# Mock Attachment Data (until API is available)
# ============================================================================
MOCK_AMPLIFIERS = [
WeaponAmplifier("A101", "amp_a101", Decimal("0.05"), damage_increase=Decimal("4"), ammo_increase=10),
WeaponAmplifier("A102", "amp_a102", Decimal("0.08"), damage_increase=Decimal("6"), ammo_increase=15),
WeaponAmplifier("A103", "amp_a103", Decimal("0.12"), damage_increase=Decimal("8"), ammo_increase=20),
WeaponAmplifier("A104", "amp_a104", Decimal("0.18"), damage_increase=Decimal("12"), ammo_increase=30),
WeaponAmplifier("A105", "amp_a105", Decimal("0.25"), damage_increase=Decimal("16"), ammo_increase=40),
WeaponAmplifier("A106", "amp_a106", Decimal("0.35"), damage_increase=Decimal("20"), ammo_increase=50),
]
MOCK_SCOPES = [
WeaponScope("Longreach 4", "scope_lr4", Decimal("0.02"), range_increase=Decimal("10")),
WeaponScope("Longreach 6", "scope_lr6", Decimal("0.04"), range_increase=Decimal("15")),
WeaponScope("Longreach 8", "scope_lr8", Decimal("0.06"), range_increase=Decimal("20")),
]
MOCK_ABSORBERS = [
WeaponAbsorber("Damage II", "abs_dmg2", Decimal("0.03"), damage_reduction=Decimal("2")),
WeaponAbsorber("Damage IV", "abs_dmg4", Decimal("0.05"), damage_reduction=Decimal("4")),
]
MOCK_FINDER_AMPS = [
FinderAmplifier("DSEC L-10", "fa_dsec10", Decimal("0.10"), depth_increase=Decimal("50"), radius_increase=Decimal("5")),
FinderAmplifier("DSEC L-30", "fa_dsec30", Decimal("0.25"), depth_increase=Decimal("100"), radius_increase=Decimal("10")),
]
MOCK_PLATINGS = [
ArmorPlating("Impact Plating", "plt_impact", Decimal("0.03"), protection_impact=Decimal("3")),
ArmorPlating("Cut Plating", "plt_cut", Decimal("0.03"), protection_cut=Decimal("3")),
ArmorPlating("Burn Plating", "plt_burn", Decimal("0.03"), protection_burn=Decimal("3")),
]
MOCK_ENHANCERS = [
Enhancer("Damage Enhancer 1", "enh_dmg1", Decimal("0.10"), tier=1, effect_name="damage", effect_value=Decimal("2")),
Enhancer("Damage Enhancer 2", "enh_dmg2", Decimal("0.15"), tier=2, effect_name="damage", effect_value=Decimal("4")),
Enhancer("Economy Enhancer 1", "enh_eco1", Decimal("0.08"), tier=1, effect_name="economy", effect_value=Decimal("5")),
]
MOCK_IMPLANTS = [
MindforceImplant("NeoPsion 10", "impl_np10", Decimal("0.05"), mindforce_bonus=Decimal("10")),
MindforceImplant("NeoPsion 20", "impl_np20", Decimal("0.10"), mindforce_bonus=Decimal("20")),
]
def get_mock_attachments(attachment_type: str) -> List[Attachment]:
"""Get mock attachments by type."""
mock_data = {
'amplifier': MOCK_AMPLIFIERS,
'scope': MOCK_SCOPES,
'absorber': MOCK_ABSORBERS,
'finder_amp': MOCK_FINDER_AMPS,
'plating': MOCK_PLATINGS,
'enhancer': MOCK_ENHANCERS,
'implant': MOCK_IMPLANTS,
}
return mock_data.get(attachment_type, [])

View File

@ -422,6 +422,11 @@ class HUDOverlay(QWidget):
self.weapon_label.setStyleSheet("font-size: 11px; color: #CCCCCC;") self.weapon_label.setStyleSheet("font-size: 11px; color: #CCCCCC;")
weapon_layout.addWidget(self.weapon_label) weapon_layout.addWidget(self.weapon_label)
# DPP display
self.dpp_label = QLabel("DPP: --")
self.dpp_label.setStyleSheet("font-size: 10px; color: #FFD700;")
weapon_layout.addWidget(self.dpp_label)
weapon_layout.addStretch() weapon_layout.addStretch()
loadout_label = QLabel("Loadout:") loadout_label = QLabel("Loadout:")
@ -943,9 +948,13 @@ class HUDOverlay(QWidget):
self.dealt_value_label.setText(str(self._stats.damage_dealt)) self.dealt_value_label.setText(str(self._stats.damage_dealt))
self.taken_value_label.setText(str(self._stats.damage_taken)) self.taken_value_label.setText(str(self._stats.damage_taken))
# Weapon/Loadout # Weapon/Loadout/DPP
self.weapon_label.setText(self._stats.current_weapon[:20]) # Truncate long names self.weapon_label.setText(self._stats.current_weapon[:20])
self.loadout_label.setText(self._stats.current_loadout[:15]) self.loadout_label.setText(self._stats.current_loadout[:15])
if self._stats.weapon_dpp > 0:
self.dpp_label.setText(f"DPP: {self._stats.weapon_dpp:.2f}")
else:
self.dpp_label.setText("DPP: --")
# Update time if session active # Update time if session active
self._update_time_display() self._update_time_display()