diff --git a/Modules/__pycache__/data_collector.cpython-312.pyc b/Modules/__pycache__/data_collector.cpython-312.pyc
new file mode 100644
index 0000000..bb082f3
Binary files /dev/null and b/Modules/__pycache__/data_collector.cpython-312.pyc differ
diff --git a/Modules/__pycache__/data_manager.cpython-312.pyc b/Modules/__pycache__/data_manager.cpython-312.pyc
new file mode 100644
index 0000000..16fcb0f
Binary files /dev/null and b/Modules/__pycache__/data_manager.cpython-312.pyc differ
diff --git a/Modules/data_collector.py b/Modules/data_collector.py
index da9a44e..2144aa8 100644
--- a/Modules/data_collector.py
+++ b/Modules/data_collector.py
@@ -1,326 +1,206 @@
-#!/usr/bin/env python3
+# Modules/data_collector.py
+
+import threading
 import time
 import re
-import threading
 import numpy as np
 import cv2
 import pytesseract
-from flask import Flask, jsonify
 from PIL import Image, ImageGrab, ImageFilter
-from PyQt5.QtWidgets import QApplication, QWidget
-from PyQt5.QtCore import QTimer, QRect, QPoint, Qt, QMutex
+
+from PyQt5.QtWidgets import QWidget, QApplication
+from PyQt5.QtCore import QRect, QPoint, Qt, QMutex, QTimer
 from PyQt5.QtGui import QPainter, QPen, QColor, QFont
 
 pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"
 
-# =============================================================================
-#                          Global Config
-# =============================================================================
-
-POLLING_RATE_MS = 500
-MAX_DATA_POINTS = 8
 DEFAULT_WIDTH = 180
 DEFAULT_HEIGHT = 130
 HANDLE_SIZE = 8
 LABEL_HEIGHT = 20
 
-# Template Matching Threshold (Define it here)
-MATCH_THRESHOLD = 0.4  # Set to 0.4 as a typical value for correlation threshold
+collector_mutex = QMutex()
+regions = {}
+overlay_window = None
 
-# Flask API setup
-app = Flask(__name__)
-
-# **Shared Region Data (Thread-Safe)**
-region_lock = QMutex()  # Mutex to synchronize access between UI and OCR thread
-shared_region = {
-    "x": 250,
-    "y": 50,
-    "w": DEFAULT_WIDTH,
-    "h": DEFAULT_HEIGHT
-}
-
-# Global variable for OCR data
-latest_data = {
-    "hp_current": 0,
-    "hp_total": 0,
-    "mp_current": 0,
-    "mp_total": 0,
-    "fp_current": 0,
-    "fp_total": 0,
-    "exp": 0.0000
-}
-
-# =============================================================================
-#                           OCR Data Collection
-# =============================================================================
-
-def preprocess_image(image):
-    """
-    Preprocess the image for OCR: convert to grayscale, resize, and apply thresholding.
-    """
-    gray = image.convert("L")  # Convert to grayscale
-    scaled = gray.resize((gray.width * 3, gray.height * 3))  # Upscale the image for better accuracy
-    thresh = scaled.point(lambda p: p > 200 and 255)  # Apply a threshold to clean up the image
-    return thresh.filter(ImageFilter.MedianFilter(3))  # Apply a median filter to remove noise
-
-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 locate_bars_opencv(template_path, threshold=MATCH_THRESHOLD):
-    """
-    Attempt to locate the bars via OpenCV template matching.
-    """
-    screenshot_pil = ImageGrab.grab()
-    screenshot_np = np.array(screenshot_pil)
-    screenshot_bgr = cv2.cvtColor(screenshot_np, cv2.COLOR_RGB2BGR)
-    template_bgr = cv2.imread(template_path, cv2.IMREAD_COLOR)
-    if template_bgr is None:
-        return None
-    result = cv2.matchTemplate(screenshot_bgr, template_bgr, cv2.TM_CCOEFF_NORMED)
-    min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
-    th, tw, _ = template_bgr.shape
-    if max_val >= threshold:
-        found_x, found_y = max_loc
-        return (found_x, found_y, tw, th)
-    else:
-        return None
-
-def parse_all_stats(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
+def create_ocr_region(region_id, x=250, y=50, w=DEFAULT_WIDTH, h=DEFAULT_HEIGHT):
+    collector_mutex.lock()
+    if region_id in regions:
+        collector_mutex.unlock()
+        return
+    regions[region_id] = {
+        'bbox': [x, y, w, h],
+        'raw_text': ""
     }
-    if len(lines) < 4:
-        return stats_dict
+    collector_mutex.unlock()
+    _ensure_overlay()
 
-    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)))
+def get_raw_text(region_id):
+    collector_mutex.lock()
+    if region_id not in regions:
+        collector_mutex.unlock()
+        return ""
+    text = regions[region_id]['raw_text']
+    collector_mutex.unlock()
+    return text
 
-    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)))
+def start_collector():
+    t = threading.Thread(target=_update_ocr_loop, daemon=True)
+    t.start()
 
-    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
-
-# =============================================================================
-#                           Region & UI
-# =============================================================================
-
-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),
-        ]
-
-# =============================================================================
-#                          Flask API Server
-# =============================================================================
-
-@app.route('/data')
-def get_data():
-    """Returns the latest OCR data as JSON."""
-    return jsonify(latest_data)
-
-def collect_ocr_data():
-    """
-    Collects OCR data every 0.5 seconds and updates global latest_data.
-    """
-    global latest_data
+def _update_ocr_loop():
     while True:
-        # **Fetch updated region values from UI (thread-safe)**
-        region_lock.lock()  # Lock for thread safety
-        x, y, w, h = shared_region["x"], shared_region["y"], shared_region["w"], shared_region["h"]
-        region_lock.unlock()
+        collector_mutex.lock()
+        region_ids = list(regions.keys())
+        collector_mutex.unlock()
 
-        # **Grab the image of the updated region**
-        screenshot = ImageGrab.grab(bbox=(x, y, x + w, y + h))
+        for rid in region_ids:
+            collector_mutex.lock()
+            bbox = regions[rid]['bbox'][:]
+            collector_mutex.unlock()
 
-        # **Debug: Save screenshots to verify capture**
-        screenshot.save("debug_screenshot.png")
+            x, y, w, h = bbox
+            screenshot = ImageGrab.grab(bbox=(x, y, x + w, y + h))
+            processed = _preprocess_image(screenshot)
+            raw_text = pytesseract.image_to_string(processed, config='--psm 4 --oem 1')
 
-        # Preprocess image
-        processed = preprocess_image(screenshot)
-        processed.save("debug_processed.png")  # Debug: Save processed image
+            collector_mutex.lock()
+            if rid in regions:
+                regions[rid]['raw_text'] = raw_text
+            collector_mutex.unlock()
 
-        # Run OCR
-        text = pytesseract.image_to_string(processed, config='--psm 4 --oem 1')
+        time.sleep(0.7)
 
-        stats = parse_all_stats(text.strip())
-        hp_cur, hp_max = stats["hp"]
-        mp_cur, mp_max = stats["mp"]
-        fp_cur, fp_max = stats["fp"]
-        exp_val = stats["exp"]
-        
-        # Update latest data
-        latest_data = {
-            "hp_current": hp_cur,
-            "hp_total": hp_max,
-            "mp_current": mp_cur,
-            "mp_total": mp_max,
-            "fp_current": fp_cur,
-            "fp_total": fp_max,
-            "exp": exp_val
-        }
+def _preprocess_image(image):
+    gray = image.convert("L")
+    scaled = gray.resize((gray.width * 3, gray.height * 3))
+    thresh = scaled.point(lambda p: 255 if p > 200 else 0)
+    return thresh.filter(ImageFilter.MedianFilter(3))
 
-        # DEBUG OUTPUT
-        print(f"Flyff - Character Status: HP: {hp_cur}/{hp_max}, MP: {mp_cur}/{mp_max}, FP: {fp_cur}/{fp_max}, EXP: {exp_val}%")
+def _ensure_overlay():
+    """
+    Creates the overlay window if none exists.
+    If no QApplication instance is running yet, schedule the creation after
+    the main application event loop starts (to avoid "Must construct a QApplication first" errors).
+    """
+    global overlay_window
+    if overlay_window is not None:
+        return
 
-        time.sleep(0.5)
+    # If there's already a running QApplication, create overlay immediately.
+    if QApplication.instance() is not None:
+        overlay_window = OverlayCanvas()
+        overlay_window.show()
+    else:
+        # Schedule creation for when the app event loop is up.
+        def delayed_create():
+            global overlay_window
+            if overlay_window is None:
+                overlay_window = OverlayCanvas()
+                overlay_window.show()
 
-# =============================================================================
-#                           OverlayCanvas (UI)
-# =============================================================================
+        QTimer.singleShot(0, delayed_create)
 
 class OverlayCanvas(QWidget):
-    """
-    UI overlay that allows dragging/resizing of the OCR region.
-    """
     def __init__(self, parent=None):
         super().__init__(parent)
-
-        # **Full-screen overlay**
         screen_geo = QApplication.primaryScreen().geometry()
-        self.setGeometry(screen_geo)  # Set to full screen
-
+        self.setGeometry(screen_geo)
         self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
         self.setAttribute(Qt.WA_TranslucentBackground, True)
 
-        # **Load shared region**
-        self.region = shared_region
         self.drag_offset = None
         self.selected_handle = None
+        self.selected_region_id = None
 
     def paintEvent(self, event):
-        """Draw the blue OCR region."""
         painter = QPainter(self)
         pen = QPen(QColor(0, 0, 255))
-        pen.setWidth(5)  # Thicker lines
+        pen.setWidth(5)
         painter.setPen(pen)
-        painter.drawRect(self.region["x"], self.region["y"], self.region["w"], self.region["h"])
-        painter.setFont(QFont("Arial", 12, QFont.Bold))
-        painter.setPen(QColor(0, 0, 255))
-        painter.drawText(self.region["x"], self.region["y"] - 5, "Character Status")
+
+        collector_mutex.lock()
+        region_copy = {rid: data['bbox'][:] for rid, data in regions.items()}
+        collector_mutex.unlock()
+
+        for rid, bbox in region_copy.items():
+            x, y, w, h = bbox
+            painter.drawRect(x, y, w, h)
+            painter.setFont(QFont("Arial", 12, QFont.Bold))
+            painter.setPen(QColor(0, 0, 255))
+            painter.drawText(x, y - 5, f"OCR Region: {rid}")
 
     def mousePressEvent(self, event):
-        """Handle drag and resize interactions."""
         if event.button() == Qt.LeftButton:
-            region_lock.lock()  # Lock for thread safety
-            x, y, w, h = self.region["x"], self.region["y"], self.region["w"], self.region["h"]
-            region_lock.unlock()
+            collector_mutex.lock()
+            all_items = list(regions.items())
+            collector_mutex.unlock()
 
-            for i, handle in enumerate(self.resize_handles()):
-                if handle.contains(event.pos()):
-                    self.selected_handle = i
+            for rid, data in all_items:
+                x, y, w, h = data['bbox']
+                handles = self._resize_handles(x, y, w, h)
+                for i, handle_rect in enumerate(handles):
+                    if handle_rect.contains(event.pos()):
+                        self.selected_handle = i
+                        self.selected_region_id = rid
+                        return
+                if QRect(x, y, w, h).contains(event.pos()):
+                    self.drag_offset = event.pos() - QPoint(x, y)
+                    self.selected_region_id = rid
                     return
 
-            if QRect(x, y, w, h).contains(event.pos()):
-                self.drag_offset = event.pos() - QPoint(x, y)
-
     def mouseMoveEvent(self, event):
-        """Allow dragging and resizing."""
+        if not self.selected_region_id:
+            return
+        collector_mutex.lock()
+        if self.selected_region_id not in regions:
+            collector_mutex.unlock()
+            return
+        bbox = regions[self.selected_region_id]['bbox']
+        collector_mutex.unlock()
+
+        x, y, w, h = bbox
         if self.selected_handle is not None:
-            region_lock.lock()
-            sr = self.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:  # 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)
-            region_lock.unlock()
-
+            # resizing
+            if self.selected_handle == 0:  # top-left
+                new_w = w + (x - event.x())
+                new_h = h + (y - event.y())
+                new_x = event.x()
+                new_y = event.y()
+                if new_w < 10: new_w = 10
+                if new_h < 10: new_h = 10
+                collector_mutex.lock()
+                if self.selected_region_id in regions:
+                    regions[self.selected_region_id]['bbox'] = [new_x, new_y, new_w, new_h]
+                collector_mutex.unlock()
+            elif self.selected_handle == 1:  # bottom-right
+                new_w = event.x() - x
+                new_h = event.y() - y
+                if new_w < 10: new_w = 10
+                if new_h < 10: new_h = 10
+                collector_mutex.lock()
+                if self.selected_region_id in regions:
+                    regions[self.selected_region_id]['bbox'] = [x, y, new_w, new_h]
+                collector_mutex.unlock()
             self.update()
-
         elif self.drag_offset:
-            region_lock.lock()
-            self.region["x"] = event.x() - self.drag_offset.x()
-            self.region["y"] = event.y() - self.drag_offset.y()
-            region_lock.unlock()
-
+            # dragging
+            new_x = event.x() - self.drag_offset.x()
+            new_y = event.y() - self.drag_offset.y()
+            collector_mutex.lock()
+            if self.selected_region_id in regions:
+                regions[self.selected_region_id]['bbox'][0] = new_x
+                regions[self.selected_region_id]['bbox'][1] = new_y
+            collector_mutex.unlock()
             self.update()
 
     def mouseReleaseEvent(self, event):
-        """End drag or resize event."""
         self.selected_handle = None
         self.drag_offset = None
+        self.selected_region_id = None
 
