diff --git a/core/armor_system.py b/core/armor_system.py index cf97cb5..aed6fdb 100644 --- a/core/armor_system.py +++ b/core/armor_system.py @@ -479,15 +479,25 @@ class EquippedArmor: return total def get_total_decay_per_hit(self) -> Decimal: - """Get total decay per hit (armor + plates).""" + """ + 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(): - total += piece.decay_per_hit + armor_absorb = min(typical_hit, piece.protection.get_total()) + total += piece.get_decay_for_damage(armor_absorb) + if piece.attached_plate: - total += piece.attached_plate.decay_per_hit + 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]: @@ -561,13 +571,23 @@ def calculate_hit_protection( hit_location: Optional[ArmorSlot] = None ) -> HitResult: """ - Calculate damage absorption for a hit. + Calculate damage absorption for a hit using Loot 2.0 mechanics. - In Entropia Universe: - 1. If hit_location specified, only that slot's protection applies - 2. Plates absorb damage FIRST (shield layer) - 3. Armor absorbs remaining damage - 4. Decay is calculated based on damage absorbed + 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 @@ -583,6 +603,9 @@ def calculate_hit_protection( 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 @@ -592,25 +615,36 @@ def calculate_hit_protection( result.damage_to_avatar = incoming_damage return result - plate_prot = piece.attached_plate.protection.get_effective_against(damage_type) if piece.attached_plate else Decimal("0") + # 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 + # 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 + # 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 + # 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_hit(plate_absorb) + result.plate_decay = piece.attached_plate.get_decay_for_damage(plate_absorb) if armor_absorb > 0: - result.armor_decay = piece.get_decay_for_hit(armor_absorb) + result.armor_decay = piece.get_decay_for_damage(armor_absorb) result.total_decay = result.plate_decay + result.armor_decay @@ -628,7 +662,7 @@ def calculate_hit_protection( 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.protection.get_effective_against(damage_type) + total_plate_prot += piece.attached_plate.get_effective_protection(damage_type) # Plate absorbs FIRST plate_absorb = min(total_plate_prot, incoming_damage) @@ -640,25 +674,26 @@ def calculate_hit_protection( result.armor_absorbed = armor_absorb result.damage_to_avatar = remaining - armor_absorb - # Distribute decay across all pieces that contributed - piece_count = len(pieces) - if piece_count > 0: - plate_decay_per = result.plate_decay / Decimal(piece_count) if plate_absorb > 0 else Decimal("0") - armor_decay_per = result.armor_decay / Decimal(piece_count) if armor_absorb > 0 else Decimal("0") - - # Sum actual decay from all pieces with plates/armor - total_plate_decay = Decimal("0") - total_armor_decay = Decimal("0") - + # 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 and plate_absorb > 0: - total_plate_decay += piece.attached_plate.get_decay_for_hit(plate_absorb / piece_count) - if armor_absorb > 0: - total_armor_decay += piece.get_decay_for_hit(armor_absorb / piece_count) - - result.plate_decay = total_plate_decay - result.armor_decay = total_armor_decay - result.total_decay = total_plate_decay + total_armor_decay + 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 @@ -669,13 +704,16 @@ 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") + pieces = { ArmorSlot.HEAD: ArmorPiece( name="Ghost Helmet", item_id="ghost_helmet", slot=ArmorSlot.HEAD, set_name="Ghost", - decay_per_hit=Decimal("0.015"), + decay_per_hp=ghost_economy, protection=ProtectionProfile(impact=Decimal("2"), cut=Decimal("2"), stab=Decimal("2"), burn=Decimal("5"), cold=Decimal("5")), weight=Decimal("0.3"), ), @@ -684,7 +722,7 @@ def create_ghost_set() -> ArmorSet: item_id="ghost_harness", slot=ArmorSlot.CHEST, set_name="Ghost", - decay_per_hit=Decimal("0.035"), + decay_per_hp=ghost_economy, protection=ProtectionProfile(impact=Decimal("4"), cut=Decimal("4"), stab=Decimal("4"), burn=Decimal("8"), cold=Decimal("8")), weight=Decimal("0.7"), ), @@ -693,7 +731,7 @@ def create_ghost_set() -> ArmorSet: item_id="ghost_arm_l", slot=ArmorSlot.LEFT_ARM, set_name="Ghost", - decay_per_hit=Decimal("0.015"), + decay_per_hp=ghost_economy, protection=ProtectionProfile(impact=Decimal("2"), cut=Decimal("2"), stab=Decimal("2"), burn=Decimal("5"), cold=Decimal("5")), weight=Decimal("0.3"), ), @@ -702,7 +740,7 @@ def create_ghost_set() -> ArmorSet: item_id="ghost_arm_r", slot=ArmorSlot.RIGHT_ARM, set_name="Ghost", - decay_per_hit=Decimal("0.015"), + decay_per_hp=ghost_economy, protection=ProtectionProfile(impact=Decimal("2"), cut=Decimal("2"), stab=Decimal("2"), burn=Decimal("5"), cold=Decimal("5")), weight=Decimal("0.3"), ), @@ -711,7 +749,7 @@ def create_ghost_set() -> ArmorSet: item_id="ghost_gloves_l", slot=ArmorSlot.LEFT_HAND, set_name="Ghost", - decay_per_hit=Decimal("0.010"), + decay_per_hp=ghost_economy, protection=ProtectionProfile(impact=Decimal("1"), cut=Decimal("1"), stab=Decimal("1"), burn=Decimal("3"), cold=Decimal("3")), weight=Decimal("0.2"), ), @@ -720,7 +758,7 @@ def create_ghost_set() -> ArmorSet: item_id="ghost_gloves_r", slot=ArmorSlot.RIGHT_HAND, set_name="Ghost", - decay_per_hit=Decimal("0.010"), + decay_per_hp=ghost_economy, protection=ProtectionProfile(impact=Decimal("1"), cut=Decimal("1"), stab=Decimal("1"), burn=Decimal("3"), cold=Decimal("3")), weight=Decimal("0.2"), ), @@ -729,7 +767,7 @@ def create_ghost_set() -> ArmorSet: item_id="ghost_legs", slot=ArmorSlot.LEGS, set_name="Ghost", - decay_per_hit=Decimal("0.030"), + decay_per_hp=ghost_economy, protection=ProtectionProfile(impact=Decimal("3"), cut=Decimal("3"), stab=Decimal("3"), burn=Decimal("7"), cold=Decimal("7")), weight=Decimal("0.6"), ), diff --git a/core/healing_tools.py b/core/healing_tools.py new file mode 100644 index 0000000..f5d81c0 --- /dev/null +++ b/core/healing_tools.py @@ -0,0 +1,153 @@ +""" +Healing Tools Database for Lemontropia Suite +Medical tools, Restoration Chips, and FAPs with decay data. +""" + +from decimal import Decimal +from dataclasses import dataclass +from typing import Optional, Dict, List + + +@dataclass +class HealingTool: + """Represents a healing tool in Entropia Universe.""" + name: str + item_id: str + heal_amount: Decimal # HP healed per use + decay_pec: Decimal # Decay in PEC per heal + professional_level: int = 0 # Required profession level (0 = no requirement) + is_chip: bool = False # True if it's a restoration chip + + @property + def economy(self) -> Decimal: + """Calculate economy in hp/pec (higher is better).""" + if self.decay_pec > 0: + return self.heal_amount / self.decay_pec + return Decimal('0') + + @property + def cost_per_heal_ped(self) -> Decimal: + """Calculate cost per heal in PED.""" + return self.decay_pec / Decimal('100') + + +# Healing Tools Database +# Data from EntropiaWiki and community research +# Format: name, item_id, heal_amount, decay_pec, prof_level, is_chip + +HEALING_TOOLS: List[HealingTool] = [ + # === VIVO SERIES (Entry Level) === + HealingTool("Vivo T10", "vivo_t10", Decimal("10"), Decimal("0.815"), 0, False), + HealingTool("Vivo T15", "vivo_t15", Decimal("15"), Decimal("1.19"), 0, False), + HealingTool("Vivo S10", "vivo_s10", Decimal("21"), Decimal("1.705"), 0, False), + HealingTool("Vivo S15", "vivo_s15", Decimal("27"), Decimal("2.155"), 0, False), + + # === HE DOC SERIES (Mid Level) === + HealingTool("Hedoc MM10", "hedoc_mm10", Decimal("44"), Decimal("2.09"), 0, False), + HealingTool("Hedoc MM20", "hedoc_mm20", Decimal("52"), Decimal("2.48"), 0, False), + HealingTool("Hedoc MM30", "hedoc_mm30", Decimal("64"), Decimal("3.04"), 0, False), + HealingTool("Hedoc MK50", "hedoc_mk50", Decimal("75"), Decimal("3.55"), 0, False), + HealingTool("Hedoc SK80", "hedoc_sk80", Decimal("120"), Decimal("5.65"), 0, False), + + # === EMT KIT SERIES === + HealingTool("EMT Kit Ek-2350", "emt_2350", Decimal("35"), Decimal("8.75"), 0, False), # Low eco + HealingTool("EMT Kit Ek-2600", "emt_2600", Decimal("52"), Decimal("2.60"), 0, False), # 20 hp/pec + HealingTool("EMT Kit Ek-2600 Improved", "emt_2600_imp", Decimal("52"), Decimal("2.60"), 0, False), + HealingTool("EMT Kit Ek-2350 Adjusted", "emt_2350_adj", Decimal("52"), Decimal("5.20"), 0, False), + + # === RESTORATION CHIPS (Mindforce) === + # Requires Biotropic profession level + HealingTool("Restoration Chip I", "resto_1", Decimal("15"), Decimal("1.2"), 1, True), + HealingTool("Restoration Chip II", "resto_2", Decimal("25"), Decimal("1.9"), 2, True), + HealingTool("Restoration Chip III", "resto_3", Decimal("35"), Decimal("2.6"), 3, True), + HealingTool("Restoration Chip IV", "resto_4", Decimal("45"), Decimal("3.3"), 4, True), + HealingTool("Restoration Chip V", "resto_5", Decimal("55"), Decimal("4.0"), 5, True), + HealingTool("Restoration Chip VI", "resto_6", Decimal("65"), Decimal("4.7"), 6, True), + HealingTool("Restoration Chip VII", "resto_7", Decimal("75"), Decimal("5.4"), 7, True), + HealingTool("Restoration Chip VIII", "resto_8", Decimal("85"), Decimal("6.1"), 8, True), + HealingTool("Restoration Chip IX", "resto_9", Decimal("95"), Decimal("6.8"), 9, True), + HealingTool("Restoration Chip X", "resto_10", Decimal("110"), Decimal("7.8"), 10, True), + + # Adjusted Restoration Chip (Popular mid-level) + HealingTool("Adjusted Restoration Chip", "resto_adj", Decimal("60"), Decimal("2.88"), 5, True), # ~20.8 hp/pec + + # === SPECIAL/UNIQUE TOOLS === + HealingTool("Refurbished H.E.A.R.T. Rank VI", "heart_vi", Decimal("108"), Decimal("6.0"), 0, False), # 18 hp/pec + HealingTool("Herb Box", "herb_box", Decimal("19"), Decimal("1.89"), 0, False), # ~10 hp/pec + HealingTool("Omegaton Fast Aid Pack", "fap_omega", Decimal("24"), Decimal("1.20"), 0, False), +] + + +def get_healing_tool(name: str) -> Optional[HealingTool]: + """Get a healing tool by name.""" + for tool in HEALING_TOOLS: + if tool.name.lower() == name.lower(): + return tool + return None + + +def get_tools_by_economy(min_economy: Decimal = Decimal("0")) -> List[HealingTool]: + """Get healing tools sorted by economy (best first).""" + tools = [t for t in HEALING_TOOLS if t.economy >= min_economy] + return sorted(tools, key=lambda x: x.economy, reverse=True) + + +def get_tools_by_heal_amount(min_heal: Decimal = Decimal("0")) -> List[HealingTool]: + """Get healing tools sorted by heal amount (highest first).""" + tools = [t for t in HEALING_TOOLS if t.heal_amount >= min_heal] + return sorted(tools, key=lambda x: x.heal_amount, reverse=True) + + +def compare_healing_tools(tool_names: List[str]) -> List[tuple]: + """Compare multiple healing tools. + + Returns list of tuples: (name, heal_amount, decay_pec, economy, cost_ped) + """ + results = [] + for name in tool_names: + tool = get_healing_tool(name) + if tool: + results.append(( + tool.name, + tool.heal_amount, + tool.decay_pec, + tool.economy, + tool.cost_per_heal_ped + )) + # Sort by economy (best first) + results.sort(key=lambda x: x[3], reverse=True) + return results + + +# Popular tool recommendations by level +RECOMMENDED_TOOLS: Dict[str, str] = { + "starter": "Vivo S10", # Everyone can use, decent economy + "mid_level": "Adjusted Restoration Chip", # Popular mid-level choice + "high_level": "Hedoc SK80", # High heal amount + "best_economy": "EMT Kit Ek-2600", # 20 hp/pec like armor +} + + +if __name__ == "__main__": + # Print comparison of popular tools + print("Healing Tools Comparison:") + print("-" * 80) + print(f"{'Tool':<35} {'Heal':<8} {'Decay':<8} {'Eco':<8} {'Cost/PED':<12}") + print("-" * 80) + + tools_to_compare = [ + "Vivo S10", + "Hedoc MM10", + "Adjusted Restoration Chip", + "EMT Kit Ek-2600", + "Refurbished H.E.A.R.T. Rank VI" + ] + + for name, heal, decay, eco, cost in compare_healing_tools(tools_to_compare): + print(f"{name:<35} {heal:<8} {decay:<8.2f} {eco:<8.2f} {cost:<12.4f}") + + print("\n\nRecommended by Level:") + for level, tool_name in RECOMMENDED_TOOLS.items(): + tool = get_healing_tool(tool_name) + if tool: + print(f"{level:<15} → {tool_name} ({tool.economy:.2f} hp/pec)") \ No newline at end of file