1406 lines
51 KiB
Python
1406 lines
51 KiB
Python
"""
|
|
Armor System for Lemontropia Suite
|
|
|
|
Implements Entropia Universe armor mechanics:
|
|
- 7 armor slots: Head, Chest/Harness, Left Arm, Right Arm, Left Hand, Right Hand, Legs/Feet
|
|
- Full armor sets (e.g., "Ghost Set", "Shogun Set") with matching pieces
|
|
- Individual armor pieces (mix & match)
|
|
- 7 plate slots (one per armor piece)
|
|
- Plates take damage FIRST (shield layer)
|
|
- Plate protection + Armor protection = Total protection
|
|
- Plate decay tracked separately from armor decay
|
|
"""
|
|
|
|
from dataclasses import dataclass, field
|
|
from decimal import Decimal
|
|
from typing import Optional, Dict, List, Tuple, ClassVar
|
|
from enum import Enum, auto
|
|
|
|
|
|
class ArmorSlot(Enum):
|
|
"""Armor slot types in Entropia Universe.
|
|
|
|
Standard 7-piece armor structure (matches Entropia Nexus):
|
|
- Head (Helmet)
|
|
- Torso (Harness/Chest)
|
|
- Arms (Arm Guards)
|
|
- Hands (Gloves)
|
|
- Legs (Thigh Guards)
|
|
- Shins (Shin Guards)
|
|
- Feet (Foot Guards)
|
|
"""
|
|
HEAD = "head"
|
|
TORSO = "torso" # Harness/Chest
|
|
ARMS = "arms" # Arm Guards
|
|
HANDS = "hands" # Gloves
|
|
LEGS = "legs" # Thigh Guards
|
|
SHINS = "shins" # Shin Guards
|
|
FEET = "feet" # Foot Guards
|
|
|
|
|
|
# Full set of 7 slots
|
|
ALL_ARMOR_SLOTS = [
|
|
ArmorSlot.HEAD,
|
|
ArmorSlot.TORSO,
|
|
ArmorSlot.ARMS,
|
|
ArmorSlot.HANDS,
|
|
ArmorSlot.LEGS,
|
|
ArmorSlot.SHINS,
|
|
ArmorSlot.FEET,
|
|
]
|
|
|
|
|
|
@dataclass
|
|
class ProtectionProfile:
|
|
"""Protection values for all damage types."""
|
|
stab: Decimal = Decimal("0")
|
|
cut: Decimal = Decimal("0")
|
|
impact: Decimal = Decimal("0")
|
|
penetration: Decimal = Decimal("0")
|
|
shrapnel: Decimal = Decimal("0")
|
|
burn: Decimal = Decimal("0")
|
|
cold: Decimal = Decimal("0")
|
|
acid: Decimal = Decimal("0")
|
|
electric: Decimal = Decimal("0")
|
|
|
|
def get_total(self) -> Decimal:
|
|
"""Get total protection across all types."""
|
|
return (
|
|
self.stab + self.cut + self.impact + self.penetration +
|
|
self.shrapnel + self.burn + self.cold + self.acid + self.electric
|
|
)
|
|
|
|
def get_effective_against(self, damage_type: str) -> Decimal:
|
|
"""Get protection value for a specific damage type."""
|
|
return getattr(self, damage_type.lower(), Decimal("0"))
|
|
|
|
def add(self, other: "ProtectionProfile") -> "ProtectionProfile":
|
|
"""Add another protection profile to this one."""
|
|
return ProtectionProfile(
|
|
stab=self.stab + other.stab,
|
|
cut=self.cut + other.cut,
|
|
impact=self.impact + other.impact,
|
|
penetration=self.penetration + other.penetration,
|
|
shrapnel=self.shrapnel + other.shrapnel,
|
|
burn=self.burn + other.burn,
|
|
cold=self.cold + other.cold,
|
|
acid=self.acid + other.acid,
|
|
electric=self.electric + other.electric,
|
|
)
|
|
|
|
def subtract(self, other: "ProtectionProfile") -> "ProtectionProfile":
|
|
"""Subtract another protection profile from this one."""
|
|
return ProtectionProfile(
|
|
stab=max(Decimal("0"), self.stab - other.stab),
|
|
cut=max(Decimal("0"), self.cut - other.cut),
|
|
impact=max(Decimal("0"), self.impact - other.impact),
|
|
penetration=max(Decimal("0"), self.penetration - other.penetration),
|
|
shrapnel=max(Decimal("0"), self.shrapnel - other.shrapnel),
|
|
burn=max(Decimal("0"), self.burn - other.burn),
|
|
cold=max(Decimal("0"), self.cold - other.cold),
|
|
acid=max(Decimal("0"), self.acid - other.acid),
|
|
electric=max(Decimal("0"), self.electric - other.electric),
|
|
)
|
|
|
|
def to_dict(self) -> Dict[str, str]:
|
|
"""Convert to dictionary with string values."""
|
|
return {
|
|
'stab': str(self.stab),
|
|
'cut': str(self.cut),
|
|
'impact': str(self.impact),
|
|
'penetration': str(self.penetration),
|
|
'shrapnel': str(self.shrapnel),
|
|
'burn': str(self.burn),
|
|
'cold': str(self.cold),
|
|
'acid': str(self.acid),
|
|
'electric': str(self.electric),
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, str]) -> "ProtectionProfile":
|
|
"""Create from dictionary."""
|
|
return cls(
|
|
stab=Decimal(data.get('stab', '0')),
|
|
cut=Decimal(data.get('cut', '0')),
|
|
impact=Decimal(data.get('impact', '0')),
|
|
penetration=Decimal(data.get('penetration', '0')),
|
|
shrapnel=Decimal(data.get('shrapnel', '0')),
|
|
burn=Decimal(data.get('burn', '0')),
|
|
cold=Decimal(data.get('cold', '0')),
|
|
acid=Decimal(data.get('acid', '0')),
|
|
electric=Decimal(data.get('electric', '0')),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class ArmorPlate:
|
|
"""
|
|
Armor plating that attaches to armor pieces.
|
|
Plates act as a shield layer - they take damage FIRST.
|
|
|
|
Official Decay Formula (VU 15.15):
|
|
Decay (PEC) = damage_absorbed * 0.05 * (1 - durability/100000)
|
|
"""
|
|
name: str
|
|
item_id: str
|
|
protection: ProtectionProfile = field(default_factory=ProtectionProfile)
|
|
durability: int = 2000 # Plate durability affects economy
|
|
block_chance: Decimal = Decimal("0") # Chance to nullify hit completely (no decay)
|
|
|
|
# Constants
|
|
BASE_DECAY_FACTOR: Decimal = Decimal("0.05")
|
|
MAX_DURABILITY: int = 100000
|
|
|
|
def get_total_protection(self) -> Decimal:
|
|
"""Get total protection value."""
|
|
return self.protection.get_total()
|
|
|
|
def get_decay_for_damage(self, damage_absorbed: Decimal) -> Decimal:
|
|
"""
|
|
Calculate decay for absorbing damage using official formula.
|
|
|
|
Formula: Decay (PEC) = damage_absorbed * 0.05 * (1 - durability/100000)
|
|
|
|
Args:
|
|
damage_absorbed: Amount of damage the plate actually absorbed
|
|
|
|
Returns:
|
|
Decay in PEC
|
|
"""
|
|
if damage_absorbed <= 0:
|
|
return Decimal("0")
|
|
|
|
durability_factor = Decimal("1") - Decimal(self.durability) / Decimal(self.MAX_DURABILITY)
|
|
decay_pec = damage_absorbed * self.BASE_DECAY_FACTOR * durability_factor
|
|
|
|
return decay_pec
|
|
|
|
def get_hp_per_pec(self) -> Decimal:
|
|
"""
|
|
Calculate economy rating (hp per pec).
|
|
Higher is better.
|
|
"""
|
|
durability_factor = Decimal("1") - Decimal(self.durability) / Decimal(self.MAX_DURABILITY)
|
|
if durability_factor <= 0:
|
|
return Decimal("0")
|
|
return Decimal("1") / (self.BASE_DECAY_FACTOR * durability_factor)
|
|
|
|
def get_effective_protection(self, damage_type: str) -> Decimal:
|
|
"""Get protection value for a specific damage type."""
|
|
return self.protection.get_effective_against(damage_type)
|
|
|
|
def to_dict(self) -> Dict:
|
|
"""Convert to dictionary."""
|
|
return {
|
|
'name': self.name,
|
|
'item_id': self.item_id,
|
|
'protection': self.protection.to_dict(),
|
|
'durability': self.durability,
|
|
'block_chance': str(self.block_chance),
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict) -> "ArmorPlate":
|
|
"""Create from dictionary."""
|
|
return cls(
|
|
name=data['name'],
|
|
item_id=data['item_id'],
|
|
protection=ProtectionProfile.from_dict(data.get('protection', {})),
|
|
durability=data.get('durability', 2000),
|
|
block_chance=Decimal(data.get('block_chance', '0')),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class ArmorPiece:
|
|
"""
|
|
Individual armor piece (e.g., Ghost Helmet, Shogun Harness).
|
|
Each piece protects one slot and can have one plate attached.
|
|
|
|
Official Decay Formula (VU 15.15):
|
|
Decay (PEC) = damage_absorbed * 0.05 * (1 - durability/100000)
|
|
|
|
Economy varies by durability:
|
|
- Ghost (2000 dur): 20.41 hp/pec
|
|
- Gremlin (2950 dur): 20.61 hp/pec
|
|
- Angel (4000 dur): 20.83 hp/pec
|
|
"""
|
|
name: str
|
|
item_id: str
|
|
slot: ArmorSlot
|
|
set_name: Optional[str] = None # e.g., "Ghost", "Shogun"
|
|
protection: ProtectionProfile = field(default_factory=ProtectionProfile)
|
|
durability: int = 2000 # Durability affects economy
|
|
weight: Decimal = Decimal("1.0") # Weight in kg
|
|
decay_per_hp: Decimal = Decimal("0.05") # PEC per HP absorbed (0.05 = 20 hp/pec)
|
|
|
|
# Optional plate attachment
|
|
attached_plate: Optional[ArmorPlate] = None
|
|
|
|
# Class constants (not instance fields)
|
|
BASE_DECAY_FACTOR: ClassVar[Decimal] = Decimal("0.05")
|
|
MAX_DURABILITY: ClassVar[int] = 100000
|
|
|
|
def get_base_protection(self) -> ProtectionProfile:
|
|
"""Get base protection without plate."""
|
|
return self.protection
|
|
|
|
def get_total_protection(self) -> ProtectionProfile:
|
|
"""Get total protection including plate."""
|
|
if self.attached_plate:
|
|
return self.protection.add(self.attached_plate.protection)
|
|
return self.protection
|
|
|
|
def get_total_protection_value(self) -> Decimal:
|
|
"""Get total protection value including plate."""
|
|
return self.get_total_protection().get_total()
|
|
|
|
def get_decay_for_damage(self, damage_absorbed: Decimal) -> Decimal:
|
|
"""
|
|
Calculate armor decay for absorbing damage.
|
|
|
|
Official Formula (VU 15.15):
|
|
Decay (PEC) = damage_absorbed * 0.05 * (1 - durability/100000)
|
|
|
|
Args:
|
|
damage_absorbed: Amount of damage the armor actually absorbed
|
|
|
|
Returns:
|
|
Decay in PEC
|
|
"""
|
|
if damage_absorbed <= 0:
|
|
return Decimal("0")
|
|
|
|
# Calculate economy factor based on durability
|
|
# Higher durability = better economy (less decay)
|
|
durability_factor = Decimal("1") - Decimal(self.durability) / Decimal(self.MAX_DURABILITY)
|
|
decay_pec = damage_absorbed * self.BASE_DECAY_FACTOR * durability_factor
|
|
|
|
return decay_pec
|
|
|
|
def get_hp_per_pec(self) -> Decimal:
|
|
"""
|
|
Calculate economy rating (hp per pec).
|
|
Higher is better (more damage absorbed per pec spent).
|
|
"""
|
|
durability_factor = Decimal("1") - Decimal(self.durability) / Decimal(self.MAX_DURABILITY)
|
|
if durability_factor <= 0:
|
|
return Decimal("0")
|
|
return Decimal("1") / (self.BASE_DECAY_FACTOR * durability_factor)
|
|
|
|
def attach_plate(self, plate: ArmorPlate) -> bool:
|
|
"""Attach a plate to this armor piece."""
|
|
self.attached_plate = plate
|
|
return True
|
|
|
|
def remove_plate(self) -> Optional[ArmorPlate]:
|
|
"""Remove and return the attached plate."""
|
|
plate = self.attached_plate
|
|
self.attached_plate = None
|
|
return plate
|
|
|
|
def get_slot_display_name(self) -> str:
|
|
"""Get human-readable slot name (matches Entropia Nexus)."""
|
|
slot_names = {
|
|
ArmorSlot.HEAD: "Head",
|
|
ArmorSlot.TORSO: "Torso",
|
|
ArmorSlot.ARMS: "Arms",
|
|
ArmorSlot.HANDS: "Hands",
|
|
ArmorSlot.LEGS: "Legs",
|
|
ArmorSlot.SHINS: "Shins",
|
|
ArmorSlot.FEET: "Feet",
|
|
}
|
|
return slot_names.get(self.slot, self.slot.value)
|
|
|
|
def to_dict(self) -> Dict:
|
|
"""Convert to dictionary."""
|
|
return {
|
|
'name': self.name,
|
|
'item_id': self.item_id,
|
|
'slot': self.slot.value,
|
|
'set_name': self.set_name,
|
|
'protection': self.protection.to_dict(),
|
|
'durability': self.durability,
|
|
'weight': str(self.weight),
|
|
'attached_plate': self.attached_plate.to_dict() if self.attached_plate else None,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict) -> "ArmorPiece":
|
|
"""Create from dictionary."""
|
|
piece = cls(
|
|
name=data['name'],
|
|
item_id=data['item_id'],
|
|
slot=ArmorSlot(data['slot']),
|
|
set_name=data.get('set_name'),
|
|
protection=ProtectionProfile.from_dict(data.get('protection', {})),
|
|
durability=data.get('durability', 2000),
|
|
weight=Decimal(data.get('weight', '1.0')),
|
|
)
|
|
if data.get('attached_plate'):
|
|
piece.attached_plate = ArmorPlate.from_dict(data['attached_plate'])
|
|
return piece
|
|
|
|
|
|
@dataclass
|
|
class ArmorSet:
|
|
"""
|
|
Complete armor set (7 pieces covering all slots).
|
|
Examples: Ghost Set, Shogun Set, Vigilante Set
|
|
"""
|
|
name: str # e.g., "Ghost Set"
|
|
set_id: str
|
|
pieces: Dict[ArmorSlot, ArmorPiece] = field(default_factory=dict)
|
|
set_bonus: Optional[ProtectionProfile] = None # Some sets have bonuses
|
|
|
|
def __post_init__(self):
|
|
"""Ensure all pieces reference this set."""
|
|
set_name = self.name.replace(" Set", "")
|
|
for slot, piece in self.pieces.items():
|
|
piece.set_name = set_name
|
|
|
|
def get_piece(self, slot: ArmorSlot) -> Optional[ArmorPiece]:
|
|
"""Get armor piece for a specific slot."""
|
|
return self.pieces.get(slot)
|
|
|
|
def is_complete(self) -> bool:
|
|
"""Check if set has all 7 pieces."""
|
|
return len(self.pieces) == 7 and all(slot in self.pieces for slot in ALL_ARMOR_SLOTS)
|
|
|
|
def get_total_protection(self) -> ProtectionProfile:
|
|
"""Get total protection from all pieces including plates."""
|
|
total = ProtectionProfile()
|
|
for piece in self.pieces.values():
|
|
total = total.add(piece.get_total_protection())
|
|
|
|
# Add set bonus if complete
|
|
if self.set_bonus and self.is_complete():
|
|
total = total.add(self.set_bonus)
|
|
|
|
return total
|
|
|
|
def get_total_decay_per_hit(self) -> Decimal:
|
|
"""
|
|
Get total decay per hit across all pieces (including plates).
|
|
Note: This is an ESTIMATE assuming average damage absorption.
|
|
Actual decay depends on how much damage each piece absorbs.
|
|
"""
|
|
# This is a rough estimate - actual decay depends on damage absorbed
|
|
# Using a typical hit value of 10 hp for estimation
|
|
typical_hit = Decimal("10")
|
|
total = Decimal("0")
|
|
|
|
for piece in self.pieces.values():
|
|
# Estimate armor decay (assuming it absorbs typical hit up to its protection)
|
|
armor_absorb = min(typical_hit, piece.protection.get_total())
|
|
total += piece.get_decay_for_damage(armor_absorb)
|
|
|
|
if piece.attached_plate:
|
|
# Estimate plate decay
|
|
plate_absorb = min(typical_hit, piece.attached_plate.get_total_protection())
|
|
total += piece.attached_plate.get_decay_for_damage(plate_absorb)
|
|
|
|
return total
|
|
|
|
def get_pieces_list(self) -> List[ArmorPiece]:
|
|
"""Get list of all pieces in slot order."""
|
|
return [self.pieces.get(slot) for slot in ALL_ARMOR_SLOTS if slot in self.pieces]
|
|
|
|
def to_dict(self) -> Dict:
|
|
"""Convert to dictionary."""
|
|
return {
|
|
'name': self.name,
|
|
'set_id': self.set_id,
|
|
'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,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict) -> "ArmorSet":
|
|
"""Create from dictionary."""
|
|
pieces = {
|
|
ArmorSlot(slot): ArmorPiece.from_dict(piece_data)
|
|
for slot, piece_data in data.get('pieces', {}).items()
|
|
}
|
|
set_bonus = None
|
|
if data.get('set_bonus'):
|
|
set_bonus = ProtectionProfile.from_dict(data['set_bonus'])
|
|
return cls(
|
|
name=data['name'],
|
|
set_id=data['set_id'],
|
|
pieces=pieces,
|
|
set_bonus=set_bonus,
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class EquippedArmor:
|
|
"""
|
|
Currently equipped armor configuration.
|
|
Can mix pieces from different sets or wear a full set.
|
|
"""
|
|
# Individual pieces (mix & match)
|
|
pieces: Dict[ArmorSlot, ArmorPiece] = field(default_factory=dict)
|
|
|
|
# Or reference a full set
|
|
full_set: Optional[ArmorSet] = None
|
|
|
|
def get_piece(self, slot: ArmorSlot) -> Optional[ArmorPiece]:
|
|
"""Get equipped piece for a slot."""
|
|
if self.full_set:
|
|
return self.full_set.get_piece(slot)
|
|
return self.pieces.get(slot)
|
|
|
|
def get_all_pieces(self) -> Dict[ArmorSlot, ArmorPiece]:
|
|
"""Get all equipped pieces as a dict."""
|
|
if self.full_set:
|
|
return self.full_set.pieces
|
|
return self.pieces
|
|
|
|
def equip_piece(self, piece: ArmorPiece) -> None:
|
|
"""Equip an individual armor piece."""
|
|
# Unequip full set if equipping individual pieces
|
|
self.full_set = None
|
|
self.pieces[piece.slot] = piece
|
|
|
|
def unequip_piece(self, slot: ArmorSlot) -> Optional[ArmorPiece]:
|
|
"""Unequip a piece from a slot."""
|
|
if self.full_set:
|
|
return None # Can't unequip individual pieces from full set
|
|
return self.pieces.pop(slot, None)
|
|
|
|
def equip_full_set(self, armor_set: ArmorSet) -> None:
|
|
"""Equip a full armor set."""
|
|
self.full_set = armor_set
|
|
self.pieces = {}
|
|
|
|
def unequip_full_set(self) -> Optional[ArmorSet]:
|
|
"""Unequip full set."""
|
|
set_ref = self.full_set
|
|
self.full_set = None
|
|
return set_ref
|
|
|
|
def attach_plate_to_slot(self, slot: ArmorSlot, plate: ArmorPlate) -> bool:
|
|
"""Attach a plate to the armor piece in a slot."""
|
|
piece = self.get_piece(slot)
|
|
if piece:
|
|
piece.attach_plate(plate)
|
|
return True
|
|
return False
|
|
|
|
def remove_plate_from_slot(self, slot: ArmorSlot) -> Optional[ArmorPlate]:
|
|
"""Remove plate from armor piece in a slot."""
|
|
piece = self.get_piece(slot)
|
|
if piece:
|
|
return piece.remove_plate()
|
|
return None
|
|
|
|
def get_plate(self, slot: ArmorSlot) -> Optional[ArmorPlate]:
|
|
"""Get attached plate for a slot."""
|
|
piece = self.get_piece(slot)
|
|
return piece.attached_plate if piece else None
|
|
|
|
def get_total_protection(self) -> ProtectionProfile:
|
|
"""Get total protection from all equipped pieces and plates."""
|
|
if self.full_set:
|
|
return self.full_set.get_total_protection()
|
|
|
|
total = ProtectionProfile()
|
|
for piece in self.pieces.values():
|
|
total = total.add(piece.get_total_protection())
|
|
return total
|
|
|
|
def get_total_decay_per_hit(self) -> Decimal:
|
|
"""
|
|
Get total decay per hit (armor + plates).
|
|
Note: This is an ESTIMATE assuming average damage absorption.
|
|
"""
|
|
if self.full_set:
|
|
return self.full_set.get_total_decay_per_hit()
|
|
|
|
# Estimate based on typical hit of 10 hp
|
|
typical_hit = Decimal("10")
|
|
total = Decimal("0")
|
|
|
|
for piece in self.pieces.values():
|
|
armor_absorb = min(typical_hit, piece.protection.get_total())
|
|
total += piece.get_decay_for_damage(armor_absorb)
|
|
|
|
if piece.attached_plate:
|
|
plate_absorb = min(typical_hit, piece.attached_plate.get_total_protection())
|
|
total += piece.attached_plate.get_decay_for_damage(plate_absorb)
|
|
|
|
return total
|
|
|
|
def get_coverage(self) -> Tuple[int, int]:
|
|
"""Get armor coverage as (equipped_slots, total_slots)."""
|
|
pieces = self.get_all_pieces()
|
|
return (len(pieces), 7)
|
|
|
|
def get_coverage_percentage(self) -> float:
|
|
"""Get armor coverage as percentage."""
|
|
equipped, total = self.get_coverage()
|
|
return (equipped / total) * 100 if total > 0 else 0
|
|
|
|
def get_slot_status(self) -> Dict[ArmorSlot, bool]:
|
|
"""Get status of each slot (True if equipped)."""
|
|
pieces = self.get_all_pieces()
|
|
return {slot: slot in pieces for slot in ALL_ARMOR_SLOTS}
|
|
|
|
def to_dict(self) -> Dict:
|
|
"""Convert to dictionary."""
|
|
return {
|
|
'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,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict) -> "EquippedArmor":
|
|
"""Create from dictionary."""
|
|
equipped = cls()
|
|
|
|
if data.get('full_set'):
|
|
equipped.full_set = ArmorSet.from_dict(data['full_set'])
|
|
else:
|
|
pieces = {
|
|
ArmorSlot(slot): ArmorPiece.from_dict(piece_data)
|
|
for slot, piece_data in data.get('pieces', {}).items()
|
|
}
|
|
equipped.pieces = pieces
|
|
|
|
return equipped
|
|
|
|
|
|
# ============================================================================
|
|
# Damage Absorption Logic
|
|
# ============================================================================
|
|
|
|
@dataclass
|
|
class HitResult:
|
|
"""Result of a hit against armored target."""
|
|
raw_damage: Decimal
|
|
damage_type: str
|
|
|
|
# Damage absorbed
|
|
plate_absorbed: Decimal = Decimal("0")
|
|
armor_absorbed: Decimal = Decimal("0")
|
|
damage_to_avatar: Decimal = Decimal("0")
|
|
|
|
# Decay incurred
|
|
plate_decay: Decimal = Decimal("0")
|
|
armor_decay: Decimal = Decimal("0")
|
|
total_decay: Decimal = Decimal("0")
|
|
|
|
# Status
|
|
plate_broken: bool = False
|
|
armor_broken: bool = False
|
|
|
|
|
|
def calculate_hit_protection(
|
|
equipped_armor: EquippedArmor,
|
|
incoming_damage: Decimal,
|
|
damage_type: str,
|
|
hit_location: Optional[ArmorSlot] = None
|
|
) -> HitResult:
|
|
"""
|
|
Calculate damage absorption for a hit using Loot 2.0 mechanics.
|
|
|
|
Loot 2.0 Armor Mechanics (June 2017):
|
|
1. Plate absorbs damage FIRST (shield layer)
|
|
2. Armor absorbs remaining damage
|
|
3. Plate decay = damage_absorbed_by_plate * plate.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)
|
|
6. Block chance on upgraded plates can nullify hit (no decay)
|
|
|
|
Damage Flow:
|
|
Incoming Damage → Plate absorbs first → Armor absorbs remainder → Player takes overflow
|
|
|
|
Example: 20 Impact hit vs Ghost Harness (4 Impact) + Impact Plate (3 Impact):
|
|
- 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
|
|
- Player takes 20 - 3 - 4 = 13 damage
|
|
|
|
Args:
|
|
equipped_armor: Currently equipped armor
|
|
incoming_damage: Raw damage from attack
|
|
damage_type: Type of damage (impact, burn, etc.)
|
|
hit_location: Specific slot hit (None for full body/average)
|
|
|
|
Returns:
|
|
HitResult with absorption details
|
|
"""
|
|
result = HitResult(
|
|
raw_damage=incoming_damage,
|
|
damage_type=damage_type,
|
|
)
|
|
|
|
# Check for block chance on plates (upgraded plates only)
|
|
# This would nullify the hit completely with no decay
|
|
|
|
# Get protection for the hit
|
|
if hit_location:
|
|
# Specific location hit
|
|
piece = equipped_armor.get_piece(hit_location)
|
|
if not piece:
|
|
# No armor on that slot - full damage
|
|
result.damage_to_avatar = incoming_damage
|
|
return result
|
|
|
|
# Check for block on plate
|
|
if piece.attached_plate and piece.attached_plate.block_chance > 0:
|
|
# Note: In real implementation, use random() to check block
|
|
# For calculation purposes, we don't factor block chance
|
|
pass
|
|
|
|
# Plate protection for this damage type
|
|
plate_prot = Decimal("0")
|
|
if piece.attached_plate:
|
|
plate_prot = piece.attached_plate.get_effective_protection(damage_type)
|
|
|
|
# Armor protection for this damage type
|
|
armor_prot = piece.protection.get_effective_against(damage_type)
|
|
|
|
# Plate absorbs FIRST (up to its protection)
|
|
plate_absorb = min(plate_prot, incoming_damage)
|
|
result.plate_absorbed = plate_absorb
|
|
remaining = incoming_damage - plate_absorb
|
|
|
|
# Armor absorbs remainder (up to its protection)
|
|
armor_absorb = min(armor_prot, remaining)
|
|
result.armor_absorbed = armor_absorb
|
|
result.damage_to_avatar = remaining - armor_absorb
|
|
|
|
# Calculate decay based on actual damage absorbed (Loot 2.0)
|
|
if piece.attached_plate and plate_absorb > 0:
|
|
result.plate_decay = piece.attached_plate.get_decay_for_damage(plate_absorb)
|
|
|
|
if armor_absorb > 0:
|
|
result.armor_decay = piece.get_decay_for_damage(armor_absorb)
|
|
|
|
result.total_decay = result.plate_decay + result.armor_decay
|
|
|
|
else:
|
|
# Full body hit - use average protection from all equipped pieces
|
|
pieces = equipped_armor.get_all_pieces()
|
|
if not pieces:
|
|
result.damage_to_avatar = incoming_damage
|
|
return result
|
|
|
|
# Calculate total protection across all slots
|
|
total_plate_prot = Decimal("0")
|
|
total_armor_prot = Decimal("0")
|
|
|
|
for piece in pieces.values():
|
|
total_armor_prot += piece.protection.get_effective_against(damage_type)
|
|
if piece.attached_plate:
|
|
total_plate_prot += piece.attached_plate.get_effective_protection(damage_type)
|
|
|
|
# Plate absorbs FIRST
|
|
plate_absorb = min(total_plate_prot, incoming_damage)
|
|
result.plate_absorbed = plate_absorb
|
|
remaining = incoming_damage - plate_absorb
|
|
|
|
# Armor absorbs remainder
|
|
armor_absorb = min(total_armor_prot, remaining)
|
|
result.armor_absorbed = armor_absorb
|
|
result.damage_to_avatar = remaining - armor_absorb
|
|
|
|
# Calculate decay based on actual damage absorbed
|
|
# Distribute decay proportionally across all pieces
|
|
if plate_absorb > 0:
|
|
for piece in pieces.values():
|
|
if piece.attached_plate:
|
|
piece_plate_prot = piece.attached_plate.get_effective_protection(damage_type)
|
|
if piece_plate_prot > 0 and total_plate_prot > 0:
|
|
# This plate's share of absorption
|
|
piece_plate_share = plate_absorb * (piece_plate_prot / total_plate_prot)
|
|
result.plate_decay += piece.attached_plate.get_decay_for_damage(piece_plate_share)
|
|
|
|
if armor_absorb > 0:
|
|
for piece in pieces.values():
|
|
piece_armor_prot = piece.protection.get_effective_against(damage_type)
|
|
if piece_armor_prot > 0 and total_armor_prot > 0:
|
|
# This armor's share of absorption
|
|
piece_armor_share = armor_absorb * (piece_armor_prot / total_armor_prot)
|
|
result.armor_decay += piece.get_decay_for_damage(piece_armor_share)
|
|
|
|
result.total_decay = result.plate_decay + result.armor_decay
|
|
|
|
return result
|
|
|
|
|
|
# ============================================================================
|
|
# Mock Data - Real Entropia Universe Armor Sets
|
|
# ============================================================================
|
|
|
|
def create_ghost_set() -> ArmorSet:
|
|
"""Create the Ghost armor set (light, good vs cold/burn)."""
|
|
# Ghost: 2000 durability = 20.41 hp/pec
|
|
ghost_durability = 2000
|
|
|
|
pieces = {
|
|
ArmorSlot.HEAD: ArmorPiece(
|
|
name="Ghost Helmet",
|
|
item_id="ghost_helmet",
|
|
slot=ArmorSlot.HEAD,
|
|
set_name="Ghost",
|
|
durability=ghost_durability,
|
|
protection=ProtectionProfile(impact=Decimal("2"), cut=Decimal("2"), stab=Decimal("2"), burn=Decimal("5"), cold=Decimal("5")),
|
|
weight=Decimal("0.3"),
|
|
),
|
|
ArmorSlot.TORSO: ArmorPiece(
|
|
name="Ghost Harness",
|
|
item_id="ghost_harness",
|
|
slot=ArmorSlot.TORSO,
|
|
set_name="Ghost",
|
|
durability=ghost_durability,
|
|
protection=ProtectionProfile(impact=Decimal("4"), cut=Decimal("4"), stab=Decimal("4"), burn=Decimal("8"), cold=Decimal("8")),
|
|
weight=Decimal("0.7"),
|
|
),
|
|
ArmorSlot.ARMS: ArmorPiece(
|
|
name="Ghost Arm Guards (L)",
|
|
item_id="ghost_arm_l",
|
|
slot=ArmorSlot.ARMS,
|
|
set_name="Ghost",
|
|
durability=ghost_durability,
|
|
protection=ProtectionProfile(impact=Decimal("2"), cut=Decimal("2"), stab=Decimal("2"), burn=Decimal("5"), cold=Decimal("5")),
|
|
weight=Decimal("0.3"),
|
|
),
|
|
ArmorSlot.ARMS: ArmorPiece(
|
|
name="Ghost Arm Guards (R)",
|
|
item_id="ghost_arm_r",
|
|
slot=ArmorSlot.ARMS,
|
|
set_name="Ghost",
|
|
durability=ghost_durability,
|
|
protection=ProtectionProfile(impact=Decimal("2"), cut=Decimal("2"), stab=Decimal("2"), burn=Decimal("5"), cold=Decimal("5")),
|
|
weight=Decimal("0.3"),
|
|
),
|
|
ArmorSlot.HANDS: ArmorPiece(
|
|
name="Ghost Gloves (L)",
|
|
item_id="ghost_gloves_l",
|
|
slot=ArmorSlot.HANDS,
|
|
set_name="Ghost",
|
|
durability=ghost_durability,
|
|
protection=ProtectionProfile(impact=Decimal("1"), cut=Decimal("1"), stab=Decimal("1"), burn=Decimal("3"), cold=Decimal("3")),
|
|
weight=Decimal("0.2"),
|
|
),
|
|
ArmorSlot.HANDS: ArmorPiece(
|
|
name="Ghost Gloves (R)",
|
|
item_id="ghost_gloves_r",
|
|
slot=ArmorSlot.HANDS,
|
|
set_name="Ghost",
|
|
durability=ghost_durability,
|
|
protection=ProtectionProfile(impact=Decimal("1"), cut=Decimal("1"), stab=Decimal("1"), burn=Decimal("3"), cold=Decimal("3")),
|
|
weight=Decimal("0.2"),
|
|
),
|
|
ArmorSlot.LEGS: ArmorPiece(
|
|
name="Ghost Thigh+Shin Guards",
|
|
item_id="ghost_legs",
|
|
slot=ArmorSlot.LEGS,
|
|
set_name="Ghost",
|
|
durability=ghost_durability,
|
|
protection=ProtectionProfile(impact=Decimal("3"), cut=Decimal("3"), stab=Decimal("3"), burn=Decimal("7"), cold=Decimal("7")),
|
|
weight=Decimal("0.6"),
|
|
),
|
|
}
|
|
return ArmorSet(
|
|
name="Ghost Set",
|
|
set_id="ghost_set",
|
|
pieces=pieces,
|
|
)
|
|
|
|
|
|
def create_shogun_set() -> ArmorSet:
|
|
"""Create the Shogun armor set (medium, good vs impact/cut)."""
|
|
# Shogun: 2500 durability = better economy than Ghost
|
|
shogun_durability = 2500
|
|
|
|
pieces = {
|
|
ArmorSlot.HEAD: ArmorPiece(
|
|
name="Shogun Helmet",
|
|
item_id="shogun_helmet",
|
|
slot=ArmorSlot.HEAD,
|
|
set_name="Shogun",
|
|
durability=shogun_durability,
|
|
protection=ProtectionProfile(impact=Decimal("5"), cut=Decimal("4"), stab=Decimal("3"), burn=Decimal("2"), cold=Decimal("2")),
|
|
weight=Decimal("0.8"),
|
|
),
|
|
ArmorSlot.TORSO: ArmorPiece(
|
|
name="Shogun Harness",
|
|
item_id="shogun_harness",
|
|
slot=ArmorSlot.TORSO,
|
|
set_name="Shogun",
|
|
durability=shogun_durability,
|
|
protection=ProtectionProfile(impact=Decimal("8"), cut=Decimal("6"), stab=Decimal("5"), burn=Decimal("4"), cold=Decimal("4")),
|
|
weight=Decimal("1.5"),
|
|
),
|
|
ArmorSlot.ARMS: ArmorPiece(
|
|
name="Shogun Arm Guards (L)",
|
|
item_id="shogun_arm_l",
|
|
slot=ArmorSlot.ARMS,
|
|
set_name="Shogun",
|
|
durability=shogun_durability,
|
|
protection=ProtectionProfile(impact=Decimal("5"), cut=Decimal("4"), stab=Decimal("3"), burn=Decimal("2"), cold=Decimal("2")),
|
|
weight=Decimal("0.8"),
|
|
),
|
|
ArmorSlot.ARMS: ArmorPiece(
|
|
name="Shogun Arm Guards (R)",
|
|
item_id="shogun_arm_r",
|
|
slot=ArmorSlot.ARMS,
|
|
set_name="Shogun",
|
|
durability=shogun_durability,
|
|
protection=ProtectionProfile(impact=Decimal("5"), cut=Decimal("4"), stab=Decimal("3"), burn=Decimal("2"), cold=Decimal("2")),
|
|
weight=Decimal("0.8"),
|
|
),
|
|
ArmorSlot.HANDS: ArmorPiece(
|
|
name="Shogun Gloves (L)",
|
|
item_id="shogun_gloves_l",
|
|
slot=ArmorSlot.HANDS,
|
|
set_name="Shogun",
|
|
durability=shogun_durability,
|
|
protection=ProtectionProfile(impact=Decimal("3"), cut=Decimal("2"), stab=Decimal("2"), burn=Decimal("1"), cold=Decimal("1")),
|
|
weight=Decimal("0.4"),
|
|
),
|
|
ArmorSlot.HANDS: ArmorPiece(
|
|
name="Shogun Gloves (R)",
|
|
item_id="shogun_gloves_r",
|
|
slot=ArmorSlot.HANDS,
|
|
set_name="Shogun",
|
|
durability=shogun_durability,
|
|
protection=ProtectionProfile(impact=Decimal("3"), cut=Decimal("2"), stab=Decimal("2"), burn=Decimal("1"), cold=Decimal("1")),
|
|
weight=Decimal("0.4"),
|
|
),
|
|
ArmorSlot.LEGS: ArmorPiece(
|
|
name="Shogun Thigh+Shin Guards",
|
|
item_id="shogun_legs",
|
|
slot=ArmorSlot.LEGS,
|
|
set_name="Shogun",
|
|
durability=shogun_durability,
|
|
protection=ProtectionProfile(impact=Decimal("7"), cut=Decimal("5"), stab=Decimal("4"), burn=Decimal("3"), cold=Decimal("3")),
|
|
weight=Decimal("1.2"),
|
|
),
|
|
}
|
|
return ArmorSet(
|
|
name="Shogun Set",
|
|
set_id="shogun_set",
|
|
pieces=pieces,
|
|
)
|
|
|
|
|
|
def create_vigilante_set() -> ArmorSet:
|
|
"""Create the Vigilante armor set (light, good all-around)."""
|
|
vigilante_durability = 2000 # Same as Ghost
|
|
|
|
pieces = {
|
|
ArmorSlot.HEAD: ArmorPiece(
|
|
name="Vigilante Helmet",
|
|
item_id="vigilante_helmet",
|
|
slot=ArmorSlot.HEAD,
|
|
set_name="Vigilante",
|
|
durability=vigilante_durability,
|
|
protection=ProtectionProfile(impact=Decimal("3"), cut=Decimal("2"), stab=Decimal("2")),
|
|
weight=Decimal("0.4"),
|
|
),
|
|
ArmorSlot.TORSO: ArmorPiece(
|
|
name="Vigilante Harness",
|
|
item_id="vigilante_harness",
|
|
slot=ArmorSlot.TORSO,
|
|
set_name="Vigilante",
|
|
durability=vigilante_durability,
|
|
protection=ProtectionProfile(impact=Decimal("6"), cut=Decimal("5"), stab=Decimal("4")),
|
|
weight=Decimal("1.0"),
|
|
),
|
|
ArmorSlot.ARMS: ArmorPiece(
|
|
name="Vigilante Arm Guards (L)",
|
|
item_id="vigilante_arm_l",
|
|
slot=ArmorSlot.ARMS,
|
|
set_name="Vigilante",
|
|
durability=vigilante_durability,
|
|
protection=ProtectionProfile(impact=Decimal("3"), cut=Decimal("2"), stab=Decimal("2")),
|
|
weight=Decimal("0.4"),
|
|
),
|
|
ArmorSlot.ARMS: ArmorPiece(
|
|
name="Vigilante Arm Guards (R)",
|
|
item_id="vigilante_arm_r",
|
|
slot=ArmorSlot.ARMS,
|
|
set_name="Vigilante",
|
|
durability=vigilante_durability,
|
|
protection=ProtectionProfile(impact=Decimal("3"), cut=Decimal("2"), stab=Decimal("2")),
|
|
weight=Decimal("0.4"),
|
|
),
|
|
ArmorSlot.HANDS: ArmorPiece(
|
|
name="Vigilante Gloves (L)",
|
|
item_id="vigilante_gloves_l",
|
|
slot=ArmorSlot.HANDS,
|
|
set_name="Vigilante",
|
|
durability=vigilante_durability,
|
|
protection=ProtectionProfile(impact=Decimal("2"), cut=Decimal("1"), stab=Decimal("1")),
|
|
weight=Decimal("0.2"),
|
|
),
|
|
ArmorSlot.HANDS: ArmorPiece(
|
|
name="Vigilante Gloves (R)",
|
|
item_id="vigilante_gloves_r",
|
|
slot=ArmorSlot.HANDS,
|
|
set_name="Vigilante",
|
|
durability=vigilante_durability,
|
|
protection=ProtectionProfile(impact=Decimal("2"), cut=Decimal("1"), stab=Decimal("1")),
|
|
weight=Decimal("0.2"),
|
|
),
|
|
ArmorSlot.LEGS: ArmorPiece(
|
|
name="Vigilante Thigh+Shin Guards",
|
|
item_id="vigilante_legs",
|
|
slot=ArmorSlot.LEGS,
|
|
set_name="Vigilante",
|
|
durability=vigilante_durability,
|
|
protection=ProtectionProfile(impact=Decimal("5"), cut=Decimal("4"), stab=Decimal("3")),
|
|
weight=Decimal("0.8"),
|
|
),
|
|
}
|
|
return ArmorSet(
|
|
name="Vigilante Set",
|
|
set_id="vigilante_set",
|
|
pieces=pieces,
|
|
)
|
|
|
|
|
|
def create_hermes_set() -> ArmorSet:
|
|
"""Create the Hermes armor set (medium, good vs penetration)."""
|
|
hermes_economy = Decimal("0.05")
|
|
|
|
pieces = {
|
|
ArmorSlot.HEAD: ArmorPiece(
|
|
name="Hermes Helmet",
|
|
item_id="hermes_helmet",
|
|
slot=ArmorSlot.HEAD,
|
|
set_name="Hermes",
|
|
decay_per_hp=hermes_economy,
|
|
protection=ProtectionProfile(impact=Decimal("3"), cut=Decimal("3"), stab=Decimal("2"), penetration=Decimal("3")),
|
|
weight=Decimal("0.5"),
|
|
),
|
|
ArmorSlot.TORSO: ArmorPiece(
|
|
name="Hermes Harness",
|
|
item_id="hermes_harness",
|
|
slot=ArmorSlot.TORSO,
|
|
set_name="Hermes",
|
|
decay_per_hp=hermes_economy,
|
|
protection=ProtectionProfile(impact=Decimal("10"), cut=Decimal("8"), stab=Decimal("7"), penetration=Decimal("5")),
|
|
weight=Decimal("1.2"),
|
|
),
|
|
ArmorSlot.ARMS: ArmorPiece(
|
|
name="Hermes Arm Guards (L)",
|
|
item_id="hermes_arm_l",
|
|
slot=ArmorSlot.ARMS,
|
|
set_name="Hermes",
|
|
decay_per_hp=hermes_economy,
|
|
protection=ProtectionProfile(impact=Decimal("4"), cut=Decimal("4"), stab=Decimal("3"), penetration=Decimal("2")),
|
|
weight=Decimal("0.5"),
|
|
),
|
|
ArmorSlot.ARMS: ArmorPiece(
|
|
name="Hermes Arm Guards (R)",
|
|
item_id="hermes_arm_r",
|
|
slot=ArmorSlot.ARMS,
|
|
set_name="Hermes",
|
|
decay_per_hp=hermes_economy,
|
|
protection=ProtectionProfile(impact=Decimal("4"), cut=Decimal("4"), stab=Decimal("3"), penetration=Decimal("2")),
|
|
weight=Decimal("0.5"),
|
|
),
|
|
ArmorSlot.HANDS: ArmorPiece(
|
|
name="Hermes Gloves (L)",
|
|
item_id="hermes_gloves_l",
|
|
slot=ArmorSlot.HANDS,
|
|
set_name="Hermes",
|
|
decay_per_hp=hermes_economy,
|
|
protection=ProtectionProfile(impact=Decimal("2"), cut=Decimal("2"), stab=Decimal("1"), penetration=Decimal("1")),
|
|
weight=Decimal("0.25"),
|
|
),
|
|
ArmorSlot.HANDS: ArmorPiece(
|
|
name="Hermes Gloves (R)",
|
|
item_id="hermes_gloves_r",
|
|
slot=ArmorSlot.HANDS,
|
|
set_name="Hermes",
|
|
decay_per_hp=hermes_economy,
|
|
protection=ProtectionProfile(impact=Decimal("2"), cut=Decimal("2"), stab=Decimal("1"), penetration=Decimal("1")),
|
|
weight=Decimal("0.25"),
|
|
),
|
|
ArmorSlot.LEGS: ArmorPiece(
|
|
name="Hermes Thigh+Shin Guards",
|
|
item_id="hermes_legs",
|
|
slot=ArmorSlot.LEGS,
|
|
set_name="Hermes",
|
|
decay_per_hp=hermes_economy,
|
|
protection=ProtectionProfile(impact=Decimal("7"), cut=Decimal("6"), stab=Decimal("5"), penetration=Decimal("4")),
|
|
weight=Decimal("1.0"),
|
|
),
|
|
}
|
|
return ArmorSet(
|
|
name="Hermes Set",
|
|
set_id="hermes_set",
|
|
pieces=pieces,
|
|
)
|
|
|
|
|
|
def create_pixie_set() -> ArmorSet:
|
|
"""Create the Pixie armor set (light starter armor)."""
|
|
# Pixie is starter armor, slightly worse economy (18 hp/pec)
|
|
pixie_economy = Decimal("0.055")
|
|
|
|
pieces = {
|
|
ArmorSlot.HEAD: ArmorPiece(
|
|
name="Pixie Helmet",
|
|
item_id="pixie_helmet",
|
|
slot=ArmorSlot.HEAD,
|
|
set_name="Pixie",
|
|
decay_per_hp=pixie_economy,
|
|
protection=ProtectionProfile(impact=Decimal("1"), cut=Decimal("1"), stab=Decimal("1")),
|
|
weight=Decimal("0.2"),
|
|
),
|
|
ArmorSlot.TORSO: ArmorPiece(
|
|
name="Pixie Harness",
|
|
item_id="pixie_harness",
|
|
slot=ArmorSlot.TORSO,
|
|
set_name="Pixie",
|
|
decay_per_hp=pixie_economy,
|
|
protection=ProtectionProfile(impact=Decimal("4"), cut=Decimal("3"), stab=Decimal("2")),
|
|
weight=Decimal("0.5"),
|
|
),
|
|
ArmorSlot.ARMS: ArmorPiece(
|
|
name="Pixie Arm Guards (L)",
|
|
item_id="pixie_arm_l",
|
|
slot=ArmorSlot.ARMS,
|
|
set_name="Pixie",
|
|
decay_per_hp=pixie_economy,
|
|
protection=ProtectionProfile(impact=Decimal("1"), cut=Decimal("1"), stab=Decimal("1")),
|
|
weight=Decimal("0.2"),
|
|
),
|
|
ArmorSlot.ARMS: ArmorPiece(
|
|
name="Pixie Arm Guards (R)",
|
|
item_id="pixie_arm_r",
|
|
slot=ArmorSlot.ARMS,
|
|
set_name="Pixie",
|
|
decay_per_hp=pixie_economy,
|
|
protection=ProtectionProfile(impact=Decimal("1"), cut=Decimal("1"), stab=Decimal("1")),
|
|
weight=Decimal("0.2"),
|
|
),
|
|
ArmorSlot.HANDS: ArmorPiece(
|
|
name="Pixie Gloves (L)",
|
|
item_id="pixie_gloves_l",
|
|
slot=ArmorSlot.HANDS,
|
|
set_name="Pixie",
|
|
decay_per_hp=pixie_economy,
|
|
protection=ProtectionProfile(impact=Decimal("1"), cut=Decimal("1")),
|
|
weight=Decimal("0.1"),
|
|
),
|
|
ArmorSlot.HANDS: ArmorPiece(
|
|
name="Pixie Gloves (R)",
|
|
item_id="pixie_gloves_r",
|
|
slot=ArmorSlot.HANDS,
|
|
set_name="Pixie",
|
|
decay_per_hp=pixie_economy,
|
|
protection=ProtectionProfile(impact=Decimal("1"), cut=Decimal("1")),
|
|
weight=Decimal("0.1"),
|
|
),
|
|
ArmorSlot.LEGS: ArmorPiece(
|
|
name="Pixie Thigh+Shin Guards",
|
|
item_id="pixie_legs",
|
|
slot=ArmorSlot.LEGS,
|
|
set_name="Pixie",
|
|
decay_per_hp=pixie_economy,
|
|
protection=ProtectionProfile(impact=Decimal("2"), cut=Decimal("2"), stab=Decimal("1")),
|
|
weight=Decimal("0.4"),
|
|
),
|
|
}
|
|
return ArmorSet(
|
|
name="Pixie Set",
|
|
set_id="pixie_set",
|
|
pieces=pieces,
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# Mock Plates
|
|
# ============================================================================
|
|
|
|
def get_mock_plates() -> List[ArmorPlate]:
|
|
"""Get list of available armor plates."""
|
|
# Standard plate economy: 20 hp/pec = 0.05 pec per hp
|
|
# Upgraded plates might have better economy (25 hp/pec = 0.04 pec per hp)
|
|
standard_economy = Decimal("0.05")
|
|
improved_economy = Decimal("0.04") # 25 hp/pec
|
|
|
|
return [
|
|
# General purpose plates
|
|
ArmorPlate(
|
|
name="Impact Plating I",
|
|
item_id="plate_impact_1",
|
|
decay_per_hp=standard_economy,
|
|
protection=ProtectionProfile(impact=Decimal("3")),
|
|
),
|
|
ArmorPlate(
|
|
name="Impact Plating II",
|
|
item_id="plate_impact_2",
|
|
decay_per_hp=improved_economy,
|
|
protection=ProtectionProfile(impact=Decimal("6")),
|
|
),
|
|
# 5B Plates (the standard mid-game plate - good all-around)
|
|
ArmorPlate(
|
|
name="5B Plating Set",
|
|
item_id="plate_5b",
|
|
decay_per_hp=standard_economy,
|
|
protection=ProtectionProfile(
|
|
impact=Decimal("5"), cut=Decimal("5"), stab=Decimal("5"),
|
|
burn=Decimal("3"), cold=Decimal("3")
|
|
),
|
|
block_chance=Decimal("0.01"), # 1% block chance
|
|
),
|
|
ArmorPlate(
|
|
name="Cut Plating I",
|
|
item_id="plate_cut_1",
|
|
decay_per_hp=standard_economy,
|
|
protection=ProtectionProfile(cut=Decimal("3")),
|
|
),
|
|
ArmorPlate(
|
|
name="Stab Plating I",
|
|
item_id="plate_stab_1",
|
|
decay_per_hp=standard_economy,
|
|
protection=ProtectionProfile(stab=Decimal("3")),
|
|
),
|
|
ArmorPlate(
|
|
name="Burn Plating I",
|
|
item_id="plate_burn_1",
|
|
decay_per_hp=standard_economy,
|
|
protection=ProtectionProfile(burn=Decimal("3")),
|
|
),
|
|
ArmorPlate(
|
|
name="Cold Plating I",
|
|
item_id="plate_cold_1",
|
|
decay_per_hp=standard_economy,
|
|
protection=ProtectionProfile(cold=Decimal("3")),
|
|
),
|
|
ArmorPlate(
|
|
name="Penetration Plating I",
|
|
item_id="plate_pen_1",
|
|
decay_per_hp=standard_economy,
|
|
protection=ProtectionProfile(penetration=Decimal("3")),
|
|
),
|
|
# Composite plates (multi-type)
|
|
ArmorPlate(
|
|
name="Composite Plating I",
|
|
item_id="plate_composite_1",
|
|
decay_per_hp=standard_economy,
|
|
protection=ProtectionProfile(
|
|
impact=Decimal("2"), cut=Decimal("2"), stab=Decimal("2")
|
|
),
|
|
),
|
|
ArmorPlate(
|
|
name="Elemental Plating I",
|
|
item_id="plate_elemental_1",
|
|
decay_per_hp=standard_economy,
|
|
protection=ProtectionProfile(
|
|
burn=Decimal("2"), cold=Decimal("2"), acid=Decimal("2"), electric=Decimal("2")
|
|
),
|
|
),
|
|
# Heavy plates
|
|
ArmorPlate(
|
|
name="Heavy Impact Plating",
|
|
item_id="plate_heavy_impact",
|
|
decay_per_hp=improved_economy,
|
|
protection=ProtectionProfile(impact=Decimal("10")),
|
|
),
|
|
ArmorPlate(
|
|
name="Heavy Universal Plating",
|
|
item_id="plate_heavy_uni",
|
|
decay_per_hp=improved_economy,
|
|
protection=ProtectionProfile(
|
|
impact=Decimal("3"), cut=Decimal("3"), stab=Decimal("3"),
|
|
burn=Decimal("3"), cold=Decimal("3"), penetration=Decimal("3")
|
|
),
|
|
),
|
|
]
|
|
|
|
|
|
# ============================================================================
|
|
# Helper Functions
|
|
# ============================================================================
|
|
|
|
def create_frontier_set() -> ArmorSet:
|
|
"""Create the Frontier armor set (Arkadia mid-level, good mobility)."""
|
|
# Frontier Adjusted has 3800 durability = 20.83 hp/pec economy
|
|
frontier_economy = Decimal("0.048") # ~20.8 hp/pec
|
|
|
|
pieces = {
|
|
ArmorSlot.HEAD: ArmorPiece(
|
|
name="Frontier Helmet, Adjusted (M)",
|
|
item_id="frontier_helmet_adj_m",
|
|
slot=ArmorSlot.HEAD,
|
|
set_name="Frontier",
|
|
decay_per_hp=frontier_economy,
|
|
protection=ProtectionProfile(impact=Decimal("4"), cut=Decimal("3"), stab=Decimal("3")),
|
|
weight=Decimal("0.5"),
|
|
),
|
|
ArmorSlot.TORSO: ArmorPiece(
|
|
name="Frontier Harness, Adjusted (M)",
|
|
item_id="frontier_harness_adj_m",
|
|
slot=ArmorSlot.TORSO,
|
|
set_name="Frontier",
|
|
decay_per_hp=frontier_economy,
|
|
protection=ProtectionProfile(impact=Decimal("8"), cut=Decimal("6"), stab=Decimal("6")),
|
|
weight=Decimal("1.0"),
|
|
),
|
|
ArmorSlot.ARMS: ArmorPiece(
|
|
name="Frontier Arm Guards, Adjusted (M)",
|
|
item_id="frontier_arm_adj_m",
|
|
slot=ArmorSlot.ARMS,
|
|
set_name="Frontier",
|
|
decay_per_hp=frontier_economy,
|
|
protection=ProtectionProfile(impact=Decimal("5"), cut=Decimal("4"), stab=Decimal("4")),
|
|
weight=Decimal("0.8"),
|
|
),
|
|
ArmorSlot.HANDS: ArmorPiece(
|
|
name="Frontier Gloves, Adjusted (M)",
|
|
item_id="frontier_gloves_adj_m",
|
|
slot=ArmorSlot.HANDS,
|
|
set_name="Frontier",
|
|
decay_per_hp=frontier_economy,
|
|
protection=ProtectionProfile(impact=Decimal("3"), cut=Decimal("2"), stab=Decimal("2")),
|
|
weight=Decimal("0.6"),
|
|
),
|
|
ArmorSlot.LEGS: ArmorPiece(
|
|
name="Frontier Thigh Guards, Adjusted (M)",
|
|
item_id="frontier_thigh_adj_m",
|
|
slot=ArmorSlot.LEGS,
|
|
set_name="Frontier",
|
|
decay_per_hp=frontier_economy,
|
|
protection=ProtectionProfile(impact=Decimal("4"), cut=Decimal("3"), stab=Decimal("3")),
|
|
weight=Decimal("0.6"),
|
|
),
|
|
ArmorSlot.SHINS: ArmorPiece(
|
|
name="Frontier Shin Guards, Adjusted (M)",
|
|
item_id="frontier_shin_adj_m",
|
|
slot=ArmorSlot.SHINS,
|
|
set_name="Frontier",
|
|
decay_per_hp=frontier_economy,
|
|
protection=ProtectionProfile(impact=Decimal("3"), cut=Decimal("2"), stab=Decimal("2")),
|
|
weight=Decimal("0.4"),
|
|
),
|
|
ArmorSlot.FEET: ArmorPiece(
|
|
name="Frontier Foot Guards, Adjusted (M)",
|
|
item_id="frontier_foot_adj_m",
|
|
slot=ArmorSlot.FEET,
|
|
set_name="Frontier",
|
|
decay_per_hp=frontier_economy,
|
|
protection=ProtectionProfile(impact=Decimal("2"), cut=Decimal("1"), stab=Decimal("1")),
|
|
weight=Decimal("0.3"),
|
|
),
|
|
}
|
|
return ArmorSet(
|
|
name="Frontier Set (Adjusted)",
|
|
set_id="frontier_set_adj",
|
|
pieces=pieces,
|
|
)
|
|
|
|
|
|
def get_all_armor_sets() -> List[ArmorSet]:
|
|
"""Get all available armor sets."""
|
|
return [
|
|
create_pixie_set(),
|
|
create_vigilante_set(),
|
|
create_ghost_set(),
|
|
create_shogun_set(),
|
|
create_frontier_set(),
|
|
create_hermes_set(),
|
|
]
|
|
|
|
|
|
def get_armor_set_by_name(name: str) -> Optional[ArmorSet]:
|
|
"""Get an armor set by name."""
|
|
for armor_set in get_all_armor_sets():
|
|
if armor_set.name.lower() == name.lower() or armor_set.set_id.lower() == name.lower():
|
|
return armor_set
|
|
return None
|
|
|
|
|
|
def get_all_armor_pieces() -> List[ArmorPiece]:
|
|
"""Get all individual armor pieces from all sets."""
|
|
pieces = []
|
|
for armor_set in get_all_armor_sets():
|
|
pieces.extend(armor_set.pieces.values())
|
|
return pieces
|
|
|
|
|
|
def get_pieces_by_slot(slot: ArmorSlot) -> List[ArmorPiece]:
|
|
"""Get all armor pieces for a specific slot."""
|
|
return [p for p in get_all_armor_pieces() if p.slot == slot]
|
|
|
|
|
|
def get_pieces_by_set(set_name: str) -> List[ArmorPiece]:
|
|
"""Get all armor pieces from a specific set."""
|
|
return [p for p in get_all_armor_pieces() if p.set_name and p.set_name.lower() == set_name.lower()]
|
|
|
|
|
|
def create_mixed_armor(selections: Dict[ArmorSlot, str]) -> EquippedArmor:
|
|
"""
|
|
Create mixed armor from piece names.
|
|
|
|
Args:
|
|
selections: Dict mapping slots to piece names
|
|
|
|
Returns:
|
|
EquippedArmor with mixed pieces
|
|
"""
|
|
equipped = EquippedArmor()
|
|
all_pieces = {p.name.lower(): p for p in get_all_armor_pieces()}
|
|
|
|
for slot, piece_name in selections.items():
|
|
piece_key = piece_name.lower()
|
|
if piece_key in all_pieces:
|
|
# Create a copy to avoid modifying original
|
|
original = all_pieces[piece_key]
|
|
piece_copy = ArmorPiece(
|
|
name=original.name,
|
|
item_id=original.item_id,
|
|
slot=original.slot,
|
|
set_name=original.set_name,
|
|
decay_per_hit=original.decay_per_hit,
|
|
protection=ProtectionProfile(
|
|
stab=original.protection.stab,
|
|
cut=original.protection.cut,
|
|
impact=original.protection.impact,
|
|
penetration=original.protection.penetration,
|
|
shrapnel=original.protection.shrapnel,
|
|
burn=original.protection.burn,
|
|
cold=original.protection.cold,
|
|
acid=original.protection.acid,
|
|
electric=original.protection.electric,
|
|
),
|
|
durability=original.durability,
|
|
weight=original.weight,
|
|
)
|
|
equipped.equip_piece(piece_copy)
|
|
|
|
return equipped
|
|
|
|
|
|
def format_protection(profile: ProtectionProfile) -> str:
|
|
"""Format protection profile for display."""
|
|
parts = []
|
|
if profile.impact > 0:
|
|
parts.append(f"Imp:{profile.impact}")
|
|
if profile.cut > 0:
|
|
parts.append(f"Cut:{profile.cut}")
|
|
if profile.stab > 0:
|
|
parts.append(f"Stab:{profile.stab}")
|
|
if profile.penetration > 0:
|
|
parts.append(f"Pen:{profile.penetration}")
|
|
if profile.shrapnel > 0:
|
|
parts.append(f"Shr:{profile.shrapnel}")
|
|
if profile.burn > 0:
|
|
parts.append(f"Burn:{profile.burn}")
|
|
if profile.cold > 0:
|
|
parts.append(f"Cold:{profile.cold}")
|
|
if profile.acid > 0:
|
|
parts.append(f"Acid:{profile.acid}")
|
|
if profile.electric > 0:
|
|
parts.append(f"Elec:{profile.electric}")
|
|
return ", ".join(parts) if parts else "None"
|