From ea9a73c8b41e98176280355bc0adb41093e8df29 Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Fri, 13 Feb 2026 13:26:46 +0000 Subject: [PATCH] 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! --- memory/2026-02-13.md | 67 ++ .../plugins/skill_scanner/__init__.py | 7 + .../plugins/skill_scanner/plugin.py | 673 ++++++++++++++++++ projects/EU-Utility/requirements.txt | 5 + 4 files changed, 752 insertions(+) create mode 100644 memory/2026-02-13.md create mode 100644 projects/EU-Utility/plugins/skill_scanner/__init__.py create mode 100644 projects/EU-Utility/plugins/skill_scanner/plugin.py diff --git a/memory/2026-02-13.md b/memory/2026-02-13.md new file mode 100644 index 0000000..360dda3 --- /dev/null +++ b/memory/2026-02-13.md @@ -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 diff --git a/projects/EU-Utility/plugins/skill_scanner/__init__.py b/projects/EU-Utility/plugins/skill_scanner/__init__.py new file mode 100644 index 0000000..bf789dd --- /dev/null +++ b/projects/EU-Utility/plugins/skill_scanner/__init__.py @@ -0,0 +1,7 @@ +""" +Skill Scanner Plugin for EU-Utility +""" + +from .plugin import SkillScannerPlugin + +__all__ = ["SkillScannerPlugin"] diff --git a/projects/EU-Utility/plugins/skill_scanner/plugin.py b/projects/EU-Utility/plugins/skill_scanner/plugin.py new file mode 100644 index 0000000..3a50135 --- /dev/null +++ b/projects/EU-Utility/plugins/skill_scanner/plugin.py @@ -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 diff --git a/projects/EU-Utility/requirements.txt b/projects/EU-Utility/requirements.txt index f815e66..9aebc28 100644 --- a/projects/EU-Utility/requirements.txt +++ b/projects/EU-Utility/requirements.txt @@ -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: