879 lines
29 KiB
Python
879 lines
29 KiB
Python
"""
|
|
Entropia Nexus API Client - Full Implementation
|
|
|
|
This module provides a complete API client for Entropia Nexus.
|
|
Currently uses mock data as the public API endpoints are not available.
|
|
When the API becomes available, update BASE_URL and remove mock_mode.
|
|
|
|
API Documentation: https://api.entropianexus.com/docs/
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
from dataclasses import dataclass, field, asdict
|
|
from decimal import Decimal, InvalidOperation
|
|
from typing import Dict, List, Optional, Any, Union
|
|
from functools import wraps
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
|
|
# Optional HTTP libraries
|
|
try:
|
|
import aiohttp
|
|
HAS_AIOHTTP = True
|
|
except ImportError:
|
|
HAS_AIOHTTP = False
|
|
aiohttp = None
|
|
|
|
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
|
|
range: int = 0
|
|
attacks_per_min: int = 0
|
|
total_cost_pec: Optional[Decimal] = None
|
|
markup_percent: Decimal = field(default_factory=lambda: Decimal("100.0"))
|
|
item_id: str = ""
|
|
item_class: str = "" # e.g., "Laser rifle", "BLP pistol"
|
|
weight: Decimal = field(default_factory=lambda: Decimal("0.0"))
|
|
power_cost: Decimal = field(default_factory=lambda: Decimal("0.0"))
|
|
|
|
def __post_init__(self):
|
|
if self.total_cost_pec is None:
|
|
self.total_cost_pec = self.decay_pec + self.ammo_pec
|
|
# Validate DPP calculation
|
|
if self.dpp == Decimal("0") and self.total_cost_pec and self.total_cost_pec > 0:
|
|
self.dpp = self.damage / self.total_cost_pec
|
|
|
|
def calculate_cost_per_hour(self) -> Decimal:
|
|
"""Calculate total cost per hour of use in PED."""
|
|
if not self.attacks_per_min or self.attacks_per_min <= 0:
|
|
return Decimal("0")
|
|
shots_per_hour = self.attacks_per_min * 60
|
|
total_pec = self.total_cost_pec * shots_per_hour if self.total_cost_pec else Decimal("0")
|
|
return total_pec / 100 # Convert PEC to PED
|
|
|
|
def calculate_dpp(self) -> Decimal:
|
|
"""Calculate Damage Per PEC."""
|
|
if not self.total_cost_pec or self.total_cost_pec == 0:
|
|
return Decimal("0")
|
|
return self.damage / self.total_cost_pec
|
|
|
|
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) if self.total_cost_pec else "0",
|
|
'markup_percent': str(self.markup_percent),
|
|
'item_id': self.item_id,
|
|
'item_class': self.item_class,
|
|
'weight': str(self.weight),
|
|
'power_cost': str(self.power_cost),
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> 'WeaponStats':
|
|
"""Create from dictionary."""
|
|
return cls(
|
|
name=data.get('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=int(data.get('range', 0)),
|
|
attacks_per_min=int(data.get('attacks_per_min', 0)),
|
|
markup_percent=Decimal(data.get('markup_percent', 100)),
|
|
item_id=data.get('item_id', ''),
|
|
item_class=data.get('item_class', ''),
|
|
weight=Decimal(data.get('weight', 0)),
|
|
power_cost=Decimal(data.get('power_cost', 0)),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class ArmorPiece:
|
|
"""Single armor piece stats."""
|
|
slot: str # head, body, arms, hands, thighs, shins, feet
|
|
protection: Dict[str, Decimal] = field(default_factory=dict)
|
|
decay_pec: Decimal = field(default_factory=lambda: Decimal("0.0"))
|
|
weight: Decimal = field(default_factory=lambda: Decimal("0.0"))
|
|
|
|
|
|
@dataclass
|
|
class ArmorStats:
|
|
"""Armor set statistics."""
|
|
name: str
|
|
decay_pec: Decimal
|
|
protection: Dict[str, Decimal] = field(default_factory=dict)
|
|
pieces: Dict[str, ArmorPiece] = field(default_factory=dict)
|
|
durability: int = 0
|
|
slot: str = "body"
|
|
item_id: str = ""
|
|
markup_percent: Decimal = field(default_factory=lambda: Decimal("100.0"))
|
|
|
|
def get_total_protection(self) -> Decimal:
|
|
"""Calculate total protection across all damage types."""
|
|
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.lower(), Decimal("0"))
|
|
|
|
def get_durability_hours(self) -> Decimal:
|
|
"""Estimate durability in hours of use."""
|
|
if self.durability <= 0:
|
|
return Decimal("0")
|
|
# Rough estimate: 1000 durability ≈ 10 hours
|
|
return Decimal(str(self.durability)) / Decimal("100")
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Convert to dictionary."""
|
|
return {
|
|
'name': self.name,
|
|
'decay_pec': str(self.decay_pec),
|
|
'protection': {k: str(v) for k, v in self.protection.items()},
|
|
'durability': self.durability,
|
|
'slot': self.slot,
|
|
'item_id': self.item_id,
|
|
'markup_percent': str(self.markup_percent),
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> 'ArmorStats':
|
|
"""Create from dictionary."""
|
|
return cls(
|
|
name=data.get('name', ''),
|
|
decay_pec=Decimal(data.get('decay_pec', 0)),
|
|
protection={k: Decimal(v) for k, v in data.get('protection', {}).items()},
|
|
durability=int(data.get('durability', 0)),
|
|
slot=data.get('slot', 'body'),
|
|
item_id=data.get('item_id', ''),
|
|
markup_percent=Decimal(data.get('markup_percent', 100)),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class ToolStats:
|
|
"""Mining/scanning tool statistics."""
|
|
name: str
|
|
depth: Decimal
|
|
radius: Decimal
|
|
decay_pec: Decimal
|
|
tool_type: str = "finder" # finder, extractor, scanner
|
|
probe_cost: Optional[Decimal] = None
|
|
item_id: str = ""
|
|
markup_percent: Decimal = field(default_factory=lambda: Decimal("100.0"))
|
|
|
|
def calculate_cost_per_drop(self) -> Decimal:
|
|
"""Calculate cost per mining drop in PED."""
|
|
total_pec = self.decay_pec
|
|
if self.probe_cost:
|
|
total_pec += self.probe_cost
|
|
return total_pec / 100 # Convert to PED
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Convert to dictionary."""
|
|
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) if self.probe_cost else "0.5",
|
|
'item_id': self.item_id,
|
|
'markup_percent': str(self.markup_percent),
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> 'ToolStats':
|
|
"""Create from dictionary."""
|
|
return cls(
|
|
name=data.get('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', ''),
|
|
markup_percent=Decimal(data.get('markup_percent', 100)),
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Gear Loadout
|
|
# =============================================================================
|
|
|
|
@dataclass
|
|
class GearLoadout:
|
|
"""Complete hunting/mining/crafting loadout."""
|
|
name: str
|
|
weapon: Optional[WeaponStats] = None
|
|
armor: Optional[ArmorStats] = None
|
|
tool: Optional[ToolStats] = None
|
|
amplifier: Optional[Dict[str, Any]] = None
|
|
scope: Optional[Dict[str, Any]] = None
|
|
|
|
def get_total_cost_per_hour(self) -> Decimal:
|
|
"""Calculate total cost per hour for this loadout."""
|
|
total = Decimal("0")
|
|
if self.weapon:
|
|
total += self.weapon.calculate_cost_per_hour()
|
|
# Armor decay is much slower, estimate based on hits taken
|
|
if self.armor:
|
|
# Rough estimate: 100 hits per hour
|
|
armor_cost_ped = self.armor.decay_pec / 100
|
|
total += armor_cost_ped * Decimal("100") # 100 hits
|
|
return total
|
|
|
|
def get_total_dpp(self) -> Optional[Decimal]:
|
|
"""Get DPP of primary weapon."""
|
|
if self.weapon:
|
|
return self.weapon.dpp
|
|
return None
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Convert to dictionary."""
|
|
return {
|
|
'name': self.name,
|
|
'weapon': self.weapon.to_dict() if self.weapon else None,
|
|
'armor': self.armor.to_dict() if self.armor else None,
|
|
'tool': self.tool.to_dict() if self.tool else None,
|
|
'total_cost_per_hour': str(self.get_total_cost_per_hour()),
|
|
'dpp': str(self.get_total_dpp()) if self.get_total_dpp() else None,
|
|
}
|
|
|
|
|
|
# =============================================================================
|
|
# Cache Implementation
|
|
# =============================================================================
|
|
|
|
class SimpleCache:
|
|
"""Simple in-memory cache with TTL."""
|
|
|
|
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:
|
|
"""Set cache value."""
|
|
self._cache[key] = value
|
|
self._timestamps[key] = time.time()
|
|
|
|
def delete(self, key: str) -> None:
|
|
"""Delete cache entry."""
|
|
self._cache.pop(key, None)
|
|
self._timestamps.pop(key, None)
|
|
|
|
def clear(self) -> None:
|
|
"""Clear all cache."""
|
|
self._cache.clear()
|
|
self._timestamps.clear()
|
|
|
|
|
|
# =============================================================================
|
|
# API Client
|
|
# =============================================================================
|
|
|
|
class EntropiaNexusAPI:
|
|
"""
|
|
Client for Entropia Nexus API.
|
|
|
|
Currently operates in mock mode as public API endpoints are not available.
|
|
When API becomes available, set mock_mode=False and provide base_url.
|
|
"""
|
|
|
|
# TODO: Update with real API endpoints when available
|
|
BASE_URL = "https://api.entropianexus.com"
|
|
API_VERSION = "v1"
|
|
|
|
def __init__(self, api_key: Optional[str] = None,
|
|
mock_mode: bool = True,
|
|
cache_ttl: int = 3600,
|
|
base_url: Optional[str] = None):
|
|
"""
|
|
Initialize API client.
|
|
|
|
Args:
|
|
api_key: API key for authentication (if required)
|
|
mock_mode: Use mock data instead of API calls
|
|
cache_ttl: Cache time-to-live in seconds
|
|
base_url: Override base URL for API
|
|
"""
|
|
self.api_key = api_key
|
|
self.mock_mode = mock_mode
|
|
self._cache = SimpleCache(cache_ttl)
|
|
self._session: Optional[Any] = None
|
|
self._base_url = base_url or self.BASE_URL
|
|
|
|
logger.info(f"EntropiaNexusAPI initialized (mock_mode={mock_mode})")
|
|
|
|
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()
|
|
return False
|
|
|
|
async def close(self):
|
|
"""Close API client and cleanup."""
|
|
if self._session and HAS_AIOHTTP:
|
|
await self._session.close()
|
|
self._session = None
|
|
|
|
async def _get_session(self) -> Optional[Any]:
|
|
"""Get or create HTTP session."""
|
|
if not HAS_AIOHTTP or self.mock_mode:
|
|
return None
|
|
|
|
if self._session is None or self._session.closed:
|
|
self._session = aiohttp.ClientSession(
|
|
headers={
|
|
'Accept': 'application/json',
|
|
'User-Agent': 'Lemontropia-Suite/0.2.0'
|
|
},
|
|
timeout=aiohttp.ClientTimeout(total=15)
|
|
)
|
|
return self._session
|
|
|
|
async def _make_request(self, endpoint: str,
|
|
params: Optional[Dict] = None) -> Optional[Dict]:
|
|
"""
|
|
Make API request.
|
|
|
|
Args:
|
|
endpoint: API endpoint path
|
|
params: Query parameters
|
|
|
|
Returns:
|
|
JSON response or None on error
|
|
"""
|
|
if self.mock_mode:
|
|
return None
|
|
|
|
url = f"{self._base_url}/{self.API_VERSION}/{endpoint}"
|
|
|
|
try:
|
|
session = await self._get_session()
|
|
if not session:
|
|
return None
|
|
|
|
headers = {}
|
|
if self.api_key:
|
|
headers['Authorization'] = f'Bearer {self.api_key}'
|
|
|
|
async with session.get(url, params=params, headers=headers) as resp:
|
|
if resp.status == 200:
|
|
return await resp.json()
|
|
else:
|
|
logger.warning(f"API error: {resp.status}")
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.error(f"API request failed: {e}")
|
|
return None
|
|
|
|
# ========================================================================
|
|
# Search Methods
|
|
# ========================================================================
|
|
|
|
async def search_items(self, query: str,
|
|
item_type: Optional[str] = None) -> List[Dict]:
|
|
"""
|
|
Search for items.
|
|
|
|
Args:
|
|
query: Search term
|
|
item_type: Filter by type ('weapon', 'armor', 'tool')
|
|
|
|
Returns:
|
|
List of item summaries
|
|
"""
|
|
cache_key = f"search:{query}:{item_type}"
|
|
cached = self._cache.get(cache_key)
|
|
if cached:
|
|
return cached
|
|
|
|
# Try API
|
|
params = {'q': query}
|
|
if item_type:
|
|
params['type'] = item_type
|
|
|
|
data = await self._make_request('items/search', params)
|
|
if data:
|
|
self._cache.set(cache_key, data)
|
|
return data
|
|
|
|
# Fallback to mock
|
|
if self.mock_mode:
|
|
return self._mock_search(query, item_type)
|
|
|
|
return []
|
|
|
|
def _mock_search(self, query: str,
|
|
item_type: Optional[str]) -> List[Dict]:
|
|
"""Mock search implementation."""
|
|
results = []
|
|
query_lower = query.lower()
|
|
|
|
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',
|
|
'category': weapon.item_class or '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',
|
|
'category': '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',
|
|
'category': tool.tool_type.title()
|
|
})
|
|
|
|
return results
|
|
|
|
# ========================================================================
|
|
# Weapon Methods
|
|
# ========================================================================
|
|
|
|
async def get_weapon(self, item_id: str) -> Optional[WeaponStats]:
|
|
"""Get weapon details by ID."""
|
|
cache_key = f"weapon:{item_id}"
|
|
cached = self._cache.get(cache_key)
|
|
if cached:
|
|
return WeaponStats.from_dict(cached)
|
|
|
|
# Try API
|
|
data = await self._make_request(f'items/{item_id}')
|
|
if data and data.get('type') == 'weapon':
|
|
weapon = self._parse_weapon_data(data)
|
|
self._cache.set(cache_key, weapon.to_dict())
|
|
return weapon
|
|
|
|
# Fallback to mock
|
|
if self.mock_mode and item_id in MOCK_WEAPONS:
|
|
return MOCK_WEAPONS[item_id]
|
|
|
|
return None
|
|
|
|
def _parse_weapon_data(self, data: Dict) -> WeaponStats:
|
|
"""Parse API weapon data."""
|
|
stats = data.get('stats', {})
|
|
return WeaponStats(
|
|
name=data.get('name', 'Unknown'),
|
|
damage=Decimal(str(stats.get('damage', 0))),
|
|
decay_pec=Decimal(str(stats.get('decay', 0))),
|
|
ammo_pec=Decimal(str(stats.get('ammo', 0))),
|
|
dpp=Decimal(str(stats.get('dpp', 0))),
|
|
range=stats.get('range', 0),
|
|
attacks_per_min=stats.get('attacks_per_min', 0),
|
|
item_id=str(data.get('id', '')),
|
|
item_class=stats.get('class', ''),
|
|
)
|
|
|
|
async def get_all_weapons(self) -> List[WeaponStats]:
|
|
"""Get all weapons."""
|
|
return list(MOCK_WEAPONS.values())
|
|
|
|
# ========================================================================
|
|
# Armor Methods
|
|
# ========================================================================
|
|
|
|
async def get_armor(self, item_id: str) -> Optional[ArmorStats]:
|
|
"""Get armor details by ID."""
|
|
cache_key = f"armor:{item_id}"
|
|
cached = self._cache.get(cache_key)
|
|
if cached:
|
|
return ArmorStats.from_dict(cached)
|
|
|
|
data = await self._make_request(f'items/{item_id}')
|
|
if data and data.get('type') == 'armor':
|
|
armor = self._parse_armor_data(data)
|
|
self._cache.set(cache_key, armor.to_dict())
|
|
return armor
|
|
|
|
if self.mock_mode and item_id in MOCK_ARMORS:
|
|
return MOCK_ARMORS[item_id]
|
|
|
|
return None
|
|
|
|
def _parse_armor_data(self, data: Dict) -> ArmorStats:
|
|
"""Parse API armor data."""
|
|
stats = data.get('stats', {})
|
|
protection = {}
|
|
for dmg_type, value in stats.get('protection', {}).items():
|
|
protection[dmg_type] = Decimal(str(value))
|
|
|
|
return ArmorStats(
|
|
name=data.get('name', 'Unknown'),
|
|
decay_pec=Decimal(str(stats.get('decay', 0))),
|
|
protection=protection,
|
|
durability=stats.get('durability', 0),
|
|
item_id=str(data.get('id', '')),
|
|
)
|
|
|
|
async def get_all_armors(self) -> List[ArmorStats]:
|
|
"""Get all armors."""
|
|
return list(MOCK_ARMORS.values())
|
|
|
|
# ========================================================================
|
|
# Tool Methods
|
|
# ========================================================================
|
|
|
|
async def get_tool(self, item_id: str) -> Optional[ToolStats]:
|
|
"""Get tool details by ID."""
|
|
cache_key = f"tool:{item_id}"
|
|
cached = self._cache.get(cache_key)
|
|
if cached:
|
|
return ToolStats.from_dict(cached)
|
|
|
|
data = await self._make_request(f'items/{item_id}')
|
|
if data and data.get('type') == 'tool':
|
|
tool = self._parse_tool_data(data)
|
|
self._cache.set(cache_key, tool.to_dict())
|
|
return tool
|
|
|
|
if self.mock_mode and item_id in MOCK_TOOLS:
|
|
return MOCK_TOOLS[item_id]
|
|
|
|
return None
|
|
|
|
def _parse_tool_data(self, data: Dict) -> ToolStats:
|
|
"""Parse API tool data."""
|
|
stats = data.get('stats', {})
|
|
return ToolStats(
|
|
name=data.get('name', 'Unknown'),
|
|
depth=Decimal(str(stats.get('depth', 0))),
|
|
radius=Decimal(str(stats.get('radius', 0))),
|
|
decay_pec=Decimal(str(stats.get('decay', 0))),
|
|
tool_type=stats.get('tool_type', 'finder'),
|
|
item_id=str(data.get('id', '')),
|
|
)
|
|
|
|
async def get_all_tools(self) -> List[ToolStats]:
|
|
"""Get all tools."""
|
|
return list(MOCK_TOOLS.values())
|
|
|
|
# ========================================================================
|
|
# Loadout Methods
|
|
# ========================================================================
|
|
|
|
async def calculate_loadout(self, weapon_id: Optional[str] = None,
|
|
armor_id: Optional[str] = None,
|
|
tool_id: Optional[str] = None) -> Optional[GearLoadout]:
|
|
"""Calculate stats for a complete loadout."""
|
|
loadout = GearLoadout(name="Custom Loadout")
|
|
|
|
if weapon_id:
|
|
loadout.weapon = await self.get_weapon(weapon_id)
|
|
|
|
if armor_id:
|
|
loadout.armor = await self.get_armor(armor_id)
|
|
|
|
if tool_id:
|
|
loadout.tool = await self.get_tool(tool_id)
|
|
|
|
return loadout
|
|
|
|
|
|
# =============================================================================
|
|
# Mock Data
|
|
# =============================================================================
|
|
|
|
MOCK_WEAPONS: Dict[str, WeaponStats] = {
|
|
"sollomate_opalo": WeaponStats(
|
|
name="Sollomate Opalo",
|
|
damage=Decimal("4.0"),
|
|
decay_pec=Decimal("0.13"),
|
|
ammo_pec=Decimal("1.07"),
|
|
dpp=Decimal("3.33"),
|
|
range=26,
|
|
attacks_per_min=56,
|
|
item_id="sollomate_opalo",
|
|
item_class="Laser Rifle"
|
|
),
|
|
"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",
|
|
item_class="Laser Carbine"
|
|
),
|
|
"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",
|
|
item_class="BLP Rifle"
|
|
),
|
|
"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",
|
|
item_class="BLP Pistol"
|
|
),
|
|
"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",
|
|
item_class="Laser Sniper"
|
|
),
|
|
"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",
|
|
item_class="Laser Pistol"
|
|
),
|
|
}
|
|
|
|
MOCK_ARMORS: Dict[str, ArmorStats] = {
|
|
"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=2400,
|
|
item_id="pixie"
|
|
),
|
|
"goblin": ArmorStats(
|
|
name="Goblin",
|
|
decay_pec=Decimal("0.20"),
|
|
protection={
|
|
"impact": Decimal("4.0"),
|
|
"cut": Decimal("4.0"),
|
|
"stab": Decimal("4.0"),
|
|
"burn": Decimal("3.0"),
|
|
"cold": Decimal("2.0"),
|
|
"acid": Decimal("1.0"),
|
|
},
|
|
slot="body",
|
|
durability=2800,
|
|
item_id="goblin"
|
|
),
|
|
"shogun": ArmorStats(
|
|
name="Shogun",
|
|
decay_pec=Decimal("0.55"),
|
|
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"
|
|
),
|
|
}
|
|
|
|
MOCK_TOOLS: Dict[str, ToolStats] = {
|
|
"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"
|
|
),
|
|
}
|
|
|
|
|
|
# =============================================================================
|
|
# Utility Functions
|
|
# =============================================================================
|
|
|
|
def calculate_dpp(damage: Decimal, decay_pec: Decimal, ammo_pec: Decimal) -> Decimal:
|
|
"""Calculate Damage Per PEC."""
|
|
total_cost = decay_pec + ammo_pec
|
|
if total_cost == 0:
|
|
return Decimal("0")
|
|
return damage / total_cost
|
|
|
|
|
|
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 get_weapon_by_name(name: str) -> Optional[WeaponStats]:
|
|
"""Find weapon by name (case insensitive)."""
|
|
name_lower = name.lower()
|
|
for weapon in MOCK_WEAPONS.values():
|
|
if name_lower in weapon.name.lower():
|
|
return weapon
|
|
return None
|
|
|
|
|
|
# =============================================================================
|
|
# Module Exports
|
|
# =============================================================================
|
|
|
|
__all__ = [
|
|
'WeaponStats',
|
|
'ArmorStats',
|
|
'ArmorPiece',
|
|
'ToolStats',
|
|
'GearLoadout',
|
|
'EntropiaNexusAPI',
|
|
'SimpleCache',
|
|
'MOCK_WEAPONS',
|
|
'MOCK_ARMORS',
|
|
'MOCK_TOOLS',
|
|
'calculate_dpp',
|
|
'get_mock_weapons',
|
|
'get_mock_armors',
|
|
'get_mock_tools',
|
|
'get_weapon_by_name',
|
|
] |