fix(armor): match Entropia Nexus slot naming exactly

- TORSO (was CHEST) - Harness/Chest piece
- LEGS (was THIGHS) - Thigh Guards
- Updated Frontier set with correct slots
- Display names match Nexus: Head, Torso, Arms, Hands, Legs, Shins, Feet

Now matches Nexus JSON export structure exactly
This commit is contained in:
LemonNexus 2026-02-09 11:06:46 +00:00
parent fd2b34e395
commit 9e55c751b9
1 changed files with 139 additions and 139 deletions

View File

@ -19,21 +19,21 @@ from enum import Enum, auto
class ArmorSlot(Enum): class ArmorSlot(Enum):
"""Armor slot types in Entropia Universe. """Armor slot types in Entropia Universe.
Standard 7-piece armor structure: Standard 7-piece armor structure (matches Entropia Nexus):
- Head (Helmet) - Head (Helmet)
- Chest (Harness) - Torso (Harness/Chest)
- Arms (Arm Guards - both arms) - Arms (Arm Guards)
- Hands (Gloves - both hands) - Hands (Gloves)
- Thighs (Thigh Guards) - Legs (Thigh Guards)
- Shins (Shin Guards) - Shins (Shin Guards)
- Feet (Foot Guards) - Feet (Foot Guards)
""" """
HEAD = "head" HEAD = "head"
CHEST = "chest" # Harness TORSO = "torso" # Harness/Chest
ARMS = "arms" # Arm Guards (both arms) ARMS = "arms" # Arm Guards
HANDS = "hands" # Gloves (both hands) HANDS = "hands" # Gloves
THIGHS = "thighs" # Thigh Guards LEGS = "legs" # Thigh Guards
SHINS = "shins" # Shin Guards SHINS = "shins" # Shin Guards
FEET = "feet" # Foot Guards FEET = "feet" # Foot Guards
@ -41,10 +41,10 @@ class ArmorSlot(Enum):
# Full set of 7 slots # Full set of 7 slots
ALL_ARMOR_SLOTS = [ ALL_ARMOR_SLOTS = [
ArmorSlot.HEAD, ArmorSlot.HEAD,
ArmorSlot.CHEST, ArmorSlot.TORSO,
ArmorSlot.ARMS, ArmorSlot.ARMS,
ArmorSlot.HANDS, ArmorSlot.HANDS,
ArmorSlot.THIGHS, ArmorSlot.LEGS,
ArmorSlot.SHINS, ArmorSlot.SHINS,
ArmorSlot.FEET, ArmorSlot.FEET,
] ]
@ -62,18 +62,18 @@ class ProtectionProfile:
cold: Decimal = Decimal("0") cold: Decimal = Decimal("0")
acid: Decimal = Decimal("0") acid: Decimal = Decimal("0")
electric: Decimal = Decimal("0") electric: Decimal = Decimal("0")
def get_total(self) -> Decimal: def get_total(self) -> Decimal:
"""Get total protection across all types.""" """Get total protection across all types."""
return ( return (
self.stab + self.cut + self.impact + self.penetration + self.stab + self.cut + self.impact + self.penetration +
self.shrapnel + self.burn + self.cold + self.acid + self.electric self.shrapnel + self.burn + self.cold + self.acid + self.electric
) )
def get_effective_against(self, damage_type: str) -> Decimal: def get_effective_against(self, damage_type: str) -> Decimal:
"""Get protection value for a specific damage type.""" """Get protection value for a specific damage type."""
return getattr(self, damage_type.lower(), Decimal("0")) return getattr(self, damage_type.lower(), Decimal("0"))
def add(self, other: "ProtectionProfile") -> "ProtectionProfile": def add(self, other: "ProtectionProfile") -> "ProtectionProfile":
"""Add another protection profile to this one.""" """Add another protection profile to this one."""
return ProtectionProfile( return ProtectionProfile(
@ -87,7 +87,7 @@ class ProtectionProfile:
acid=self.acid + other.acid, acid=self.acid + other.acid,
electric=self.electric + other.electric, electric=self.electric + other.electric,
) )
def subtract(self, other: "ProtectionProfile") -> "ProtectionProfile": def subtract(self, other: "ProtectionProfile") -> "ProtectionProfile":
"""Subtract another protection profile from this one.""" """Subtract another protection profile from this one."""
return ProtectionProfile( return ProtectionProfile(
@ -101,7 +101,7 @@ class ProtectionProfile:
acid=max(Decimal("0"), self.acid - other.acid), acid=max(Decimal("0"), self.acid - other.acid),
electric=max(Decimal("0"), self.electric - other.electric), electric=max(Decimal("0"), self.electric - other.electric),
) )
def to_dict(self) -> Dict[str, str]: def to_dict(self) -> Dict[str, str]:
"""Convert to dictionary with string values.""" """Convert to dictionary with string values."""
return { return {
@ -115,7 +115,7 @@ class ProtectionProfile:
'acid': str(self.acid), 'acid': str(self.acid),
'electric': str(self.electric), 'electric': str(self.electric),
} }
@classmethod @classmethod
def from_dict(cls, data: Dict[str, str]) -> "ProtectionProfile": def from_dict(cls, data: Dict[str, str]) -> "ProtectionProfile":
"""Create from dictionary.""" """Create from dictionary."""
@ -137,7 +137,7 @@ class ArmorPlate:
""" """
Armor plating that attaches to armor pieces. Armor plating that attaches to armor pieces.
Plates act as a shield layer - they take damage FIRST. Plates act as a shield layer - they take damage FIRST.
Official Decay Formula (VU 15.15): Official Decay Formula (VU 15.15):
Decay (PEC) = damage_absorbed * 0.05 * (1 - durability/100000) Decay (PEC) = damage_absorbed * 0.05 * (1 - durability/100000)
""" """
@ -146,35 +146,35 @@ class ArmorPlate:
protection: ProtectionProfile = field(default_factory=ProtectionProfile) protection: ProtectionProfile = field(default_factory=ProtectionProfile)
durability: int = 2000 # Plate durability affects economy durability: int = 2000 # Plate durability affects economy
block_chance: Decimal = Decimal("0") # Chance to nullify hit completely (no decay) block_chance: Decimal = Decimal("0") # Chance to nullify hit completely (no decay)
# Constants # Constants
BASE_DECAY_FACTOR: Decimal = Decimal("0.05") BASE_DECAY_FACTOR: Decimal = Decimal("0.05")
MAX_DURABILITY: int = 100000 MAX_DURABILITY: int = 100000
def get_total_protection(self) -> Decimal: def get_total_protection(self) -> Decimal:
"""Get total protection value.""" """Get total protection value."""
return self.protection.get_total() return self.protection.get_total()
def get_decay_for_damage(self, damage_absorbed: Decimal) -> Decimal: def get_decay_for_damage(self, damage_absorbed: Decimal) -> Decimal:
""" """
Calculate decay for absorbing damage using official formula. Calculate decay for absorbing damage using official formula.
Formula: Decay (PEC) = damage_absorbed * 0.05 * (1 - durability/100000) Formula: Decay (PEC) = damage_absorbed * 0.05 * (1 - durability/100000)
Args: Args:
damage_absorbed: Amount of damage the plate actually absorbed damage_absorbed: Amount of damage the plate actually absorbed
Returns: Returns:
Decay in PEC Decay in PEC
""" """
if damage_absorbed <= 0: if damage_absorbed <= 0:
return Decimal("0") return Decimal("0")
durability_factor = Decimal("1") - Decimal(self.durability) / Decimal(self.MAX_DURABILITY) durability_factor = Decimal("1") - Decimal(self.durability) / Decimal(self.MAX_DURABILITY)
decay_pec = damage_absorbed * self.BASE_DECAY_FACTOR * durability_factor decay_pec = damage_absorbed * self.BASE_DECAY_FACTOR * durability_factor
return decay_pec return decay_pec
def get_hp_per_pec(self) -> Decimal: def get_hp_per_pec(self) -> Decimal:
""" """
Calculate economy rating (hp per pec). Calculate economy rating (hp per pec).
@ -184,11 +184,11 @@ class ArmorPlate:
if durability_factor <= 0: if durability_factor <= 0:
return Decimal("0") return Decimal("0")
return Decimal("1") / (self.BASE_DECAY_FACTOR * durability_factor) return Decimal("1") / (self.BASE_DECAY_FACTOR * durability_factor)
def get_effective_protection(self, damage_type: str) -> Decimal: def get_effective_protection(self, damage_type: str) -> Decimal:
"""Get protection value for a specific damage type.""" """Get protection value for a specific damage type."""
return self.protection.get_effective_against(damage_type) return self.protection.get_effective_against(damage_type)
def to_dict(self) -> Dict: def to_dict(self) -> Dict:
"""Convert to dictionary.""" """Convert to dictionary."""
return { return {
@ -198,7 +198,7 @@ class ArmorPlate:
'durability': self.durability, 'durability': self.durability,
'block_chance': str(self.block_chance), 'block_chance': str(self.block_chance),
} }
@classmethod @classmethod
def from_dict(cls, data: Dict) -> "ArmorPlate": def from_dict(cls, data: Dict) -> "ArmorPlate":
"""Create from dictionary.""" """Create from dictionary."""
@ -216,10 +216,10 @@ class ArmorPiece:
""" """
Individual armor piece (e.g., Ghost Helmet, Shogun Harness). Individual armor piece (e.g., Ghost Helmet, Shogun Harness).
Each piece protects one slot and can have one plate attached. Each piece protects one slot and can have one plate attached.
Official Decay Formula (VU 15.15): Official Decay Formula (VU 15.15):
Decay (PEC) = damage_absorbed * 0.05 * (1 - durability/100000) Decay (PEC) = damage_absorbed * 0.05 * (1 - durability/100000)
Economy varies by durability: Economy varies by durability:
- Ghost (2000 dur): 20.41 hp/pec - Ghost (2000 dur): 20.41 hp/pec
- Gremlin (2950 dur): 20.61 hp/pec - Gremlin (2950 dur): 20.61 hp/pec
@ -232,51 +232,51 @@ class ArmorPiece:
protection: ProtectionProfile = field(default_factory=ProtectionProfile) protection: ProtectionProfile = field(default_factory=ProtectionProfile)
durability: int = 2000 # Durability affects economy durability: int = 2000 # Durability affects economy
weight: Decimal = Decimal("1.0") # Weight in kg weight: Decimal = Decimal("1.0") # Weight in kg
# Base decay factor: 0.05 PEC per HP (20 hp/pec standard) # Base decay factor: 0.05 PEC per HP (20 hp/pec standard)
BASE_DECAY_FACTOR: Decimal = Decimal("0.05") BASE_DECAY_FACTOR: Decimal = Decimal("0.05")
MAX_DURABILITY: int = 100000 MAX_DURABILITY: int = 100000
# Optional plate attachment # Optional plate attachment
attached_plate: Optional[ArmorPlate] = None attached_plate: Optional[ArmorPlate] = None
def get_base_protection(self) -> ProtectionProfile: def get_base_protection(self) -> ProtectionProfile:
"""Get base protection without plate.""" """Get base protection without plate."""
return self.protection return self.protection
def get_total_protection(self) -> ProtectionProfile: def get_total_protection(self) -> ProtectionProfile:
"""Get total protection including plate.""" """Get total protection including plate."""
if self.attached_plate: if self.attached_plate:
return self.protection.add(self.attached_plate.protection) return self.protection.add(self.attached_plate.protection)
return self.protection return self.protection
def get_total_protection_value(self) -> Decimal: def get_total_protection_value(self) -> Decimal:
"""Get total protection value including plate.""" """Get total protection value including plate."""
return self.get_total_protection().get_total() return self.get_total_protection().get_total()
def get_decay_for_damage(self, damage_absorbed: Decimal) -> Decimal: def get_decay_for_damage(self, damage_absorbed: Decimal) -> Decimal:
""" """
Calculate armor decay for absorbing damage. Calculate armor decay for absorbing damage.
Official Formula (VU 15.15): Official Formula (VU 15.15):
Decay (PEC) = damage_absorbed * 0.05 * (1 - durability/100000) Decay (PEC) = damage_absorbed * 0.05 * (1 - durability/100000)
Args: Args:
damage_absorbed: Amount of damage the armor actually absorbed damage_absorbed: Amount of damage the armor actually absorbed
Returns: Returns:
Decay in PEC Decay in PEC
""" """
if damage_absorbed <= 0: if damage_absorbed <= 0:
return Decimal("0") return Decimal("0")
# Calculate economy factor based on durability # Calculate economy factor based on durability
# Higher durability = better economy (less decay) # Higher durability = better economy (less decay)
durability_factor = Decimal("1") - Decimal(self.durability) / Decimal(self.MAX_DURABILITY) durability_factor = Decimal("1") - Decimal(self.durability) / Decimal(self.MAX_DURABILITY)
decay_pec = damage_absorbed * self.BASE_DECAY_FACTOR * durability_factor decay_pec = damage_absorbed * self.BASE_DECAY_FACTOR * durability_factor
return decay_pec return decay_pec
def get_hp_per_pec(self) -> Decimal: def get_hp_per_pec(self) -> Decimal:
""" """
Calculate economy rating (hp per pec). Calculate economy rating (hp per pec).
@ -286,31 +286,31 @@ class ArmorPiece:
if durability_factor <= 0: if durability_factor <= 0:
return Decimal("0") return Decimal("0")
return Decimal("1") / (self.BASE_DECAY_FACTOR * durability_factor) return Decimal("1") / (self.BASE_DECAY_FACTOR * durability_factor)
def attach_plate(self, plate: ArmorPlate) -> bool: def attach_plate(self, plate: ArmorPlate) -> bool:
"""Attach a plate to this armor piece.""" """Attach a plate to this armor piece."""
self.attached_plate = plate self.attached_plate = plate
return True return True
def remove_plate(self) -> Optional[ArmorPlate]: def remove_plate(self) -> Optional[ArmorPlate]:
"""Remove and return the attached plate.""" """Remove and return the attached plate."""
plate = self.attached_plate plate = self.attached_plate
self.attached_plate = None self.attached_plate = None
return plate return plate
def get_slot_display_name(self) -> str: def get_slot_display_name(self) -> str:
"""Get human-readable slot name.""" """Get human-readable slot name (matches Entropia Nexus)."""
slot_names = { slot_names = {
ArmorSlot.HEAD: "Helmet", ArmorSlot.HEAD: "Head",
ArmorSlot.CHEST: "Harness", ArmorSlot.TORSO: "Torso",
ArmorSlot.ARMS: "Arm Guards", ArmorSlot.ARMS: "Arms",
ArmorSlot.HANDS: "Gloves", ArmorSlot.HANDS: "Hands",
ArmorSlot.THIGHS: "Thigh Guards", ArmorSlot.LEGS: "Legs",
ArmorSlot.SHINS: "Shin Guards", ArmorSlot.SHINS: "Shins",
ArmorSlot.FEET: "Foot Guards", ArmorSlot.FEET: "Feet",
} }
return slot_names.get(self.slot, self.slot.value) return slot_names.get(self.slot, self.slot.value)
def to_dict(self) -> Dict: def to_dict(self) -> Dict:
"""Convert to dictionary.""" """Convert to dictionary."""
return { return {
@ -323,7 +323,7 @@ class ArmorPiece:
'weight': str(self.weight), 'weight': str(self.weight),
'attached_plate': self.attached_plate.to_dict() if self.attached_plate else None, 'attached_plate': self.attached_plate.to_dict() if self.attached_plate else None,
} }
@classmethod @classmethod
def from_dict(cls, data: Dict) -> "ArmorPiece": def from_dict(cls, data: Dict) -> "ArmorPiece":
"""Create from dictionary.""" """Create from dictionary."""
@ -351,33 +351,33 @@ class ArmorSet:
set_id: str set_id: str
pieces: Dict[ArmorSlot, ArmorPiece] = field(default_factory=dict) pieces: Dict[ArmorSlot, ArmorPiece] = field(default_factory=dict)
set_bonus: Optional[ProtectionProfile] = None # Some sets have bonuses set_bonus: Optional[ProtectionProfile] = None # Some sets have bonuses
def __post_init__(self): def __post_init__(self):
"""Ensure all pieces reference this set.""" """Ensure all pieces reference this set."""
set_name = self.name.replace(" Set", "") set_name = self.name.replace(" Set", "")
for slot, piece in self.pieces.items(): for slot, piece in self.pieces.items():
piece.set_name = set_name piece.set_name = set_name
def get_piece(self, slot: ArmorSlot) -> Optional[ArmorPiece]: def get_piece(self, slot: ArmorSlot) -> Optional[ArmorPiece]:
"""Get armor piece for a specific slot.""" """Get armor piece for a specific slot."""
return self.pieces.get(slot) return self.pieces.get(slot)
def is_complete(self) -> bool: def is_complete(self) -> bool:
"""Check if set has all 7 pieces.""" """Check if set has all 7 pieces."""
return len(self.pieces) == 7 and all(slot in self.pieces for slot in ALL_ARMOR_SLOTS) return len(self.pieces) == 7 and all(slot in self.pieces for slot in ALL_ARMOR_SLOTS)
def get_total_protection(self) -> ProtectionProfile: def get_total_protection(self) -> ProtectionProfile:
"""Get total protection from all pieces including plates.""" """Get total protection from all pieces including plates."""
total = ProtectionProfile() total = ProtectionProfile()
for piece in self.pieces.values(): for piece in self.pieces.values():
total = total.add(piece.get_total_protection()) total = total.add(piece.get_total_protection())
# Add set bonus if complete # Add set bonus if complete
if self.set_bonus and self.is_complete(): if self.set_bonus and self.is_complete():
total = total.add(self.set_bonus) total = total.add(self.set_bonus)
return total return total
def get_total_decay_per_hit(self) -> Decimal: def get_total_decay_per_hit(self) -> Decimal:
""" """
Get total decay per hit across all pieces (including plates). Get total decay per hit across all pieces (including plates).
@ -388,23 +388,23 @@ class ArmorSet:
# Using a typical hit value of 10 hp for estimation # Using a typical hit value of 10 hp for estimation
typical_hit = Decimal("10") typical_hit = Decimal("10")
total = Decimal("0") total = Decimal("0")
for piece in self.pieces.values(): for piece in self.pieces.values():
# Estimate armor decay (assuming it absorbs typical hit up to its protection) # Estimate armor decay (assuming it absorbs typical hit up to its protection)
armor_absorb = min(typical_hit, piece.protection.get_total()) armor_absorb = min(typical_hit, piece.protection.get_total())
total += piece.get_decay_for_damage(armor_absorb) total += piece.get_decay_for_damage(armor_absorb)
if piece.attached_plate: if piece.attached_plate:
# Estimate plate decay # Estimate plate decay
plate_absorb = min(typical_hit, piece.attached_plate.get_total_protection()) plate_absorb = min(typical_hit, piece.attached_plate.get_total_protection())
total += piece.attached_plate.get_decay_for_damage(plate_absorb) total += piece.attached_plate.get_decay_for_damage(plate_absorb)
return total return total
def get_pieces_list(self) -> List[ArmorPiece]: def get_pieces_list(self) -> List[ArmorPiece]:
"""Get list of all pieces in slot order.""" """Get list of all pieces in slot order."""
return [self.pieces.get(slot) for slot in ALL_ARMOR_SLOTS if slot in self.pieces] return [self.pieces.get(slot) for slot in ALL_ARMOR_SLOTS if slot in self.pieces]
def to_dict(self) -> Dict: def to_dict(self) -> Dict:
"""Convert to dictionary.""" """Convert to dictionary."""
return { return {
@ -413,7 +413,7 @@ class ArmorSet:
'pieces': {slot.value: piece.to_dict() for slot, piece in self.pieces.items()}, 'pieces': {slot.value: piece.to_dict() for slot, piece in self.pieces.items()},
'set_bonus': self.set_bonus.to_dict() if self.set_bonus else None, 'set_bonus': self.set_bonus.to_dict() if self.set_bonus else None,
} }
@classmethod @classmethod
def from_dict(cls, data: Dict) -> "ArmorSet": def from_dict(cls, data: Dict) -> "ArmorSet":
"""Create from dictionary.""" """Create from dictionary."""
@ -440,45 +440,45 @@ class EquippedArmor:
""" """
# Individual pieces (mix & match) # Individual pieces (mix & match)
pieces: Dict[ArmorSlot, ArmorPiece] = field(default_factory=dict) pieces: Dict[ArmorSlot, ArmorPiece] = field(default_factory=dict)
# Or reference a full set # Or reference a full set
full_set: Optional[ArmorSet] = None full_set: Optional[ArmorSet] = None
def get_piece(self, slot: ArmorSlot) -> Optional[ArmorPiece]: def get_piece(self, slot: ArmorSlot) -> Optional[ArmorPiece]:
"""Get equipped piece for a slot.""" """Get equipped piece for a slot."""
if self.full_set: if self.full_set:
return self.full_set.get_piece(slot) return self.full_set.get_piece(slot)
return self.pieces.get(slot) return self.pieces.get(slot)
def get_all_pieces(self) -> Dict[ArmorSlot, ArmorPiece]: def get_all_pieces(self) -> Dict[ArmorSlot, ArmorPiece]:
"""Get all equipped pieces as a dict.""" """Get all equipped pieces as a dict."""
if self.full_set: if self.full_set:
return self.full_set.pieces return self.full_set.pieces
return self.pieces return self.pieces
def equip_piece(self, piece: ArmorPiece) -> None: def equip_piece(self, piece: ArmorPiece) -> None:
"""Equip an individual armor piece.""" """Equip an individual armor piece."""
# Unequip full set if equipping individual pieces # Unequip full set if equipping individual pieces
self.full_set = None self.full_set = None
self.pieces[piece.slot] = piece self.pieces[piece.slot] = piece
def unequip_piece(self, slot: ArmorSlot) -> Optional[ArmorPiece]: def unequip_piece(self, slot: ArmorSlot) -> Optional[ArmorPiece]:
"""Unequip a piece from a slot.""" """Unequip a piece from a slot."""
if self.full_set: if self.full_set:
return None # Can't unequip individual pieces from full set return None # Can't unequip individual pieces from full set
return self.pieces.pop(slot, None) return self.pieces.pop(slot, None)
def equip_full_set(self, armor_set: ArmorSet) -> None: def equip_full_set(self, armor_set: ArmorSet) -> None:
"""Equip a full armor set.""" """Equip a full armor set."""
self.full_set = armor_set self.full_set = armor_set
self.pieces = {} self.pieces = {}
def unequip_full_set(self) -> Optional[ArmorSet]: def unequip_full_set(self) -> Optional[ArmorSet]:
"""Unequip full set.""" """Unequip full set."""
set_ref = self.full_set set_ref = self.full_set
self.full_set = None self.full_set = None
return set_ref return set_ref
def attach_plate_to_slot(self, slot: ArmorSlot, plate: ArmorPlate) -> bool: def attach_plate_to_slot(self, slot: ArmorSlot, plate: ArmorPlate) -> bool:
"""Attach a plate to the armor piece in a slot.""" """Attach a plate to the armor piece in a slot."""
piece = self.get_piece(slot) piece = self.get_piece(slot)
@ -486,29 +486,29 @@ class EquippedArmor:
piece.attach_plate(plate) piece.attach_plate(plate)
return True return True
return False return False
def remove_plate_from_slot(self, slot: ArmorSlot) -> Optional[ArmorPlate]: def remove_plate_from_slot(self, slot: ArmorSlot) -> Optional[ArmorPlate]:
"""Remove plate from armor piece in a slot.""" """Remove plate from armor piece in a slot."""
piece = self.get_piece(slot) piece = self.get_piece(slot)
if piece: if piece:
return piece.remove_plate() return piece.remove_plate()
return None return None
def get_plate(self, slot: ArmorSlot) -> Optional[ArmorPlate]: def get_plate(self, slot: ArmorSlot) -> Optional[ArmorPlate]:
"""Get attached plate for a slot.""" """Get attached plate for a slot."""
piece = self.get_piece(slot) piece = self.get_piece(slot)
return piece.attached_plate if piece else None return piece.attached_plate if piece else None
def get_total_protection(self) -> ProtectionProfile: def get_total_protection(self) -> ProtectionProfile:
"""Get total protection from all equipped pieces and plates.""" """Get total protection from all equipped pieces and plates."""
if self.full_set: if self.full_set:
return self.full_set.get_total_protection() return self.full_set.get_total_protection()
total = ProtectionProfile() total = ProtectionProfile()
for piece in self.pieces.values(): for piece in self.pieces.values():
total = total.add(piece.get_total_protection()) total = total.add(piece.get_total_protection())
return total return total
def get_total_decay_per_hit(self) -> Decimal: def get_total_decay_per_hit(self) -> Decimal:
""" """
Get total decay per hit (armor + plates). Get total decay per hit (armor + plates).
@ -516,48 +516,48 @@ class EquippedArmor:
""" """
if self.full_set: if self.full_set:
return self.full_set.get_total_decay_per_hit() return self.full_set.get_total_decay_per_hit()
# Estimate based on typical hit of 10 hp # Estimate based on typical hit of 10 hp
typical_hit = Decimal("10") typical_hit = Decimal("10")
total = Decimal("0") total = Decimal("0")
for piece in self.pieces.values(): for piece in self.pieces.values():
armor_absorb = min(typical_hit, piece.protection.get_total()) armor_absorb = min(typical_hit, piece.protection.get_total())
total += piece.get_decay_for_damage(armor_absorb) total += piece.get_decay_for_damage(armor_absorb)
if piece.attached_plate: if piece.attached_plate:
plate_absorb = min(typical_hit, piece.attached_plate.get_total_protection()) plate_absorb = min(typical_hit, piece.attached_plate.get_total_protection())
total += piece.attached_plate.get_decay_for_damage(plate_absorb) total += piece.attached_plate.get_decay_for_damage(plate_absorb)
return total return total
def get_coverage(self) -> Tuple[int, int]: def get_coverage(self) -> Tuple[int, int]:
"""Get armor coverage as (equipped_slots, total_slots).""" """Get armor coverage as (equipped_slots, total_slots)."""
pieces = self.get_all_pieces() pieces = self.get_all_pieces()
return (len(pieces), 7) return (len(pieces), 7)
def get_coverage_percentage(self) -> float: def get_coverage_percentage(self) -> float:
"""Get armor coverage as percentage.""" """Get armor coverage as percentage."""
equipped, total = self.get_coverage() equipped, total = self.get_coverage()
return (equipped / total) * 100 if total > 0 else 0 return (equipped / total) * 100 if total > 0 else 0
def get_slot_status(self) -> Dict[ArmorSlot, bool]: def get_slot_status(self) -> Dict[ArmorSlot, bool]:
"""Get status of each slot (True if equipped).""" """Get status of each slot (True if equipped)."""
pieces = self.get_all_pieces() pieces = self.get_all_pieces()
return {slot: slot in pieces for slot in ALL_ARMOR_SLOTS} return {slot: slot in pieces for slot in ALL_ARMOR_SLOTS}
def to_dict(self) -> Dict: def to_dict(self) -> Dict:
"""Convert to dictionary.""" """Convert to dictionary."""
return { return {
'pieces': {slot.value: piece.to_dict() for slot, piece in self.pieces.items()}, 'pieces': {slot.value: piece.to_dict() for slot, piece in self.pieces.items()},
'full_set': self.full_set.to_dict() if self.full_set else None, 'full_set': self.full_set.to_dict() if self.full_set else None,
} }
@classmethod @classmethod
def from_dict(cls, data: Dict) -> "EquippedArmor": def from_dict(cls, data: Dict) -> "EquippedArmor":
"""Create from dictionary.""" """Create from dictionary."""
equipped = cls() equipped = cls()
if data.get('full_set'): if data.get('full_set'):
equipped.full_set = ArmorSet.from_dict(data['full_set']) equipped.full_set = ArmorSet.from_dict(data['full_set'])
else: else:
@ -566,7 +566,7 @@ class EquippedArmor:
for slot, piece_data in data.get('pieces', {}).items() for slot, piece_data in data.get('pieces', {}).items()
} }
equipped.pieces = pieces equipped.pieces = pieces
return equipped return equipped
@ -579,17 +579,17 @@ class HitResult:
"""Result of a hit against armored target.""" """Result of a hit against armored target."""
raw_damage: Decimal raw_damage: Decimal
damage_type: str damage_type: str
# Damage absorbed # Damage absorbed
plate_absorbed: Decimal = Decimal("0") plate_absorbed: Decimal = Decimal("0")
armor_absorbed: Decimal = Decimal("0") armor_absorbed: Decimal = Decimal("0")
damage_to_avatar: Decimal = Decimal("0") damage_to_avatar: Decimal = Decimal("0")
# Decay incurred # Decay incurred
plate_decay: Decimal = Decimal("0") plate_decay: Decimal = Decimal("0")
armor_decay: Decimal = Decimal("0") armor_decay: Decimal = Decimal("0")
total_decay: Decimal = Decimal("0") total_decay: Decimal = Decimal("0")
# Status # Status
plate_broken: bool = False plate_broken: bool = False
armor_broken: bool = False armor_broken: bool = False
@ -603,7 +603,7 @@ def calculate_hit_protection(
) -> HitResult: ) -> HitResult:
""" """
Calculate damage absorption for a hit using Loot 2.0 mechanics. Calculate damage absorption for a hit using Loot 2.0 mechanics.
Loot 2.0 Armor Mechanics (June 2017): Loot 2.0 Armor Mechanics (June 2017):
1. Plate absorbs damage FIRST (shield layer) 1. Plate absorbs damage FIRST (shield layer)
2. Armor absorbs remaining damage 2. Armor absorbs remaining damage
@ -611,21 +611,21 @@ def calculate_hit_protection(
4. Armor decay = damage_absorbed_by_armor * armor.decay_per_hp 4. Armor decay = damage_absorbed_by_armor * armor.decay_per_hp
5. Decay is LINEAR per damage point (20 hp/pec standard = 0.05 pec/hp) 5. Decay is LINEAR per damage point (20 hp/pec standard = 0.05 pec/hp)
6. Block chance on upgraded plates can nullify hit (no decay) 6. Block chance on upgraded plates can nullify hit (no decay)
Damage Flow: Damage Flow:
Incoming Damage Plate absorbs first Armor absorbs remainder Player takes overflow Incoming Damage Plate absorbs first Armor absorbs remainder Player takes overflow
Example: 20 Impact hit vs Ghost Harness (4 Impact) + Impact Plate (3 Impact): Example: 20 Impact hit vs Ghost Harness (4 Impact) + Impact Plate (3 Impact):
- Plate absorbs 3, decays for 3 * 0.05 = 0.15 PEC - Plate absorbs 3, decays for 3 * 0.05 = 0.15 PEC
- Armor absorbs 4 (remaining after plate), decays for 4 * 0.05 = 0.20 PEC - Armor absorbs 4 (remaining after plate), decays for 4 * 0.05 = 0.20 PEC
- Player takes 20 - 3 - 4 = 13 damage - Player takes 20 - 3 - 4 = 13 damage
Args: Args:
equipped_armor: Currently equipped armor equipped_armor: Currently equipped armor
incoming_damage: Raw damage from attack incoming_damage: Raw damage from attack
damage_type: Type of damage (impact, burn, etc.) damage_type: Type of damage (impact, burn, etc.)
hit_location: Specific slot hit (None for full body/average) hit_location: Specific slot hit (None for full body/average)
Returns: Returns:
HitResult with absorption details HitResult with absorption details
""" """
@ -633,10 +633,10 @@ def calculate_hit_protection(
raw_damage=incoming_damage, raw_damage=incoming_damage,
damage_type=damage_type, damage_type=damage_type,
) )
# Check for block chance on plates (upgraded plates only) # Check for block chance on plates (upgraded plates only)
# This would nullify the hit completely with no decay # This would nullify the hit completely with no decay
# Get protection for the hit # Get protection for the hit
if hit_location: if hit_location:
# Specific location hit # Specific location hit
@ -645,66 +645,66 @@ def calculate_hit_protection(
# No armor on that slot - full damage # No armor on that slot - full damage
result.damage_to_avatar = incoming_damage result.damage_to_avatar = incoming_damage
return result return result
# Check for block on plate # Check for block on plate
if piece.attached_plate and piece.attached_plate.block_chance > 0: if piece.attached_plate and piece.attached_plate.block_chance > 0:
# Note: In real implementation, use random() to check block # Note: In real implementation, use random() to check block
# For calculation purposes, we don't factor block chance # For calculation purposes, we don't factor block chance
pass pass
# Plate protection for this damage type # Plate protection for this damage type
plate_prot = Decimal("0") plate_prot = Decimal("0")
if piece.attached_plate: if piece.attached_plate:
plate_prot = piece.attached_plate.get_effective_protection(damage_type) plate_prot = piece.attached_plate.get_effective_protection(damage_type)
# Armor protection for this damage type # Armor protection for this damage type
armor_prot = piece.protection.get_effective_against(damage_type) armor_prot = piece.protection.get_effective_against(damage_type)
# Plate absorbs FIRST (up to its protection) # Plate absorbs FIRST (up to its protection)
plate_absorb = min(plate_prot, incoming_damage) plate_absorb = min(plate_prot, incoming_damage)
result.plate_absorbed = plate_absorb result.plate_absorbed = plate_absorb
remaining = incoming_damage - plate_absorb remaining = incoming_damage - plate_absorb
# Armor absorbs remainder (up to its protection) # Armor absorbs remainder (up to its protection)
armor_absorb = min(armor_prot, remaining) armor_absorb = min(armor_prot, remaining)
result.armor_absorbed = armor_absorb result.armor_absorbed = armor_absorb
result.damage_to_avatar = remaining - armor_absorb result.damage_to_avatar = remaining - armor_absorb
# Calculate decay based on actual damage absorbed (Loot 2.0) # Calculate decay based on actual damage absorbed (Loot 2.0)
if piece.attached_plate and plate_absorb > 0: if piece.attached_plate and plate_absorb > 0:
result.plate_decay = piece.attached_plate.get_decay_for_damage(plate_absorb) result.plate_decay = piece.attached_plate.get_decay_for_damage(plate_absorb)
if armor_absorb > 0: if armor_absorb > 0:
result.armor_decay = piece.get_decay_for_damage(armor_absorb) result.armor_decay = piece.get_decay_for_damage(armor_absorb)
result.total_decay = result.plate_decay + result.armor_decay result.total_decay = result.plate_decay + result.armor_decay
else: else:
# Full body hit - use average protection from all equipped pieces # Full body hit - use average protection from all equipped pieces
pieces = equipped_armor.get_all_pieces() pieces = equipped_armor.get_all_pieces()
if not pieces: if not pieces:
result.damage_to_avatar = incoming_damage result.damage_to_avatar = incoming_damage
return result return result
# Calculate total protection across all slots # Calculate total protection across all slots
total_plate_prot = Decimal("0") total_plate_prot = Decimal("0")
total_armor_prot = Decimal("0") total_armor_prot = Decimal("0")
for piece in pieces.values(): for piece in pieces.values():
total_armor_prot += piece.protection.get_effective_against(damage_type) total_armor_prot += piece.protection.get_effective_against(damage_type)
if piece.attached_plate: if piece.attached_plate:
total_plate_prot += piece.attached_plate.get_effective_protection(damage_type) total_plate_prot += piece.attached_plate.get_effective_protection(damage_type)
# Plate absorbs FIRST # Plate absorbs FIRST
plate_absorb = min(total_plate_prot, incoming_damage) plate_absorb = min(total_plate_prot, incoming_damage)
result.plate_absorbed = plate_absorb result.plate_absorbed = plate_absorb
remaining = incoming_damage - plate_absorb remaining = incoming_damage - plate_absorb
# Armor absorbs remainder # Armor absorbs remainder
armor_absorb = min(total_armor_prot, remaining) armor_absorb = min(total_armor_prot, remaining)
result.armor_absorbed = armor_absorb result.armor_absorbed = armor_absorb
result.damage_to_avatar = remaining - armor_absorb result.damage_to_avatar = remaining - armor_absorb
# Calculate decay based on actual damage absorbed # Calculate decay based on actual damage absorbed
# Distribute decay proportionally across all pieces # Distribute decay proportionally across all pieces
if plate_absorb > 0: if plate_absorb > 0:
@ -715,7 +715,7 @@ def calculate_hit_protection(
# This plate's share of absorption # This plate's share of absorption
piece_plate_share = plate_absorb * (piece_plate_prot / total_plate_prot) piece_plate_share = plate_absorb * (piece_plate_prot / total_plate_prot)
result.plate_decay += piece.attached_plate.get_decay_for_damage(piece_plate_share) result.plate_decay += piece.attached_plate.get_decay_for_damage(piece_plate_share)
if armor_absorb > 0: if armor_absorb > 0:
for piece in pieces.values(): for piece in pieces.values():
piece_armor_prot = piece.protection.get_effective_against(damage_type) piece_armor_prot = piece.protection.get_effective_against(damage_type)
@ -723,9 +723,9 @@ def calculate_hit_protection(
# This armor's share of absorption # This armor's share of absorption
piece_armor_share = armor_absorb * (piece_armor_prot / total_armor_prot) piece_armor_share = armor_absorb * (piece_armor_prot / total_armor_prot)
result.armor_decay += piece.get_decay_for_damage(piece_armor_share) result.armor_decay += piece.get_decay_for_damage(piece_armor_share)
result.total_decay = result.plate_decay + result.armor_decay result.total_decay = result.plate_decay + result.armor_decay
return result return result
@ -737,7 +737,7 @@ def create_ghost_set() -> ArmorSet:
"""Create the Ghost armor set (light, good vs cold/burn).""" """Create the Ghost armor set (light, good vs cold/burn)."""
# Ghost: 2000 durability = 20.41 hp/pec # Ghost: 2000 durability = 20.41 hp/pec
ghost_durability = 2000 ghost_durability = 2000
pieces = { pieces = {
ArmorSlot.HEAD: ArmorPiece( ArmorSlot.HEAD: ArmorPiece(
name="Ghost Helmet", name="Ghost Helmet",
@ -814,7 +814,7 @@ def create_shogun_set() -> ArmorSet:
"""Create the Shogun armor set (medium, good vs impact/cut).""" """Create the Shogun armor set (medium, good vs impact/cut)."""
# Shogun: 2500 durability = better economy than Ghost # Shogun: 2500 durability = better economy than Ghost
shogun_durability = 2500 shogun_durability = 2500
pieces = { pieces = {
ArmorSlot.HEAD: ArmorPiece( ArmorSlot.HEAD: ArmorPiece(
name="Shogun Helmet", name="Shogun Helmet",
@ -890,7 +890,7 @@ def create_shogun_set() -> ArmorSet:
def create_vigilante_set() -> ArmorSet: def create_vigilante_set() -> ArmorSet:
"""Create the Vigilante armor set (light, good all-around).""" """Create the Vigilante armor set (light, good all-around)."""
vigilante_durability = 2000 # Same as Ghost vigilante_durability = 2000 # Same as Ghost
pieces = { pieces = {
ArmorSlot.HEAD: ArmorPiece( ArmorSlot.HEAD: ArmorPiece(
name="Vigilante Helmet", name="Vigilante Helmet",
@ -966,7 +966,7 @@ def create_vigilante_set() -> ArmorSet:
def create_hermes_set() -> ArmorSet: def create_hermes_set() -> ArmorSet:
"""Create the Hermes armor set (medium, good vs penetration).""" """Create the Hermes armor set (medium, good vs penetration)."""
hermes_economy = Decimal("0.05") hermes_economy = Decimal("0.05")
pieces = { pieces = {
ArmorSlot.HEAD: ArmorPiece( ArmorSlot.HEAD: ArmorPiece(
name="Hermes Helmet", name="Hermes Helmet",
@ -1043,7 +1043,7 @@ def create_pixie_set() -> ArmorSet:
"""Create the Pixie armor set (light starter armor).""" """Create the Pixie armor set (light starter armor)."""
# Pixie is starter armor, slightly worse economy (18 hp/pec) # Pixie is starter armor, slightly worse economy (18 hp/pec)
pixie_economy = Decimal("0.055") pixie_economy = Decimal("0.055")
pieces = { pieces = {
ArmorSlot.HEAD: ArmorPiece( ArmorSlot.HEAD: ArmorPiece(
name="Pixie Helmet", name="Pixie Helmet",
@ -1126,7 +1126,7 @@ def get_mock_plates() -> List[ArmorPlate]:
# Upgraded plates might have better economy (25 hp/pec = 0.04 pec per hp) # Upgraded plates might have better economy (25 hp/pec = 0.04 pec per hp)
standard_economy = Decimal("0.05") standard_economy = Decimal("0.05")
improved_economy = Decimal("0.04") # 25 hp/pec improved_economy = Decimal("0.04") # 25 hp/pec
return [ return [
# General purpose plates # General purpose plates
ArmorPlate( ArmorPlate(
@ -1226,7 +1226,7 @@ def create_frontier_set() -> ArmorSet:
"""Create the Frontier armor set (Arkadia mid-level, good mobility).""" """Create the Frontier armor set (Arkadia mid-level, good mobility)."""
# Frontier Adjusted has 3800 durability = 20.83 hp/pec economy # Frontier Adjusted has 3800 durability = 20.83 hp/pec economy
frontier_economy = Decimal("0.048") # ~20.8 hp/pec frontier_economy = Decimal("0.048") # ~20.8 hp/pec
pieces = { pieces = {
ArmorSlot.HEAD: ArmorPiece( ArmorSlot.HEAD: ArmorPiece(
name="Frontier Helmet, Adjusted (M)", name="Frontier Helmet, Adjusted (M)",
@ -1237,17 +1237,17 @@ def create_frontier_set() -> ArmorSet:
protection=ProtectionProfile(impact=Decimal("4"), cut=Decimal("3"), stab=Decimal("3")), protection=ProtectionProfile(impact=Decimal("4"), cut=Decimal("3"), stab=Decimal("3")),
weight=Decimal("0.5"), weight=Decimal("0.5"),
), ),
ArmorSlot.CHEST: ArmorPiece( ArmorSlot.TORSO: ArmorPiece(
name="Frontier Harness, Adjusted (M)", name="Frontier Harness, Adjusted (M)",
item_id="frontier_harness_adj_m", item_id="frontier_harness_adj_m",
slot=ArmorSlot.CHEST, slot=ArmorSlot.TORSO,
set_name="Frontier", set_name="Frontier",
decay_per_hp=frontier_economy, decay_per_hp=frontier_economy,
protection=ProtectionProfile(impact=Decimal("8"), cut=Decimal("6"), stab=Decimal("6")), protection=ProtectionProfile(impact=Decimal("8"), cut=Decimal("6"), stab=Decimal("6")),
weight=Decimal("1.0"), weight=Decimal("1.0"),
), ),
ArmorSlot.ARMS: ArmorPiece( ArmorSlot.ARMS: ArmorPiece(
name="Frontier Arm Guards, Adjusted (M)", # Both arms name="Frontier Arm Guards, Adjusted (M)",
item_id="frontier_arm_adj_m", item_id="frontier_arm_adj_m",
slot=ArmorSlot.ARMS, slot=ArmorSlot.ARMS,
set_name="Frontier", set_name="Frontier",
@ -1256,7 +1256,7 @@ def create_frontier_set() -> ArmorSet:
weight=Decimal("0.8"), weight=Decimal("0.8"),
), ),
ArmorSlot.HANDS: ArmorPiece( ArmorSlot.HANDS: ArmorPiece(
name="Frontier Gloves, Adjusted (M)", # Both hands name="Frontier Gloves, Adjusted (M)",
item_id="frontier_gloves_adj_m", item_id="frontier_gloves_adj_m",
slot=ArmorSlot.HANDS, slot=ArmorSlot.HANDS,
set_name="Frontier", set_name="Frontier",
@ -1264,10 +1264,10 @@ def create_frontier_set() -> ArmorSet:
protection=ProtectionProfile(impact=Decimal("3"), cut=Decimal("2"), stab=Decimal("2")), protection=ProtectionProfile(impact=Decimal("3"), cut=Decimal("2"), stab=Decimal("2")),
weight=Decimal("0.6"), weight=Decimal("0.6"),
), ),
ArmorSlot.THIGHS: ArmorPiece( ArmorSlot.LEGS: ArmorPiece(
name="Frontier Thigh Guards, Adjusted (M)", name="Frontier Thigh Guards, Adjusted (M)",
item_id="frontier_thigh_adj_m", item_id="frontier_thigh_adj_m",
slot=ArmorSlot.THIGHS, slot=ArmorSlot.LEGS,
set_name="Frontier", set_name="Frontier",
decay_per_hp=frontier_economy, decay_per_hp=frontier_economy,
protection=ProtectionProfile(impact=Decimal("4"), cut=Decimal("3"), stab=Decimal("3")), protection=ProtectionProfile(impact=Decimal("4"), cut=Decimal("3"), stab=Decimal("3")),
@ -1340,16 +1340,16 @@ def get_pieces_by_set(set_name: str) -> List[ArmorPiece]:
def create_mixed_armor(selections: Dict[ArmorSlot, str]) -> EquippedArmor: def create_mixed_armor(selections: Dict[ArmorSlot, str]) -> EquippedArmor:
""" """
Create mixed armor from piece names. Create mixed armor from piece names.
Args: Args:
selections: Dict mapping slots to piece names selections: Dict mapping slots to piece names
Returns: Returns:
EquippedArmor with mixed pieces EquippedArmor with mixed pieces
""" """
equipped = EquippedArmor() equipped = EquippedArmor()
all_pieces = {p.name.lower(): p for p in get_all_armor_pieces()} all_pieces = {p.name.lower(): p for p in get_all_armor_pieces()}
for slot, piece_name in selections.items(): for slot, piece_name in selections.items():
piece_key = piece_name.lower() piece_key = piece_name.lower()
if piece_key in all_pieces: if piece_key in all_pieces:
@ -1376,7 +1376,7 @@ def create_mixed_armor(selections: Dict[ArmorSlot, str]) -> EquippedArmor:
weight=original.weight, weight=original.weight,
) )
equipped.equip_piece(piece_copy) equipped.equip_piece(piece_copy)
return equipped return equipped