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