-    def resize_handles(self):
-        """Get the resizing handles of the region."""
+    def _resize_handles(self, x, y, w, h):
         return [
-            QRect(self.region["x"] - HANDLE_SIZE // 2, self.region["y"] - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
-            QRect(self.region["x"] + self.region["w"] - HANDLE_SIZE // 2, self.region["y"] + self.region["h"] - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE)
+            QRect(x - HANDLE_SIZE//2, y - HANDLE_SIZE//2, HANDLE_SIZE, HANDLE_SIZE),  # top-left
+            QRect(x + w - HANDLE_SIZE//2, y + h - HANDLE_SIZE//2, HANDLE_SIZE, HANDLE_SIZE)  # bottom-right
         ]
-
-# =============================================================================
-#                          Start Application
-# =============================================================================
-
-def run_flask_app():
-    """Runs the Flask API server in a separate thread."""
-    app.run(host="127.0.0.1", port=5000)
-
-if __name__ == '__main__':
-    # Start the OCR thread
-    collector_thread = threading.Thread(target=collect_ocr_data, daemon=True)
-    collector_thread.start()
-    
-    # Start the Flask API thread
-    flask_thread = threading.Thread(target=run_flask_app, daemon=True)
-    flask_thread.start()
-
-    # Start PyQt5 GUI
-    app_gui = QApplication([])
-    overlay_window = OverlayCanvas()
-    overlay_window.show()
-
-    # Run event loop
-    app_gui.exec_()
diff --git a/Modules/data_manager.py b/Modules/data_manager.py
new file mode 100644
index 0000000..72e9521
--- /dev/null
+++ b/Modules/data_manager.py
@@ -0,0 +1,66 @@
+# Modules/data_manager.py
+import threading
+import time
+from flask import Flask, jsonify
+from PyQt5.QtCore import QMutex
+
+# Global datastore for character metrics
+data_store = {
+    "hp_current": 0,
+    "hp_total": 0,
+    "mp_current": 0,
+    "mp_total": 0,
+    "fp_current": 0,
+    "fp_total": 0,
+    "exp": 0.0
+}
+
+# Mutex for thread safety
+data_mutex = QMutex()
+
+# Flag to ensure only one character status collector node exists
+character_status_collector_exists = False
+
+# Flask Application
+app = Flask(__name__)
+
+@app.route('/data')
+def data_api():
+    """
+    Returns the current character metrics as JSON.
+    """
+    return jsonify(get_data())
+
+def start_api_server():
+    """
+    Starts the Flask API server in a separate daemon thread.
+    """
+    def run():
+        app.run(host="127.0.0.1", port=5000)
+    t = threading.Thread(target=run, daemon=True)
+    t.start()
+
+def get_data():
+    """
+    Return a copy of the global data_store.
+    """
+    data_mutex.lock()
+    data_copy = data_store.copy()
+    data_mutex.unlock()
+    return data_copy
+
+def set_data(key, value):
+    """
+    Set a single metric in the global data_store.
+    """
+    data_mutex.lock()
+    data_store[key] = value
+    data_mutex.unlock()
+
+def set_data_bulk(metrics_dict):
+    """
+    Update multiple metrics in the global data_store at once.
+    """
+    data_mutex.lock()
+    data_store.update(metrics_dict)
+    data_mutex.unlock()
diff --git a/Nodes/Flyff/__pycache__/flyff_EXP_current.cpython-312.pyc b/Nodes/Flyff/__pycache__/flyff_EXP_current.cpython-312.pyc
new file mode 100644
index 0000000..84bb711
Binary files /dev/null and b/Nodes/Flyff/__pycache__/flyff_EXP_current.cpython-312.pyc differ
diff --git a/Nodes/Flyff/__pycache__/flyff_FP_current.cpython-312.pyc b/Nodes/Flyff/__pycache__/flyff_FP_current.cpython-312.pyc
new file mode 100644
index 0000000..01a998e
Binary files /dev/null and b/Nodes/Flyff/__pycache__/flyff_FP_current.cpython-312.pyc differ
diff --git a/Nodes/Flyff/__pycache__/flyff_FP_total.cpython-312.pyc b/Nodes/Flyff/__pycache__/flyff_FP_total.cpython-312.pyc
new file mode 100644
index 0000000..8f157eb
Binary files /dev/null and b/Nodes/Flyff/__pycache__/flyff_FP_total.cpython-312.pyc differ
diff --git a/Nodes/Flyff/__pycache__/flyff_HP_current.cpython-312.pyc b/Nodes/Flyff/__pycache__/flyff_HP_current.cpython-312.pyc
new file mode 100644
index 0000000..c81788a
Binary files /dev/null and b/Nodes/Flyff/__pycache__/flyff_HP_current.cpython-312.pyc differ
diff --git a/Nodes/Flyff/__pycache__/flyff_HP_total.cpython-312.pyc b/Nodes/Flyff/__pycache__/flyff_HP_total.cpython-312.pyc
new file mode 100644
index 0000000..4c3b6e7
Binary files /dev/null and b/Nodes/Flyff/__pycache__/flyff_HP_total.cpython-312.pyc differ
diff --git a/Nodes/Flyff/__pycache__/flyff_MP_current.cpython-312.pyc b/Nodes/Flyff/__pycache__/flyff_MP_current.cpython-312.pyc
new file mode 100644
index 0000000..347d4ce
Binary files /dev/null and b/Nodes/Flyff/__pycache__/flyff_MP_current.cpython-312.pyc differ
diff --git a/Nodes/Flyff/__pycache__/flyff_MP_total.cpython-312.pyc b/Nodes/Flyff/__pycache__/flyff_MP_total.cpython-312.pyc
new file mode 100644
index 0000000..815f1a6
Binary files /dev/null and b/Nodes/Flyff/__pycache__/flyff_MP_total.cpython-312.pyc differ
diff --git a/Nodes/Flyff/__pycache__/flyff_character_status_node.cpython-312.pyc b/Nodes/Flyff/__pycache__/flyff_character_status_node.cpython-312.pyc
new file mode 100644
index 0000000..fa3bce4
Binary files /dev/null and b/Nodes/Flyff/__pycache__/flyff_character_status_node.cpython-312.pyc differ
diff --git a/Nodes/Flyff/__pycache__/flyff_low_health_alert_node.cpython-312.pyc b/Nodes/Flyff/__pycache__/flyff_low_health_alert_node.cpython-312.pyc
new file mode 100644
index 0000000..4439490
Binary files /dev/null and b/Nodes/Flyff/__pycache__/flyff_low_health_alert_node.cpython-312.pyc differ
diff --git a/Nodes/flyff_EXP_current.py b/Nodes/Flyff/flyff_EXP_current.py
similarity index 100%
rename from Nodes/flyff_EXP_current.py
rename to Nodes/Flyff/flyff_EXP_current.py
diff --git a/Nodes/flyff_FP_current.py b/Nodes/Flyff/flyff_FP_current.py
similarity index 100%
rename from Nodes/flyff_FP_current.py
rename to Nodes/Flyff/flyff_FP_current.py
diff --git a/Nodes/flyff_FP_total.py b/Nodes/Flyff/flyff_FP_total.py
similarity index 100%
rename from Nodes/flyff_FP_total.py
rename to Nodes/Flyff/flyff_FP_total.py
diff --git a/Nodes/flyff_HP_current.py b/Nodes/Flyff/flyff_HP_current.py
similarity index 100%
rename from Nodes/flyff_HP_current.py
rename to Nodes/Flyff/flyff_HP_current.py
diff --git a/Nodes/flyff_HP_total.py b/Nodes/Flyff/flyff_HP_total.py
similarity index 100%
rename from Nodes/flyff_HP_total.py
rename to Nodes/Flyff/flyff_HP_total.py
diff --git a/Nodes/flyff_MP_current.py b/Nodes/Flyff/flyff_MP_current.py
similarity index 100%
rename from Nodes/flyff_MP_current.py
rename to Nodes/Flyff/flyff_MP_current.py
diff --git a/Nodes/flyff_MP_total.py b/Nodes/Flyff/flyff_MP_total.py
similarity index 100%
rename from Nodes/flyff_MP_total.py
rename to Nodes/Flyff/flyff_MP_total.py
diff --git a/Nodes/Flyff/flyff_character_status_node.py b/Nodes/Flyff/flyff_character_status_node.py
new file mode 100644
index 0000000..afd9701
--- /dev/null
+++ b/Nodes/Flyff/flyff_character_status_node.py
@@ -0,0 +1,108 @@
+#!/usr/bin/env python3
+"""
+Flyff Character Status Node (New Version):
+ - Has no inputs/outputs.
+ - Creates an OCR region in data_collector.
+ - Periodically grabs raw text from that region, parses it here in the node,
+   and sets data_manager's HP, MP, FP, EXP accordingly.
+ - Also updates its own text fields with the parsed values.
+"""
+
+import re
+from OdenGraphQt import BaseNode
+from PyQt5.QtWidgets import QMessageBox
+from Modules import data_manager, data_collector
+
+class FlyffCharacterStatusNode(BaseNode):
+    __identifier__ = 'bunny-lab.io.flyff_character_status_node'
+    NODE_NAME = 'Flyff - Character Status'
+
+    def __init__(self):
+        super(FlyffCharacterStatusNode, self).__init__()
+        # Prevent duplicates
+        if data_manager.character_status_collector_exists:
+            QMessageBox.critical(None, "Error", "Only one Flyff Character Status Collector node is allowed.")
+            raise Exception("Duplicate Character Status Node.")
+        data_manager.character_status_collector_exists = True
+
+        # Add text fields for display
+        self.add_text_input('hp', 'HP', text="HP: 0/0")
+        self.add_text_input('mp', 'MP', text="MP: 0/0")
+        self.add_text_input('fp', 'FP', text="FP: 0/0")
+        self.add_text_input('exp', 'EXP', text="EXP: 0%")
+
+        # Create a unique region id for this node (or just "character_status")
+        self.region_id = "character_status"
+        data_collector.create_ocr_region(self.region_id, x=250, y=50, w=180, h=130)
+
+        # Start the data_collector background thread (if not already started)
+        data_collector.start_collector()
+
+        # Set the node name
+        self.set_name("Flyff - Character Status")
+
+    def parse_character_stats(self, raw_text):
+        """
+        Extract HP, MP, FP, EXP from the raw OCR text lines.
+        """
+        lines = [l.strip() for l in raw_text.splitlines() if l.strip()]
+        hp_current, hp_total = 0, 0
+        mp_current, mp_total = 0, 0
+        fp_current, fp_total = 0, 0
+        exp_value = 0.0
+
+        if len(lines) >= 4:
+            # line 1: HP
+            hp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[0])
+            if hp_match:
+                hp_current = int(hp_match.group(1))
+                hp_total = int(hp_match.group(2))
+
+            # line 2: MP
+            mp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[1])
+            if mp_match:
+                mp_current = int(mp_match.group(1))
+                mp_total = int(mp_match.group(2))
+
+            # line 3: FP
+            fp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[2])
+            if fp_match:
+                fp_current = int(fp_match.group(1))
+                fp_total = int(fp_match.group(2))
+
+            # line 4: EXP
+            exp_match = re.search(r"(\d+(?:\.\d+)?)", lines[3])
+            if exp_match:
+                val = float(exp_match.group(1))
+                if val < 0: val = 0
+                if val > 100: val = 100
+                exp_value = val
+
+        return hp_current, hp_total, mp_current, mp_total, fp_current, fp_total, exp_value
+
+    def process_input(self):
+        """
+        Called periodically by the global timer in your main application (borealis.py).
+        """
+        # Grab raw text from data_collector
+        raw_text = data_collector.get_raw_text(self.region_id)
+
+        # Parse it
+        hp_c, hp_t, mp_c, mp_t, fp_c, fp_t, exp_v = self.parse_character_stats(raw_text)
+
+        # Update data_manager
+        data_manager.set_data_bulk({
+            "hp_current": hp_c,
+            "hp_total": hp_t,
+            "mp_current": mp_c,
+            "mp_total": mp_t,
+            "fp_current": fp_c,
+            "fp_total": fp_t,
+            "exp": exp_v
+        })
+
+        # Update the node's text fields
+        self.set_property('hp', f"HP: {hp_c}/{hp_t}")
+        self.set_property('mp', f"MP: {mp_c}/{mp_t}")
+        self.set_property('fp', f"FP: {fp_c}/{fp_t}")
+        self.set_property('exp', f"EXP: {exp_v}%")
diff --git a/Nodes/flyff_low_health_alert_node.py b/Nodes/Flyff/flyff_low_health_alert_node.py
similarity index 100%
rename from Nodes/flyff_low_health_alert_node.py
rename to Nodes/Flyff/flyff_low_health_alert_node.py
diff --git a/Nodes/Organization/__pycache__/backdrop_node.cpython-312.pyc b/Nodes/Organization/__pycache__/backdrop_node.cpython-312.pyc
new file mode 100644
index 0000000..6280241
Binary files /dev/null and b/Nodes/Organization/__pycache__/backdrop_node.cpython-312.pyc differ
diff --git a/Nodes/backdrop_node.py b/Nodes/Organization/backdrop_node.py
similarity index 100%
rename from Nodes/backdrop_node.py
rename to Nodes/Organization/backdrop_node.py
diff --git a/Nodes/__pycache__/flyff_character_status_node.cpython-312.pyc b/Nodes/__pycache__/flyff_character_status_node.cpython-312.pyc
index 4921b72..379ae78 100644
Binary files a/Nodes/__pycache__/flyff_character_status_node.cpython-312.pyc and b/Nodes/__pycache__/flyff_character_status_node.cpython-312.pyc differ
diff --git a/Nodes/__pycache__/widget_node.cpython-312.pyc b/Nodes/__pycache__/widget_node.cpython-312.pyc
index 0476609..4f511ec 100644
Binary files a/Nodes/__pycache__/widget_node.cpython-312.pyc and b/Nodes/__pycache__/widget_node.cpython-312.pyc differ
diff --git a/Nodes/flyff_character_status_node.py b/Nodes/flyff_character_status_node.py
deleted file mode 100644
index d48e9dd..0000000
--- a/Nodes/flyff_character_status_node.py
+++ /dev/null
@@ -1,126 +0,0 @@
-#!/usr/bin/env python3
-
-"""
-Standardized Flyff Character Status Node:
-  - Polls an API for character stats and updates values dynamically.
-  - Uses a global update timer for processing.
-  - Immediately transmits updated values to connected nodes.
-  - If the API is unreachable, it will wait for 5 seconds before retrying
-    and log the error only once per retry period.
-  - Calls self.view.draw_node() after updating the node name, ensuring
-    the node's bounding box recalculates even when the API is disconnected.
-  - Port colors adjusted to match earlier styling.
-"""
-
-from OdenGraphQt import BaseNode
-from Qt import QtCore
-import requests
-import traceback
-import time
-
-class FlyffCharacterStatusNode(BaseNode):
-    __identifier__ = 'bunny-lab.io.flyff_character_status_node'
-    NODE_NAME = 'Flyff - Character Status'
-
-    def __init__(self):
-        super(FlyffCharacterStatusNode, self).__init__()
-        self.values = {
-            "HP: Current": "N/A",
-            "HP: Total":   "N/A",
-            "MP: Current": "N/A",
-            "MP: Total":   "N/A",
-            "FP: Current": "N/A",
-            "FP: Total":   "N/A",
-            "EXP":         "N/A"
-        }
-
-        # Set each output with a custom color:
-        # (Choose values close to the screenshot for each port.)
-        self.add_output('HP: Current', color=(126, 36, 57))
-        self.add_output('HP: Total',   color=(126, 36, 57))
-        self.add_output('MP: Current', color=(35, 89, 144))
-        self.add_output('MP: Total',   color=(35, 89, 144))
-        self.add_output('FP: Current', color=(36, 116, 32))
-        self.add_output('FP: Total',   color=(36, 116, 32))
-        self.add_output('EXP',         color=(48, 116, 143))
-
-        self.set_name("Flyff - Character Status (API Disconnected)")
-        self.view.draw_node()  # ensure bounding box updates initially
-
-        # Variables to handle API downtime gracefully:
-        self._api_down = False
-        self._last_api_attempt = 0
-        self._retry_interval = 5  # seconds to wait before retrying after a failure
-        self._last_error_printed = 0
-
-    def process_input(self):
-        current_time = time.time()
-        # If the API is down, only retry after _retry_interval seconds
-        if self._api_down and (current_time - self._last_api_attempt < self._retry_interval):
-            return
-
-        self._last_api_attempt = current_time
-        try:
-            response = requests.get("http://127.0.0.1:5000/data", timeout=1)
-            if response.status_code == 200:
-                data = response.json()
-                if isinstance(data, dict):
-                    mapping = {
-                        "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"
-                    }
-                    updated = False
-                    for key, value in data.items():
-                        if key in mapping:
-                            formatted_key = mapping[key]
-                            if str(value) != self.values.get(formatted_key, None):
-                                self.values[formatted_key] = str(value)
-                                updated = True
-                    self._api_down = False
-                    if updated:
-                        self.set_name("Flyff - Character Status (API Connected)")
-                        self.view.draw_node()  # recalc bounding box on connect
-                        self.transmit_data()
-                else:
-                    if current_time - self._last_error_printed >= self._retry_interval:
-                        print("[ERROR] Unexpected API response format (not a dict):", data)
-                        self._last_error_printed = current_time
-                    self.set_name("Flyff - Character Status (API Disconnected)")
-                    self.view.draw_node()  # recalc bounding box on disconnect
-                    self._api_down = True
-            else:
-                if current_time - self._last_error_printed >= self._retry_interval:
-                    print(f"[ERROR] API request failed with status code {response.status_code}")
-                    self._last_error_printed = current_time
-                self.set_name("Flyff - Character Status (API Disconnected)")
-                self.view.draw_node()
-                self._api_down = True
-        except Exception as e:
-            if current_time - self._last_error_printed >= self._retry_interval:
-                print("[ERROR] Error polling API in CharacterStatusNode:", str(e))
-                self._last_error_printed = current_time
-            self.set_name("Flyff - Character Status (API Disconnected)")
-            self.view.draw_node()
-            self._api_down = True
-
-    def transmit_data(self):
-        for stat, value in self.values.items():
-            port = self.get_output(stat)
-            if port and port.connected_ports():
-                for connected_port in port.connected_ports():
-                    connected_node = connected_port.node()
-                    if hasattr(connected_node, 'receive_data'):
-                        try:
-                            connected_node.receive_data(value, stat)
-                        except Exception as e:
-                            print(f"[ERROR] Error transmitting data to {connected_node}: {e}")
-                            print("[ERROR] Stack Trace:\n", traceback.format_exc())
-
-    def receive_data(self, data, source_port_name=None):
-        # This node only transmits data; it does not receive external data.
-        pass
diff --git a/Nodes/group_node.py b/Nodes/group_node.py
deleted file mode 100644
index 9b937b5..0000000
--- a/Nodes/group_node.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from OdenGraphQt import GroupNode
-
-
-class MyGroupNode(GroupNode):
-    """
-    example test group node with a in port and out port.
-    """
-
-    # set a unique node identifier.
-    __identifier__ = 'nodes.group'
-
-    # set the initial default node name.
-    NODE_NAME = 'group node'
-
-    def __init__(self):
-        super(MyGroupNode, self).__init__()
-        self.set_color(50, 8, 25)
-
-        # create input and output port.
-        self.add_input('in')
-        self.add_output('out')
\ No newline at end of file
diff --git a/Nodes/widget_node.py b/Nodes/widget_node.py
deleted file mode 100644
index eff1ac2..0000000
--- a/Nodes/widget_node.py
+++ /dev/null
@@ -1,155 +0,0 @@
-from OdenGraphQt import BaseNode
-from OdenGraphQt.constants import NodePropWidgetEnum
-from OdenGraphQt.widgets.node_widgets import NodeLineEditValidatorCheckBox
-
-
-class DropdownMenuNode(BaseNode):
-    """
-    An example node with a embedded added QCombobox menu.
-    """
-
-    # unique node identifier.
-    __identifier__ = 'nodes.widget'
-
-    # initial default node name.
-    NODE_NAME = 'menu'
-
-    def __init__(self):
-        super(DropdownMenuNode, self).__init__()
-
-        # create input & output ports
-        self.add_input('in 1')
-        self.add_output('out 1')
-        self.add_output('out 2')
-
-        # create the QComboBox menu.
-        items = ["item 1", "item 2", "item 3"]
-        self.add_combo_menu(
-            "my_menu",
-            "Menu Test",
-            items=items,
-            tooltip="example custom tooltip",
-        )
-
-
-class TextInputNode(BaseNode):
-    """
-    An example of a node with a embedded QLineEdit.
-    """
-
-    # unique node identifier.
-    __identifier__ = 'nodes.widget'
-
-    # initial default node name.
-    NODE_NAME = 'text'
-
-    def __init__(self):
-        super().__init__()
-        pattern = r"^[A-Za-z0-9]*$"
-        placeholder = ""
-        tooltip = "Valid characters: A-Z a-z 0-9"
-        is_case_sensitive = True
-        checkbox_label = "Use Parser?"
-
-        # create input & output ports
-        self.add_input('in')
-        self.add_output('out')
-
-        # create QLineEdit text input widget.
-        self.add_text_input('my_input', 'Text Input', tab='widgets')
-
-        tool_btn_kwargs = {
-            "func": self._callback,
-            "tooltip": "Awesome"
-        }
-        kwargs = {
-            "validator": {
-                "pattern": pattern,
-                "placeholder": placeholder,
-                "tooltip": tooltip,
-                "is_case_insensitive": is_case_sensitive,
-                "checkbox_visible": True,
-                "tool_btn_visible": True,
-            },
-            "checkbox_label": checkbox_label,
-            "tool_btn": tool_btn_kwargs,
-        }
-        node_widget = NodeLineEditValidatorCheckBox(
-            "src_path",
-            pattern,
-            placeholder,
-            tooltip,
-            is_case_sensitive,
-            checkbox_label,
-            checkbox_visible=True,
-            tool_btn_visible=True,
-            widget_label="src_path",
-            parent=self.view,
-        )
-        node_widget.get_custom_widget().set_tool_btn(**tool_btn_kwargs)
-        self.add_custom_widget(
-            node_widget,
-            NodePropWidgetEnum.LINEEDIT_VALIDATOR_CHECKBOX.value,
-            "widgets",
-            **kwargs,
-        )
-
-        kwargs2 = {
-            "validator": {
-                "pattern": pattern,
-                "placeholder": placeholder,
-                "tooltip": tooltip,
-                "is_case_insensitive": is_case_sensitive,
-                "checkbox_visible": False,
-                "tool_btn_visible": False,
-            },
-            "checkbox_label": "Check In Luggage?",
-            "tool_btn": tool_btn_kwargs,
-        }
-        node_widget2 = NodeLineEditValidatorCheckBox(
-            "dst_path",
-            pattern,
-            placeholder,
-            tooltip,
-            is_case_sensitive,
-            "Check In Luggage?",
-            checkbox_visible=False,
-            tool_btn_visible=False,
-            widget_label="dst_path",
-            parent=self.view,
-        )
-        node_widget2.get_custom_widget().set_tool_btn(**tool_btn_kwargs)
-        node_widget2.set_checkbox_visible(False)
-        node_widget2.set_tool_btn_visible(False)
-        self.add_custom_widget(
-            node_widget2,
-            NodePropWidgetEnum.LINEEDIT_VALIDATOR_CHECKBOX.value,
-            "widgets",
-            **kwargs2,
-        )
-
-    def _callback(self):
-        print(f"YOU HAVE CLICKED ON '{self.NODE_NAME}'")
-
-
-class CheckboxNode(BaseNode):
-    """
-    An example of a node with 2 embedded QCheckBox widgets.
-    """
-
-    # set a unique node identifier.
-    __identifier__ = 'nodes.widget'
-
-    # set the initial default node name.
-    NODE_NAME = 'checkbox'
-
-    def __init__(self):
-        super(CheckboxNode, self).__init__()
-
-        # create the checkboxes.
-        self.add_checkbox('cb_1', '', 'Checkbox 1', True)
-        self.add_checkbox('cb_2', '', 'Checkbox 2', False)
-
-        # create input and output port.
-        self.add_input('in', color=(200, 100, 0))
-        self.add_output('out', color=(0, 100, 200))
\ No newline at end of file
diff --git a/borealis.py b/borealis.py
index 18cc76a..dba6bee 100644
--- a/borealis.py
+++ b/borealis.py
@@ -1,11 +1,29 @@
 # -*- coding: utf-8 -*-
 #!/usr/bin/env python3
 
-from Qt import QtWidgets, QtCore, QtGui
 import sys
 import pkgutil
 import importlib
 import inspect
+import os
+
+from Qt import QtWidgets, QtCore, QtGui
+
+# ------------------------------------------------------------------
+# MONKEY-PATCH to fix "module 'qtpy.QtGui' has no attribute 'QUndoStack'"
+# OdenGraphQt tries to do QtGui.QUndoStack(self).
+# We'll import QUndoStack from QtWidgets and attach it to QtGui.
+try:
+    from qtpy.QtWidgets import QUndoStack
+    import qtpy
+    # Force QtGui.QUndoStack to reference QtWidgets.QUndoStack
+    qtpy.QtGui.QUndoStack = QUndoStack
+except ImportError:
+    print("WARNING: Could not monkey-patch QUndoStack. You may see an error if OdenGraphQt needs it.")
+# ------------------------------------------------------------------
+
+# Import your data_manager so we can start the Flask server
+from Modules import data_manager
 
 # --- BEGIN MONKEY PATCH FOR PIPE COLOR (Optional) ---
 # If you want custom pipe colors, uncomment this patch:
@@ -59,34 +77,78 @@ from OdenGraphQt import NodeGraph, BaseNode
 
 def import_nodes_from_folder(package_name):
     """
-    Dynamically import all modules from the given package
+    Recursively import all modules from the given package
     and return a list of classes that subclass BaseNode.
     """
     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)
+    
+    # Get the root directory of the package
+    package_path = package.__path__[0]
+    
+    for root, _, files in os.walk(package_path):
+        rel_path = os.path.relpath(root, package_path).replace(os.sep, '.')
+        module_prefix = f"{package_name}.{rel_path}" if rel_path != '.' else package_name
+
+        for file in files:
+            if file.endswith(".py") and file != "__init__.py":
+                module_name = f"{module_prefix}.{file[:-3]}"
+                try:
+                    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)
+                except Exception as e:
+                    print(f"Failed to import {module_name}: {e}")
+    
     return imported_nodes
 
 def make_node_command(graph, node_type_str):
     """
     Return a function that creates a node of the given type at the current cursor position.
+    For the Flyff Character Status Collector node, check if one already exists.
+    If so, schedule an error message to be shown.
+
+    Also ensure that node creation is delayed until after QApplication is up, to avoid
+    'QWidget: Must construct a QApplication before a QWidget' errors.
     """
-    def command():
+    def real_create():
+        # Check if we are about to create a duplicate Character Status Collector node.
+        if node_type_str.startswith("bunny-lab.io.flyff_character_status_node"):
+            for node in graph.all_nodes():
+                if node.__class__.__name__ == "FlyffCharacterStatusNode":
+                    # Show error message about duplicates
+                    QtWidgets.QMessageBox.critical(
+                        None,
+                        "Error",
+                        "Only one Flyff Character Status Collector node is allowed. If you added more, things would break (really) badly."
+                    )
+                    return
         try:
             pos = graph.cursor_pos()
             graph.create_node(node_type_str, pos=pos)
         except Exception as e:
-            print(f"Error creating node of type {node_type_str}: {e}")
+            QtWidgets.QMessageBox.critical(None, "Error", str(e))
+
+    def command():
+        # If there's already a QApplication running, just create the node now.
+        if QtWidgets.QApplication.instance():
+            real_create()
+        else:
+            # Otherwise, schedule the node creation for the next event cycle.
+            QtCore.QTimer.singleShot(0, real_create)
+
     return command
 
 if __name__ == "__main__":
+    # Create the QApplication first
     app = QtWidgets.QApplication([])
 
-    # Create the NodeGraph controller.
+    # Start the Flask server from data_manager so /data is always available
+    data_manager.start_api_server()
+
+    # Create the NodeGraph controller
+    # (the monkey-patch ensures NodeGraph won't crash if it tries QtGui.QUndoStack(self))
     graph = NodeGraph()
     graph.widget.setWindowTitle("Project Borealis - Flyff Information Overlay")
 
diff --git a/debug_processed.png b/debug_processed.png
deleted file mode 100644
index 5e945f8..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 9fe9c30..0000000
Binary files a/debug_screenshot.png and /dev/null differ