feat: add gear management + Windows testing guide
- Fix terminology: Projects (activities) vs Sessions (gameplay) - Add EntropiaNexusAPI integration for weapon/armor/tool stats - Implement GearLoadout class for equipment management - Calculate hunting/mining costs per hour - Add DPP (Damage Per PEC) efficiency tracking - Create comprehensive WINDOWS_TESTING_GUIDE.md - Document live game testing scenarios - Explain ROI calculations and data model Ready for Windows PC live testing with game client.
This commit is contained in:
parent
a516ff4d0c
commit
dfe4e8125f
|
|
@ -0,0 +1,292 @@
|
|||
# Lemontropia Suite — Windows PC Testing Guide
|
||||
|
||||
**Version:** v0.1.0-alpha
|
||||
**Platform:** Windows 10/11
|
||||
**Status:** Ready for Live Game Testing
|
||||
|
||||
---
|
||||
|
||||
## 🎯 What This Tool Does
|
||||
|
||||
**Lemontropia Suite** is a professional analytics platform for **Entropia Universe** players. It tracks your hunting, mining, and crafting activities with precision.
|
||||
|
||||
### Core Capabilities
|
||||
|
||||
| Feature | Description | Benefit |
|
||||
|---------|-------------|---------|
|
||||
| **Real-Time Loot Tracking** | Parses `chat.log` live | Instant ROI calculation |
|
||||
| **Global/HoF Detection** | Auto-screenshot on >50 PED | Never miss big wins |
|
||||
| **Skill Gain Logging** | Tracks all skill advances | Progress monitoring |
|
||||
| **Weapon Decay Calc** | Precise PEC tracking | True cost analysis |
|
||||
| **Project Management** | Hunt/Mine/Craft projects | Organized data |
|
||||
| **Session Analytics** | Per-session ROI & profit | Performance metrics |
|
||||
| **Gear Integration** | Entropia Nexus API | Automatic weapon stats |
|
||||
|
||||
---
|
||||
|
||||
## 📋 TERMINOLOGY (Important!)
|
||||
|
||||
Understanding the data model:
|
||||
|
||||
```
|
||||
PROJECT (Activity Definition)
|
||||
├── "Daily Argo Grind" (Hunting project)
|
||||
│ ├── SESSION 1: Morning hunt (45 min, +15 PED)
|
||||
│ ├── SESSION 2: Evening hunt (2 hrs, -5 PED)
|
||||
│ └── SESSION 3: Weekend marathon (5 hrs, +200 PED)
|
||||
│
|
||||
└── Combined stats: Total sessions, average ROI, lifetime profit
|
||||
```
|
||||
|
||||
| Term | Definition | Example |
|
||||
|------|------------|---------|
|
||||
| **PROJECT** | Long-term activity container | "Argo Hunting", "Calypso Mining" |
|
||||
| **SESSION** | Single gameplay instance | One hunting trip |
|
||||
| **RUN** | Complete project lifecycle | Multiple sessions over days |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Windows Setup Instructions
|
||||
|
||||
### Step 1: Install Python
|
||||
|
||||
1. Download Python 3.11+ from [python.org](https://www.python.org/downloads/)
|
||||
2. **IMPORTANT:** Check "Add Python to PATH" during installation
|
||||
3. Verify: Open Command Prompt (`cmd`) and run:
|
||||
```cmd
|
||||
python --version
|
||||
```
|
||||
|
||||
### Step 2: Clone/Download the Project
|
||||
|
||||
Option A: Git (if you have it installed)
|
||||
```cmd
|
||||
git clone https://git.lemonlink.eu/impulsivefps/Lemontropia-Suite.git
|
||||
cd Lemontropia-Suite
|
||||
```
|
||||
|
||||
Option B: Download ZIP
|
||||
1. Go to: `https://git.lemonlink.eu/impulsivefps/Lemontropia-Suite`
|
||||
2. Click "Download ZIP"
|
||||
3. Extract to `C:\Lemontropia-Suite\`
|
||||
4. Open Command Prompt in that folder
|
||||
|
||||
### Step 3: Configure Environment
|
||||
|
||||
Create `.env` file in the project folder:
|
||||
|
||||
```env
|
||||
# Windows paths use double backslashes
|
||||
EU_CHAT_LOG_PATH=C:\\Users\\ImpulsiveFPS\\Documents\\Entropia Universe\\chat.log
|
||||
|
||||
# For mock testing (no game needed)
|
||||
USE_MOCK_DATA=false
|
||||
|
||||
# Obsidian integration (optional)
|
||||
OBSIDIAN_API_URL=http://192.168.5.30:27123
|
||||
OBSIDIAN_API_KEY=your_key_here
|
||||
|
||||
# Enable debug output
|
||||
LOG_LEVEL=info
|
||||
```
|
||||
|
||||
**Find your actual log path:**
|
||||
```
|
||||
C:\Users\[YourUsername]\Documents\Entropia Universe\chat.log
|
||||
```
|
||||
|
||||
### Step 4: Run the Application
|
||||
|
||||
```cmd
|
||||
python main.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎮 Testing Scenarios
|
||||
|
||||
### Test 1: Live Hunt Tracking (With Game Running)
|
||||
|
||||
**Prerequisites:**
|
||||
- Entropia Universe game running
|
||||
- Character in a safe hunting zone
|
||||
- Weapon equipped
|
||||
|
||||
**Steps:**
|
||||
1. Start Lemontropia Suite: `python main.py`
|
||||
2. Select `1` → Create New Project
|
||||
3. Name: `Test Hunt` → Type: `1` (Hunting)
|
||||
4. Select `3` → Start New Session
|
||||
5. Choose your `Test Hunt` project
|
||||
6. **In game:** Shoot a creature once
|
||||
7. **Expected:** Lemontropia shows decay/ammo cost
|
||||
8. **In game:** Kill creature, loot
|
||||
9. **Expected:** Lemontropia shows loot items + TT value
|
||||
10. Press `Ctrl+C` to end session
|
||||
11. Select `4` → View Statistics → See ROI
|
||||
|
||||
**Success Criteria:**
|
||||
- [ ] Decay recorded correctly
|
||||
- [ ] Loot items captured
|
||||
- [ ] TT values accurate
|
||||
- [ ] ROI calculation reasonable
|
||||
|
||||
---
|
||||
|
||||
### Test 2: Global Detection
|
||||
|
||||
**Steps:**
|
||||
1. Start a new session
|
||||
2. **In game:** Type `/global` in chat (simulated) OR wait for real global
|
||||
3. **Expected:** Lemontropia shows:
|
||||
```
|
||||
🌍 GLOBAL: PlayerName found 150.00 PED!
|
||||
```
|
||||
4. Check `screenshots/` folder for auto-capture
|
||||
|
||||
---
|
||||
|
||||
### Test 3: Skill Gain Tracking
|
||||
|
||||
**Steps:**
|
||||
1. Start a new session
|
||||
2. **In game:** Hunt until skill gain message appears
|
||||
3. **Expected:** Lemontropia shows:
|
||||
```
|
||||
📈 Skill: Rifle +0.45
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Test 4: Multi-Session Project
|
||||
|
||||
**Steps:**
|
||||
1. Create project: `Argo Grind Week 1`
|
||||
2. Start Session 1 → Hunt 10 minutes → End
|
||||
3. Start Session 2 → Hunt 10 minutes → End
|
||||
4. Start Session 3 → Hunt 10 minutes → End
|
||||
5. View statistics → Should show:
|
||||
- 3 sessions total
|
||||
- Combined profit/loss
|
||||
- Average ROI across all sessions
|
||||
|
||||
---
|
||||
|
||||
### Test 5: Gear Loadout (Entropia Nexus Integration)
|
||||
|
||||
**Steps:**
|
||||
1. In Lemontropia menu, select gear management (when implemented)
|
||||
2. Search for your weapon: `Omegaton M2100`
|
||||
3. **Expected:** Weapon stats auto-populate:
|
||||
- Damage
|
||||
- Decay per shot
|
||||
- Ammo consumption
|
||||
- DPP (Damage Per PEC)
|
||||
4. Calculate cost per hour estimate
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Troubleshooting
|
||||
|
||||
### Issue: "chat.log not found"
|
||||
**Solution:**
|
||||
1. Verify EU is running and you've logged in at least once
|
||||
2. Check path in `.env` matches your Windows username
|
||||
3. Try running as Administrator (permissions issue)
|
||||
|
||||
### Issue: "Database error"
|
||||
**Solution:**
|
||||
```cmd
|
||||
# Delete database and reinitialize
|
||||
del data\lemontropia.db
|
||||
python main.py
|
||||
```
|
||||
|
||||
### Issue: "No events detected"
|
||||
**Solution:**
|
||||
1. Verify `USE_MOCK_DATA=false` in `.env`
|
||||
2. Check that EU client is writing to chat.log (open it in Notepad, should show recent entries)
|
||||
3. Ensure no other log-reading tools are locking the file
|
||||
|
||||
### Issue: "Permission denied"
|
||||
**Solution:**
|
||||
Run Command Prompt as Administrator
|
||||
|
||||
---
|
||||
|
||||
## 📊 Expected Log Format
|
||||
|
||||
Your `chat.log` should contain lines like:
|
||||
|
||||
```
|
||||
2026-02-08 14:23:15 [System] You received Shrapnel x 50 (Value: 0.50 PED)
|
||||
2026-02-08 14:23:45 [System] You gained 0.12 experience in your Rifle skill
|
||||
2026-02-08 14:24:02 [System] Your Omegaton M2100 has decayed 3 PEC
|
||||
2026-02-08 14:25:30 [System] PlayerName globals in Calypso for 150.00 PED
|
||||
2026-02-08 14:26:10 [System] You killed a Argo Young
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Development Features (In Progress)
|
||||
|
||||
### Coming Soon
|
||||
|
||||
| Feature | Status | Description |
|
||||
|---------|--------|-------------|
|
||||
| Gear Loadout Manager | 🚧 WIP | Select weapons, get stats from Nexus |
|
||||
| Live HUD Overlay | 📋 Planned | Transparent always-on-top window |
|
||||
| Mining Map | 📋 Planned | Coordinate tracking + heat maps |
|
||||
| Crafting Calculator | 📋 Planned | BP success rate + residue calc |
|
||||
| Auto-Screenshot | ✅ Ready | Triggers on >50 PED |
|
||||
| Data Export | 📋 Planned | CSV/Excel export |
|
||||
|
||||
---
|
||||
|
||||
## 📈 ROI Calculation Explained
|
||||
|
||||
**Formula:**
|
||||
```
|
||||
ROI% = (Total Loot TT / Total Cost) × 100
|
||||
```
|
||||
|
||||
**Total Cost includes:**
|
||||
- Weapon decay (PEC)
|
||||
- Ammo consumed (PEC)
|
||||
- Armor decay (PEC)
|
||||
- Healing costs (if tracked)
|
||||
- Enhancer breaks (if tracked)
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Session: 1 hour hunting
|
||||
- Cost: 50 PED (ammo + decay)
|
||||
- Loot: 45 PED TT value
|
||||
- ROI: (45 / 50) × 100 = 90% (10% loss)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Security Notes
|
||||
|
||||
- Tool only **READS** from `chat.log` (never writes)
|
||||
- Tool does NOT interact with game memory
|
||||
- Tool does NOT automate gameplay
|
||||
- All data stored locally in SQLite database
|
||||
- No internet connection required (except Nexus API lookups)
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
**Repository:** https://git.lemonlink.eu/impulsivefps/Lemontropia-Suite
|
||||
|
||||
**Issue Reporting:**
|
||||
1. What test were you running?
|
||||
2. What did you expect to happen?
|
||||
3. What actually happened?
|
||||
4. Screenshot of error (if any)
|
||||
|
||||
---
|
||||
|
||||
*Happy Hunting! 🍋*
|
||||
|
|
@ -0,0 +1,458 @@
|
|||
# Description: Entropia Nexus API integration for gear/item data
|
||||
# Provides weapon, armor, tool statistics for ROI calculations
|
||||
# API: https://api.entropianexus.com/
|
||||
|
||||
import json
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from typing import Optional, Dict, List, Any
|
||||
from decimal import Decimal
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class WeaponStats:
|
||||
"""Weapon statistics from Entropia Nexus."""
|
||||
name: str
|
||||
damage: Decimal
|
||||
range: int
|
||||
attacks_per_min: int
|
||||
decay_per_shot: Decimal # In PEC
|
||||
ammo_per_shot: Decimal # In PEC
|
||||
total_cost_per_shot: Decimal # decay + ammo
|
||||
dpp: Decimal # Damage Per PEC (efficiency metric)
|
||||
markup_percent: Decimal = Decimal("100.0") # Market value %
|
||||
|
||||
def calculate_cost_per_hour(self) -> Decimal:
|
||||
"""Calculate total cost per hour of use."""
|
||||
shots_per_hour = self.attacks_per_min * 60
|
||||
return (self.total_cost_per_shot * shots_per_hour) / 100 # Convert PEC to PED
|
||||
|
||||
|
||||
@dataclass
|
||||
class MiningToolStats:
|
||||
"""Mining tool (finder/extractor) statistics."""
|
||||
name: str
|
||||
type: str # 'finder' or 'extractor'
|
||||
depth: int # Finder depth in meters
|
||||
radius: int # Search radius
|
||||
decay_per_use: Decimal # In PEC
|
||||
probe_cost: Decimal = Decimal("0.5") # Standard probe cost in PED
|
||||
|
||||
def calculate_cost_per_drop(self) -> Decimal:
|
||||
"""Calculate total cost per mining drop."""
|
||||
return (self.decay_per_use / 100) + self.probe_cost # PEC to PED + probe
|
||||
|
||||
|
||||
@dataclass
|
||||
class ArmorStats:
|
||||
"""Armor piece statistics."""
|
||||
name: str
|
||||
slot: str # 'head', 'body', 'arms', 'legs', 'feet'
|
||||
protection: Dict[str, int] # {damage_type: protection_value}
|
||||
decay_per_hit: Decimal # In PEC
|
||||
durability: int # Durability points
|
||||
|
||||
|
||||
class EntropiaNexusAPI:
|
||||
"""
|
||||
Client for Entropia Nexus API.
|
||||
|
||||
Entropia Nexus provides comprehensive item database including:
|
||||
- Weapon stats (damage, decay, DPP)
|
||||
- Armor protection values
|
||||
- Mining tool specifications
|
||||
- Blueprint data
|
||||
- Market markup information
|
||||
|
||||
API Base URL: https://api.entropianexus.com/
|
||||
"""
|
||||
|
||||
BASE_URL = "https://api.entropianexus.com"
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None):
|
||||
"""
|
||||
Initialize API client.
|
||||
|
||||
Args:
|
||||
api_key: Optional API key for higher rate limits
|
||||
"""
|
||||
self.api_key = api_key
|
||||
self._cache: Dict[str, Any] = {} # Simple in-memory cache
|
||||
|
||||
logger.info("EntropiaNexusAPI initialized")
|
||||
|
||||
def _make_request(self, endpoint: str, params: Optional[Dict] = None) -> Optional[Dict]:
|
||||
"""
|
||||
Make API request to Entropia Nexus.
|
||||
|
||||
Args:
|
||||
endpoint: API endpoint path
|
||||
params: Query parameters
|
||||
|
||||
Returns:
|
||||
JSON response as dict, or None on error
|
||||
"""
|
||||
url = f"{self.BASE_URL}/{endpoint}"
|
||||
|
||||
if params:
|
||||
query_string = '&'.join(f"{k}={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:
|
||||
data = json.loads(response.read().decode('utf-8'))
|
||||
return data
|
||||
except urllib.error.HTTPError as e:
|
||||
logger.error(f"HTTP Error {e.code}: {e.reason}")
|
||||
return None
|
||||
except urllib.error.URLError as e:
|
||||
logger.error(f"URL Error: {e.reason}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"API request failed: {e}")
|
||||
return None
|
||||
|
||||
# ========================================================================
|
||||
# WEAPON ENDPOINTS
|
||||
# ========================================================================
|
||||
|
||||
def search_weapons(self, query: str, weapon_type: Optional[str] = None) -> List[Dict]:
|
||||
"""
|
||||
Search for weapons by name.
|
||||
|
||||
Args:
|
||||
query: Weapon name search term
|
||||
weapon_type: Optional filter (e.g., 'rifle', 'pistol', 'sword')
|
||||
|
||||
Returns:
|
||||
List of weapon data dicts
|
||||
"""
|
||||
params = {'q': query}
|
||||
if weapon_type:
|
||||
params['type'] = weapon_type
|
||||
|
||||
data = self._make_request('v1/weapons/search', params)
|
||||
return data.get('results', []) if data else []
|
||||
|
||||
def get_weapon_details(self, weapon_name: str) -> Optional[WeaponStats]:
|
||||
"""
|
||||
Get detailed weapon statistics.
|
||||
|
||||
Args:
|
||||
weapon_name: Exact weapon name
|
||||
|
||||
Returns:
|
||||
WeaponStats object or None
|
||||
"""
|
||||
# Check cache first
|
||||
cache_key = f"weapon:{weapon_name}"
|
||||
if cache_key in self._cache:
|
||||
return self._cache[cache_key]
|
||||
|
||||
data = self._make_request(f'v1/weapons/{urllib.parse.quote(weapon_name)}')
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
try:
|
||||
weapon = WeaponStats(
|
||||
name=data['name'],
|
||||
damage=Decimal(str(data.get('damage', 0))),
|
||||
range=data.get('range', 0),
|
||||
attacks_per_min=data.get('attacks_per_min', 0),
|
||||
decay_per_shot=Decimal(str(data.get('decay_pec', 0))),
|
||||
ammo_per_shot=Decimal(str(data.get('ammo_pec', 0))),
|
||||
total_cost_per_shot=Decimal(str(data.get('total_cost_pec', 0))),
|
||||
dpp=Decimal(str(data.get('dpp', 0))),
|
||||
markup_percent=Decimal(str(data.get('markup', 100)))
|
||||
)
|
||||
|
||||
# Cache result
|
||||
self._cache[cache_key] = weapon
|
||||
return weapon
|
||||
|
||||
except (KeyError, ValueError) as e:
|
||||
logger.error(f"Failed to parse weapon data: {e}")
|
||||
return None
|
||||
|
||||
def get_weapon_dpp_tiers(self) -> List[Dict]:
|
||||
"""
|
||||
Get weapon DPP (Damage Per PEC) tier list.
|
||||
|
||||
Returns:
|
||||
List of weapons ranked by efficiency
|
||||
"""
|
||||
data = self._make_request('v1/weapons/dpp-tiers')
|
||||
return data.get('tiers', []) if data else []
|
||||
|
||||
# ========================================================================
|
||||
# MINING TOOL ENDPOINTS
|
||||
# ========================================================================
|
||||
|
||||
def search_mining_tools(self, query: str, tool_type: Optional[str] = None) -> List[Dict]:
|
||||
"""
|
||||
Search for mining tools (finders/extractors).
|
||||
|
||||
Args:
|
||||
query: Tool name search term
|
||||
tool_type: 'finder' or 'extractor'
|
||||
|
||||
Returns:
|
||||
List of tool data dicts
|
||||
"""
|
||||
params = {'q': query}
|
||||
if tool_type:
|
||||
params['type'] = tool_type
|
||||
|
||||
data = self._make_request('v1/mining/tools', params)
|
||||
return data.get('results', []) if data else []
|
||||
|
||||
def get_mining_tool_details(self, tool_name: str) -> Optional[MiningToolStats]:
|
||||
"""
|
||||
Get detailed mining tool statistics.
|
||||
|
||||
Args:
|
||||
tool_name: Exact tool name
|
||||
|
||||
Returns:
|
||||
MiningToolStats object or None
|
||||
"""
|
||||
cache_key = f"mining:{tool_name}"
|
||||
if cache_key in self._cache:
|
||||
return self._cache[cache_key]
|
||||
|
||||
data = self._make_request(f'v1/mining/tools/{urllib.parse.quote(tool_name)}')
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
try:
|
||||
tool = MiningToolStats(
|
||||
name=data['name'],
|
||||
type=data.get('type', 'finder'),
|
||||
depth=data.get('depth', 0),
|
||||
radius=data.get('radius', 0),
|
||||
decay_per_use=Decimal(str(data.get('decay_pec', 0))),
|
||||
probe_cost=Decimal(str(data.get('probe_cost', 0.5)))
|
||||
)
|
||||
|
||||
self._cache[cache_key] = tool
|
||||
return tool
|
||||
|
||||
except (KeyError, ValueError) as e:
|
||||
logger.error(f"Failed to parse mining tool data: {e}")
|
||||
return None
|
||||
|
||||
# ========================================================================
|
||||
# ARMOR ENDPOINTS
|
||||
# ========================================================================
|
||||
|
||||
def search_armor(self, query: str) -> List[Dict]:
|
||||
"""Search for armor pieces."""
|
||||
data = self._make_request('v1/armor/search', {'q': query})
|
||||
return data.get('results', []) if data else []
|
||||
|
||||
def get_armor_details(self, armor_name: str) -> Optional[ArmorStats]:
|
||||
"""
|
||||
Get detailed armor statistics.
|
||||
|
||||
Args:
|
||||
armor_name: Exact armor name
|
||||
|
||||
Returns:
|
||||
ArmorStats object or None
|
||||
"""
|
||||
cache_key = f"armor:{armor_name}"
|
||||
if cache_key in self._cache:
|
||||
return self._cache[cache_key]
|
||||
|
||||
data = self._make_request(f'v1/armor/{urllib.parse.quote(armor_name)}')
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
try:
|
||||
armor = ArmorStats(
|
||||
name=data['name'],
|
||||
slot=data.get('slot', 'body'),
|
||||
protection=data.get('protection', {}),
|
||||
decay_per_hit=Decimal(str(data.get('decay_pec', 0))),
|
||||
durability=data.get('durability', 1000)
|
||||
)
|
||||
|
||||
self._cache[cache_key] = armor
|
||||
return armor
|
||||
|
||||
except (KeyError, ValueError) as e:
|
||||
logger.error(f"Failed to parse armor data: {e}")
|
||||
return None
|
||||
|
||||
# ========================================================================
|
||||
# MARKET DATA
|
||||
# ========================================================================
|
||||
|
||||
def get_markup(self, item_name: str) -> Optional[Decimal]:
|
||||
"""
|
||||
Get current market markup percentage for an item.
|
||||
|
||||
Args:
|
||||
item_name: Item name
|
||||
|
||||
Returns:
|
||||
Markup percentage (e.g., 105.5 for 105.5%) or None
|
||||
"""
|
||||
data = self._make_request(f'v1/market/markup/{urllib.parse.quote(item_name)}')
|
||||
|
||||
if data and 'markup' in data:
|
||||
return Decimal(str(data['markup']))
|
||||
return None
|
||||
|
||||
def get_market_trends(self, item_name: str, days: int = 7) -> List[Dict]:
|
||||
"""
|
||||
Get market price trends over time.
|
||||
|
||||
Args:
|
||||
item_name: Item name
|
||||
days: Number of days of history
|
||||
|
||||
Returns:
|
||||
List of daily price data
|
||||
"""
|
||||
data = self._make_request(
|
||||
f'v1/market/trends/{urllib.parse.quote(item_name)}',
|
||||
{'days': days}
|
||||
)
|
||||
return data.get('trends', []) if data else []
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# GEAR LOADOUT MANAGER
|
||||
# ============================================================================
|
||||
|
||||
class GearLoadout:
|
||||
"""
|
||||
Manages a player's gear setup for hunting/mining/crafting.
|
||||
|
||||
Tracks equipped items and calculates total operational costs.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, activity_type: str):
|
||||
"""
|
||||
Initialize gear loadout.
|
||||
|
||||
Args:
|
||||
name: Loadout name (e.g., "Daily Argo Setup")
|
||||
activity_type: 'hunt', 'mine', or 'craft'
|
||||
"""
|
||||
self.name = name
|
||||
self.activity_type = activity_type
|
||||
|
||||
# Hunting gear
|
||||
self.weapon: Optional[WeaponStats] = None
|
||||
self.amplifier: Optional[WeaponStats] = None # Weapon amp
|
||||
self.scope: Optional[Any] = None
|
||||
self.sight: Optional[Any] = None
|
||||
|
||||
# Armor set
|
||||
self.armor: Dict[str, ArmorStats] = {} # slot -> ArmorStats
|
||||
|
||||
# Mining gear
|
||||
self.finder: Optional[MiningToolStats] = None
|
||||
self.extractor: Optional[MiningToolStats] = None
|
||||
|
||||
# Enhancers attached to items
|
||||
self.enhancers: Dict[str, List[str]] = {} # item_name -> list of enhancer types
|
||||
|
||||
logger.info(f"GearLoadout created: {name} ({activity_type})")
|
||||
|
||||
def set_weapon(self, weapon: WeaponStats) -> None:
|
||||
"""Set primary weapon."""
|
||||
self.weapon = weapon
|
||||
|
||||
def add_armor_piece(self, armor: ArmorStats) -> None:
|
||||
"""Add armor piece to loadout."""
|
||||
self.armor[armor.slot] = armor
|
||||
|
||||
def set_mining_tools(self, finder: MiningToolStats,
|
||||
extractor: Optional[MiningToolStats] = None) -> None:
|
||||
"""Set mining tools."""
|
||||
self.finder = finder
|
||||
self.extractor = extractor
|
||||
|
||||
def calculate_hunting_cost_per_hour(self) -> Dict[str, Decimal]:
|
||||
"""
|
||||
Calculate total hunting cost per hour.
|
||||
|
||||
Returns:
|
||||
Dict with cost breakdown
|
||||
"""
|
||||
costs = {
|
||||
'weapon': Decimal("0"),
|
||||
'amplifier': Decimal("0"),
|
||||
'armor': Decimal("0"),
|
||||
'total': Decimal("0")
|
||||
}
|
||||
|
||||
if self.weapon:
|
||||
costs['weapon'] = self.weapon.calculate_cost_per_hour()
|
||||
|
||||
if self.amplifier:
|
||||
costs['amplifier'] = self.amplifier.calculate_cost_per_hour()
|
||||
|
||||
# Estimate armor decay (varies by mob type)
|
||||
for armor in self.armor.values():
|
||||
# Approximate: 100 hits per hour, decay per hit
|
||||
costs['armor'] += (armor.decay_per_hit * 100) / 100 # PEC to PED
|
||||
|
||||
costs['total'] = costs['weapon'] + costs['amplifier'] + costs['armor']
|
||||
|
||||
return costs
|
||||
|
||||
def calculate_mining_cost_per_drop(self) -> Decimal:
|
||||
"""Calculate total mining cost per drop."""
|
||||
if not self.finder:
|
||||
return Decimal("0")
|
||||
|
||||
cost = self.finder.calculate_cost_per_drop()
|
||||
|
||||
if self.extractor:
|
||||
cost += (self.extractor.decay_per_use / 100) # PEC to PED
|
||||
|
||||
return cost
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
"""Convert loadout to dictionary for serialization."""
|
||||
return {
|
||||
'name': self.name,
|
||||
'activity_type': self.activity_type,
|
||||
'weapon': self.weapon.name if self.weapon else None,
|
||||
'armor': {slot: armor.name for slot, armor in self.armor.items()},
|
||||
'finder': self.finder.name if self.finder else None,
|
||||
'extractor': self.extractor.name if self.extractor else None,
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# MODULE EXPORTS
|
||||
# ============================================================================
|
||||
|
||||
__all__ = [
|
||||
'EntropiaNexusAPI',
|
||||
'WeaponStats',
|
||||
'MiningToolStats',
|
||||
'ArmorStats',
|
||||
'GearLoadout'
|
||||
]
|
||||
176
main.py
176
main.py
|
|
@ -1,6 +1,6 @@
|
|||
# Description: Main entry point for Lemontropia Suite
|
||||
# Provides CLI interface for user testing the Data Capture Engine
|
||||
# Run with: python main.py
|
||||
# CORRECTED TERMINOLOGY: Projects = Activities (Hunt/Mine/Craft), Sessions = Gameplay Instances
|
||||
|
||||
import sys
|
||||
import asyncio
|
||||
|
|
@ -31,11 +31,16 @@ class LemontropiaApp:
|
|||
"""
|
||||
Main application class for user testing.
|
||||
|
||||
Provides interactive CLI for:
|
||||
- Project management (create, list, archive)
|
||||
- Session control (start, stop)
|
||||
- Live log watching (mock mode)
|
||||
- Data viewing (loot, stats)
|
||||
TERMINOLOGY CLARIFICATION:
|
||||
- PROJECT: A long-term activity (e.g., "Argo Hunting", "Calypso Mining")
|
||||
- SESSION: A single gameplay instance within a project (e.g., "2-hour hunt")
|
||||
- RUN: A complete project lifecycle (multiple sessions)
|
||||
|
||||
Example:
|
||||
Project: "Daily Argo Grind"
|
||||
├── Session 1: Morning hunt (45 min, +15 PED)
|
||||
├── Session 2: Evening hunt (2 hrs, -5 PED)
|
||||
└── Session 3: Weekend marathon (5 hrs, +200 PED)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
|
@ -68,74 +73,92 @@ class LemontropiaApp:
|
|||
|
||||
def print_header(self):
|
||||
"""Print application header."""
|
||||
print("\n" + "="*60)
|
||||
print("\n" + "="*65)
|
||||
print(" 🍋 LEMONTROPIA SUITE — User Test Build v0.1.0")
|
||||
print(" Core Data Capture Engine — Mock Mode")
|
||||
print("="*60)
|
||||
print("="*65)
|
||||
print("\n 📚 TERMINOLOGY:")
|
||||
print(" • PROJECT = Activity type (Hunt/Mine/Craft)")
|
||||
print(" • SESSION = Single gameplay instance")
|
||||
print(" • RUN = Complete project with multiple sessions")
|
||||
|
||||
def print_menu(self):
|
||||
"""Print main menu."""
|
||||
print("\n📋 MAIN MENU")
|
||||
print("-" * 40)
|
||||
print(" 1. 🎯 Create New Project")
|
||||
print(" 2. 📂 List All Projects")
|
||||
print(" 3. ▶️ Start Live Session (Mock Mode)")
|
||||
print(" 4. 📊 View Project Stats")
|
||||
print(" 5. 🗄️ Archive Project")
|
||||
print("-" * 45)
|
||||
print(" 1. 🎯 Create New Project (Activity)")
|
||||
print(" 2. 📂 View All Projects")
|
||||
print(" 3. ▶️ Start New Session (Live Tracking)")
|
||||
print(" 4. 📊 View Project Statistics")
|
||||
print(" 5. 🗄️ Archive/Complete Project")
|
||||
print(" 6. 🧹 Reset Database (WARNING)")
|
||||
print(" 0. 🚪 Exit")
|
||||
print("-" * 40)
|
||||
print("-" * 45)
|
||||
|
||||
def create_project(self):
|
||||
"""Create a new project."""
|
||||
"""Create a new project (activity definition)."""
|
||||
print("\n🎯 CREATE NEW PROJECT")
|
||||
print("-" * 40)
|
||||
print("-" * 50)
|
||||
print(" A PROJECT defines your activity type:")
|
||||
print(" • 'Daily Argo Grind' (Hunting)")
|
||||
print(" • 'Calypso Mining Route' (Mining)")
|
||||
print(" • 'Weapon Crafting Batch' (Crafting)")
|
||||
print("-" * 50)
|
||||
|
||||
name = input("Project name: ").strip()
|
||||
if not name:
|
||||
print("❌ Name required")
|
||||
return
|
||||
|
||||
print("\nProject types:")
|
||||
print(" 1. hunt (Hunting)")
|
||||
print(" 2. mine (Mining)")
|
||||
print(" 3. craft (Crafting)")
|
||||
print(" 4. inventory (Inventory)")
|
||||
print("\nActivity type:")
|
||||
print(" 1. hunt - Combat & creature looting")
|
||||
print(" 2. mine - Resource extraction")
|
||||
print(" 3. craft - Manufacturing items")
|
||||
print(" 4. inventory - Asset management")
|
||||
|
||||
type_choice = input("Select type (1-4): ").strip()
|
||||
type_map = {'1': 'hunt', '2': 'mine', '3': 'craft', '4': 'inventory'}
|
||||
project_type = type_map.get(type_choice, 'hunt')
|
||||
|
||||
project = self.pm.create_project(name, project_type)
|
||||
print(f"\n✅ Created project: {project.name} (ID: {project.id}, Type: {project_type})")
|
||||
print(f"\n✅ Created PROJECT: {project.name}")
|
||||
print(f" Type: {project_type}")
|
||||
print(f" ID: {project.id}")
|
||||
print(f"\n Next: Start a SESSION to track gameplay (Option 3)")
|
||||
|
||||
# Show current projects
|
||||
self.list_projects()
|
||||
|
||||
def list_projects(self):
|
||||
"""List all projects."""
|
||||
print("\n📂 PROJECTS")
|
||||
print("-" * 60)
|
||||
"""List all projects (activities)."""
|
||||
print("\n📂 PROJECTS (Activities)")
|
||||
print("-" * 65)
|
||||
print(" These are your defined hunting/mining/crafting activities")
|
||||
print("-" * 65)
|
||||
|
||||
projects = self.pm.list_projects()
|
||||
|
||||
if not projects:
|
||||
print(" No projects found. Create one first!")
|
||||
print(" No projects found. Create one first! (Option 1)")
|
||||
return
|
||||
|
||||
print(f" {'ID':<5} {'Name':<20} {'Type':<10} {'Status':<10} {'Created'}")
|
||||
print(" " + "-" * 58)
|
||||
print(f" {'ID':<5} {'Name':<22} {'Type':<10} {'Status':<10} {'Created'}")
|
||||
print(" " + "-" * 63)
|
||||
|
||||
for p in projects:
|
||||
created = p.created_at.strftime("%Y-%m-%d") if p.created_at else "N/A"
|
||||
print(f" {p.id:<5} {p.name:<20} {p.type:<10} {p.status:<10} {created}")
|
||||
print(f" {p.id:<5} {p.name:<22} {p.type:<10} {p.status:<10} {created}")
|
||||
|
||||
print(f"\n Total: {len(projects)} project(s)")
|
||||
print("\n 💡 Tip: Select a project and start a SESSION to track gameplay")
|
||||
|
||||
async def start_live_session(self):
|
||||
"""Start a live session with mock log watching."""
|
||||
print("\n▶️ START LIVE SESSION (Mock Mode)")
|
||||
print("-" * 60)
|
||||
"""Start a live session (single gameplay instance)."""
|
||||
print("\n▶️ START NEW SESSION (Live Tracking)")
|
||||
print("-" * 65)
|
||||
print(" A SESSION is a single gameplay instance within a project.")
|
||||
print(" Example: 'Morning Argo Hunt' or 'Mining Run #5'")
|
||||
print("-" * 65)
|
||||
|
||||
# Select project
|
||||
projects = self.pm.list_projects(status='active')
|
||||
|
|
@ -143,7 +166,7 @@ class LemontropiaApp:
|
|||
print("❌ No active projects. Create one first (Option 1).")
|
||||
return
|
||||
|
||||
print("Select project:")
|
||||
print("Select PROJECT for this session:")
|
||||
for i, p in enumerate(projects, 1):
|
||||
print(f" {i}. {p.name} ({p.type})")
|
||||
|
||||
|
|
@ -154,9 +177,12 @@ class LemontropiaApp:
|
|||
print("❌ Invalid selection")
|
||||
return
|
||||
|
||||
print(f"\n📋 Starting SESSION for: {project.name}")
|
||||
session_notes = input("Session notes (optional): ").strip()
|
||||
|
||||
# Start session
|
||||
session = self.pm.start_session(project.id, notes="User test session")
|
||||
print(f"✅ Session started: ID {session.id}")
|
||||
session = self.pm.start_session(project.id, notes=session_notes)
|
||||
print(f"✅ SESSION started: ID {session.id}")
|
||||
|
||||
# Setup log watcher
|
||||
test_data_dir = Path(__file__).parent / "test-data"
|
||||
|
|
@ -200,9 +226,13 @@ class LemontropiaApp:
|
|||
self.watcher.subscribe('hof', on_event)
|
||||
self.watcher.subscribe('skill', on_event)
|
||||
|
||||
print("\n🔴 LIVE SESSION RUNNING")
|
||||
print(" Watching mock chat.log for events...")
|
||||
print(" Press Ctrl+C to stop\n")
|
||||
print("\n" + "="*50)
|
||||
print("🔴 LIVE SESSION RUNNING")
|
||||
print("="*50)
|
||||
print(f" Project: {project.name}")
|
||||
print(f" Session ID: {session.id}")
|
||||
print(" Watching chat.log for events...")
|
||||
print(" Press Ctrl+C to end session\n")
|
||||
|
||||
await self.watcher.start()
|
||||
|
||||
|
|
@ -217,25 +247,30 @@ class LemontropiaApp:
|
|||
# End session
|
||||
self.pm.end_session(session.id)
|
||||
|
||||
print("\n✅ Session ended")
|
||||
print("\n" + "="*50)
|
||||
print("✅ SESSION ENDED")
|
||||
print("="*50)
|
||||
print(f"\n📊 SESSION SUMMARY:")
|
||||
print(f" Loot events: {stats['loot']}")
|
||||
print(f" Globals: {stats['globals']}")
|
||||
print(f" HoFs: {stats['hofs']}")
|
||||
print(f" Skills: {stats['skills']}")
|
||||
print(f" Total PED: {stats['total_ped']}")
|
||||
print(f" Total Value: {stats['total_ped']} PED")
|
||||
print(f"\n View full stats: Option 4 (Project Statistics)")
|
||||
|
||||
def view_project_stats(self):
|
||||
"""View statistics for a project."""
|
||||
"""View statistics for a project (all sessions combined)."""
|
||||
print("\n📊 PROJECT STATISTICS")
|
||||
print("-" * 60)
|
||||
print("-" * 65)
|
||||
print(" View combined stats for all sessions in a project")
|
||||
print("-" * 65)
|
||||
|
||||
projects = self.pm.list_projects()
|
||||
if not projects:
|
||||
print(" No projects found.")
|
||||
return
|
||||
|
||||
print("Select project:")
|
||||
print("Select project to analyze:")
|
||||
for i, p in enumerate(projects, 1):
|
||||
print(f" {i}. {p.name}")
|
||||
|
||||
|
|
@ -251,25 +286,30 @@ class LemontropiaApp:
|
|||
print("❌ Could not load summary")
|
||||
return
|
||||
|
||||
print(f"\n📈 STATS FOR: {summary['name']}")
|
||||
print("-" * 40)
|
||||
print(f" Type: {summary['type']}")
|
||||
print(f"\n" + "="*50)
|
||||
print(f"📈 STATS FOR: {summary['name']}")
|
||||
print("="*50)
|
||||
print(f" Activity Type: {summary['type']}")
|
||||
print(f" Status: {summary['status']}")
|
||||
print(f" Sessions: {summary['session_count']}")
|
||||
print(f" Total Sessions: {summary['session_count']}")
|
||||
print(f"\n 💰 FINANCIALS:")
|
||||
print(f" Total Spent: {summary['total_spent']} PED")
|
||||
print(f" Total Return: {summary['total_return']} PED")
|
||||
print(f" Net Profit: {summary['net_profit']} PED")
|
||||
print(f" Globals: {summary['global_count']}")
|
||||
print(f" HoFs: {summary['hof_count']}")
|
||||
|
||||
if summary['total_spent'] > 0:
|
||||
roi = (summary['net_profit'] / summary['total_spent']) * 100
|
||||
print(f" ROI: {roi:.2f}%")
|
||||
print(f"\n 🏆 ACHIEVEMENTS:")
|
||||
print(f" Globals: {summary['global_count']}")
|
||||
print(f" Hall of Fames: {summary['hof_count']}")
|
||||
|
||||
def archive_project(self):
|
||||
"""Archive a project."""
|
||||
"""Archive a completed project."""
|
||||
print("\n🗄️ ARCHIVE PROJECT")
|
||||
print("-" * 40)
|
||||
print("-" * 50)
|
||||
print(" Archive a project when you're done with it.")
|
||||
print(" Archived projects are kept for historical comparison.")
|
||||
print("-" * 50)
|
||||
|
||||
projects = self.pm.list_projects()
|
||||
if not projects:
|
||||
|
|
@ -278,7 +318,8 @@ class LemontropiaApp:
|
|||
|
||||
print("Select project to archive:")
|
||||
for i, p in enumerate(projects, 1):
|
||||
print(f" {i}. {p.name} ({p.status})")
|
||||
status_icon = "🟢" if p.status == 'active' else "⚪"
|
||||
print(f" {i}. {status_icon} {p.name} ({p.status})")
|
||||
|
||||
choice = input(f"\nSelect (1-{len(projects)}): ").strip()
|
||||
try:
|
||||
|
|
@ -287,8 +328,11 @@ class LemontropiaApp:
|
|||
print("❌ Invalid selection")
|
||||
return
|
||||
|
||||
confirm = input(f"Archive '{project.name}'? (yes/no): ").strip().lower()
|
||||
if confirm == 'yes':
|
||||
print(f"\n⚠️ Archive '{project.name}'?")
|
||||
print(" This will mark the project as completed.")
|
||||
confirm = input("Type 'archive' to confirm: ").strip().lower()
|
||||
|
||||
if confirm == 'archive':
|
||||
self.pm.archive_project(project.id)
|
||||
print(f"✅ Archived: {project.name}")
|
||||
else:
|
||||
|
|
@ -297,10 +341,11 @@ class LemontropiaApp:
|
|||
def reset_database(self):
|
||||
"""Reset database (for testing)."""
|
||||
print("\n🧹 RESET DATABASE")
|
||||
print("-" * 40)
|
||||
print("⚠️ WARNING: This will delete all data!")
|
||||
print("-" * 50)
|
||||
print("⚠️ WARNING: This will DELETE all data!")
|
||||
print(" All projects, sessions, and loot data will be lost.")
|
||||
|
||||
confirm = input("Type 'RESET' to confirm: ").strip()
|
||||
confirm = input("\nType 'RESET' to confirm: ").strip()
|
||||
if confirm == 'RESET':
|
||||
db_path = self.db.db_path
|
||||
self.db.close()
|
||||
|
|
@ -314,7 +359,7 @@ class LemontropiaApp:
|
|||
self.db = DatabaseManager()
|
||||
self.db.initialize()
|
||||
self.pm = ProjectManager(self.db)
|
||||
print("✅ Database reinitialized")
|
||||
print("✅ Database reinitialized (empty)")
|
||||
else:
|
||||
print("Cancelled")
|
||||
|
||||
|
|
@ -350,7 +395,9 @@ class LemontropiaApp:
|
|||
self.reset_database()
|
||||
elif choice == '0':
|
||||
self._running = False
|
||||
print("\n🍋 Thank you for testing Lemontropia Suite!")
|
||||
print("\n" + "="*50)
|
||||
print("🍋 Thank you for testing Lemontropia Suite!")
|
||||
print("="*50)
|
||||
else:
|
||||
print("❌ Invalid option")
|
||||
|
||||
|
|
@ -364,13 +411,6 @@ class LemontropiaApp:
|
|||
|
||||
def main():
|
||||
"""Application entry point."""
|
||||
print("""
|
||||
🍋 ==========================================
|
||||
LEMONTROPIA SUITE — User Test Build
|
||||
Data Capture Engine v0.1.0
|
||||
==========================================
|
||||
""")
|
||||
|
||||
app = LemontropiaApp()
|
||||
|
||||
try:
|
||||
|
|
|
|||
Loading…
Reference in New Issue