docs: add official armor guide from PlanetCalypsoForum 2020 edition

- Loot 2.0 armor changes (June 2017)
- Correct decay mechanics (plate/armor independent)
- Armor progression guide (Gremlin → Ghost → Adj Nemesis)
- 20 hp/pec base economy standard
- Damage absorption flow and formulas
This commit is contained in:
LemonNexus 2026-02-09 10:21:57 +00:00
parent 5f01f2f763
commit 08aec368a9
5 changed files with 2003 additions and 36 deletions

1172
core/armor_system.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@ import re
import os
from pathlib import Path
from typing import Callable, List, Dict, Any, Optional
from dataclasses import dataclass
from dataclasses import dataclass, field
from datetime import datetime
from decimal import Decimal
import logging
@ -25,6 +25,79 @@ class LogEvent:
data: Dict[str, Any]
@dataclass
class LootItem:
"""Represents a single loot item."""
name: str
quantity: int
value_ped: Decimal
is_shrapnel: bool = False
is_universal_ammo: bool = False
@dataclass
class HuntingSessionStats:
"""Statistics for a hunting session tracked from chat.log."""
# Financial tracking
total_loot_ped: Decimal = Decimal('0.0')
total_shrapnel_ped: Decimal = Decimal('0.0')
total_universal_ammo_ped: Decimal = Decimal('0.0')
total_other_loot_ped: Decimal = Decimal('0.0') # Non-shrapnel, non-UA loot
# Cost tracking
weapon_cost_ped: Decimal = Decimal('0.0')
armor_cost_ped: Decimal = Decimal('0.0')
healing_cost_ped: Decimal = Decimal('0.0')
plates_cost_ped: Decimal = Decimal('0.0')
total_cost_ped: Decimal = Decimal('0.0')
# Combat tracking
damage_dealt: Decimal = Decimal('0.0')
damage_taken: Decimal = Decimal('0.0')
healing_done: Decimal = Decimal('0.0')
shots_fired: int = 0
kills: int = 0
# Special events
globals_count: int = 0
hofs_count: int = 0
personal_globals: List[Dict[str, Any]] = field(default_factory=list)
# Calculated metrics
@property
def net_profit_ped(self) -> Decimal:
"""Calculate net profit (excluding shrapnel from loot)."""
return self.total_other_loot_ped - self.total_cost_ped
@property
def return_percentage(self) -> Decimal:
"""Calculate return percentage (loot/cost * 100)."""
if self.total_cost_ped > 0:
return (self.total_other_loot_ped / self.total_cost_ped) * Decimal('100')
return Decimal('0.0')
@property
def cost_per_kill(self) -> Decimal:
"""Calculate cost per kill."""
if self.kills > 0:
return self.total_cost_ped / self.kills
return Decimal('0.0')
@property
def dpp(self) -> Decimal:
"""Calculate Damage Per PED (efficiency metric)."""
if self.total_cost_ped > 0:
return self.damage_dealt / self.total_cost_ped
return Decimal('0.0')
@property
def damage_per_kill(self) -> Decimal:
"""Calculate average damage per kill."""
if self.kills > 0:
return self.damage_dealt / self.kills
return Decimal('0.0')
class LogWatcher:
"""
Watches Entropia Universe chat.log and notifies observers of events.
@ -55,6 +128,35 @@ class LogWatcher:
r'Värde:\s+(\d+(?:\.\d+)?)\s+PED',
re.IGNORECASE
)
# LOOT PATTERN WITHOUT VALUE (some items don't show value)
# English: "You received Animal Thyroid Oil x 5"
PATTERN_LOOT_NO_VALUE_EN = re.compile(
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]:?\s*\[?\]?\s*'
r'You\s+received\s+\[?([\w\s\-()]+?)\]?\s+x\s*(\d+)',
re.IGNORECASE
)
PATTERN_LOOT_NO_VALUE_SV = re.compile(
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
r'Du\s+fick\s+([\w\s\-()]+?)\s+x\s*(\d+)',
re.IGNORECASE
)
# KILL PATTERNS - "You killed" messages (for accurate kill counting)
# English: "You killed [Creature Name]"
# Swedish: "Du dödade [Creature Name]"
PATTERN_KILL_EN = re.compile(
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]:?\s*\[?\]?\s*'
r'You\s+killed\s+\[?([\w\s\-()]+?)\]?',
re.IGNORECASE
)
PATTERN_KILL_SV = re.compile(
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
r'Du\s+dödade\s+\[?([\w\s\-()]+?)\]?',
re.IGNORECASE
)
# GLOBAL PATTERNS (Other players)
# English: "PlayerName globals in Zone for 150.00 PED"
@ -92,6 +194,14 @@ class LogWatcher:
# HALL OF FAME PATTERNS
# Swedish: "...En post har lagts till i Hall of Fame!"
# English: "[Hall of Fame] Player killed a creature (Creature) for X PED"
PATTERN_HOF_EN = re.compile(
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[Hall\s+of\s+Fame\]\s+\[?\]?\s*'
r'([\w\s]+?)\s+killed\s+a\s+creature\s+\(([^)]+)\)\s+'
r'(?:for|with\s+a\s+value\s+of)\s+(\d+(?:\.\d+)?)\s+PED',
re.IGNORECASE
)
PATTERN_HOF_MARKER = re.compile(
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[\w+\]\s+\[?\]?\s*'
r'.*?Hall\s+of\s+Fame',
@ -122,6 +232,12 @@ class LogWatcher:
r'You\s+have\s+advanced\s+to\s+level\s+(\d+)\s+in\s+([\w\s]+)',
re.IGNORECASE
)
PATTERN_LEVEL_UP_SV = re.compile(
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
r'Du\s+har\s+avancerat\s+till\s+nivå\s+(\d+)\s+i\s+([\w\s]+)',
re.IGNORECASE
)
# DAMAGE DEALT - Swedish & English
# Swedish: "Du orsakade 13.5 poäng skada"
@ -173,7 +289,7 @@ class LogWatcher:
# English: "You healed yourself 25.5 points"
PATTERN_HEAL_SV = re.compile(
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
r'Du\s+läkte\s+dig\s+jälv\s+(\d+(?:\.\d+)?)\s+poäng',
r'Du\s+läkte\s+dig\själv\s+(\d+(?:\.\d+)?)\s+poäng',
re.IGNORECASE
)
@ -188,7 +304,7 @@ class LogWatcher:
# English: "Your Piron PBP-17 (L) has reached tier 2.68"
PATTERN_WEAPON_TIER_SV = re.compile(
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
r'Din\s+([\w\s\-()]+?)\s+har\s+nått\s+nivå\s+(\d+(?:\.\d+)?)',
r'Din\s+([\w\s\-()]+?)\s+har\snått\s+nivå\s+(\d+(?:\.\d+)?)',
re.IGNORECASE
)
@ -200,24 +316,43 @@ class LogWatcher:
# COMBAT EVADE/DODGE/MISS
# English: "You Evaded", "The target Evaded your attack", "The attack missed you"
PATTERN_EVADE = re.compile(
# Swedish: "Du undvek", "Målet undvek din attack"
PATTERN_EVADE_EN = re.compile(
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
r'(You\s+Evaded|You\s+dodged|The\s+target\s+Evaded\s+your\s+attack|The\s+target\s+Dodged|The\s+attack\s+missed\s+you)',
re.IGNORECASE
)
PATTERN_EVADE_SV = re.compile(
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
r'(Du\s+undvek|Målet\s+undvek\s+din\s+attack|Attacken\s+missade\s+dig)',
re.IGNORECASE
)
# DECAY (when weapon durability decreases)
# English: "Your Omegaton M2100 has decayed 15 PEC"
# Swedish: "Din Piron PBP-17 (L) har nått minimalt skick"
PATTERN_DECAY = re.compile(
PATTERN_DECAY_EN = re.compile(
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
r'(?:Your|Din)\s+([\w\s\-()]+?)\s+(?:has\s+decayed|har\s+nått\s+minimalt\s+skick)',
r'Your\s+([\w\s\-()]+?)\s+has\s+decayed\s+(\d+(?:\.\d+)?)\s+PEC',
re.IGNORECASE
)
PATTERN_DECAY_SV = re.compile(
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
r'Din\s+([\w\s\-()]+?)\s+har\s+decayed\s+(\d+(?:\.\d+)?)\s+PEC',
re.IGNORECASE
)
PATTERN_WEAPON_BROKEN_SV = re.compile(
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
r'Din\s+([\w\s\-()]+?)\s+har\s+nått\s+minimalt\s+skick',
re.IGNORECASE
)
# BROKEN ENHANCERS
# English: "Your enhancer Weapon Damage Enhancer 1 on your Piron PBP-17 (L) broke"
PATTERN_ENHANCER_BROKEN = re.compile(
PATTERN_ENHANCER_BROKEN_EN = re.compile(
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
r'Your\s+enhancer\s+([\w\s]+?)\s+on\s+your\s+([\w\s\-()]+?)\s+broke',
re.IGNORECASE
@ -225,11 +360,17 @@ class LogWatcher:
# PED TRANSFER
# Swedish: "Överföring slutförd! 3.38000 PED har överförts till ditt PED-kort."
PATTERN_PED_TRANSFER = re.compile(
PATTERN_PED_TRANSFER_SV = re.compile(
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+'
r'Överföring\s+slutförd.*?((\d+(?:\.\d+)?))\s+PED',
re.IGNORECASE
)
PATTERN_PED_TRANSFER_EN = re.compile(
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+'
r'Transfer\s+complete.*?((\d+(?:\.\d+)?))\s+PED',
re.IGNORECASE
)
# ATTRIBUTE GAIN (Agility, etc)
# Swedish: "Din Agility har förbättrats med 0.0001"
@ -246,14 +387,33 @@ class LogWatcher:
re.IGNORECASE
)
# TEAM HUNT PATTERNS
# "You received 0.1234 PED from your teammates' activity."
PATTERN_TEAM_SHARE_EN = re.compile(
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
r'You\s+received\s+(\d+(?:\.\d+)?)\s+PED\s+from\s+your\s+teammates',
re.IGNORECASE
)
PATTERN_TEAM_SHARE_SV = re.compile(
r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*'
r'Du\s+fick\s+(\d+(?:\.\d+)?)\s+PED\s+från\s+dina\s+lagkamrater',
re.IGNORECASE
)
EVENT_PATTERNS = {
'loot_en': PATTERN_LOOT_EN,
'loot_sv': PATTERN_LOOT_SV,
'loot_no_value_en': PATTERN_LOOT_NO_VALUE_EN,
'loot_no_value_sv': PATTERN_LOOT_NO_VALUE_SV,
'kill_en': PATTERN_KILL_EN,
'kill_sv': PATTERN_KILL_SV,
'global_en': PATTERN_GLOBAL_EN,
'global_sv': PATTERN_GLOBAL_SV,
'personal_global_en': PATTERN_PERSONAL_GLOBAL_EN,
'personal_global_sv': PATTERN_PERSONAL_GLOBAL_SV,
'hof': PATTERN_HOF_MARKER,
'hof_en': PATTERN_HOF_EN,
'hof_marker': PATTERN_HOF_MARKER,
'skill_en': PATTERN_SKILL_EN,
'skill_sv': PATTERN_SKILL_SV,
'damage_dealt_sv': PATTERN_DAMAGE_DEALT_SV,
@ -266,12 +426,18 @@ class LogWatcher:
'heal_en': PATTERN_HEAL_EN,
'weapon_tier_sv': PATTERN_WEAPON_TIER_SV,
'weapon_tier_en': PATTERN_WEAPON_TIER_EN,
'evade': PATTERN_EVADE,
'decay': PATTERN_DECAY,
'enhancer_broken': PATTERN_ENHANCER_BROKEN,
'ped_transfer': PATTERN_PED_TRANSFER,
'evade_en': PATTERN_EVADE_EN,
'evade_sv': PATTERN_EVADE_SV,
'decay_en': PATTERN_DECAY_EN,
'decay_sv': PATTERN_DECAY_SV,
'weapon_broken_sv': PATTERN_WEAPON_BROKEN_SV,
'enhancer_broken_en': PATTERN_ENHANCER_BROKEN_EN,
'ped_transfer_sv': PATTERN_PED_TRANSFER_SV,
'ped_transfer_en': PATTERN_PED_TRANSFER_EN,
'attribute_sv': PATTERN_ATTRIBUTE_SV,
'attribute_en': PATTERN_ATTRIBUTE_EN,
'team_share_en': PATTERN_TEAM_SHARE_EN,
'team_share_sv': PATTERN_TEAM_SHARE_SV,
}
def __init__(self, log_path: Optional[str] = None,
@ -295,7 +461,7 @@ class LogWatcher:
'damage_dealt': [], 'damage_taken': [], 'heal': [],
'weapon_tier': [], 'evade': [], 'decay': [],
'critical_hit': [], 'ped_transfer': [], 'attribute': [],
'any': [],
'kill': [], 'team_share': [], 'any': [],
}
self._running = False
@ -360,6 +526,28 @@ class LogWatcher:
"""Parse EU timestamp format."""
return datetime.strptime(ts_str, "%Y-%m-%d %H:%M:%S")
def _is_shrapnel(self, item_name: str) -> bool:
"""Check if item is Shrapnel."""
return item_name.strip().lower() == 'shrapnel'
def _is_universal_ammo(self, item_name: str) -> bool:
"""Check if item is Universal Ammo."""
name = item_name.strip().lower()
return name == 'universal ammo' or name == 'universell ammunition'
def _categorize_loot(self, item_name: str, value_ped: Decimal) -> LootItem:
"""Categorize loot item and return LootItem."""
is_shrapnel = self._is_shrapnel(item_name)
is_ua = self._is_universal_ammo(item_name)
return LootItem(
name=item_name.strip(),
quantity=1, # Will be set by caller
value_ped=value_ped,
is_shrapnel=is_shrapnel,
is_universal_ammo=is_ua
)
def _parse_line(self, line: str) -> Optional[LogEvent]:
"""
Parse a single log line.
@ -369,7 +557,18 @@ class LogWatcher:
if not line:
return None
# Try each pattern
# Try each pattern in priority order
# KILL - Swedish
match = self.PATTERN_KILL_SV.match(line)
if match:
return self._create_kill_event(match, line, 'swedish')
# KILL - English
match = self.PATTERN_KILL_EN.match(line)
if match:
return self._create_kill_event(match, line, 'english')
# LOOT - Swedish (prioritize based on your game client)
match = self.PATTERN_LOOT_SV.match(line)
if match:
@ -379,6 +578,16 @@ class LogWatcher:
match = self.PATTERN_LOOT_EN.match(line)
if match:
return self._create_loot_event_en(match, line)
# LOOT WITHOUT VALUE - Swedish
match = self.PATTERN_LOOT_NO_VALUE_SV.match(line)
if match:
return self._create_loot_event_no_value(match, line, 'swedish')
# LOOT WITHOUT VALUE - English
match = self.PATTERN_LOOT_NO_VALUE_EN.match(line)
if match:
return self._create_loot_event_no_value(match, line, 'english')
# GLOBAL - Swedish
match = self.PATTERN_GLOBAL_SV.match(line)
@ -399,8 +608,13 @@ class LogWatcher:
match = self.PATTERN_PERSONAL_GLOBAL_EN.match(line)
if match:
return self._create_personal_global_event(match, line, 'english')
# HOF - English
match = self.PATTERN_HOF_EN.match(line)
if match:
return self._create_hof_event(match, line, 'english')
# HOF
# HOF Marker
match = self.PATTERN_HOF_MARKER.match(line)
if match:
return LogEvent(
@ -451,6 +665,20 @@ class LogWatcher:
'language': 'english'
}
)
# LEVEL UP - Swedish
match = self.PATTERN_LEVEL_UP_SV.match(line)
if match:
return LogEvent(
timestamp=self._parse_timestamp(match.group(1)),
event_type='level_up',
raw_line=line,
data={
'new_level': int(match.group(2)),
'skill_name': match.group(3).strip(),
'language': 'swedish'
}
)
# DAMAGE DEALT - Swedish
match = self.PATTERN_DAMAGE_DEALT_SV.match(line)
@ -560,28 +788,56 @@ class LogWatcher:
}
)
# EVADE/DODGE/MISS
match = self.PATTERN_EVADE.match(line)
# EVADE/DODGE/MISS - English
match = self.PATTERN_EVADE_EN.match(line)
if match:
return LogEvent(
timestamp=self._parse_timestamp(match.group(1)),
event_type='evade',
raw_line=line,
data={'type': match.group(2)}
data={'type': match.group(2), 'language': 'english'}
)
# EVADE/DODGE/MISS - Swedish
match = self.PATTERN_EVADE_SV.match(line)
if match:
return LogEvent(
timestamp=self._parse_timestamp(match.group(1)),
event_type='evade',
raw_line=line,
data={'type': match.group(2), 'language': 'swedish'}
)
# DECAY
match = self.PATTERN_DECAY.match(line)
# DECAY - English
match = self.PATTERN_DECAY_EN.match(line)
if match:
return LogEvent(
timestamp=self._parse_timestamp(match.group(1)),
event_type='decay',
raw_line=line,
data={'item': match.group(2).strip()}
data={
'item': match.group(2).strip(),
'amount_pec': Decimal(match.group(3)),
'language': 'english'
}
)
# DECAY - Swedish
match = self.PATTERN_DECAY_SV.match(line)
if match:
return LogEvent(
timestamp=self._parse_timestamp(match.group(1)),
event_type='decay',
raw_line=line,
data={
'item': match.group(2).strip(),
'amount_pec': Decimal(match.group(3)),
'language': 'swedish'
}
)
# BROKEN ENHANCER
match = self.PATTERN_ENHANCER_BROKEN.match(line)
# BROKEN ENHANCER - English
match = self.PATTERN_ENHANCER_BROKEN_EN.match(line)
if match:
return LogEvent(
timestamp=self._parse_timestamp(match.group(1)),
@ -589,7 +845,8 @@ class LogWatcher:
raw_line=line,
data={
'enhancer_type': match.group(2).strip(),
'weapon': match.group(3).strip()
'weapon': match.group(3).strip(),
'language': 'english'
}
)
@ -620,37 +877,114 @@ class LogWatcher:
'language': 'english'
}
)
# TEAM SHARE - English
match = self.PATTERN_TEAM_SHARE_EN.match(line)
if match:
return LogEvent(
timestamp=self._parse_timestamp(match.group(1)),
event_type='team_share',
raw_line=line,
data={
'amount_ped': Decimal(match.group(2)),
'language': 'english'
}
)
# TEAM SHARE - Swedish
match = self.PATTERN_TEAM_SHARE_SV.match(line)
if match:
return LogEvent(
timestamp=self._parse_timestamp(match.group(1)),
event_type='team_share',
raw_line=line,
data={
'amount_ped': Decimal(match.group(2)),
'language': 'swedish'
}
)
return None
def _create_loot_event_sv(self, match: re.Match, line: str) -> LogEvent:
"""Create loot event from Swedish pattern."""
item_name = match.group(2).strip()
quantity = int(match.group(3))
value_ped = Decimal(match.group(4))
loot_item = self._categorize_loot(item_name, value_ped)
loot_item.quantity = quantity
return LogEvent(
timestamp=self._parse_timestamp(match.group(1)),
event_type='loot',
raw_line=line,
data={
'item_name': match.group(2).strip(),
'quantity': int(match.group(3)),
'value_ped': Decimal(match.group(4)),
'item_name': loot_item.name,
'quantity': loot_item.quantity,
'value_ped': loot_item.value_ped,
'is_shrapnel': loot_item.is_shrapnel,
'is_universal_ammo': loot_item.is_universal_ammo,
'language': 'swedish'
}
)
def _create_loot_event_en(self, match: re.Match, line: str) -> LogEvent:
"""Create loot event from English pattern."""
value = match.group(4)
item_name = match.group(2).strip()
quantity = int(match.group(3))
value_ped = Decimal(match.group(4)) if match.group(4) else Decimal('0')
loot_item = self._categorize_loot(item_name, value_ped)
loot_item.quantity = quantity
return LogEvent(
timestamp=self._parse_timestamp(match.group(1)),
event_type='loot',
raw_line=line,
data={
'item_name': match.group(2).strip(),
'quantity': int(match.group(3)),
'value_ped': Decimal(value) if value else Decimal('0'),
'item_name': loot_item.name,
'quantity': loot_item.quantity,
'value_ped': loot_item.value_ped,
'is_shrapnel': loot_item.is_shrapnel,
'is_universal_ammo': loot_item.is_universal_ammo,
'language': 'english'
}
)
def _create_loot_event_no_value(self, match: re.Match, line: str, language: str) -> LogEvent:
"""Create loot event without value."""
item_name = match.group(2).strip()
quantity = int(match.group(3))
loot_item = self._categorize_loot(item_name, Decimal('0'))
loot_item.quantity = quantity
return LogEvent(
timestamp=self._parse_timestamp(match.group(1)),
event_type='loot',
raw_line=line,
data={
'item_name': loot_item.name,
'quantity': loot_item.quantity,
'value_ped': loot_item.value_ped,
'is_shrapnel': loot_item.is_shrapnel,
'is_universal_ammo': loot_item.is_universal_ammo,
'language': language
}
)
def _create_kill_event(self, match: re.Match, line: str, language: str) -> LogEvent:
"""Create kill event."""
return LogEvent(
timestamp=self._parse_timestamp(match.group(1)),
event_type='kill',
raw_line=line,
data={
'creature_name': match.group(2).strip(),
'language': language
}
)
def _create_global_event_sv(self, match: re.Match, line: str) -> LogEvent:
"""Create global event from Swedish pattern."""
@ -693,6 +1027,20 @@ class LogWatcher:
'language': language
}
)
def _create_hof_event(self, match: re.Match, line: str, language: str) -> LogEvent:
"""Create Hall of Fame event."""
return LogEvent(
timestamp=self._parse_timestamp(match.group(1)),
event_type='hof',
raw_line=line,
data={
'player_name': match.group(2).strip(),
'creature': match.group(3).strip(),
'value_ped': Decimal(match.group(4)),
'language': language
}
)
# ========================================================================
# ASYNC POLLING LOOP
@ -763,6 +1111,219 @@ class LogWatcher:
self._last_file_size = current_size
# ============================================================================
# HUNTING SESSION TRACKER
# ============================================================================
class HuntingSessionTracker:
"""
Tracks hunting session statistics from LogWatcher events.
This class accumulates all hunting-related data and provides
real-time metrics like profit/loss, return percentage, etc.
"""
def __init__(self):
self.stats = HuntingSessionStats()
self._session_start: Optional[datetime] = None
self._session_active = False
# Callbacks for real-time updates
self._on_stats_update: Optional[Callable] = None
def start_session(self):
"""Start a new hunting session."""
self._session_start = datetime.now()
self._session_active = True
self.stats = HuntingSessionStats()
logger.info("Hunting session started")
def end_session(self) -> HuntingSessionStats:
"""End the current hunting session and return final stats."""
self._session_active = False
logger.info("Hunting session ended")
return self.stats
def is_active(self) -> bool:
"""Check if session is active."""
return self._session_active
def set_stats_callback(self, callback: Callable[[HuntingSessionStats], None]):
"""Set callback for real-time stats updates."""
self._on_stats_update = callback
def _notify_update(self):
"""Notify listeners of stats update."""
if self._on_stats_update:
try:
self._on_stats_update(self.stats)
except Exception as e:
logger.error(f"Stats callback error: {e}")
def on_loot(self, event: LogEvent):
"""Process loot event."""
if not self._session_active:
return
data = event.data
value_ped = data.get('value_ped', Decimal('0.0'))
is_shrapnel = data.get('is_shrapnel', False)
is_ua = data.get('is_universal_ammo', False)
self.stats.total_loot_ped += value_ped
if is_shrapnel:
self.stats.total_shrapnel_ped += value_ped
elif is_ua:
self.stats.total_universal_ammo_ped += value_ped
else:
self.stats.total_other_loot_ped += value_ped
self._notify_update()
def on_kill(self, event: LogEvent):
"""Process kill event."""
if not self._session_active:
return
self.stats.kills += 1
self._notify_update()
def on_damage_dealt(self, event: LogEvent):
"""Process damage dealt event."""
if not self._session_active:
return
damage = event.data.get('damage', Decimal('0.0'))
self.stats.damage_dealt += damage
self.stats.shots_fired += 1 # Each damage event = 1 shot
self._notify_update()
def on_damage_taken(self, event: LogEvent):
"""Process damage taken event."""
if not self._session_active:
return
damage = event.data.get('damage', Decimal('0.0'))
self.stats.damage_taken += damage
self._notify_update()
def on_heal(self, event: LogEvent):
"""Process heal event."""
if not self._session_active:
return
heal_amount = event.data.get('heal_amount', Decimal('0.0'))
self.stats.healing_done += heal_amount
self._notify_update()
def on_global(self, event: LogEvent):
"""Process global event."""
if not self._session_active:
return
self.stats.globals_count += 1
# Store personal global details
if event.event_type == 'personal_global':
self.stats.personal_globals.append({
'timestamp': event.timestamp,
'creature': event.data.get('creature', 'Unknown'),
'value_ped': event.data.get('value_ped', Decimal('0.0'))
})
self._notify_update()
def on_hof(self, event: LogEvent):
"""Process Hall of Fame event."""
if not self._session_active:
return
self.stats.hofs_count += 1
# Store HoF details
if 'creature' in event.data:
self.stats.personal_globals.append({
'timestamp': event.timestamp,
'creature': event.data.get('creature', 'Unknown'),
'value_ped': event.data.get('value_ped', Decimal('0.0')),
'is_hof': True
})
self._notify_update()
def on_decay(self, event: LogEvent):
"""Process decay event."""
if not self._session_active:
return
# Convert PEC to PED
amount_pec = event.data.get('amount_pec', Decimal('0.0'))
amount_ped = amount_pec / Decimal('100')
self.stats.weapon_cost_ped += amount_ped
self.stats.total_cost_ped += amount_ped
self._notify_update()
def add_weapon_cost(self, cost_ped: Decimal):
"""Manually add weapon cost (for calculated decay)."""
if not self._session_active:
return
self.stats.weapon_cost_ped += cost_ped
self.stats.total_cost_ped += cost_ped
self._notify_update()
def add_armor_cost(self, cost_ped: Decimal):
"""Manually add armor cost."""
if not self._session_active:
return
self.stats.armor_cost_ped += cost_ped
self.stats.total_cost_ped += cost_ped
self._notify_update()
def add_healing_cost(self, cost_ped: Decimal):
"""Manually add healing cost."""
if not self._session_active:
return
self.stats.healing_cost_ped += cost_ped
self.stats.total_cost_ped += cost_ped
self._notify_update()
def get_stats(self) -> HuntingSessionStats:
"""Get current stats."""
return self.stats
def get_summary(self) -> Dict[str, Any]:
"""Get session summary as dictionary."""
return {
'session_active': self._session_active,
'session_start': self._session_start.isoformat() if self._session_start else None,
'total_loot_ped': float(self.stats.total_loot_ped),
'total_shrapnel_ped': float(self.stats.total_shrapnel_ped),
'total_universal_ammo_ped': float(self.stats.total_universal_ammo_ped),
'total_other_loot_ped': float(self.stats.total_other_loot_ped),
'total_cost_ped': float(self.stats.total_cost_ped),
'weapon_cost_ped': float(self.stats.weapon_cost_ped),
'armor_cost_ped': float(self.stats.armor_cost_ped),
'healing_cost_ped': float(self.stats.healing_cost_ped),
'net_profit_ped': float(self.stats.net_profit_ped),
'return_percentage': float(self.stats.return_percentage),
'cost_per_kill': float(self.stats.cost_per_kill),
'dpp': float(self.stats.dpp),
'damage_dealt': float(self.stats.damage_dealt),
'damage_taken': float(self.stats.damage_taken),
'healing_done': float(self.stats.healing_done),
'shots_fired': self.stats.shots_fired,
'kills': self.stats.kills,
'globals_count': self.stats.globals_count,
'hofs_count': self.stats.hofs_count,
'damage_per_kill': float(self.stats.damage_per_kill),
}
# ============================================================================
# MOCK MODE SUPPORT
# ============================================================================
@ -774,14 +1335,22 @@ class MockLogGenerator:
"2026-02-08 14:23:15 [System] You received Shrapnel x 123 (Value: 1.23 PED)",
"2026-02-08 14:23:45 [System] You gained 0.45 experience in your Rifle skill",
"2026-02-08 14:24:02 [System] Your Omegaton M2100 has decayed 15 PEC",
"2026-02-08 14:25:30 [System] PlayerOne globals in Twin Peaks for 150.00 PED",
"2026-02-08 14:26:10 [System] You received Animal Thyroid Oil x 5",
"2026-02-08 14:25:30 [Globals] PlayerOne globals in Twin Peaks for 150.00 PED",
"2026-02-08 14:26:10 [System] You received Animal Thyroid Oil x 5 (Value: 2.50 PED)",
"2026-02-08 14:26:15 [System] You killed Araneatrox Young",
"2026-02-08 14:27:55 [System] Congratulations! You have advanced to level 45 in Rifle",
"2026-02-08 14:30:00 [System] PlayerTwo is in the Hall of Fame! Loot of 2500.00 PED",
"2026-02-08 14:28:30 [Globals] You killed a creature (Cornundacauda) with a value of 75.00 PED",
"2026-02-08 14:30:00 [Hall of Fame] PlayerTwo killed a creature (Atrox) for 2500.00 PED",
"2026-02-08 14:31:15 [System] You received Universal Ammo x 50 (Value: 0.50 PED)",
"2026-02-08 14:32:20 [System] You inflicted 45.5 points of damage",
"2026-02-08 14:32:25 [System] You took 12.3 points of damage",
"2026-02-08 14:33:00 [System] You healed yourself 25.0 points",
"2026-02-08 14:34:10 [System] Critical hit - Additional damage! You inflicted 89.2 points of damage",
# Swedish examples
"2025-09-23 19:36:43 [System] Du fick Shrapnel x (4627) Värde: 0.4627 PED",
"2025-09-23 19:36:08 [System] Du har fått 0.3238 erfarenhet i din Translocation färdighet",
"2025-09-23 19:36:18 [System] Du orsakade 13.5 poäng skada",
"2025-09-23 19:37:00 [System] Du dödade Araneatrox Young",
]
@classmethod
@ -804,5 +1373,8 @@ class MockLogGenerator:
__all__ = [
'LogWatcher',
'LogEvent',
'LootItem',
'HuntingSessionStats',
'HuntingSessionTracker',
'MockLogGenerator'
]

176
docs/ARMOR_GUIDE_2020.md Normal file
View File

@ -0,0 +1,176 @@
# Entropia Universe Armor Mechanics - Official Guide
**Source:** "A Most Complete Guide to Armors (2020 Edition)" - PlanetCalypsoForum
**Thread:** https://www.planetcalypsoforum.com/forum/index.php?threads/a-most-complete-guide-to-armors-2020-edition.270884/
**Date:** 2026-02-09
---
## Loot 2.0 Armor Changes (VU 15.15, June 2017)
MindArk made significant changes to how armors work:
### Key Changes:
1. **Armor decays significantly less** per point of damage absorbed
2. **No minimum decay** based on total protection - always proportional to damage absorbed
3. **Armor absorbs ALL incoming damage** - no more 1.0 damage taken on full absorption (shown as "Deflected" message)
4. **Armor and platings decay independently** based on actual damage each absorbs, not both decaying as if absorbing full damage
5. **Armor decay is now linear** per point of damage absorbed (not increasing cost per damage)
---
## Armor Economy (Durability)
### Base Protection Cost:
**20 hp / pec** - This is the standard armor durability baseline.
### Comparison with Healing Tools:
| Protection Method | Economy |
|-------------------|---------|
| Armor (base) | ~20 hp/pec |
| EMT Kit Ek-2600 Improved | 20 hp/pec |
| Refurbished H.E.A.R.T Rank VI | 18 hp/pec |
| Vivo S10 | 12.31 hp/pec |
| Herb Box | 10.06 hp/pec |
| EMT Kit Ek-2350 | 4 hp/pec |
**Conclusion:** Armor is very economical compared to most healing tools.
---
## Armor Progression Guide
### 1. First Armor: Graduation Set
- Get by graduating with a Mentor
- Each planet has unique graduation armor
- **Best choice:** Atlas (NI) or Musca Adjusted (Arkadia)
### 2. Early Game: Before Ghost
**Add plates first!** Get 5B plates for your graduation armor.
Then choose:
- **Gremlin** - Most versatile armor
- **Nemesis** - Upgradeable to Adjusted Nemesis (robot armor)
- **Ghost** - Most protection for the money (recommended)
### 3. Mid Game: After Ghost+5B
Options (price jump!):
- **Adjusted Nemesis** (~2.5k PED) - Good for robots
- **Lion** (~5-6.5k PED) - Robot Beacon drops
- **Adjusted Boar** (~7.5-9.5k PED) - 30 Impact, Dodge buff
- **Looted (L) pieces** - Cheap bridge option
### 4. End Game
- Upgraded Angel
- Upgraded Gorgon
- Modified Viceroy (avatar bound, ~9k PED)
---
## Damage Absorption Mechanics (Post-Loot 2.0)
### How Protection Works:
1. **Plate takes damage first** (if equipped)
2. **Armor takes remaining damage**
3. **Player takes overflow** (if any)
### Decay Calculation:
```
Plate decay = (damage_absorbed_by_plate / total_plate_protection) × plate_decay_rate
Armor decay = (damage_absorbed_by_armor / total_armor_protection) × armor_decay_rate
```
**Key Point:** Plate and armor decay **independently** based on actual damage each absorbs.
### Example:
- Monster hits: 20 Impact damage
- Plate protects: 15 Impact
- Armor protects: 5 Impact (remaining)
- Player takes: 0 damage (deflected)
**Decay:**
- Plate decays for absorbing 15 damage
- Armor decays for absorbing 5 damage
---
## Armor Buffs
Many armor sets offer buffs:
- **20% increased Dodge chance**
- **10-20% increased Evade chance**
- **3% faster reload speed**
- **5% Acceleration**
- **1-4% Block chance**
- Heal over time
**Block Chance:** Some upgraded plates have Block Chance - when triggered, hit is nullified (no damage, no decay).
---
## Armor Sets Reference
### Entry Level:
- **Gremlin** - Most versatile early armor
- **Ghost** - Best protection for price
- **Nemesis** - Robot hunting, upgradeable
### Mid Level:
- **Adjusted Nemesis** - Robot specialist
- **Lion** - Stab-focused protection
- **Adjusted Boar** - 30 Impact, Dodge buff
### High Level:
- **Angel** (upgraded)
- **Gorgon** (upgraded)
- **Modified Viceroy** (avatar bound)
---
## Plating Strategy
### General Rule:
- **5B plates** are the standard early-game addition
- Match plates to mob's damage type
- All 7 plates should match (don't mix types)
### Plate Types:
- **5B** - General purpose (Impact/Stab/Cut)
- **6A** - Cold/Electric protection
- **5C** - Burn/Acid protection
- Custom plates for specific mobs
---
## Implementation Notes for Lemontropia Suite
1. **Decay Formula:** Linear per damage point absorbed
2. **Plate Priority:** Plates absorb first, then armor
3. **Independent Decay:** Each piece decays separately
4. **Deflected Message:** Indicates full absorption (0 player damage)
5. **Block Chance:** Roll for block before applying damage
6. **Protection Stacking:** Plate + Armor protection values add
---
## Key Formulas
### Cost Per Hit:
```
armor_cost = (damage_absorbed_by_armor / armor_protection) × armor_decay_per_pec
plate_cost = (damage_absorbed_by_plate / plate_protection) × plate_decay_per_pec
total_cost = armor_cost + plate_cost
```
### Protection Calculation:
```
total_protection = armor_protection + plate_protection
if total_protection >= damage:
player_damage = 0
absorbed_by_plate = min(damage, plate_protection)
absorbed_by_armor = damage - absorbed_by_plate
else:
player_damage = damage - total_protection
absorbed_by_plate = plate_protection
absorbed_by_armor = armor_protection
```

View File

@ -13,7 +13,7 @@ from PyQt6.QtCore import Qt, pyqtSignal, QThread
from decimal import Decimal
from typing import Optional, List
from core.nexus_api import EntropiaNexusAPI, WeaponStats, ArmorStats, FinderStats
from core.nexus_api import EntropiaNexusAPI, WeaponStats, ArmorStats, FinderStats, MedicalTool
class WeaponLoaderThread(QThread):
@ -61,6 +61,21 @@ class FinderLoaderThread(QThread):
self.error_occurred.emit(str(e))
class MedicalToolLoaderThread(QThread):
"""Thread to load medical tools (FAPs) from API."""
medical_tools_loaded = pyqtSignal(list)
error_occurred = pyqtSignal(str)
def run(self):
try:
api = EntropiaNexusAPI()
tools = api.get_all_medical_tools()
self.medical_tools_loaded.emit(tools)
except Exception as e:
self.error_occurred.emit(str(e))
class GearSelectorDialog(QDialog):
"""Dialog for selecting gear from Entropia Nexus."""

View File

@ -258,6 +258,9 @@ class MainWindow(QMainWindow):
self._selected_armor_stats: Optional[dict] = None
self._selected_finder: Optional[str] = None
self._selected_finder_stats: Optional[dict] = None
self._selected_medical_tool: Optional[str] = None
self._selected_medical_tool_stats: Optional[dict] = None
self._selected_loadout: Optional[Any] = None
# Setup UI
self.setup_ui()
@ -948,6 +951,34 @@ class MainWindow(QMainWindow):
from core.project_manager import LootEvent
from decimal import Decimal
def on_heal(event):
"""Handle heal events from chat.log.
Pattern: "You healed yourself X points"
Calculates healing cost based on FAP decay and updates HUD.
"""
heal_amount = event.data.get('heal_amount', Decimal('0'))
# Calculate heal cost based on selected medical tool decay
# Get decay per heal from loadout or use default
decay_cost = Decimal('0')
if self._selected_loadout and hasattr(self._selected_loadout, 'heal_cost_pec'):
# heal_cost_pec is the decay per heal in PEC
# Convert to PED for cost calculation
decay_cost = self._selected_loadout.heal_cost_pec / Decimal('100')
elif self._selected_medical_tool_stats and 'decay' in self._selected_medical_tool_stats:
decay_pec = Decimal(str(self._selected_medical_tool_stats['decay']))
decay_cost = decay_pec / Decimal('100')
else:
# Default estimate: 2 PEC per heal
decay_cost = Decimal('0.02')
# Update HUD with heal event
self.hud.on_heal_event(heal_amount, decay_cost)
# Log to UI
self.log_info("Heal", f"Healed {heal_amount} HP (Cost: {decay_cost:.4f} PED)")
def on_loot(event):
"""Handle loot events."""
item_name = event.data.get('item_name', 'Unknown')
@ -1059,6 +1090,7 @@ class MainWindow(QMainWindow):
self.log_watcher.subscribe('critical_hit', on_critical_hit)
self.log_watcher.subscribe('damage_taken', on_damage_taken)
self.log_watcher.subscribe('evade', on_evade)
self.log_watcher.subscribe('heal', on_heal) # NEW: Heal event tracking
def _start_log_watcher(self):
"""Start LogWatcher in background thread."""