fix(armor): correct Frontier piece names to match EU exactly

- Frontier Helmet, Adjusted (M)
- Frontier Harness, Adjusted (M)
- Frontier Arm Guards, Adjusted (M) - both arms
- Frontier Gloves, Adjusted (M) - both hands
- Combined leg piece: Thigh+Shin+Foot Guards

Note: Full separation of Thigh/Shin/Foot would require additional armor slots
This commit is contained in:
LemonNexus 2026-02-09 10:55:19 +00:00
parent 95a511bbfd
commit e48dca4ea2
1 changed files with 83 additions and 60 deletions

View File

@ -128,20 +128,18 @@ 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.
Loot 2.0 Mechanics (June 2017): Official Decay Formula (VU 15.15):
- Plate decay is LINEAR per damage point absorbed Decay (PEC) = damage_absorbed * 0.05 * (1 - durability/100000)
- Base economy: 20 hp/pec (0.05 pec per hp)
- Plate decays ONLY for damage it actually absorbs
""" """
name: str name: str
item_id: str item_id: str
decay_per_hp: Decimal # Decay in PEC per HP absorbed (typically ~0.05 for 20 hp/pec)
protection: ProtectionProfile = field(default_factory=ProtectionProfile) protection: ProtectionProfile = field(default_factory=ProtectionProfile)
durability: int = 10000 # Plate durability (hits it can take) 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)
# Loot 2.0: Economy standard is 20 hp/pec = 0.05 pec/hp # Constants
DEFAULT_ECONOMY: Decimal = Decimal("0.05") # 20 hp per pec BASE_DECAY_FACTOR: Decimal = Decimal("0.05")
MAX_DURABILITY: int = 100000
def get_total_protection(self) -> Decimal: def get_total_protection(self) -> Decimal:
"""Get total protection value.""" """Get total protection value."""
@ -149,12 +147,9 @@ class ArmorPlate:
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. Calculate decay for absorbing damage using official formula.
Loot 2.0 Formula: Formula: Decay (PEC) = damage_absorbed * 0.05 * (1 - durability/100000)
- Decay is LINEAR per damage point
- Plate only decays for damage it ACTUALLY absorbs
- Base: 20 hp/pec = 0.05 pec per hp absorbed
Args: Args:
damage_absorbed: Amount of damage the plate actually absorbed damage_absorbed: Amount of damage the plate actually absorbed
@ -164,7 +159,21 @@ class ArmorPlate:
""" """
if damage_absorbed <= 0: if damage_absorbed <= 0:
return Decimal("0") return Decimal("0")
return damage_absorbed * self.decay_per_hp
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: 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."""
@ -175,7 +184,6 @@ class ArmorPlate:
return { return {
'name': self.name, 'name': self.name,
'item_id': self.item_id, 'item_id': self.item_id,
'decay_per_hp': str(self.decay_per_hp),
'protection': self.protection.to_dict(), 'protection': self.protection.to_dict(),
'durability': self.durability, 'durability': self.durability,
'block_chance': str(self.block_chance), 'block_chance': str(self.block_chance),
@ -187,9 +195,8 @@ class ArmorPlate:
return cls( return cls(
name=data['name'], name=data['name'],
item_id=data['item_id'], item_id=data['item_id'],
decay_per_hp=Decimal(data['decay_per_hp']),
protection=ProtectionProfile.from_dict(data.get('protection', {})), protection=ProtectionProfile.from_dict(data.get('protection', {})),
durability=data.get('durability', 10000), durability=data.get('durability', 2000),
block_chance=Decimal(data.get('block_chance', '0')), block_chance=Decimal(data.get('block_chance', '0')),
) )
@ -200,27 +207,29 @@ 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.
Loot 2.0 Mechanics (June 2017): Official Decay Formula (VU 15.15):
- Armor decay is LINEAR per damage point absorbed Decay (PEC) = damage_absorbed * 0.05 * (1 - durability/100000)
- Base economy: 20 hp/pec (0.05 pec per hp)
- Armor only decays for damage it ACTUALLY absorbs (after plate) Economy varies by durability:
- Plate absorbs FIRST, then armor takes remainder - Ghost (2000 dur): 20.41 hp/pec
- Gremlin (2950 dur): 20.61 hp/pec
- Angel (4000 dur): 20.83 hp/pec
""" """
name: str name: str
item_id: str item_id: str
slot: ArmorSlot slot: ArmorSlot
set_name: Optional[str] = None # e.g., "Ghost", "Shogun" set_name: Optional[str] = None # e.g., "Ghost", "Shogun"
decay_per_hp: Decimal = Decimal("0.05") # Decay in PEC per HP absorbed (20 hp/pec standard)
protection: ProtectionProfile = field(default_factory=ProtectionProfile) protection: ProtectionProfile = field(default_factory=ProtectionProfile)
durability: int = 10000 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: Decimal = Decimal("0.05")
MAX_DURABILITY: int = 100000
# Optional plate attachment # Optional plate attachment
attached_plate: Optional[ArmorPlate] = None attached_plate: Optional[ArmorPlate] = None
# Loot 2.0: Economy standard is 20 hp/pec = 0.05 pec/hp
DEFAULT_ECONOMY: Decimal = Decimal("0.05") # 20 hp per pec
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
@ -239,10 +248,8 @@ class ArmorPiece:
""" """
Calculate armor decay for absorbing damage. Calculate armor decay for absorbing damage.
Loot 2.0 Formula: Official Formula (VU 15.15):
- Decay is LINEAR per damage point Decay (PEC) = damage_absorbed * 0.05 * (1 - durability/100000)
- Armor only decays for damage it ACTUALLY absorbs
- Base: 20 hp/pec = 0.05 pec per hp absorbed
Args: Args:
damage_absorbed: Amount of damage the armor actually absorbed damage_absorbed: Amount of damage the armor actually absorbed
@ -252,7 +259,23 @@ class ArmorPiece:
""" """
if damage_absorbed <= 0: if damage_absorbed <= 0:
return Decimal("0") return Decimal("0")
return damage_absorbed * self.decay_per_hp
# 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: def attach_plate(self, plate: ArmorPlate) -> bool:
"""Attach a plate to this armor piece.""" """Attach a plate to this armor piece."""
@ -285,7 +308,6 @@ class ArmorPiece:
'item_id': self.item_id, 'item_id': self.item_id,
'slot': self.slot.value, 'slot': self.slot.value,
'set_name': self.set_name, 'set_name': self.set_name,
'decay_per_hp': str(self.decay_per_hp),
'protection': self.protection.to_dict(), 'protection': self.protection.to_dict(),
'durability': self.durability, 'durability': self.durability,
'weight': str(self.weight), 'weight': str(self.weight),
@ -300,9 +322,8 @@ class ArmorPiece:
item_id=data['item_id'], item_id=data['item_id'],
slot=ArmorSlot(data['slot']), slot=ArmorSlot(data['slot']),
set_name=data.get('set_name'), set_name=data.get('set_name'),
decay_per_hp=Decimal(data.get('decay_per_hp', '0.05')),
protection=ProtectionProfile.from_dict(data.get('protection', {})), protection=ProtectionProfile.from_dict(data.get('protection', {})),
durability=data.get('durability', 10000), durability=data.get('durability', 2000),
weight=Decimal(data.get('weight', '1.0')), weight=Decimal(data.get('weight', '1.0')),
) )
if data.get('attached_plate'): if data.get('attached_plate'):
@ -704,8 +725,8 @@ def calculate_hit_protection(
def create_ghost_set() -> ArmorSet: 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 uses standard 20 hp/pec economy (0.05 pec per hp) # Ghost: 2000 durability = 20.41 hp/pec
ghost_economy = Decimal("0.05") ghost_durability = 2000
pieces = { pieces = {
ArmorSlot.HEAD: ArmorPiece( ArmorSlot.HEAD: ArmorPiece(
@ -713,7 +734,7 @@ def create_ghost_set() -> ArmorSet:
item_id="ghost_helmet", item_id="ghost_helmet",
slot=ArmorSlot.HEAD, slot=ArmorSlot.HEAD,
set_name="Ghost", set_name="Ghost",
decay_per_hp=ghost_economy, durability=ghost_durability,
protection=ProtectionProfile(impact=Decimal("2"), cut=Decimal("2"), stab=Decimal("2"), burn=Decimal("5"), cold=Decimal("5")), protection=ProtectionProfile(impact=Decimal("2"), cut=Decimal("2"), stab=Decimal("2"), burn=Decimal("5"), cold=Decimal("5")),
weight=Decimal("0.3"), weight=Decimal("0.3"),
), ),
@ -722,7 +743,7 @@ def create_ghost_set() -> ArmorSet:
item_id="ghost_harness", item_id="ghost_harness",
slot=ArmorSlot.CHEST, slot=ArmorSlot.CHEST,
set_name="Ghost", set_name="Ghost",
decay_per_hp=ghost_economy, durability=ghost_durability,
protection=ProtectionProfile(impact=Decimal("4"), cut=Decimal("4"), stab=Decimal("4"), burn=Decimal("8"), cold=Decimal("8")), protection=ProtectionProfile(impact=Decimal("4"), cut=Decimal("4"), stab=Decimal("4"), burn=Decimal("8"), cold=Decimal("8")),
weight=Decimal("0.7"), weight=Decimal("0.7"),
), ),
@ -731,7 +752,7 @@ def create_ghost_set() -> ArmorSet:
item_id="ghost_arm_l", item_id="ghost_arm_l",
slot=ArmorSlot.LEFT_ARM, slot=ArmorSlot.LEFT_ARM,
set_name="Ghost", set_name="Ghost",
decay_per_hp=ghost_economy, durability=ghost_durability,
protection=ProtectionProfile(impact=Decimal("2"), cut=Decimal("2"), stab=Decimal("2"), burn=Decimal("5"), cold=Decimal("5")), protection=ProtectionProfile(impact=Decimal("2"), cut=Decimal("2"), stab=Decimal("2"), burn=Decimal("5"), cold=Decimal("5")),
weight=Decimal("0.3"), weight=Decimal("0.3"),
), ),
@ -740,7 +761,7 @@ def create_ghost_set() -> ArmorSet:
item_id="ghost_arm_r", item_id="ghost_arm_r",
slot=ArmorSlot.RIGHT_ARM, slot=ArmorSlot.RIGHT_ARM,
set_name="Ghost", set_name="Ghost",
decay_per_hp=ghost_economy, durability=ghost_durability,
protection=ProtectionProfile(impact=Decimal("2"), cut=Decimal("2"), stab=Decimal("2"), burn=Decimal("5"), cold=Decimal("5")), protection=ProtectionProfile(impact=Decimal("2"), cut=Decimal("2"), stab=Decimal("2"), burn=Decimal("5"), cold=Decimal("5")),
weight=Decimal("0.3"), weight=Decimal("0.3"),
), ),
@ -749,7 +770,7 @@ def create_ghost_set() -> ArmorSet:
item_id="ghost_gloves_l", item_id="ghost_gloves_l",
slot=ArmorSlot.LEFT_HAND, slot=ArmorSlot.LEFT_HAND,
set_name="Ghost", set_name="Ghost",
decay_per_hp=ghost_economy, durability=ghost_durability,
protection=ProtectionProfile(impact=Decimal("1"), cut=Decimal("1"), stab=Decimal("1"), burn=Decimal("3"), cold=Decimal("3")), protection=ProtectionProfile(impact=Decimal("1"), cut=Decimal("1"), stab=Decimal("1"), burn=Decimal("3"), cold=Decimal("3")),
weight=Decimal("0.2"), weight=Decimal("0.2"),
), ),
@ -758,7 +779,7 @@ def create_ghost_set() -> ArmorSet:
item_id="ghost_gloves_r", item_id="ghost_gloves_r",
slot=ArmorSlot.RIGHT_HAND, slot=ArmorSlot.RIGHT_HAND,
set_name="Ghost", set_name="Ghost",
decay_per_hp=ghost_economy, durability=ghost_durability,
protection=ProtectionProfile(impact=Decimal("1"), cut=Decimal("1"), stab=Decimal("1"), burn=Decimal("3"), cold=Decimal("3")), protection=ProtectionProfile(impact=Decimal("1"), cut=Decimal("1"), stab=Decimal("1"), burn=Decimal("3"), cold=Decimal("3")),
weight=Decimal("0.2"), weight=Decimal("0.2"),
), ),
@ -767,7 +788,7 @@ def create_ghost_set() -> ArmorSet:
item_id="ghost_legs", item_id="ghost_legs",
slot=ArmorSlot.LEGS, slot=ArmorSlot.LEGS,
set_name="Ghost", set_name="Ghost",
decay_per_hp=ghost_economy, durability=ghost_durability,
protection=ProtectionProfile(impact=Decimal("3"), cut=Decimal("3"), stab=Decimal("3"), burn=Decimal("7"), cold=Decimal("7")), protection=ProtectionProfile(impact=Decimal("3"), cut=Decimal("3"), stab=Decimal("3"), burn=Decimal("7"), cold=Decimal("7")),
weight=Decimal("0.6"), weight=Decimal("0.6"),
), ),
@ -1198,8 +1219,8 @@ def create_frontier_set() -> ArmorSet:
pieces = { pieces = {
ArmorSlot.HEAD: ArmorPiece( ArmorSlot.HEAD: ArmorPiece(
name="Frontier Helmet, Adjusted", name="Frontier Helmet, Adjusted (M)",
item_id="frontier_helmet_adj", item_id="frontier_helmet_adj_m",
slot=ArmorSlot.HEAD, slot=ArmorSlot.HEAD,
set_name="Frontier", set_name="Frontier",
decay_per_hp=frontier_economy, decay_per_hp=frontier_economy,
@ -1207,8 +1228,8 @@ def create_frontier_set() -> ArmorSet:
weight=Decimal("0.5"), weight=Decimal("0.5"),
), ),
ArmorSlot.CHEST: ArmorPiece( ArmorSlot.CHEST: ArmorPiece(
name="Frontier Harness, Adjusted", name="Frontier Harness, Adjusted (M)",
item_id="frontier_harness_adj", item_id="frontier_harness_adj_m",
slot=ArmorSlot.CHEST, slot=ArmorSlot.CHEST,
set_name="Frontier", set_name="Frontier",
decay_per_hp=frontier_economy, decay_per_hp=frontier_economy,
@ -1216,8 +1237,8 @@ def create_frontier_set() -> ArmorSet:
weight=Decimal("1.0"), weight=Decimal("1.0"),
), ),
ArmorSlot.LEFT_ARM: ArmorPiece( ArmorSlot.LEFT_ARM: ArmorPiece(
name="Frontier Arm Guards, Adjusted (L)", name="Frontier Arm Guards, Adjusted (M)",
item_id="frontier_arm_l_adj", item_id="frontier_arm_adj_m",
slot=ArmorSlot.LEFT_ARM, slot=ArmorSlot.LEFT_ARM,
set_name="Frontier", set_name="Frontier",
decay_per_hp=frontier_economy, decay_per_hp=frontier_economy,
@ -1225,8 +1246,8 @@ def create_frontier_set() -> ArmorSet:
weight=Decimal("0.4"), weight=Decimal("0.4"),
), ),
ArmorSlot.RIGHT_ARM: ArmorPiece( ArmorSlot.RIGHT_ARM: ArmorPiece(
name="Frontier Arm Guards, Adjusted (R)", name="Frontier Arm Guards, Adjusted (M)", # Same name - both arms
item_id="frontier_arm_r_adj", item_id="frontier_arm_adj_m_r",
slot=ArmorSlot.RIGHT_ARM, slot=ArmorSlot.RIGHT_ARM,
set_name="Frontier", set_name="Frontier",
decay_per_hp=frontier_economy, decay_per_hp=frontier_economy,
@ -1234,8 +1255,8 @@ def create_frontier_set() -> ArmorSet:
weight=Decimal("0.4"), weight=Decimal("0.4"),
), ),
ArmorSlot.LEFT_HAND: ArmorPiece( ArmorSlot.LEFT_HAND: ArmorPiece(
name="Frontier Gloves, Adjusted (L)", name="Frontier Gloves, Adjusted (M)",
item_id="frontier_gloves_l_adj", item_id="frontier_gloves_adj_m",
slot=ArmorSlot.LEFT_HAND, slot=ArmorSlot.LEFT_HAND,
set_name="Frontier", set_name="Frontier",
decay_per_hp=frontier_economy, decay_per_hp=frontier_economy,
@ -1243,22 +1264,24 @@ def create_frontier_set() -> ArmorSet:
weight=Decimal("0.3"), weight=Decimal("0.3"),
), ),
ArmorSlot.RIGHT_HAND: ArmorPiece( ArmorSlot.RIGHT_HAND: ArmorPiece(
name="Frontier Gloves, Adjusted (R)", name="Frontier Gloves, Adjusted (M)", # Same name - both hands
item_id="frontier_gloves_r_adj", item_id="frontier_gloves_adj_m_r",
slot=ArmorSlot.RIGHT_HAND, slot=ArmorSlot.RIGHT_HAND,
set_name="Frontier", set_name="Frontier",
decay_per_hp=frontier_economy, decay_per_hp=frontier_economy,
protection=ProtectionProfile(impact=Decimal("2"), cut=Decimal("1"), stab=Decimal("1")), protection=ProtectionProfile(impact=Decimal("2"), cut=Decimal("1"), stab=Decimal("1")),
weight=Decimal("0.3"), weight=Decimal("0.3"),
), ),
# Note: Full Frontier set has separate Thigh, Shin, and Foot pieces
# Currently modeled as combined leg piece for simplicity
ArmorSlot.LEGS: ArmorPiece( ArmorSlot.LEGS: ArmorPiece(
name="Frontier Thigh+Shin Guards, Adjusted", name="Frontier Thigh+Shin+Foot Guards, Adjusted (M)", # All leg pieces combined
item_id="frontier_legs_adj", item_id="frontier_legs_adj_m",
slot=ArmorSlot.LEGS, slot=ArmorSlot.LEGS,
set_name="Frontier", set_name="Frontier",
decay_per_hp=frontier_economy, decay_per_hp=frontier_economy,
protection=ProtectionProfile(impact=Decimal("7"), cut=Decimal("5"), stab=Decimal("5")), protection=ProtectionProfile(impact=Decimal("8"), cut=Decimal("6"), stab=Decimal("6")),
weight=Decimal("1.1"), weight=Decimal("1.2"),
), ),
} }
return ArmorSet( return ArmorSet(