feat: Skill Scanner plugin for formula analysis
- Scan Skills window with OCR for rank + points - Detect progress bars for decimal precision - ESI scanner for skill gain predictions - Track skill gains from chat messages - Store data in JSON for analysis - Export to CSV for graphing - Hotkey: Ctrl+Shift+S Dependencies: pip install easyocr pyautogui pillow Data stored in data/skills/ for formula analysis!
This commit is contained in:
parent
b00792726d
commit
ea9a73c8b4
|
|
@ -0,0 +1,67 @@
|
|||
# 2026-02-13 - EU-Utility Development Session
|
||||
|
||||
## Bug Fixes
|
||||
- **Fixed**: `AttributeError: 'dict' object has no attribute 'lower'` in universal_search plugin
|
||||
- Location: `plugins/universal_search/plugin.py` line ~501
|
||||
- Fix: Changed `'click' in item.lower()` to `'click' in item`
|
||||
- Committed: `5e08f56`
|
||||
|
||||
## UI Redesign - Spotlight Style
|
||||
- **Complete redesign** of overlay window to match macOS Spotlight aesthetic
|
||||
- Features:
|
||||
- Frosted glass effect with `rgba(40, 40, 40, 180)` background
|
||||
- 20px rounded corners
|
||||
- Drop shadow for depth
|
||||
- Header bar with search icon
|
||||
- Circular plugin icon buttons at bottom (🔍 🧮 🎵 🌐)
|
||||
- Transparent content area for plugins
|
||||
- Committed: `8dbbf4d`
|
||||
|
||||
## Game Integration Features
|
||||
|
||||
### Floating Icon
|
||||
- Draggable floating button (⚡) for in-game access
|
||||
- Positioned at top-left (250, 10) near game UI icons
|
||||
- EU-styled: dark blue/gray `rgba(20, 25, 35, 220)` with subtle border
|
||||
- Single-click opens overlay, drag to reposition
|
||||
- Hover effect with brighter border
|
||||
- Blue glow effect using QGraphicsDropShadowEffect
|
||||
- Committed: `b007927`
|
||||
|
||||
### Game Reader Plugin (OCR)
|
||||
- New plugin for reading in-game menus and text
|
||||
- Hotkey: `Ctrl+Shift+R`
|
||||
- Features:
|
||||
- Screen capture with OCR (EasyOCR or Tesseract)
|
||||
- Extract text from NPC dialogue, missions, prices
|
||||
- Copy captured text to clipboard
|
||||
- Status feedback (capturing/error/success)
|
||||
- Location: `plugins/game_reader/plugin.py`
|
||||
- Dependencies: `pip install easyocr` or `pip install pytesseract`
|
||||
- Committed: `d74de07`
|
||||
|
||||
## User Feedback - Game Screenshots
|
||||
User shared screenshots showing ideal use cases:
|
||||
1. **Trade Terminal** - Price checking, category browsing
|
||||
2. **Inventory** - Item management, PED balance (26.02 PED), weight tracking
|
||||
3. **Auction** - Price tracking, markup analysis, bid monitoring
|
||||
|
||||
## All Hotkeys
|
||||
| Hotkey | Action |
|
||||
|--------|--------|
|
||||
| `Ctrl+Shift+U` | Toggle overlay |
|
||||
| `Ctrl+Shift+F` | Universal Search |
|
||||
| `Ctrl+Shift+C` | Calculator |
|
||||
| `Ctrl+Shift+M` | Spotify |
|
||||
| `Ctrl+Shift+R` | Game Reader (OCR) |
|
||||
|
||||
## Repository
|
||||
- EU-Utility: `https://git.lemonlink.eu/impulsivefps/EU-Utility`
|
||||
- Local: `/home/impulsivefps/.openclaw/workspace/projects/EU-Utility/`
|
||||
- Run: `python -m core.main` (from projects/EU-Utility/)
|
||||
|
||||
## Next Ideas (From User)
|
||||
- Auction Price Tracker - capture listings, track history, price alerts
|
||||
- Inventory Value Calculator - total TT value, market price comparison
|
||||
- Auto-Capture Mode - detect window type, extract relevant data automatically
|
||||
- Better OCR region selection for specific windows
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Skill Scanner Plugin for EU-Utility
|
||||
"""
|
||||
|
||||
from .plugin import SkillScannerPlugin
|
||||
|
||||
__all__ = ["SkillScannerPlugin"]
|
||||
|
|
@ -0,0 +1,673 @@
|
|||
"""
|
||||
EU-Utility - Skill Scanner Plugin
|
||||
|
||||
Tracks skill levels with decimal precision by reading:
|
||||
1. Skills window (OCR for values + progress bar detection)
|
||||
2. ESI item windows (skill gain prediction)
|
||||
3. Chat log (skill gain messages)
|
||||
|
||||
Data stored for skill formula analysis.
|
||||
"""
|
||||
|
||||
import re
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QLabel, QPushButton, QTableWidget, QTableWidgetItem,
|
||||
QHeaderView, QTextEdit, QComboBox, QSpinBox, QGroupBox,
|
||||
QFileDialog, QMessageBox, QTabWidget
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer
|
||||
from PyQt6.QtGui import QColor
|
||||
|
||||
from plugins.base_plugin import BasePlugin
|
||||
|
||||
|
||||
class SkillDataStore:
|
||||
"""Store skill data for formula analysis."""
|
||||
|
||||
def __init__(self, data_dir="data/skills"):
|
||||
self.data_dir = Path(data_dir)
|
||||
self.data_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.skills_file = self.data_dir / "skill_snapshots.json"
|
||||
self.esi_file = self.data_dir / "esi_data.json"
|
||||
self.gains_file = self.data_dir / "skill_gains.json"
|
||||
|
||||
def save_skill_snapshot(self, skills_data):
|
||||
"""Save a skill snapshot with timestamp."""
|
||||
snapshot = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"skills": skills_data
|
||||
}
|
||||
|
||||
# Append to history
|
||||
history = self.load_skill_history()
|
||||
history.append(snapshot)
|
||||
|
||||
# Keep last 1000 snapshots
|
||||
if len(history) > 1000:
|
||||
history = history[-1000:]
|
||||
|
||||
with open(self.skills_file, 'w') as f:
|
||||
json.dump(history, f, indent=2)
|
||||
|
||||
def load_skill_history(self):
|
||||
"""Load skill history."""
|
||||
if self.skills_file.exists():
|
||||
with open(self.skills_file, 'r') as f:
|
||||
return json.load(f)
|
||||
return []
|
||||
|
||||
def save_esi_data(self, esi_data):
|
||||
"""Save ESI scan data."""
|
||||
entry = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"esi": esi_data
|
||||
}
|
||||
|
||||
history = self.load_esi_history()
|
||||
history.append(entry)
|
||||
|
||||
with open(self.esi_file, 'w') as f:
|
||||
json.dump(history, f, indent=2)
|
||||
|
||||
def load_esi_history(self):
|
||||
"""Load ESI history."""
|
||||
if self.esi_file.exists():
|
||||
with open(self.esi_file, 'r') as f:
|
||||
return json.load(f)
|
||||
return []
|
||||
|
||||
def save_skill_gain(self, skill_name, points_gained, new_total=None):
|
||||
"""Save a skill gain from chat."""
|
||||
entry = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"skill": skill_name,
|
||||
"gained": points_gained,
|
||||
"new_total": new_total
|
||||
}
|
||||
|
||||
history = self.load_gain_history()
|
||||
history.append(entry)
|
||||
|
||||
with open(self.gains_file, 'w') as f:
|
||||
json.dump(history, f, indent=2)
|
||||
|
||||
def load_gain_history(self):
|
||||
"""Load skill gain history."""
|
||||
if self.gains_file.exists():
|
||||
with open(self.gains_file, 'r') as f:
|
||||
return json.load(f)
|
||||
return []
|
||||
|
||||
def export_to_csv(self, filepath):
|
||||
"""Export all data to CSV for analysis."""
|
||||
import csv
|
||||
|
||||
with open(filepath, 'w', newline='') as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(['timestamp', 'skill', 'rank', 'points', 'progress_pct', 'source'])
|
||||
|
||||
# Export skill snapshots
|
||||
for snapshot in self.load_skill_history():
|
||||
ts = snapshot['timestamp']
|
||||
for skill_name, data in snapshot['skills'].items():
|
||||
writer.writerow([
|
||||
ts,
|
||||
skill_name,
|
||||
data.get('rank', ''),
|
||||
data.get('points', ''),
|
||||
data.get('progress_pct', ''),
|
||||
'snapshot'
|
||||
])
|
||||
|
||||
# Export skill gains
|
||||
for gain in self.load_gain_history():
|
||||
writer.writerow([
|
||||
gain['timestamp'],
|
||||
gain['skill'],
|
||||
'',
|
||||
gain.get('gained', ''),
|
||||
'',
|
||||
'chat_gain'
|
||||
])
|
||||
|
||||
|
||||
class SkillScannerThread(QThread):
|
||||
"""Background thread for skill window OCR."""
|
||||
result_ready = pyqtSignal(dict)
|
||||
error_occurred = pyqtSignal(str)
|
||||
progress_update = pyqtSignal(str)
|
||||
|
||||
def __init__(self, capture_mode="full_screen"):
|
||||
super().__init__()
|
||||
self.capture_mode = capture_mode
|
||||
|
||||
def run(self):
|
||||
"""Capture screen and extract skill data."""
|
||||
try:
|
||||
self.progress_update.emit("📸 Capturing screen...")
|
||||
|
||||
# Capture screen
|
||||
screenshot = self._capture_screen()
|
||||
if screenshot is None:
|
||||
self.error_occurred.emit("Failed to capture screen")
|
||||
return
|
||||
|
||||
self.progress_update.emit("🔍 Analyzing skills window...")
|
||||
|
||||
# Extract skill data
|
||||
skills_data = self._extract_skills(screenshot)
|
||||
|
||||
self.progress_update.emit(f"✅ Found {len(skills_data)} skills")
|
||||
self.result_ready.emit(skills_data)
|
||||
|
||||
except Exception as e:
|
||||
self.error_occurred.emit(str(e))
|
||||
|
||||
def _capture_screen(self):
|
||||
"""Capture full screen."""
|
||||
try:
|
||||
import pyautogui
|
||||
return pyautogui.screenshot()
|
||||
except ImportError:
|
||||
# Fallback to PIL
|
||||
from PIL import ImageGrab
|
||||
return ImageGrab.grab()
|
||||
|
||||
def _extract_skills(self, screenshot):
|
||||
"""Extract skill data from screenshot."""
|
||||
skills_data = {}
|
||||
|
||||
# Perform OCR
|
||||
try:
|
||||
import easyocr
|
||||
reader = easyocr.Reader(['en'], verbose=False)
|
||||
results = reader.readtext(screenshot)
|
||||
|
||||
text_lines = [result[1] for result in results]
|
||||
full_text = ' '.join(text_lines)
|
||||
|
||||
# Check if this is a skills window
|
||||
if 'SKILLS' not in full_text.upper() and 'RANK' not in full_text.upper():
|
||||
self.progress_update.emit("⚠️ Skills window not detected")
|
||||
return skills_data
|
||||
|
||||
# Extract skill lines
|
||||
# Pattern: SkillName Rank Points
|
||||
for i, line in enumerate(text_lines):
|
||||
# Look for rank keywords
|
||||
rank_match = self._match_rank(line)
|
||||
if rank_match:
|
||||
# Previous line might be skill name
|
||||
if i > 0:
|
||||
skill_name = text_lines[i-1].strip()
|
||||
rank = rank_match
|
||||
|
||||
# Next line might be points
|
||||
points = 0
|
||||
if i + 1 < len(text_lines):
|
||||
points_str = text_lines[i+1].replace(',', '').replace('.', '')
|
||||
if points_str.isdigit():
|
||||
points = int(points_str)
|
||||
|
||||
# Detect progress bar percentage (approximate from image)
|
||||
progress_pct = self._estimate_progress(screenshot, i)
|
||||
|
||||
skills_data[skill_name] = {
|
||||
'rank': rank,
|
||||
'points': points,
|
||||
'progress_pct': progress_pct,
|
||||
'raw_text': line
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.progress_update.emit(f"OCR error: {e}")
|
||||
|
||||
return skills_data
|
||||
|
||||
def _match_rank(self, text):
|
||||
"""Match rank text."""
|
||||
ranks = [
|
||||
'Newbie', 'Inept', 'Green', 'Beginner', 'Initiated', 'Trainee',
|
||||
'Apprentice', 'Assistant', 'Assistant', 'Capable', 'Competent',
|
||||
'Skilled', 'Talented', 'Expert', 'Grand', 'Great', 'Adept',
|
||||
'Professional', 'Illustrious', 'Eminent', 'Renowned', 'Master',
|
||||
'Champion', 'Legendary', 'Guru', 'Astonishing', 'Remarkable',
|
||||
'Outstanding', 'Marvelous', 'Prodigious', 'Staggering', 'Unrivaled',
|
||||
'Amazing', 'Incredible', 'Awesome', 'Mind-boggling', 'Sensational',
|
||||
'Miraculous', 'Colossal', 'Epic', 'Transcendent', 'Magnificent',
|
||||
'Unearthly', 'Phenomenal', 'Supreme', 'Omnipotent'
|
||||
]
|
||||
|
||||
for rank in ranks:
|
||||
if rank.lower() in text.lower():
|
||||
return rank
|
||||
return None
|
||||
|
||||
def _estimate_progress(self, screenshot, line_index):
|
||||
"""Estimate progress bar percentage from image analysis."""
|
||||
# This would analyze the green bar length
|
||||
# For now, return 0 (can be improved with image processing)
|
||||
return 0.0
|
||||
|
||||
|
||||
class ESIScannerThread(QThread):
|
||||
"""Scan ESI item window for skill gain prediction."""
|
||||
result_ready = pyqtSignal(dict)
|
||||
error_occurred = pyqtSignal(str)
|
||||
|
||||
def run(self):
|
||||
"""Capture and analyze ESI window."""
|
||||
try:
|
||||
import pyautogui
|
||||
screenshot = pyautogui.screenshot()
|
||||
|
||||
# OCR
|
||||
import easyocr
|
||||
reader = easyocr.Reader(['en'], verbose=False)
|
||||
results = reader.readtext(screenshot)
|
||||
|
||||
text = ' '.join([r[1] for r in results])
|
||||
|
||||
# Look for ESI patterns
|
||||
esi_data = {
|
||||
'item_name': None,
|
||||
'skill_target': None,
|
||||
'points_to_add': 0,
|
||||
'tt_value': 0.0,
|
||||
'full_text': text
|
||||
}
|
||||
|
||||
# Extract ESI info
|
||||
if 'Empty Skill Implant' in text or 'ESI' in text:
|
||||
# Try to find skill name and points
|
||||
# Pattern: "Inserting this implant will add X points to [SkillName]"
|
||||
patterns = [
|
||||
r'add\s+(\d+)\s+points?\s+to\s+([A-Za-z\s]+)',
|
||||
r'(\d+)\s+points?\s+to\s+([A-Za-z\s]+)',
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, text, re.IGNORECASE)
|
||||
if match:
|
||||
esi_data['points_to_add'] = int(match.group(1))
|
||||
esi_data['skill_target'] = match.group(2).strip()
|
||||
break
|
||||
|
||||
# Extract TT value
|
||||
tt_match = re.search(r'(\d+\.?\d*)\s*PED', text)
|
||||
if tt_match:
|
||||
esi_data['tt_value'] = float(tt_match.group(1))
|
||||
|
||||
self.result_ready.emit(esi_data)
|
||||
|
||||
except Exception as e:
|
||||
self.error_occurred.emit(str(e))
|
||||
|
||||
|
||||
class SkillScannerPlugin(BasePlugin):
|
||||
"""Scan and track skill progression with decimal precision."""
|
||||
|
||||
name = "Skill Scanner"
|
||||
version = "1.0.0"
|
||||
author = "ImpulsiveFPS"
|
||||
description = "Track skill levels with precision for formula analysis"
|
||||
hotkey = "ctrl+shift+s" # S for Skills
|
||||
|
||||
def initialize(self):
|
||||
"""Setup skill scanner."""
|
||||
self.data_store = SkillDataStore()
|
||||
self.scan_thread = None
|
||||
self.esi_thread = None
|
||||
self.last_scan = {}
|
||||
|
||||
def get_ui(self):
|
||||
"""Create skill scanner UI."""
|
||||
widget = QWidget()
|
||||
widget.setStyleSheet("background: transparent;")
|
||||
|
||||
layout = QVBoxLayout(widget)
|
||||
layout.setSpacing(15)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Title
|
||||
title = QLabel("📊 Skill Scanner")
|
||||
title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
|
||||
layout.addWidget(title)
|
||||
|
||||
# Info
|
||||
info = QLabel("Scan skills window to track progression with decimal precision")
|
||||
info.setStyleSheet("color: rgba(255, 255, 255, 150); font-size: 12px;")
|
||||
layout.addWidget(info)
|
||||
|
||||
# Tabs
|
||||
tabs = QTabWidget()
|
||||
tabs.setStyleSheet("""
|
||||
QTabWidget::pane {
|
||||
background-color: rgba(0, 0, 0, 50);
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 20);
|
||||
}
|
||||
QTabBar::tab {
|
||||
background-color: rgba(255, 255, 255, 10);
|
||||
color: rgba(255, 255, 255, 150);
|
||||
padding: 8px 16px;
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
}
|
||||
QTabBar::tab:selected {
|
||||
background-color: rgba(74, 158, 255, 150);
|
||||
color: white;
|
||||
}
|
||||
""")
|
||||
|
||||
# Skills Tab
|
||||
skills_tab = self._create_skills_tab()
|
||||
tabs.addTab(skills_tab, "🎯 Skills")
|
||||
|
||||
# ESI Tab
|
||||
esi_tab = self._create_esi_tab()
|
||||
tabs.addTab(esi_tab, "💉 ESI Scanner")
|
||||
|
||||
# Data Tab
|
||||
data_tab = self._create_data_tab()
|
||||
tabs.addTab(data_tab, "📈 Data")
|
||||
|
||||
layout.addWidget(tabs)
|
||||
|
||||
return widget
|
||||
|
||||
def _create_skills_tab(self):
|
||||
"""Create skills scanning tab."""
|
||||
tab = QWidget()
|
||||
tab.setStyleSheet("background: transparent;")
|
||||
layout = QVBoxLayout(tab)
|
||||
layout.setSpacing(10)
|
||||
layout.setContentsMargins(10, 10, 10, 10)
|
||||
|
||||
# Scan button
|
||||
scan_btn = QPushButton("📸 Scan Skills Window")
|
||||
scan_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #4a9eff;
|
||||
color: white;
|
||||
padding: 15px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #5aafff;
|
||||
}
|
||||
""")
|
||||
scan_btn.clicked.connect(self._scan_skills)
|
||||
layout.addWidget(scan_btn)
|
||||
|
||||
# Status
|
||||
self.status_label = QLabel("Ready to scan")
|
||||
self.status_label.setStyleSheet("color: #666; font-size: 11px;")
|
||||
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(self.status_label)
|
||||
|
||||
# Results table
|
||||
self.skills_table = QTableWidget()
|
||||
self.skills_table.setColumnCount(4)
|
||||
self.skills_table.setHorizontalHeaderLabels(["Skill", "Rank", "Points", "Progress"])
|
||||
self.skills_table.setStyleSheet("""
|
||||
QTableWidget {
|
||||
background-color: rgba(30, 30, 30, 100);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
gridline-color: rgba(255, 255, 255, 20);
|
||||
}
|
||||
QHeaderView::section {
|
||||
background-color: rgba(74, 158, 255, 100);
|
||||
color: white;
|
||||
padding: 6px;
|
||||
border: none;
|
||||
}
|
||||
""")
|
||||
self.skills_table.horizontalHeader().setStretchLastSection(True)
|
||||
layout.addWidget(self.skills_table)
|
||||
|
||||
return tab
|
||||
|
||||
def _create_esi_tab(self):
|
||||
"""Create ESI scanning tab."""
|
||||
tab = QWidget()
|
||||
tab.setStyleSheet("background: transparent;")
|
||||
layout = QVBoxLayout(tab)
|
||||
layout.setSpacing(10)
|
||||
layout.setContentsMargins(10, 10, 10, 10)
|
||||
|
||||
# Instructions
|
||||
instr = QLabel("Hover over an ESI item and click scan")
|
||||
instr.setStyleSheet("color: rgba(255, 255, 255, 150); font-size: 12px;")
|
||||
layout.addWidget(instr)
|
||||
|
||||
# Scan ESI button
|
||||
esi_btn = QPushButton("💉 Scan ESI Item")
|
||||
esi_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #9c27b0;
|
||||
color: white;
|
||||
padding: 15px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #ab47bc;
|
||||
}
|
||||
""")
|
||||
esi_btn.clicked.connect(self._scan_esi)
|
||||
layout.addWidget(esi_btn)
|
||||
|
||||
# ESI Results
|
||||
self.esi_result = QTextEdit()
|
||||
self.esi_result.setPlaceholderText("ESI scan results will appear here...")
|
||||
self.esi_result.setStyleSheet("""
|
||||
QTextEdit {
|
||||
background-color: rgba(30, 30, 30, 100);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
}
|
||||
""")
|
||||
self.esi_result.setMaximumHeight(150)
|
||||
layout.addWidget(self.esi_result)
|
||||
|
||||
layout.addStretch()
|
||||
return tab
|
||||
|
||||
def _create_data_tab(self):
|
||||
"""Create data management tab."""
|
||||
tab = QWidget()
|
||||
tab.setStyleSheet("background: transparent;")
|
||||
layout = QVBoxLayout(tab)
|
||||
layout.setSpacing(10)
|
||||
layout.setContentsMargins(10, 10, 10, 10)
|
||||
|
||||
# Stats
|
||||
self.stats_label = QLabel("No data collected yet")
|
||||
self.stats_label.setStyleSheet("color: rgba(255, 255, 255, 200);")
|
||||
layout.addWidget(self.stats_label)
|
||||
|
||||
# Export button
|
||||
export_btn = QPushButton("📁 Export to CSV")
|
||||
export_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: rgba(255, 255, 255, 20);
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: rgba(255, 255, 255, 30);
|
||||
}
|
||||
""")
|
||||
export_btn.clicked.connect(self._export_data)
|
||||
layout.addWidget(export_btn)
|
||||
|
||||
# View raw data
|
||||
view_btn = QPushButton("📄 View Raw JSON")
|
||||
view_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: rgba(255, 255, 255, 20);
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: rgba(255, 255, 255, 30);
|
||||
}
|
||||
""")
|
||||
view_btn.clicked.connect(self._view_raw_data)
|
||||
layout.addWidget(view_btn)
|
||||
|
||||
layout.addStretch()
|
||||
return tab
|
||||
|
||||
def _scan_skills(self):
|
||||
"""Start skills scan."""
|
||||
self.status_label.setText("📸 Capturing...")
|
||||
self.status_label.setStyleSheet("color: #4a9eff;")
|
||||
|
||||
self.scan_thread = SkillScannerThread()
|
||||
self.scan_thread.result_ready.connect(self._on_skills_scanned)
|
||||
self.scan_thread.error_occurred.connect(self._on_scan_error)
|
||||
self.scan_thread.progress_update.connect(self._on_scan_progress)
|
||||
self.scan_thread.start()
|
||||
|
||||
def _on_scan_progress(self, message):
|
||||
"""Update scan progress."""
|
||||
self.status_label.setText(message)
|
||||
|
||||
def _on_skills_scanned(self, skills_data):
|
||||
"""Handle scanned skills."""
|
||||
self.last_scan = skills_data
|
||||
|
||||
# Save to data store
|
||||
self.data_store.save_skill_snapshot(skills_data)
|
||||
|
||||
# Update table
|
||||
self.skills_table.setRowCount(len(skills_data))
|
||||
for i, (name, data) in enumerate(skills_data.items()):
|
||||
self.skills_table.setItem(i, 0, QTableWidgetItem(name))
|
||||
self.skills_table.setItem(i, 1, QTableWidgetItem(data.get('rank', '')))
|
||||
self.skills_table.setItem(i, 2, QTableWidgetItem(str(data.get('points', 0))))
|
||||
progress = data.get('progress_pct', 0)
|
||||
self.skills_table.setItem(i, 3, QTableWidgetItem(f"{progress:.1f}%"))
|
||||
|
||||
self.status_label.setText(f"✅ Scanned {len(skills_data)} skills")
|
||||
self.status_label.setStyleSheet("color: #4caf50;")
|
||||
self._update_stats()
|
||||
|
||||
def _on_scan_error(self, error):
|
||||
"""Handle scan error."""
|
||||
self.status_label.setText(f"❌ Error: {error}")
|
||||
self.status_label.setStyleSheet("color: #f44336;")
|
||||
|
||||
def _scan_esi(self):
|
||||
"""Scan ESI item."""
|
||||
self.esi_thread = ESIScannerThread()
|
||||
self.esi_thread.result_ready.connect(self._on_esi_scanned)
|
||||
self.esi_thread.error_occurred.connect(self._on_scan_error)
|
||||
self.esi_thread.start()
|
||||
|
||||
def _on_esi_scanned(self, esi_data):
|
||||
"""Handle ESI scan result."""
|
||||
self.data_store.save_esi_data(esi_data)
|
||||
|
||||
# Display results
|
||||
text = f"""
|
||||
🎯 Skill Target: {esi_data['skill_target'] or 'Unknown'}
|
||||
📊 Points to Add: {esi_data['points_to_add']}
|
||||
💰 TT Value: {esi_data['tt_value']:.2f} PED
|
||||
|
||||
Raw Text Preview:
|
||||
{esi_data['full_text'][:200]}...
|
||||
"""
|
||||
self.esi_result.setText(text)
|
||||
self._update_stats()
|
||||
|
||||
def _update_stats(self):
|
||||
"""Update statistics display."""
|
||||
skill_count = len(self.data_store.load_skill_history())
|
||||
esi_count = len(self.data_store.load_esi_history())
|
||||
gain_count = len(self.data_store.load_gain_history())
|
||||
|
||||
self.stats_label.setText(
|
||||
f"📊 Data Points: {skill_count} skill scans | "
|
||||
f"{esi_count} ESI scans | "
|
||||
f"{gain_count} chat gains"
|
||||
)
|
||||
|
||||
def _export_data(self):
|
||||
"""Export data to CSV."""
|
||||
from PyQt6.QtWidgets import QFileDialog
|
||||
|
||||
filepath, _ = QFileDialog.getSaveFileName(
|
||||
None, "Export Skill Data", "skill_data.csv", "CSV Files (*.csv)"
|
||||
)
|
||||
|
||||
if filepath:
|
||||
self.data_store.export_to_csv(filepath)
|
||||
self.status_label.setText(f"✅ Exported to {filepath}")
|
||||
|
||||
def _view_raw_data(self):
|
||||
"""Open raw data folder."""
|
||||
import subprocess
|
||||
import platform
|
||||
|
||||
path = self.data_store.data_dir.absolute()
|
||||
|
||||
if platform.system() == "Windows":
|
||||
subprocess.run(["explorer", str(path)])
|
||||
elif platform.system() == "Darwin":
|
||||
subprocess.run(["open", str(path)])
|
||||
else:
|
||||
subprocess.run(["xdg-open", str(path)])
|
||||
|
||||
def on_hotkey(self):
|
||||
"""Quick scan on hotkey."""
|
||||
self._scan_skills()
|
||||
|
||||
def parse_chat_message(self, message):
|
||||
"""Parse skill gain from chat message."""
|
||||
# Patterns for skill gains
|
||||
patterns = [
|
||||
r'(\w+(?:\s+\w+)*)\s+has\s+improved\s+by\s+(\d+(?:\.\d+)?)\s+points?',
|
||||
r'You\s+gained\s+(\d+(?:\.\d+)?)\s+points?\s+in\s+(\w+(?:\s+\w+)*)',
|
||||
r'(\w+(?:\s+\w+)*)\s+\+(\d+(?:\.\d+)?)',
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, message, re.IGNORECASE)
|
||||
if match:
|
||||
# Extract skill and points
|
||||
groups = match.groups()
|
||||
if len(groups) == 2:
|
||||
# Determine which is skill and which is points
|
||||
skill, points = groups
|
||||
try:
|
||||
points_val = float(points)
|
||||
self.data_store.save_skill_gain(skill, points_val)
|
||||
print(f"[Skill Scanner] Tracked: {skill} +{points_val}")
|
||||
except ValueError:
|
||||
# Might be reversed
|
||||
try:
|
||||
points_val = float(skill)
|
||||
self.data_store.save_skill_gain(points, points_val)
|
||||
except:
|
||||
pass
|
||||
break
|
||||
|
|
@ -2,6 +2,11 @@
|
|||
PyQt6>=6.4.0
|
||||
keyboard>=0.13.5
|
||||
|
||||
# OCR and Image Processing (for Game Reader and Skill Scanner)
|
||||
easyocr>=1.7.0
|
||||
pyautogui>=0.9.54
|
||||
pillow>=10.0.0
|
||||
|
||||
# Optional plugin dependencies
|
||||
# Uncomment if using specific plugins:
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue