diff --git a/Borealis.ui b/Borealis.ui deleted file mode 100644 index 4de4046..0000000 --- a/Borealis.ui +++ /dev/null @@ -1,114 +0,0 @@ - - - ProjectBorealis - - - - 0 - 0 - 925 - 698 - - - - Project Borealis - Flyff Information Overlay - - - false - - - - - - 10 - 10 - 260 - 41 - - - - - Microsoft YaHei UI Light - 24 - 75 - true - - - - Project Borealis - - - - - - 0 - 391 - 921 - 271 - - - - QFrame::StyledPanel - - - QFrame::Plain - - - - - - 10 - 50 - 211 - 31 - - - - - Microsoft YaHei UI Light - 12 - - - - Flyff Information Overlay - - - - - - 250 - 150 - 401 - 211 - - - - 0 - - - - Tab 1 - - - - - Tab 2 - - - - - - - 30 - 280 - 113 - 20 - - - - - - - - - diff --git a/Experimental/accept_reject_example.py b/Experimental/accept_reject_example.py deleted file mode 100644 index fd6f028..0000000 --- a/Experimental/accept_reject_example.py +++ /dev/null @@ -1,192 +0,0 @@ -import signal - -from qtpy import QtWidgets - -from OdenGraphQt import BaseNode, NodeGraph -from OdenGraphQt.constants import PortTypeEnum -from OdenGraphQt.qgraphics.node_base import NodeItem - - -class PublishWriteNodeItem(NodeItem): - def _align_widgets_horizontal(self, v_offset: int): - if not self._widgets: - return - - rect = self.boundingRect() - y = rect.y() + v_offset - for widget in self._widgets.values(): - if not widget.isVisible(): - continue - - widget_rect = widget.boundingRect() - x = rect.center().x() - (widget_rect.width() / 2) - widget.widget().setTitleAlign('center') - widget.setPos(x, y) - y += widget_rect.height() - - -class PrevNextNode(BaseNode): - __identifier__ = "action" - NODE_NAME = "Action Node" - - def __init__(self): - super().__init__() - - # create an input port. - input_port = self.add_input("_prev", color=(180, 80, 0), multi_input=False) - # create an output port. - output_port = self.add_output("_next", multi_output=False) - - input_port.port_item.set_allow_partial_match_constraint(True) - input_port.port_item.set_accept_constraint( - port_name=output_port.name(), - port_type=PortTypeEnum.OUT.value, - node_identifier=self.__identifier__, - ) - - output_port.port_item.set_allow_partial_match_constraint(True) - output_port.port_item.set_accept_constraint( - port_name=input_port.name(), - port_type=PortTypeEnum.IN.value, - node_identifier=self.__identifier__, - ) - - -class IngredientNode(BaseNode): - __identifier__ = "ingredient" - - -class SpamNode(IngredientNode): - __identifier__ = "spam" - NODE_NAME = "Spam" - - def __init__(self): - super().__init__() - spam_port = self.add_output( - "spam", - color=(50, 150, 222), - ) - - -class EggNode(IngredientNode): - __identifier__ = "egg" - NODE_NAME = "Egg" - - def __init__(self): - super().__init__() - egg_port = self.add_output( - "egg", - color=(50, 150, 222), - ) - - -class MealNode(BaseNode): - NODE_NAME = "Meal" - - def __init__(self): - super().__init__() - spam_port = self.add_input("spam", color=(222, 15, 0), multi_input=False) - spam_port.port_item.set_reject_constraint( - port_name="egg", - port_type=PortTypeEnum.OUT.value, - node_identifier="egg", - ) - egg_port = self.add_input("egg", color=(222, 15, 0), multi_input=False) - egg_port.port_item.set_reject_constraint( - port_name="spam", - port_type=PortTypeEnum.OUT.value, - node_identifier="spam", - ) - - -class BasePublishNode(PrevNextNode): - __identifier__ = "publish" - allow_multiple_write = False - - def __init__(self): - super().__init__() - port = self.add_output( - "write", - color=(184, 150, 0), - multi_output=self.allow_multiple_write, - ) - port.port_item.set_accept_constraint( - port_name="src", - port_type=PortTypeEnum.IN.value, - node_identifier="publish", - ) - - -class PubNode(PrevNextNode): - __identifier__ = "pub" - NODE_NAME = "Not Tavern" - - -class PublishFileActionNode(BasePublishNode): - NODE_NAME = "Publish File" - allow_multiple_write = False - - -class PublishFileToManyActionNode(BasePublishNode): - NODE_NAME = "Publish File to Many" - allow_multiple_write = True - - -class PublishWriteNode(BaseNode): - __identifier__ = "publish" - NODE_NAME = "Publish Write" - - def __init__(self): - super().__init__(qgraphics_item=PublishWriteNodeItem) - self.set_color(164, 130, 0) - self.add_text_input("write", "Path:") - - port = self.add_input("src", multi_input=False) - port.port_item.set_accept_constraint( - port_name="write", - port_type=PortTypeEnum.OUT.value, - node_identifier="publish", - ) - - -if __name__ == '__main__': - - # handle SIGINT to make the app terminate on CTRL+C - signal.signal(signal.SIGINT, signal.SIG_DFL) - - app = QtWidgets.QApplication([]) - - # create graph controller. - graph = NodeGraph() - - # set up context menu for the node graph. - graph.set_context_menu_from_file('../examples/hotkeys/hotkeys.json') - - # registered example nodes. - graph.register_nodes([ - SpamNode, - EggNode, - MealNode, - PubNode, - PublishFileActionNode, - PublishFileToManyActionNode, - PublishWriteNode, - ]) - - # add nodes - graph.add_node(SpamNode()) - graph.add_node(EggNode()) - graph.add_node(MealNode()) - graph.add_node(PubNode()) - graph.add_node(PublishFileToManyActionNode()) - graph.add_node(PublishFileActionNode()) - graph.add_node(PublishWriteNode()) - graph.auto_layout_nodes() - graph.clear_selection() - - # show the node graph widget. - graph_widget = graph.widget - graph_widget.resize(1100, 800) - graph_widget.show() - - app.exec_() \ No newline at end of file diff --git a/Experimental/experimental_nodes.py b/Experimental/experimental_nodes.py deleted file mode 100644 index d88b197..0000000 --- a/Experimental/experimental_nodes.py +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/env python3 -""" -Standalone NodeGraphQT Math Node Example - -This example defines a custom "Math Node" that: - - Uses two text inputs for numeric operands (via add_text_input) - - Provides a combo box for operator selection (via add_combo_menu) - - Offers a checkbox to enable/disable the operation (via add_checkbox) - - Computes a result and updates its title accordingly. -""" - -from NodeGraphQt import NodeGraph, BaseNode - -class MathNode(BaseNode): - """ - Math Node: - - Operands: Two text inputs (Operand 1 and Operand 2) - - Operator: Combo box to select 'Add', 'Subtract', 'Multiply', or 'Divide' - - Enable: Checkbox to enable/disable the math operation - - Output: Result of the math operation (if enabled) - """ - __identifier__ = 'example.math' - NODE_NAME = 'Math Node' - - def __init__(self): - super(MathNode, self).__init__() - - # Add two text inputs for operands. - self.add_text_input('operand1', 'Operand 1', text='10') - self.add_text_input('operand2', 'Operand 2', text='5') - - # Add a combo box for operator selection. - self.add_combo_menu('operator', 'Operator', items=['Add', 'Subtract', 'Multiply', 'Divide']) - - # Add a checkbox to enable/disable the operation. - self.add_checkbox('enable', 'Enable Operation', state=True) - - # Add an output port to transmit the result. - self.add_output('Result') - - self.value = 0 - self.set_name("Math Node") - self.process_input() - - def process_input(self): - """ - Gather values from the widgets, perform the math operation if enabled, - update the node title, and send the result to connected nodes. - """ - try: - op1 = float(self.get_property('operand1')) - except (ValueError, TypeError): - op1 = 0.0 - try: - op2 = float(self.get_property('operand2')) - except (ValueError, TypeError): - op2 = 0.0 - - operator = self.get_property('operator') - enable = self.get_property('enable') - - if enable: - if operator == 'Add': - result = op1 + op2 - elif operator == 'Subtract': - result = op1 - op2 - elif operator == 'Multiply': - result = op1 * op2 - elif operator == 'Divide': - result = op1 / op2 if op2 != 0 else 0.0 - else: - result = 0.0 - else: - result = 0.0 - - self.value = result - self.set_name(f"Result: {result}") - - output_port = self.output(0) - if output_port and output_port.connected_ports(): - for connected_port in output_port.connected_ports(): - connected_node = connected_port.node() - if hasattr(connected_node, 'receive_data'): - connected_node.receive_data(result, source_port_name='Result') - - -if __name__ == '__main__': - import sys - try: - from PySide2.QtWidgets import QApplication - except ImportError: - from PySide6.QtWidgets import QApplication - - app = QApplication(sys.argv) - graph = NodeGraph() - graph.register_node(MathNode) - node = graph.create_node('example.math.MathNode', name='Math Node') - node.set_pos(100, 100) - graph.widget.resize(1200, 800) - graph.widget.setWindowTitle("NodeGraphQT Math Node Demo") - graph.widget.show() - sys.exit(app.exec_()) diff --git a/Legacy/Orphaned Code/data_collector.py b/Legacy/Orphaned Code/data_collector.py deleted file mode 100644 index a191645..0000000 --- a/Legacy/Orphaned Code/data_collector.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python3 -""" -Collector Process: -- Runs the OCR engine. -- Updates OCR data every 0.5 seconds. -- Exposes the latest data via an HTTP API using Flask. - -This version splits the HP, MP, and FP values into 'current' and 'total' before -sending them via the API, so the Character Status Node can ingest them directly. -""" - -import time -import threading -from flask import Flask, jsonify - -app = Flask(__name__) - -# Global variable to hold the latest stats (HP, MP, FP, EXP) -latest_data = { - "hp_current": 0, - "hp_total": 0, - "mp_current": 0, - "mp_total": 0, - "fp_current": 0, - "fp_total": 0, - "exp": 0.0000 -} - -def ocr_collector(): - """ - This function simulates the OCR process. - Replace the code below with your actual OCR logic. - """ - global latest_data - counter = 0 - while True: - # Simulate updating stats: - hp_current = 50 + counter % 10 - hp_total = 100 - mp_current = 30 + counter % 5 - mp_total = 50 - fp_current = 20 # fixed, for example - fp_total = 20 - exp_val = round(10.0 + (counter * 0.1), 4) - - latest_data = { - "hp_current": hp_current, - "hp_total": hp_total, - "mp_current": mp_current, - "mp_total": mp_total, - "fp_current": fp_current, - "fp_total": fp_total, - "exp": exp_val - } - - counter += 1 - time.sleep(0.5) - -@app.route('/data') -def get_data(): - """Return the latest OCR data as JSON.""" - return jsonify(latest_data) - -if __name__ == '__main__': - # Start the OCR collector in a background thread. - collector_thread = threading.Thread(target=ocr_collector) - collector_thread.daemon = True - collector_thread.start() - - # Run the Flask app on localhost:5000. - app.run(host="127.0.0.1", port=5000) diff --git a/Legacy/borealis_overlay.py b/Legacy/borealis_overlay.py deleted file mode 100644 index 74b9d8f..0000000 --- a/Legacy/borealis_overlay.py +++ /dev/null @@ -1,542 +0,0 @@ -#!/usr/bin/env python3 - -import sys -import time -import re -import numpy as np -import cv2 -import pytesseract - -try: - import winsound - HAS_WINSOUND = True -except ImportError: - HAS_WINSOUND = False - -from PyQt5.QtWidgets import QApplication, QWidget -from PyQt5.QtCore import Qt, QRect, QPoint, QTimer -from PyQt5.QtGui import QPainter, QPen, QColor, QFont -from PIL import Image, ImageGrab, ImageFilter - -from rich.console import Console, Group -from rich.table import Table -from rich.progress import Progress, BarColumn, TextColumn -from rich.text import Text -from rich.live import Live - -# ============================================================================= -# Global Config -# ============================================================================= - -pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe" - -POLLING_RATE_MS = 500 -MAX_DATA_POINTS = 8 - -# We still use these defaults for Region size. -DEFAULT_WIDTH = 180 -DEFAULT_HEIGHT = 130 -HANDLE_SIZE = 8 -LABEL_HEIGHT = 20 - -GREEN_HEADER_STYLE = "bold green" - -BEEP_INTERVAL_SECONDS = 1.0 # Only beep once every 1 second - -# STATUS BAR AUTO-LOCATOR LOGIC (WILL BE BUILT-OUT TO BE MORE ROBUST LATER) -TEMPLATE_PATH = "G:\\Nextcloud\\Projects\\Scripting\\bars_template.png" # Path to your bars template file -MATCH_THRESHOLD = 0.4 # The correlation threshold to consider a "good" match - -# ============================================================================= -# Helper Functions -# ============================================================================= - -def beep_hp_warning(): - """ - Only beep if enough time has elapsed since the last beep (BEEP_INTERVAL_SECONDS). - """ - current_time = time.time() - if (beep_hp_warning.last_beep_time is None or - (current_time - beep_hp_warning.last_beep_time >= BEEP_INTERVAL_SECONDS)): - - beep_hp_warning.last_beep_time = current_time - if HAS_WINSOUND: - # frequency=376 Hz, duration=100 ms - winsound.Beep(376, 100) - else: - # Attempt terminal bell - print('\a', end='') - -beep_hp_warning.last_beep_time = None - - -def locate_bars_opencv(template_path, threshold=MATCH_THRESHOLD): - """ - Attempt to locate the bars via OpenCV template matching: - 1) Grab the full screen using PIL.ImageGrab. - 2) Convert to NumPy array in BGR format for cv2. - 3) Load template from `template_path`. - 4) Use cv2.matchTemplate to find the best match location. - 5) If max correlation > threshold, return (x, y, w, h). - 6) Else return None. - """ - # 1) Capture full screen - screenshot_pil = ImageGrab.grab() - screenshot_np = np.array(screenshot_pil) # shape (H, W, 4) possibly - # Convert RGBA or RGB to BGR - screenshot_bgr = cv2.cvtColor(screenshot_np, cv2.COLOR_RGB2BGR) - - # 2) Load template from file - template_bgr = cv2.imread(template_path, cv2.IMREAD_COLOR) - if template_bgr is None: - print(f"[WARN] Could not load template file: {template_path}") - return None - - # 3) Template matching - result = cv2.matchTemplate(screenshot_bgr, template_bgr, cv2.TM_CCOEFF_NORMED) - - # 4) Find best match - min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result) - # template width/height - th, tw, _ = template_bgr.shape - - if max_val >= threshold: - # max_loc is top-left corner of the best match - found_x, found_y = max_loc - return (found_x, found_y, tw, th) - else: - return None - - -def format_duration(seconds): - if seconds is None: - return "???" - seconds = int(seconds) - hours = seconds // 3600 - leftover = seconds % 3600 - mins = leftover // 60 - secs = leftover % 60 - if hours > 0: - return f"{hours}h {mins}m {secs}s" - else: - return f"{mins}m {secs}s" - - -def sanitize_experience_string(raw_text): - text_no_percent = raw_text.replace('%', '') - text_no_spaces = text_no_percent.replace(' ', '') - cleaned = re.sub(r'[^0-9\.]', '', text_no_spaces) - match = re.search(r'\d+(?:\.\d+)?', cleaned) - if not match: - return None - val = float(match.group(0)) - if val < 0: - val = 0 - elif val > 100: - val = 100 - return round(val, 4) - - -def format_experience_value(value): - if value < 0: - value = 0 - elif value > 100: - value = 100 - float_4 = round(value, 4) - raw_str = f"{float_4:.4f}" - int_part, dec_part = raw_str.split('.') - if int_part == "100": - pass - elif len(int_part) == 1 and int_part != "0": - int_part = "0" + int_part - elif int_part == "0": - int_part = "00" - return f"{int_part}.{dec_part}" - -# ----------------------------------------------------------------------------- -# Region Class -# ----------------------------------------------------------------------------- -class Region: - """ - Defines a draggable/resizable screen region for OCR capture. - """ - def __init__(self, x, y, label="Region", color=QColor(0,0,255)): - self.x = x - self.y = y - self.w = DEFAULT_WIDTH - self.h = DEFAULT_HEIGHT - self.label = label - self.color = color - self.visible = True - self.data = "" - - def rect(self): - return QRect(self.x, self.y, self.w, self.h) - - def label_rect(self): - return QRect(self.x, self.y - LABEL_HEIGHT, self.w, LABEL_HEIGHT) - - def resize_handles(self): - return [ - QRect(self.x - HANDLE_SIZE // 2, self.y - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE), - QRect(self.x + self.w - HANDLE_SIZE // 2, self.y - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE), - QRect(self.x - HANDLE_SIZE // 2, self.y + self.h - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE), - QRect(self.x + self.w - HANDLE_SIZE // 2, self.y + self.h - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE), - ] - -# ----------------------------------------------------------------------------- -# OverlayCanvas Class -# ----------------------------------------------------------------------------- -class OverlayCanvas(QWidget): - """ - Renders the overlay & handles region dragging/resizing. - """ - def __init__(self, regions, parent=None): - super().__init__(parent) - self.regions = regions - self.edit_mode = True - self.selected_region = None - self.selected_handle = None - self.drag_offset = QPoint() - - def paintEvent(self, event): - painter = QPainter(self) - for region in self.regions: - if region.visible: - pen = QPen(region.color) - pen.setWidth(3) - painter.setPen(pen) - painter.drawRect(region.x, region.y, region.w, region.h) - - painter.setFont(QFont("Arial", 12, QFont.Bold)) - painter.setPen(region.color) - painter.drawText(region.x, region.y - 5, region.label) - - if self.edit_mode: - for handle in region.resize_handles(): - painter.fillRect(handle, region.color) - - def mousePressEvent(self, event): - if not self.edit_mode: - return - if event.button() == Qt.LeftButton: - for region in reversed(self.regions): - for i, handle in enumerate(region.resize_handles()): - if handle.contains(event.pos()): - self.selected_region = region - self.selected_handle = i - return - if region.label_rect().contains(event.pos()): - self.selected_region = region - self.selected_handle = None - self.drag_offset = event.pos() - QPoint(region.x, region.y) - return - if region.rect().contains(event.pos()): - self.selected_region = region - self.selected_handle = None - self.drag_offset = event.pos() - QPoint(region.x, region.y) - return - - def mouseMoveEvent(self, event): - if not self.edit_mode or self.selected_region is None: - return - - if self.selected_handle is None: - self.selected_region.x = event.x() - self.drag_offset.x() - self.selected_region.y = event.y() - self.drag_offset.y() - else: - sr = self.selected_region - if self.selected_handle == 0: # top-left - sr.w += sr.x - event.x() - sr.h += sr.y - event.y() - sr.x = event.x() - sr.y = event.y() - elif self.selected_handle == 1: # top-right - sr.w = event.x() - sr.x - sr.h += sr.y - event.y() - sr.y = event.y() - elif self.selected_handle == 2: # bottom-left - sr.w += sr.x - event.x() - sr.h = event.y() - sr.y - sr.x = event.x() - elif self.selected_handle == 3: # bottom-right - sr.w = event.x() - sr.x - sr.h = event.y() - sr.y - - sr.w = max(sr.w, 10) - sr.h = max(sr.h, 10) - - self.update() - - def mouseReleaseEvent(self, event): - if not self.edit_mode: - return - if event.button() == Qt.LeftButton: - self.selected_region = None - self.selected_handle = None - -# ----------------------------------------------------------------------------- -# BorealisOverlay Class -# ----------------------------------------------------------------------------- -class BorealisOverlay(QWidget): - """ - Single Region Overlay for Player Stats (HP/MP/FP/EXP) with: - - Automatic location via OpenCV template matching at startup - - OCR scanning - - Low-HP beep - - Rich Live updates in terminal - """ - def __init__(self, live=None): - super().__init__() - screen_geo = QApplication.primaryScreen().geometry() - self.setGeometry(screen_geo) - self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) - self.setAttribute(Qt.WA_TranslucentBackground, True) - - # Try to find the bars automatically - # If found => use that location, else default - initial_x, initial_y = 250, 50 - region_w, region_h = DEFAULT_WIDTH, DEFAULT_HEIGHT - - match_result = locate_bars_opencv(TEMPLATE_PATH, MATCH_THRESHOLD) - if match_result is not None: - found_x, found_y, w, h = match_result - print(f"Character Status Located at {found_x}, {found_y} with confidence >= {MATCH_THRESHOLD}.") - initial_x, initial_y = found_x, found_y - # Optionally override region size with template size - region_w, region_h = w, h - else: - print("Could not auto-locate the character status page. Set your theme to Masquerade and Interface Scale to 140%, and browser zoom level to 110%. Using default region.") - - region = Region(initial_x, initial_y, label="Character Status") - region.w = region_w - region.h = region_h - self.regions = [region] - - self.canvas = OverlayCanvas(self.regions, self) - self.canvas.setGeometry(self.rect()) - - # Tesseract - self.engine = pytesseract - - # Keep history of EXP data - self.points = [] - - self.live = live - - # Timer for periodic OCR scanning - self.timer = QTimer(self) - self.timer.timeout.connect(self.collect_ocr_data) - self.timer.start(POLLING_RATE_MS) - - def set_live(self, live): - self.live = live - - def collect_ocr_data(self): - for region in self.regions: - if region.visible: - screenshot = ImageGrab.grab( - bbox=(region.x, region.y, region.x + region.w, region.y + region.h) - ) - processed = self.preprocess_image(screenshot) - text = pytesseract.image_to_string(processed, config='--psm 4 --oem 1') - region.data = text.strip() - - if self.live is not None: - renderable = self.build_renderable() - self.live.update(renderable) - - def preprocess_image(self, image): - gray = image.convert("L") - scaled = gray.resize((gray.width * 3, gray.height * 3)) - thresh = scaled.point(lambda p: p > 200 and 255) - return thresh.filter(ImageFilter.MedianFilter(3)) - - def parse_all_stats(self, raw_text): - raw_lines = raw_text.splitlines() - lines = [l.strip() for l in raw_lines if l.strip()] - stats_dict = { - "hp": (0,1), - "mp": (0,1), - "fp": (0,1), - "exp": None - } - if len(lines) < 4: - return stats_dict - - hp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[0]) - if hp_match: - stats_dict["hp"] = (int(hp_match.group(1)), int(hp_match.group(2))) - - mp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[1]) - if mp_match: - stats_dict["mp"] = (int(mp_match.group(1)), int(mp_match.group(2))) - - fp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[2]) - if fp_match: - stats_dict["fp"] = (int(fp_match.group(1)), int(fp_match.group(2))) - - exp_val = sanitize_experience_string(lines[3]) - stats_dict["exp"] = exp_val - return stats_dict - - def update_points(self, new_val): - now = time.time() - if self.points: - _, last_v = self.points[-1] - if abs(new_val - last_v) < 1e-6: - return - if new_val < last_v: - self.points.clear() - self.points.append((now, new_val)) - if len(self.points) > MAX_DATA_POINTS: - self.points.pop(0) - - def compute_time_to_100(self): - n = len(self.points) - if n < 2: - return None - first_t, first_v = self.points[0] - last_t, last_v = self.points[-1] - diff_v = last_v - first_v - if diff_v <= 0: - return None - - steps = n - 1 - total_time = last_t - first_t - if total_time <= 0: - return None - - avg_change = diff_v / steps - remain = 100.0 - last_v - if remain <= 0: - return None - - avg_time = total_time / steps - rate_per_s = avg_change / avg_time if avg_time > 0 else 0 - if rate_per_s <= 0: - return None - - return int(remain / rate_per_s) - - def build_renderable(self): - raw_text = self.regions[0].data - stats = self.parse_all_stats(raw_text) - hp_cur, hp_max = stats["hp"] - mp_cur, mp_max = stats["mp"] - fp_cur, fp_max = stats["fp"] - exp_val = stats["exp"] - - # HP beep logic - if hp_max > 0: - hp_ratio = hp_cur / hp_max - if 0 < hp_ratio <= 0.40: - beep_hp_warning() - - if exp_val is not None: - self.update_points(exp_val) - current_exp = self.points[-1][1] if self.points else 0.0 - - # Title - title_text = Text("Project Borealis\n", style="bold white") - subtitle_text = Text("Flyff Information Overlay\n\n", style="dim") - - # HP / MP / FP bars - bar_progress = Progress( - "{task.description}", - BarColumn(bar_width=30), - TextColumn(" {task.completed}/{task.total} ({task.percentage:>5.2f}%)"), - transient=False, - auto_refresh=False - ) - bar_progress.add_task("[bold red]HP[/bold red]", total=hp_max, completed=hp_cur, - style="red", complete_style="red") - bar_progress.add_task("[bold blue]MP[/bold blue]", total=mp_max, completed=mp_cur, - style="blue", complete_style="blue") - bar_progress.add_task("[bold green]FP[/bold green]", total=fp_max, completed=fp_cur, - style="green", complete_style="green") - bar_progress.refresh() - - # Historical EXP table - table = Table(show_header=True, header_style=GREEN_HEADER_STYLE, style=None) - table.add_column("Historical EXP", justify="center", style="green") - table.add_column("Time Since Last Kill", justify="center", style="green") - table.add_column("Average EXP Per Kill", justify="center", style="green") - table.add_column("Average Time Between Kills", justify="center", style="green") - - n = len(self.points) - if n == 0: - table.add_row("N/A", "N/A", "N/A", "N/A") - elif n == 1: - _, v0 = self.points[0] - exp_str = f"[green]{format_experience_value(v0)}%[/green]" - table.add_row(exp_str, "N/A", "N/A", "N/A") - else: - for i in range(1, n): - t_cur, v_cur = self.points[i] - t_prev, v_prev = self.points[i - 1] - delta_v = v_cur - v_prev - delta_str = f"{delta_v:+.4f}%" - exp_main = format_experience_value(v_cur) - exp_str = f"[green]{exp_main}%[/green] [dim]({delta_str})[/dim]" - - delta_t = t_cur - t_prev - t_since_str = f"{delta_t:.1f}s" - - diff_v = v_cur - self.points[0][1] - steps = i - avg_exp_str = f"{diff_v/steps:.4f}%" - - total_time = t_cur - self.points[0][0] - avg_kill_time = total_time / steps - avg_time_str = f"{avg_kill_time:.1f}s" - - table.add_row(exp_str, t_since_str, avg_exp_str, avg_time_str) - - # Predicted Time to Level - secs_left = self.compute_time_to_100() - time_str = format_duration(secs_left) - - time_bar = Progress( - TextColumn("[bold white]Predicted Time to Level:[/bold white] "), - BarColumn(bar_width=30, complete_style="magenta"), - TextColumn(" [green]{task.percentage:>5.2f}%[/green] "), - TextColumn(f"[magenta]{time_str}[/magenta] until 100%", justify="right"), - transient=False, - auto_refresh=False - ) - time_bar.add_task("", total=100, completed=current_exp) - time_bar.refresh() - - return Group( - title_text, - subtitle_text, - bar_progress, - table, - time_bar - ) - -# ----------------------------------------------------------------------------- -# main -# ----------------------------------------------------------------------------- -def main(): - """ - 1) Attempt to locate HP/MP/FP/Exp bars using OpenCV template matching. - 2) Position overlay region accordingly if found, else default. - 3) Start PyQt, periodically OCR the region, update Rich Live in terminal. - """ - app = QApplication(sys.argv) - window = BorealisOverlay() - window.setWindowTitle("Project Borealis Overlay (HP/MP/FP/EXP)") - window.show() - - console = Console() - - with Live(console=console, refresh_per_second=4) as live: - window.set_live(live) - exit_code = app.exec_() - - sys.exit(exit_code) - -if __name__ == "__main__": - main() diff --git a/data_collector_v2.py b/Modules/data_collector.py similarity index 100% rename from data_collector_v2.py rename to Modules/data_collector.py diff --git a/screen_overlays.py b/Modules/overlay_helpers.py similarity index 100% rename from screen_overlays.py rename to Modules/overlay_helpers.py diff --git a/Nodes/__pycache__/character_status_node.cpython-312.pyc b/Nodes/__pycache__/character_status_node.cpython-312.pyc deleted file mode 100644 index 02e454c..0000000 Binary files a/Nodes/__pycache__/character_status_node.cpython-312.pyc and /dev/null differ diff --git a/Nodes/__pycache__/custom_ports_node.cpython-312.pyc b/Nodes/__pycache__/custom_ports_node.cpython-312.pyc deleted file mode 100644 index 5ea85b2..0000000 Binary files a/Nodes/__pycache__/custom_ports_node.cpython-312.pyc and /dev/null differ diff --git a/Nodes/custom_ports_node.py b/Nodes/custom_ports_node.py deleted file mode 100644 index ac3eb43..0000000 --- a/Nodes/custom_ports_node.py +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/python -from qtpy import QtCore, QtGui - -from OdenGraphQt import BaseNode - - -def draw_triangle_port(painter, rect, info): - """ - Custom paint function for drawing a Triangle shaped port. - - Args: - painter (QtGui.QPainter): painter object. - rect (QtCore.QRectF): port rect used to describe parameters - needed to draw. - info (dict): information describing the ports current state. - { - 'port_type': 'in', - 'color': (0, 0, 0), - 'border_color': (255, 255, 255), - 'multi_connection': False, - 'connected': False, - 'hovered': False, - } - """ - painter.save() - - size = int(rect.height() / 2) - triangle = QtGui.QPolygonF() - triangle.append(QtCore.QPointF(-size, size)) - triangle.append(QtCore.QPointF(0.0, -size)) - triangle.append(QtCore.QPointF(size, size)) - - transform = QtGui.QTransform() - transform.translate(rect.center().x(), rect.center().y()) - port_poly = transform.map(triangle) - - # mouse over port color. - if info['hovered']: - color = QtGui.QColor(14, 45, 59) - border_color = QtGui.QColor(136, 255, 35) - # port connected color. - elif info['connected']: - color = QtGui.QColor(195, 60, 60) - border_color = QtGui.QColor(200, 130, 70) - # default port color - else: - color = QtGui.QColor(*info['color']) - border_color = QtGui.QColor(*info['border_color']) - - pen = QtGui.QPen(border_color, 1.8) - pen.setJoinStyle(QtCore.Qt.PenJoinStyle.MiterJoin) - - painter.setPen(pen) - painter.setBrush(color) - painter.drawPolygon(port_poly) - - painter.restore() - - -def draw_square_port(painter, rect, info): - """ - Custom paint function for drawing a Square shaped port. - - Args: - painter (QtGui.QPainter): painter object. - rect (QtCore.QRectF): port rect used to describe parameters - needed to draw. - info (dict): information describing the ports current state. - { - 'port_type': 'in', - 'color': (0, 0, 0), - 'border_color': (255, 255, 255), - 'multi_connection': False, - 'connected': False, - 'hovered': False, - } - """ - painter.save() - - # mouse over port color. - if info['hovered']: - color = QtGui.QColor(14, 45, 59) - border_color = QtGui.QColor(136, 255, 35, 255) - # port connected color. - elif info['connected']: - color = QtGui.QColor(195, 60, 60) - border_color = QtGui.QColor(200, 130, 70) - # default port color - else: - color = QtGui.QColor(*info['color']) - border_color = QtGui.QColor(*info['border_color']) - - pen = QtGui.QPen(border_color, 1.8) - pen.setJoinStyle(QtCore.Qt.PenJoinStyle.MiterJoin) - - painter.setPen(pen) - painter.setBrush(color) - painter.drawRect(rect) - - painter.restore() - - -class CustomPortsNode(BaseNode): - """ - example test node with custom shaped ports. - """ - - # set a unique node identifier. - __identifier__ = 'nodes.custom.ports' - - # set the initial default node name. - NODE_NAME = 'node' - - def __init__(self): - super(CustomPortsNode, self).__init__() - - # create input and output port. - self.add_input('in', color=(200, 10, 0)) - self.add_output('default') - self.add_output('square', painter_func=draw_square_port) - self.add_output('triangle', painter_func=draw_triangle_port) \ No newline at end of file diff --git a/Project_Borealis.zip b/Project_Borealis.zip new file mode 100644 index 0000000..22db142 Binary files /dev/null and b/Project_Borealis.zip differ diff --git a/QML/background_grid.qml b/QML/background_grid.qml new file mode 100644 index 0000000..f21574d --- /dev/null +++ b/QML/background_grid.qml @@ -0,0 +1,78 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Shapes 1.15 +import QtQuick.Window 2.15 + +Item { + id: root + width: Screen.width + height: Screen.height + + // Grid overlay is enabled at startup. + property bool editMode: true + + // Blue gradient background (edges fading inward) with stops shifted inward. + Rectangle { + id: gradientBackground + width: parent.width + height: parent.height + opacity: 0.5 + gradient: Gradient { + // Shifted stops: outer stops moved to 0.1 and 0.9, inner stops to 0.4 and 0.6. + GradientStop { position: 0.1; color: Qt.rgba(0, 100/255, 255/255, 0.5) } + GradientStop { position: 0.4; color: Qt.rgba(0, 50/255, 180/255, 0.2) } + GradientStop { position: 0.5; color: Qt.rgba(0, 0, 0, 0.0) } + GradientStop { position: 0.6; color: Qt.rgba(0, 50/255, 180/255, 0.2) } + GradientStop { position: 0.9; color: Qt.rgba(0, 100/255, 255/255, 0.5) } + } + visible: editMode // Only show the gradient in edit mode + } + + // Top & Bottom fade remains unchanged. + Rectangle { + id: topBottomGradient + width: parent.width + height: parent.height + opacity: 0.3 + gradient: Gradient { + orientation: Gradient.Vertical + GradientStop { position: 0.0; color: Qt.rgba(0, 100/255, 255/255, 0.4) } + GradientStop { position: 0.3; color: Qt.rgba(0, 50/255, 180/255, 0.1) } + GradientStop { position: 0.5; color: Qt.rgba(0, 0, 0, 0.0) } + GradientStop { position: 0.7; color: Qt.rgba(0, 50/255, 180/255, 0.1) } + GradientStop { position: 1.0; color: Qt.rgba(0, 100/255, 255/255, 0.4) } + } + visible: editMode + } + + // Full-Screen Dynamic Grid with 10% increased transparency (grid lines at 0.3 opacity). + Canvas { + id: gridCanvas + width: parent.width + height: parent.height + onPaint: { + var ctx = getContext("2d"); + ctx.clearRect(0, 0, width, height); + ctx.strokeStyle = "rgba(255, 255, 255, 0.3)"; // Reduced opacity from 0.4 to 0.3. + ctx.lineWidth = 1; + + var step = 120; // Grid spacing remains unchanged. + + for (var x = 0; x < width; x += step) { + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, height); + ctx.stroke(); + } + for (var y = 0; y < height; y += step) { + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(width, y); + ctx.stroke(); + } + } + Component.onCompleted: requestPaint() + onVisibleChanged: requestPaint() + visible: editMode // Hide when edit mode is off. + } +} diff --git a/borealis_transparent.py b/borealis_transparent.py index 053a6fa..b8bc0a4 100644 --- a/borealis_transparent.py +++ b/borealis_transparent.py @@ -1,104 +1,187 @@ -import os +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + import sys import pkgutil import importlib import inspect -from PyQt5.QtWidgets import QApplication, QMainWindow, QAction, QMenu, QUndoStack -from PyQt5.QtCore import Qt, QTimer, QRect -from PyQt5.QtGui import QColor, QPainter, QPen, QBrush +import types +from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QWidget +from PyQt5.QtCore import Qt, QUrl, QTimer +from PyQt5.QtGui import QGuiApplication +from PyQt5.QtQuick import QQuickView + +# OdenGraphQt Fix: Monkey-patch QUndoStack +import OdenGraphQt.base.graph as base_graph +from PyQt5 import QtWidgets +base_graph.QtGui.QUndoStack = QtWidgets.QUndoStack + +import OdenGraphQt.base.commands as base_commands +_original_redo = base_commands.NodesRemovedCmd.redo +_original_undo = base_commands.NodesRemovedCmd.undo + +def _patched_redo(self): + try: + _original_redo(self) + except TypeError as e: + if "unexpected type" in str(e) and hasattr(self, 'node'): + node_ids = [] + if isinstance(self.node, list): + node_ids = [getattr(n, 'id', str(n)) for n in self.node] + else: + node_ids = [getattr(self.node, 'id', str(self.node))] + self.graph.nodes_deleted.emit(node_ids) + else: + raise + +def _patched_undo(self): + try: + _original_undo(self) + except TypeError as e: + if "unexpected type" in str(e) and hasattr(self, 'node'): + node_ids = [] + if isinstance(self.node, list): + node_ids = [getattr(n, 'id', str(n)) for n in self.node] + else: + node_ids = [getattr(self.node, 'id', str(self.node))] + self.graph.nodes_deleted.emit(node_ids) + else: + raise + +base_commands.NodesRemovedCmd.redo = _patched_redo +base_commands.NodesRemovedCmd.undo = _patched_undo + +# OdenGraphQt Transparent Viewer +from OdenGraphQt.widgets.viewer import NodeViewer + +class TransparentViewer(NodeViewer): + """A NodeViewer that does not paint anything in drawBackground() -> Fully transparent.""" + def drawBackground(self, painter, rect): + pass # Do nothing, ensuring transparency. + +# NodeGraph & Node Import Helpers from OdenGraphQt import NodeGraph, BaseNode -# Force qtpy to use PyQt5 explicitly -os.environ["QT_API"] = "pyqt5" +def import_nodes_from_folder(package_name): + imported_nodes = [] + package = importlib.import_module(package_name) + for loader, module_name, is_pkg in pkgutil.walk_packages( + package.__path__, package.__name__ + "."): + module = importlib.import_module(module_name) + for name, obj in inspect.getmembers(module, inspect.isclass): + if issubclass(obj, BaseNode) and obj.__module__ == module.__name__: + imported_nodes.append(obj) + return imported_nodes -# --- PATCH OdenGraphQt to use QtWidgets.QUndoStack instead of QtGui.QUndoStack --- -import qtpy.QtGui -import qtpy.QtWidgets +def make_node_command(graph, node_type): + def command(): + try: + graph.create_node(node_type) + except Exception as e: + print(f"Error creating node of type {node_type}: {e}") + return command -if not hasattr(qtpy.QtGui, "QUndoStack"): # Ensure patching only if necessary - qtpy.QtGui.QUndoStack = qtpy.QtWidgets.QUndoStack +# Edit Mode Button +class EditButton(QPushButton): + """A small, frameless button to toggle edit mode.""" + def __init__(self, parent=None): + super().__init__("Enter Edit Mode", parent) + self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) + self.setStyleSheet("background-color: rgba(255,255,255,200); border: 1px solid black;") + self.resize(140, 40) -# --- END PATCH --- - -class TransparentGraphWindow(QMainWindow): +# Main Overlay Window +class MainWindow(QMainWindow): + """A frameless, transparent overlay with OdenGraphQt nodes & edit mode toggle.""" def __init__(self): super().__init__() - # Enable transparency & always-on-top behavior - self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool) + # Full-screen overlay + app = QApplication.instance() + screen_geo = app.primaryScreen().geometry() + self.setGeometry(screen_geo) + + # Frameless, top-most, fully transparent + self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) self.setAttribute(Qt.WA_TranslucentBackground, True) - # Get full-screen size for overlay - screen_geometry = QApplication.primaryScreen().geometry() - self.setGeometry(screen_geometry) + # QML Background + self.qml_view = QQuickView() + self.qml_view.setSource(QUrl("qml/background_grid.qml")) + self.qml_view.setFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) + self.qml_view.setClearBeforeRendering(True) + self.qml_view.setColor(Qt.transparent) + self.qml_view.show() - # Create Node Graph - self.graph = NodeGraph() - self.graph.widget.setParent(self) - self.graph.widget.setGeometry(self.rect()) + # Save the QML root object for later property sync + self.qml_root = self.qml_view.rootObject() - # Make bottom-left corner interactive for context menu - self.context_menu_area = QRect(10, self.height() - 40, 50, 30) + # NodeGraph with TransparentViewer + self.graph = NodeGraph(viewer=TransparentViewer()) + self.nodeGraphWidget = self.graph.widget + self.nodeGraphWidget.setStyleSheet("background: transparent; border: none;") - # Load nodes dynamically - self.import_nodes() + # Transparent central widget + central = QWidget(self) + central.setAttribute(Qt.WA_TranslucentBackground, True) + self.setCentralWidget(central) - # Global update timer for processing nodes - self.timer = QTimer() - self.timer.timeout.connect(self.update_nodes) + self.nodeGraphWidget.setParent(central) + self.nodeGraphWidget.setGeometry(central.rect()) + + # Edit Mode Button (Python controlled) + self.editButton = EditButton(self) + self.editButton.move(10, 10) + self.editButton.clicked.connect(self.toggleEditMode) + self.isEditMode = True # Set edit mode enabled by default + self.editButton.setText("Exit Edit Mode") # Reflect that grid is active + + # Ensure QML grid overlay is enabled at startup + if self.qml_root: + self.qml_root.setProperty("editMode", self.isEditMode) + + # Import custom nodes + try: + custom_nodes = import_nodes_from_folder('Nodes') + for node_class in custom_nodes: + self.graph.register_node(node_class) + + graph_menu = self.graph.get_context_menu('graph') + for node_class in custom_nodes: + node_type = f"{node_class.__identifier__}.{node_class.__name__}" + node_name = node_class.NODE_NAME + graph_menu.add_command( + f"Add {node_name}", + make_node_command(self.graph, node_type) + ) + except Exception as e: + print(f"Error setting up custom nodes: {e}") + + # Global update timer + self.timer = QTimer(self) + self.timer.timeout.connect(self.global_update) self.timer.start(500) - def import_nodes(self): - """Dynamically import all custom node classes from the 'Nodes' package.""" - try: - package_name = 'Nodes' - package = importlib.import_module(package_name) - for loader, module_name, is_pkg in pkgutil.walk_packages(package.__path__, package.__name__ + "."): - module = importlib.import_module(module_name) - for name, obj in inspect.getmembers(module, inspect.isclass): - if issubclass(obj, BaseNode) and obj.__module__ == module.__name__: - self.graph.register_node(obj) - except Exception as e: - print(f"Error loading nodes: {e}") + self.show() + self.nodeGraphWidget.setAttribute(Qt.WA_TransparentForMouseEvents, not self.isEditMode) - def update_nodes(self): - """Calls process_input() on all nodes, if applicable.""" + def toggleEditMode(self): + """Toggle edit mode (pass-through clicks vs interactive).""" + self.isEditMode = not self.isEditMode + self.nodeGraphWidget.setAttribute(Qt.WA_TransparentForMouseEvents, not self.isEditMode) + self.editButton.setText("Exit Edit Mode" if self.isEditMode else "Enter Edit Mode") + if self.qml_root: + self.qml_root.setProperty("editMode", self.isEditMode) + + def global_update(self): + """Update all nodes periodically.""" for node in self.graph.all_nodes(): if hasattr(node, "process_input"): - try: - node.process_input() - except Exception as e: - print(f"Error processing node {node}: {e}") + node.process_input() - def mousePressEvent(self, event): - """Override mouse press to handle context menu in bottom-left area.""" - if event.button() == Qt.RightButton and self.context_menu_area.contains(event.pos()): - self.show_context_menu(event.globalPos()) - - def show_context_menu(self, position): - """Displays a right-click context menu.""" - menu = QMenu(self) - quit_action = QAction("Quit", self) - quit_action.triggered.connect(self.close) - menu.addAction(quit_action) - menu.exec_(position) - - def paintEvent(self, event): - """Render transparent overlay and the small clickable menu area.""" - painter = QPainter(self) - painter.setRenderHint(QPainter.Antialiasing) - - # Draw semi-transparent context menu area - painter.setBrush(QBrush(QColor(50, 50, 50, 150))) # Dark semi-transparent box - painter.setPen(QPen(QColor(200, 200, 200, 200))) - painter.drawRect(self.context_menu_area) - - painter.setFont(self.font()) - painter.setPen(QColor(255, 255, 255)) - painter.drawText(self.context_menu_area, Qt.AlignCenter, "Menu") - -if __name__ == "__main__": +# Entry Point +if __name__ == '__main__': app = QApplication(sys.argv) - window = TransparentGraphWindow() + window = MainWindow() window.show() sys.exit(app.exec_()) diff --git a/debug_processed.png b/debug_processed.png deleted file mode 100644 index 85eebd7..0000000 Binary files a/debug_processed.png and /dev/null differ diff --git a/debug_screenshot.png b/debug_screenshot.png deleted file mode 100644 index 8d951d6..0000000 Binary files a/debug_screenshot.png and /dev/null differ