841 lines
26 KiB
Python
841 lines
26 KiB
Python
"""
|
|
Entropia Nexus API Client
|
|
|
|
Provides async access to weapon, armor, and tool statistics from entropianexus.com.
|
|
Includes mock data for offline testing and development.
|
|
|
|
API Documentation: https://api.entropianexus.com/docs/
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
from dataclasses import dataclass, field
|
|
from decimal import Decimal
|
|
from typing import Dict, List, Optional, Any
|
|
from functools import wraps
|
|
import time
|
|
|
|
try:
|
|
import aiohttp
|
|
HAS_AIOHTTP = True
|
|
except ImportError:
|
|
HAS_AIOHTTP = False
|
|
|
|
try:
|
|
import urllib.request
|
|
import urllib.error
|
|
import urllib.parse
|
|
HAS_URLLIB = True
|
|
except ImportError:
|
|
HAS_URLLIB = False
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# =============================================================================
|
|
# Data Classes
|
|
# =============================================================================
|
|
|
|
@dataclass
|
|
class WeaponStats:
|
|
"""Weapon statistics from Entropia Nexus."""
|
|
name: str
|
|
damage: Decimal
|
|
decay_pec: Decimal
|
|
ammo_pec: Decimal
|
|
dpp: Decimal # Damage Per PEC (calculated)
|
|
range: int = 0
|
|
attacks_per_min: int = 0
|
|
total_cost_pec: Decimal = field(default=None)
|
|
markup_percent: Decimal = Decimal("100.0")
|
|
item_id: str = ""
|
|
|
|
def __post_init__(self):
|
|
if self.total_cost_pec is None:
|
|
self.total_cost_pec = self.decay_pec + self.ammo_pec
|
|
# Ensure dpp is calculated if not provided
|
|
if self.dpp == Decimal("0") and self.total_cost_pec > 0:
|
|
self.dpp = self.damage / (self.total_cost_pec / 100) # Convert PEC to PED
|
|
|
|
def calculate_cost_per_hour(self) -> Decimal:
|
|
"""Calculate total cost per hour of use in PED."""
|
|
if self.attacks_per_min <= 0:
|
|
return Decimal("0")
|
|
shots_per_hour = self.attacks_per_min * 60
|
|
return (self.total_cost_pec * shots_per_hour) / 100 # Convert PEC to PED
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Convert to dictionary for serialization."""
|
|
return {
|
|
'name': self.name,
|
|
'damage': str(self.damage),
|
|
'decay_pec': str(self.decay_pec),
|
|
'ammo_pec': str(self.ammo_pec),
|
|
'dpp': str(self.dpp),
|
|
'range': self.range,
|
|
'attacks_per_min': self.attacks_per_min,
|
|
'total_cost_pec': str(self.total_cost_pec),
|
|
'markup_percent': str(self.markup_percent),
|
|
'item_id': self.item_id,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> 'WeaponStats':
|
|
"""Create from dictionary."""
|
|
return cls(
|
|
name=data['name'],
|
|
damage=Decimal(data.get('damage', 0)),
|
|
decay_pec=Decimal(data.get('decay_pec', 0)),
|
|
ammo_pec=Decimal(data.get('ammo_pec', 0)),
|
|
dpp=Decimal(data.get('dpp', 0)),
|
|
range=data.get('range', 0),
|
|
attacks_per_min=data.get('attacks_per_min', 0),
|
|
total_cost_pec=Decimal(data.get('total_cost_pec', data.get('decay_pec', 0))) if 'total_cost_pec' in data else None,
|
|
markup_percent=Decimal(data.get('markup_percent', 100)),
|
|
item_id=data.get('item_id', ''),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class ArmorStats:
|
|
"""Armor piece statistics from Entropia Nexus."""
|
|
name: str
|
|
decay_pec: Decimal
|
|
protection: Dict[str, Decimal]
|
|
slot: str = "body" # head, body, arms, legs, feet
|
|
durability: int = 10000
|
|
item_id: str = ""
|
|
|
|
def get_total_protection(self) -> Decimal:
|
|
"""Get sum of all protection values."""
|
|
return sum(self.protection.values(), Decimal("0"))
|
|
|
|
def get_protection(self, damage_type: str) -> Decimal:
|
|
"""Get protection value for specific damage type."""
|
|
return self.protection.get(damage_type, Decimal("0"))
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Convert to dictionary for serialization."""
|
|
return {
|
|
'name': self.name,
|
|
'decay_pec': str(self.decay_pec),
|
|
'protection': {k: str(v) for k, v in self.protection.items()},
|
|
'slot': self.slot,
|
|
'durability': self.durability,
|
|
'item_id': self.item_id,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> 'ArmorStats':
|
|
"""Create from dictionary."""
|
|
return cls(
|
|
name=data['name'],
|
|
decay_pec=Decimal(data.get('decay_pec', 0)),
|
|
protection={k: Decimal(v) for k, v in data.get('protection', {}).items()},
|
|
slot=data.get('slot', 'body'),
|
|
durability=data.get('durability', 10000),
|
|
item_id=data.get('item_id', ''),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class ToolStats:
|
|
"""Mining tool statistics from Entropia Nexus."""
|
|
name: str
|
|
depth: Decimal
|
|
radius: Decimal
|
|
decay_pec: Decimal
|
|
tool_type: str = "finder" # finder or extractor
|
|
probe_cost: Decimal = Decimal("0.5") # PED per drop
|
|
item_id: str = ""
|
|
|
|
def calculate_cost_per_drop(self) -> Decimal:
|
|
"""Calculate total cost per mining drop in PED."""
|
|
return (self.decay_pec / 100) + self.probe_cost # PEC to PED + probe
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Convert to dictionary for serialization."""
|
|
return {
|
|
'name': self.name,
|
|
'depth': str(self.depth),
|
|
'radius': str(self.radius),
|
|
'decay_pec': str(self.decay_pec),
|
|
'tool_type': self.tool_type,
|
|
'probe_cost': str(self.probe_cost),
|
|
'item_id': self.item_id,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> 'ToolStats':
|
|
"""Create from dictionary."""
|
|
return cls(
|
|
name=data['name'],
|
|
depth=Decimal(data.get('depth', 0)),
|
|
radius=Decimal(data.get('radius', 0)),
|
|
decay_pec=Decimal(data.get('decay_pec', 0)),
|
|
tool_type=data.get('tool_type', 'finder'),
|
|
probe_cost=Decimal(data.get('probe_cost', 0.5)),
|
|
item_id=data.get('item_id', ''),
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Mock Data
|
|
# =============================================================================
|
|
|
|
MOCK_WEAPONS = {
|
|
"sollomate_opalo": WeaponStats(
|
|
name="Sollomate Opalo",
|
|
damage=Decimal("4.0"),
|
|
decay_pec=Decimal("0.13"),
|
|
ammo_pec=Decimal("1.07"),
|
|
dpp=Decimal("3.70"),
|
|
range=26,
|
|
attacks_per_min=56,
|
|
item_id="sollomate_opalo"
|
|
),
|
|
"omegaton_m2100": WeaponStats(
|
|
name="Omegaton M2100",
|
|
damage=Decimal("5.0"),
|
|
decay_pec=Decimal("0.15"),
|
|
ammo_pec=Decimal("1.35"),
|
|
dpp=Decimal("3.33"),
|
|
range=28,
|
|
attacks_per_min=54,
|
|
item_id="omegaton_m2100"
|
|
),
|
|
"breer_m1a": WeaponStats(
|
|
name="Breer M1a",
|
|
damage=Decimal("6.0"),
|
|
decay_pec=Decimal("0.18"),
|
|
ammo_pec=Decimal("1.62"),
|
|
dpp=Decimal("3.33"),
|
|
range=30,
|
|
attacks_per_min=52,
|
|
item_id="breer_m1a"
|
|
),
|
|
"castorian_enforcer_se": WeaponStats(
|
|
name="Castorian Enforcer SE",
|
|
damage=Decimal("14.0"),
|
|
decay_pec=Decimal("1.42"),
|
|
ammo_pec=Decimal("6.08"),
|
|
dpp=Decimal("1.87"),
|
|
range=26,
|
|
attacks_per_min=44,
|
|
item_id="castorian_enforcer_se"
|
|
),
|
|
"isis_lr1": WeaponStats(
|
|
name="ISIS LR1",
|
|
damage=Decimal("8.0"),
|
|
decay_pec=Decimal("0.32"),
|
|
ammo_pec=Decimal("2.88"),
|
|
dpp=Decimal("2.50"),
|
|
range=60,
|
|
attacks_per_min=38,
|
|
item_id="isis_lr1"
|
|
),
|
|
"sollomate_ony": WeaponStats(
|
|
name="Sollomate Ony",
|
|
damage=Decimal("12.0"),
|
|
decay_pec=Decimal("0.89"),
|
|
ammo_pec=Decimal("5.11"),
|
|
dpp=Decimal("2.00"),
|
|
range=24,
|
|
attacks_per_min=48,
|
|
item_id="sollomate_ony"
|
|
),
|
|
}
|
|
|
|
MOCK_ARMORS = {
|
|
"pixie": ArmorStats(
|
|
name="Pixie",
|
|
decay_pec=Decimal("0.135"),
|
|
protection={
|
|
"impact": Decimal("3.0"),
|
|
"cut": Decimal("3.0"),
|
|
"stab": Decimal("3.0"),
|
|
"burn": Decimal("2.0"),
|
|
"cold": Decimal("1.0"),
|
|
},
|
|
slot="body",
|
|
durability=2800,
|
|
item_id="pixie"
|
|
),
|
|
"shogun": ArmorStats(
|
|
name="Shogun",
|
|
decay_pec=Decimal("0.60"),
|
|
protection={
|
|
"impact": Decimal("10.0"),
|
|
"cut": Decimal("10.0"),
|
|
"stab": Decimal("10.0"),
|
|
"burn": Decimal("6.0"),
|
|
"cold": Decimal("6.0"),
|
|
"acid": Decimal("4.0"),
|
|
},
|
|
slot="body",
|
|
durability=4200,
|
|
item_id="shogun"
|
|
),
|
|
"ghost": ArmorStats(
|
|
name="Ghost",
|
|
decay_pec=Decimal("0.62"),
|
|
protection={
|
|
"impact": Decimal("12.0"),
|
|
"cut": Decimal("12.0"),
|
|
"stab": Decimal("12.0"),
|
|
"burn": Decimal("8.0"),
|
|
"cold": Decimal("8.0"),
|
|
"acid": Decimal("6.0"),
|
|
},
|
|
slot="body",
|
|
durability=4800,
|
|
item_id="ghost"
|
|
),
|
|
"vigilante": ArmorStats(
|
|
name="Vigilante",
|
|
decay_pec=Decimal("0.45"),
|
|
protection={
|
|
"impact": Decimal("8.0"),
|
|
"cut": Decimal("8.0"),
|
|
"stab": Decimal("8.0"),
|
|
"burn": Decimal("5.0"),
|
|
"cold": Decimal("5.0"),
|
|
},
|
|
slot="body",
|
|
durability=3600,
|
|
item_id="vigilante"
|
|
),
|
|
"necconu": ArmorStats(
|
|
name="Necconu",
|
|
decay_pec=Decimal("0.38"),
|
|
protection={
|
|
"impact": Decimal("7.0"),
|
|
"cut": Decimal("7.0"),
|
|
"stab": Decimal("7.0"),
|
|
"burn": Decimal("4.0"),
|
|
"cold": Decimal("4.0"),
|
|
"electric": Decimal("3.0"),
|
|
},
|
|
slot="body",
|
|
durability=3200,
|
|
item_id="necconu"
|
|
),
|
|
}
|
|
|
|
MOCK_TOOLS = {
|
|
"ziplex_z1": ToolStats(
|
|
name="Ziplex Z1 Seeker",
|
|
depth=Decimal("219.5"),
|
|
radius=Decimal("22.0"),
|
|
decay_pec=Decimal("0.20"),
|
|
tool_type="finder",
|
|
probe_cost=Decimal("0.5"),
|
|
item_id="ziplex_z1"
|
|
),
|
|
"vrtx_1000": ToolStats(
|
|
name="VRTX 1000",
|
|
depth=Decimal("250.0"),
|
|
radius=Decimal("25.0"),
|
|
decay_pec=Decimal("0.35"),
|
|
tool_type="finder",
|
|
probe_cost=Decimal("0.5"),
|
|
item_id="vrtx_1000"
|
|
),
|
|
"ziplex_p15": ToolStats(
|
|
name="Ziplex P15",
|
|
depth=Decimal("310.0"),
|
|
radius=Decimal("30.0"),
|
|
decay_pec=Decimal("0.50"),
|
|
tool_type="finder",
|
|
probe_cost=Decimal("0.5"),
|
|
item_id="ziplex_p15"
|
|
),
|
|
"ore_extractor_md1": ToolStats(
|
|
name="Ore Extractor MD-1",
|
|
depth=Decimal("0"),
|
|
radius=Decimal("0"),
|
|
decay_pec=Decimal("0.15"),
|
|
tool_type="extractor",
|
|
item_id="ore_extractor_md1"
|
|
),
|
|
}
|
|
|
|
|
|
# =============================================================================
|
|
# Cache Implementation
|
|
# =============================================================================
|
|
|
|
class SimpleCache:
|
|
"""Simple in-memory cache with TTL support."""
|
|
|
|
def __init__(self, default_ttl: int = 3600):
|
|
self._cache: Dict[str, Any] = {}
|
|
self._timestamps: Dict[str, float] = {}
|
|
self._default_ttl = default_ttl
|
|
|
|
def get(self, key: str) -> Optional[Any]:
|
|
"""Get cached value if not expired."""
|
|
if key not in self._cache:
|
|
return None
|
|
|
|
timestamp = self._timestamps.get(key, 0)
|
|
if time.time() - timestamp > self._default_ttl:
|
|
self.delete(key)
|
|
return None
|
|
|
|
return self._cache[key]
|
|
|
|
def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None:
|
|
"""Cache a value with optional TTL."""
|
|
self._cache[key] = value
|
|
self._timestamps[key] = time.time()
|
|
|
|
def delete(self, key: str) -> None:
|
|
"""Delete a cached value."""
|
|
self._cache.pop(key, None)
|
|
self._timestamps.pop(key, None)
|
|
|
|
def clear(self) -> None:
|
|
"""Clear all cached values."""
|
|
self._cache.clear()
|
|
self._timestamps.clear()
|
|
|
|
def keys(self) -> List[str]:
|
|
"""Get all cache keys."""
|
|
return list(self._cache.keys())
|
|
|
|
|
|
# =============================================================================
|
|
# API Client
|
|
# =============================================================================
|
|
|
|
class EntropiaNexusAPI:
|
|
"""
|
|
Async client for Entropia Nexus API.
|
|
|
|
Provides access to weapon, armor, and tool statistics with:
|
|
- Automatic caching of results
|
|
- Mock data fallback for offline testing
|
|
- Graceful error handling
|
|
"""
|
|
|
|
BASE_URL = "https://api.entropianexus.com/v1"
|
|
|
|
def __init__(self, api_key: Optional[str] = None, use_mock_fallback: bool = True,
|
|
cache_ttl: int = 3600, mock_mode: bool = False):
|
|
"""
|
|
Initialize API client.
|
|
|
|
Args:
|
|
api_key: Optional API key for higher rate limits
|
|
use_mock_fallback: Whether to fall back to mock data on API failure
|
|
cache_ttl: Cache time-to-live in seconds (default: 1 hour)
|
|
mock_mode: If True, always use mock data (no API calls)
|
|
"""
|
|
self.api_key = api_key
|
|
self.use_mock_fallback = use_mock_fallback
|
|
self.mock_mode = mock_mode
|
|
self._cache = SimpleCache(default_ttl=cache_ttl)
|
|
self._session: Optional[Any] = None
|
|
|
|
logger.info(f"EntropiaNexusAPI initialized (mock_mode={mock_mode})")
|
|
|
|
async def _get_session(self) -> Optional[Any]:
|
|
"""Get or create aiohttp session."""
|
|
if not HAS_AIOHTTP:
|
|
return None
|
|
|
|
if self._session is None or self._session.closed:
|
|
self._session = aiohttp.ClientSession(
|
|
headers={
|
|
'Accept': 'application/json',
|
|
'User-Agent': 'Lemontropia-Suite/0.1.0'
|
|
},
|
|
timeout=aiohttp.ClientTimeout(total=10)
|
|
)
|
|
return self._session
|
|
|
|
async def _make_request(self, endpoint: str, params: Optional[Dict] = None) -> Optional[Dict]:
|
|
"""
|
|
Make async API request to Entropia Nexus.
|
|
|
|
Args:
|
|
endpoint: API endpoint path (without base URL)
|
|
params: Query parameters
|
|
|
|
Returns:
|
|
JSON response as dict, or None on error
|
|
"""
|
|
if self.mock_mode:
|
|
return None
|
|
|
|
url = f"{self.BASE_URL}/{endpoint}"
|
|
|
|
try:
|
|
session = await self._get_session()
|
|
if session is None:
|
|
logger.warning("aiohttp not available, falling back to sync request")
|
|
return self._make_sync_request(endpoint, params)
|
|
|
|
async with session.get(url, params=params) as response:
|
|
if response.status == 200:
|
|
return await response.json()
|
|
else:
|
|
logger.warning(f"API returned status {response.status}")
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.error(f"API request failed: {e}")
|
|
return None
|
|
|
|
def _make_sync_request(self, endpoint: str, params: Optional[Dict] = None) -> Optional[Dict]:
|
|
"""Synchronous fallback request using urllib."""
|
|
if not HAS_URLLIB or self.mock_mode:
|
|
return None
|
|
|
|
url = f"{self.BASE_URL}/{endpoint}"
|
|
|
|
if params:
|
|
query_string = '&'.join(f"{k}={urllib.parse.quote(str(v))}" for k, v in params.items())
|
|
url = f"{url}?{query_string}"
|
|
|
|
headers = {
|
|
'Accept': 'application/json',
|
|
'User-Agent': 'Lemontropia-Suite/0.1.0'
|
|
}
|
|
|
|
if self.api_key:
|
|
headers['Authorization'] = f'Bearer {self.api_key}'
|
|
|
|
try:
|
|
req = urllib.request.Request(url, headers=headers)
|
|
with urllib.request.urlopen(req, timeout=10) as response:
|
|
return json.loads(response.read().decode('utf-8'))
|
|
except Exception as e:
|
|
logger.error(f"Sync API request failed: {e}")
|
|
return None
|
|
|
|
async def search_items(self, query: str, item_type: Optional[str] = None) -> List[Dict]:
|
|
"""
|
|
Search for items by name.
|
|
|
|
Args:
|
|
query: Item name search term
|
|
item_type: Optional filter ('weapon', 'armor', 'tool')
|
|
|
|
Returns:
|
|
List of item data dicts with 'id', 'name', 'type' keys
|
|
"""
|
|
cache_key = f"search:{query}:{item_type}"
|
|
cached = self._cache.get(cache_key)
|
|
if cached:
|
|
return cached
|
|
|
|
# Try API first
|
|
params = {'q': query}
|
|
if item_type:
|
|
params['type'] = item_type
|
|
|
|
data = await self._make_request('items/search', params)
|
|
|
|
if data and 'results' in data:
|
|
results = data['results']
|
|
self._cache.set(cache_key, results)
|
|
return results
|
|
|
|
# Fallback to mock data
|
|
if self.use_mock_fallback:
|
|
return self._search_mock_items(query, item_type)
|
|
|
|
return []
|
|
|
|
def _search_mock_items(self, query: str, item_type: Optional[str] = None) -> List[Dict]:
|
|
"""Search mock items by query string."""
|
|
query_lower = query.lower()
|
|
results = []
|
|
|
|
if not item_type or item_type == 'weapon':
|
|
for item_id, weapon in MOCK_WEAPONS.items():
|
|
if query_lower in weapon.name.lower():
|
|
results.append({
|
|
'id': item_id,
|
|
'name': weapon.name,
|
|
'type': 'weapon'
|
|
})
|
|
|
|
if not item_type or item_type == 'armor':
|
|
for item_id, armor in MOCK_ARMORS.items():
|
|
if query_lower in armor.name.lower():
|
|
results.append({
|
|
'id': item_id,
|
|
'name': armor.name,
|
|
'type': 'armor'
|
|
})
|
|
|
|
if not item_type or item_type == 'tool':
|
|
for item_id, tool in MOCK_TOOLS.items():
|
|
if query_lower in tool.name.lower():
|
|
results.append({
|
|
'id': item_id,
|
|
'name': tool.name,
|
|
'type': 'tool'
|
|
})
|
|
|
|
return results
|
|
|
|
async def get_weapon_stats(self, item_id: str) -> Optional[WeaponStats]:
|
|
"""
|
|
Get detailed weapon statistics.
|
|
|
|
Args:
|
|
item_id: Weapon item ID or name
|
|
|
|
Returns:
|
|
WeaponStats object or None if not found
|
|
"""
|
|
cache_key = f"weapon:{item_id}"
|
|
cached = self._cache.get(cache_key)
|
|
if cached:
|
|
return cached
|
|
|
|
# Try API first
|
|
data = await self._make_request(f'weapons/{item_id}')
|
|
|
|
if data:
|
|
try:
|
|
weapon = WeaponStats(
|
|
name=data['name'],
|
|
damage=Decimal(str(data.get('damage', 0))),
|
|
decay_pec=Decimal(str(data.get('decay_pec', 0))),
|
|
ammo_pec=Decimal(str(data.get('ammo_pec', 0))),
|
|
dpp=Decimal(str(data.get('dpp', 0))),
|
|
range=data.get('range', 0),
|
|
attacks_per_min=data.get('attacks_per_min', 0),
|
|
markup_percent=Decimal(str(data.get('markup', 100))),
|
|
item_id=item_id
|
|
)
|
|
self._cache.set(cache_key, weapon)
|
|
return weapon
|
|
except (KeyError, ValueError) as e:
|
|
logger.error(f"Failed to parse weapon data: {e}")
|
|
|
|
# Fallback to mock data
|
|
if self.use_mock_fallback:
|
|
normalized_id = item_id.lower().replace(' ', '_')
|
|
if normalized_id in MOCK_WEAPONS:
|
|
weapon = MOCK_WEAPONS[normalized_id]
|
|
self._cache.set(cache_key, weapon)
|
|
return weapon
|
|
|
|
# Try partial match
|
|
for mock_id, weapon in MOCK_WEAPONS.items():
|
|
if normalized_id in mock_id or normalized_id in weapon.name.lower():
|
|
self._cache.set(cache_key, weapon)
|
|
return weapon
|
|
|
|
return None
|
|
|
|
async def get_armor_stats(self, item_id: str) -> Optional[ArmorStats]:
|
|
"""
|
|
Get detailed armor statistics.
|
|
|
|
Args:
|
|
item_id: Armor item ID or name
|
|
|
|
Returns:
|
|
ArmorStats object or None if not found
|
|
"""
|
|
cache_key = f"armor:{item_id}"
|
|
cached = self._cache.get(cache_key)
|
|
if cached:
|
|
return cached
|
|
|
|
# Try API first
|
|
data = await self._make_request(f'armor/{item_id}')
|
|
|
|
if data:
|
|
try:
|
|
protection = data.get('protection', {})
|
|
armor = ArmorStats(
|
|
name=data['name'],
|
|
decay_pec=Decimal(str(data.get('decay_pec', 0))),
|
|
protection={k: Decimal(str(v)) for k, v in protection.items()},
|
|
slot=data.get('slot', 'body'),
|
|
durability=data.get('durability', 10000),
|
|
item_id=item_id
|
|
)
|
|
self._cache.set(cache_key, armor)
|
|
return armor
|
|
except (KeyError, ValueError) as e:
|
|
logger.error(f"Failed to parse armor data: {e}")
|
|
|
|
# Fallback to mock data
|
|
if self.use_mock_fallback:
|
|
normalized_id = item_id.lower().replace(' ', '_')
|
|
if normalized_id in MOCK_ARMORS:
|
|
armor = MOCK_ARMORS[normalized_id]
|
|
self._cache.set(cache_key, armor)
|
|
return armor
|
|
|
|
# Try partial match
|
|
for mock_id, armor in MOCK_ARMORS.items():
|
|
if normalized_id in mock_id or normalized_id in armor.name.lower():
|
|
self._cache.set(cache_key, armor)
|
|
return armor
|
|
|
|
return None
|
|
|
|
async def get_tool_stats(self, item_id: str) -> Optional[ToolStats]:
|
|
"""
|
|
Get detailed tool statistics.
|
|
|
|
Args:
|
|
item_id: Tool item ID or name
|
|
|
|
Returns:
|
|
ToolStats object or None if not found
|
|
"""
|
|
cache_key = f"tool:{item_id}"
|
|
cached = self._cache.get(cache_key)
|
|
if cached:
|
|
return cached
|
|
|
|
# Try API first
|
|
data = await self._make_request(f'tools/{item_id}')
|
|
|
|
if data:
|
|
try:
|
|
tool = ToolStats(
|
|
name=data['name'],
|
|
depth=Decimal(str(data.get('depth', 0))),
|
|
radius=Decimal(str(data.get('radius', 0))),
|
|
decay_pec=Decimal(str(data.get('decay_pec', 0))),
|
|
tool_type=data.get('type', 'finder'),
|
|
probe_cost=Decimal(str(data.get('probe_cost', 0.5))),
|
|
item_id=item_id
|
|
)
|
|
self._cache.set(cache_key, tool)
|
|
return tool
|
|
except (KeyError, ValueError) as e:
|
|
logger.error(f"Failed to parse tool data: {e}")
|
|
|
|
# Fallback to mock data
|
|
if self.use_mock_fallback:
|
|
normalized_id = item_id.lower().replace(' ', '_')
|
|
if normalized_id in MOCK_TOOLS:
|
|
tool = MOCK_TOOLS[normalized_id]
|
|
self._cache.set(cache_key, tool)
|
|
return tool
|
|
|
|
# Try partial match
|
|
for mock_id, tool in MOCK_TOOLS.items():
|
|
if normalized_id in mock_id or normalized_id in tool.name.lower():
|
|
self._cache.set(cache_key, tool)
|
|
return tool
|
|
|
|
return None
|
|
|
|
async def get_market_markup(self, item_id: str) -> Optional[Decimal]:
|
|
"""
|
|
Get current market markup percentage for an item.
|
|
|
|
Args:
|
|
item_id: Item ID or name
|
|
|
|
Returns:
|
|
Markup percentage (e.g., 105.5 for 105.5%) or None
|
|
"""
|
|
cache_key = f"markup:{item_id}"
|
|
cached = self._cache.get(cache_key)
|
|
if cached:
|
|
return cached
|
|
|
|
data = await self._make_request(f'markup/{item_id}')
|
|
|
|
if data and 'markup' in data:
|
|
markup = Decimal(str(data['markup']))
|
|
self._cache.set(cache_key, markup, ttl=300) # 5 min cache for market data
|
|
return markup
|
|
|
|
return None
|
|
|
|
def clear_cache(self) -> None:
|
|
"""Clear all cached data."""
|
|
self._cache.clear()
|
|
logger.info("Cache cleared")
|
|
|
|
async def close(self) -> None:
|
|
"""Close the API client and cleanup resources."""
|
|
if self._session and not self._session.closed:
|
|
await self._session.close()
|
|
self._session = None
|
|
|
|
async def __aenter__(self):
|
|
"""Async context manager entry."""
|
|
return self
|
|
|
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
"""Async context manager exit."""
|
|
await self.close()
|
|
|
|
|
|
# =============================================================================
|
|
# Utility Functions
|
|
# =============================================================================
|
|
|
|
def get_mock_weapons() -> Dict[str, WeaponStats]:
|
|
"""Get all mock weapons."""
|
|
return MOCK_WEAPONS.copy()
|
|
|
|
|
|
def get_mock_armors() -> Dict[str, ArmorStats]:
|
|
"""Get all mock armors."""
|
|
return MOCK_ARMORS.copy()
|
|
|
|
|
|
def get_mock_tools() -> Dict[str, ToolStats]:
|
|
"""Get all mock tools."""
|
|
return MOCK_TOOLS.copy()
|
|
|
|
|
|
def calculate_dpp(damage: Decimal, decay_pec: Decimal, ammo_pec: Decimal) -> Decimal:
|
|
"""
|
|
Calculate Damage Per PEC (DPP).
|
|
|
|
DPP is a key efficiency metric in Entropia Universe.
|
|
Higher DPP means more damage per PEC spent.
|
|
|
|
Args:
|
|
damage: Weapon damage
|
|
decay_pec: Decay per shot in PEC
|
|
ammo_pec: Ammo cost per shot in PEC
|
|
|
|
Returns:
|
|
DPP value
|
|
"""
|
|
total_cost_ped = (decay_pec + ammo_pec) / 100
|
|
if total_cost_ped == 0:
|
|
return Decimal("0")
|
|
return damage / total_cost_ped
|
|
|
|
|
|
# =============================================================================
|
|
# Module Exports
|
|
# =============================================================================
|
|
|
|
__all__ = [
|
|
'WeaponStats',
|
|
'ArmorStats',
|
|
'ToolStats',
|
|
'EntropiaNexusAPI',
|
|
'SimpleCache',
|
|
'MOCK_WEAPONS',
|
|
'MOCK_ARMORS',
|
|
'MOCK_TOOLS',
|
|
'get_mock_weapons',
|
|
'get_mock_armors',
|
|
'get_mock_tools',
|
|
'calculate_dpp',
|
|
]
|