diff --git a/Nodes/__pycache__/array_node.cpython-312.pyc b/Nodes/__pycache__/array_node.cpython-312.pyc index aaf81b7..af5df88 100644 Binary files a/Nodes/__pycache__/array_node.cpython-312.pyc and b/Nodes/__pycache__/array_node.cpython-312.pyc differ diff --git a/Nodes/__pycache__/comparison_node.cpython-312.pyc b/Nodes/__pycache__/comparison_node.cpython-312.pyc index d49ae69..d2429cc 100644 Binary files a/Nodes/__pycache__/comparison_node.cpython-312.pyc and b/Nodes/__pycache__/comparison_node.cpython-312.pyc differ diff --git a/Nodes/__pycache__/data_node.cpython-312.pyc b/Nodes/__pycache__/data_node.cpython-312.pyc index be9ba52..29c8379 100644 Binary files a/Nodes/__pycache__/data_node.cpython-312.pyc and b/Nodes/__pycache__/data_node.cpython-312.pyc differ diff --git a/Nodes/__pycache__/flyff_character_status_node.cpython-312.pyc b/Nodes/__pycache__/flyff_character_status_node.cpython-312.pyc index 6b78cfb..d3efe3d 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__/flyff_low_health_alert_node.cpython-312.pyc b/Nodes/__pycache__/flyff_low_health_alert_node.cpython-312.pyc index cca471d..f6b97ce 100644 Binary files a/Nodes/__pycache__/flyff_low_health_alert_node.cpython-312.pyc and b/Nodes/__pycache__/flyff_low_health_alert_node.cpython-312.pyc differ diff --git a/Nodes/__pycache__/math_operation_node.cpython-312.pyc b/Nodes/__pycache__/math_operation_node.cpython-312.pyc index 75e12f5..50d15b8 100644 Binary files a/Nodes/__pycache__/math_operation_node.cpython-312.pyc and b/Nodes/__pycache__/math_operation_node.cpython-312.pyc differ diff --git a/Nodes/array_node.py b/Nodes/array_node.py index 5a4a26a..8f4e09e 100644 --- a/Nodes/array_node.py +++ b/Nodes/array_node.py @@ -5,8 +5,8 @@ class ArrayNode(BaseNode): Array Node: - Inputs: 'in' (value to store), 'ArraySize' (defines maximum length) - Output: 'Array' (the current array as a string) - - Stores incoming values in an array with size defined by ArraySize. - When full, it removes the oldest value. + - Stores incoming values in an array with a size defined by ArraySize. + - Updates are now handled via a global update timer. """ __identifier__ = 'bunny-lab.io.array_node' NODE_NAME = 'Array' diff --git a/Nodes/basic_nodes.py b/Nodes/basic_nodes.py deleted file mode 100644 index 4d36de1..0000000 --- a/Nodes/basic_nodes.py +++ /dev/null @@ -1,86 +0,0 @@ -from OdenGraphQt import BaseNode, BaseNodeCircle - - -class BasicNodeA(BaseNode): - """ - A node class with 2 inputs and 2 outputs. - """ - - # unique node identifier. - __identifier__ = 'nodes.basic' - - # initial default node name. - NODE_NAME = 'node A' - - def __init__(self): - super(BasicNodeA, self).__init__() - - # create node inputs. - self.add_input('in A') - self.add_input('in B') - - # create node outputs. - self.add_output('out A') - self.add_output('out B') - - -class BasicNodeB(BaseNode): - """ - A node class with 3 inputs and 3 outputs. - The last input and last output can take in multiple pipes. - """ - - # unique node identifier. - __identifier__ = 'nodes.basic' - - # initial default node name. - NODE_NAME = 'node B' - - def __init__(self): - super(BasicNodeB, self).__init__() - - # create node inputs - self.add_input('single 1') - self.add_input('single 2') - self.add_input('multi in', multi_input=True) - - # create node outputs - self.add_output('single 1', multi_output=False) - self.add_output('single 2', multi_output=False) - self.add_output('multi out') - - -class CircleNode(BaseNodeCircle): - """ - A node class with 3 inputs and 3 outputs. - This node is a circular design. - """ - - # unique node identifier. - __identifier__ = 'nodes.basic' - - # initial default node name. - NODE_NAME = 'Circle Node' - - def __init__(self): - super(CircleNode, self).__init__() - self.set_color(10, 24, 38) - - # create node inputs - p = self.add_input('in 1') - p.add_accept_port_type( - port_name='single 1', - port_type='out', - node_type='nodes.basic.BasicNodeB' - ) - - self.add_input('in 2') - self.add_input('in 3', multi_input=True) - self.add_input('in 4', display_name=False) - self.add_input('in 5', display_name=False) - - # create node outputs - self.add_output('out 1') - self.add_output('out 2', multi_output=False) - self.add_output('out 3', multi_output=True, display_name=False) - self.add_output('out 4', multi_output=True, display_name=False) \ No newline at end of file diff --git a/Nodes/comparison_node.py b/Nodes/comparison_node.py index deb52ab..34ad35a 100644 --- a/Nodes/comparison_node.py +++ b/Nodes/comparison_node.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 + """ -Comparison Node: - - Inputs: Two input ports ("A" and "B"). - - Output: One output port ("Result"). - - Operation: A dropdown (combo menu) to select: - Equal (==), Not Equal (!=), Greater Than (>), Less Than (<), - Greater Than or Equal (>=), Less Than or Equal (<=). - - Displays the computed result in a read-only text box labeled "Result". +Standardized Comparison Node: + - Compares two input values using a selected operator (==, !=, >, <, >=, <=). + - Outputs a result of 1 (True) or 0 (False). + - Uses a global update timer for processing. + - Supports an additional 'Input Type' dropdown to choose between 'Number' and 'String'. """ from OdenGraphQt import BaseNode +from Qt import QtCore class ComparisonNode(BaseNode): __identifier__ = 'bunny-lab.io.comparison_node' @@ -17,112 +17,106 @@ class ComparisonNode(BaseNode): def __init__(self): super(ComparisonNode, self).__init__() - - # ---------------------------------------------------------------------- - # Initialization Section: - # - Create two input ports: A, B - # - Create one output port: Result - # - Add a combo box for logical operator selection - # - Add a text input for displaying the computed result - # ---------------------------------------------------------------------- self.add_input('A') self.add_input('B') self.add_output('Result') - # Operator combo box (==, !=, >, <, >=, <=) + # Add the Input Type dropdown first. + self.add_combo_menu('input_type', 'Input Type', items=['Number', 'String']) self.add_combo_menu('operator', 'Operator', items=[ - 'Equal (==)', - 'Not Equal (!=)', - 'Greater Than (>)', - 'Less Than (<)', - 'Greater Than or Equal (>=)', - 'Less Than or Equal (<=)' + 'Equal (==)', 'Not Equal (!=)', 'Greater Than (>)', + 'Less Than (<)', 'Greater Than or Equal (>=)', 'Less Than or Equal (<=)' ]) - - # Text input for displaying the computed result. - # We'll make it read-only by accessing the underlying QLineEdit. - self.add_text_input('calc_result', 'Result', text='0') - result_widget = self.get_widget('calc_result') # This is a NodeLineEdit wrapper - if result_widget: - # Get the underlying QLineEdit - line_edit = result_widget.get_custom_widget() - # Make the QLineEdit read-only - line_edit.setReadOnly(True) - + # Replace calc_result with a standardized "value" text input. + self.add_text_input('value', 'Value', text='0') self.value = 0 self.set_name("Comparison Node") - self.process_input() + self.processing = False # Guard for process_input - def process_input(self, event=None): - """ - Compute Section: - - For each input port (A, B), if connected, grab the 'value' from - the upstream node; otherwise default to 0.0. - - Convert to float when possible, apply the selected comparison operator, - update the "Result" text box, node title, and output port. - """ - # Gather input A + # Set default properties explicitly + self.set_property('input_type', 'Number') + self.set_property('operator', 'Equal (==)') + + def process_input(self): + if self.processing: + return + self.processing = True + + # Retrieve input values; if no connection or None, default to "0" input_a = self.input(0) - if input_a and input_a.connected_ports(): - a_raw = input_a.connected_ports()[0].node().get_property('value') - else: - a_raw = 0.0 - - # Gather input B input_b = self.input(1) - if input_b and input_b.connected_ports(): - b_raw = input_b.connected_ports()[0].node().get_property('value') + a_raw = (input_a.connected_ports()[0].node().get_property('value') + if input_a.connected_ports() else "0") + b_raw = (input_b.connected_ports()[0].node().get_property('value') + if input_b.connected_ports() else "0") + a_raw = a_raw if a_raw is not None else "0" + b_raw = b_raw if b_raw is not None else "0" + + # Get input type property + input_type = self.get_property('input_type') + + # Convert values based on input type + if input_type == 'Number': + try: + a_val = float(a_raw) + except (ValueError, TypeError): + a_val = 0.0 + try: + b_val = float(b_raw) + except (ValueError, TypeError): + b_val = 0.0 + elif input_type == 'String': + a_val = str(a_raw) + b_val = str(b_raw) else: - b_raw = 0.0 + try: + a_val = float(a_raw) + except (ValueError, TypeError): + a_val = 0.0 + try: + b_val = float(b_raw) + except (ValueError, TypeError): + b_val = 0.0 - # Convert raw inputs to float if possible, otherwise keep as-is for string comparison. - try: - a_val = float(a_raw) - b_val = float(b_raw) - except (ValueError, TypeError): - a_val = a_raw - b_val = b_raw - - # Retrieve the selected operator from the combo box. operator = self.get_property('operator') - result = False - - if operator == 'Equal (==)': - result = a_val == b_val - elif operator == 'Not Equal (!=)': - result = a_val != b_val - elif operator == 'Greater Than (>)': - result = a_val > b_val - elif operator == 'Less Than (<)': - result = a_val < b_val - elif operator == 'Greater Than or Equal (>=)': - result = a_val >= b_val - elif operator == 'Less Than or Equal (<=)': - result = a_val <= b_val - - # Convert boolean result to integer (1 for True, 0 for False) - self.value = 1 if result else 0 - - # Update the read-only text input and node title. - self.set_property('calc_result', str(self.value)) - - # Transmit the numeric result to any connected output nodes. - output_port = self.output(0) - if output_port and output_port.connected_ports(): - for cp in output_port.connected_ports(): - connected_node = cp.node() - if hasattr(connected_node, 'receive_data'): - connected_node.receive_data(self.value, source_port_name='Result') + + # Perform the comparison + result = { + 'Equal (==)': a_val == b_val, + 'Not Equal (!=)': a_val != b_val, + 'Greater Than (>)': a_val > b_val, + 'Less Than (<)': a_val < b_val, + 'Greater Than or Equal (>=)': a_val >= b_val, + 'Less Than or Equal (<=)': a_val <= b_val + }.get(operator, False) + + new_value = 1 if result else 0 + self.value = new_value + self.set_property('value', str(self.value)) + self.transmit_data(self.value) + + self.processing = False def on_input_connected(self, input_port, output_port): - self.process_input() + pass def on_input_disconnected(self, input_port, output_port): - self.process_input() + pass def property_changed(self, property_name): - if property_name in ['operator']: - self.process_input() + pass def receive_data(self, data, source_port_name=None): - self.process_input() + pass + + def transmit_data(self, data): + 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'): + try: + data_int = int(data) + connected_node.receive_data(data_int, source_port_name='Result') + except ValueError: + pass diff --git a/Nodes/data_node.py b/Nodes/data_node.py index 1b1304b..6803833 100644 --- a/Nodes/data_node.py +++ b/Nodes/data_node.py @@ -1,21 +1,13 @@ #!/usr/bin/env python3 """ -Data Node: - - Input: Accepts a value (string, integer, or float) from an input port. - - Output: Outputs the current value, either from the input port or set manually via a text box. - -Behavior: -- If both input and output are connected: - - Acts as a passthrough, displaying the input value and transmitting it to the output. - - Manual input is disabled. -- If only the output is connected: - - Allows manual value entry, which is sent to the output. -- If only the input is connected: - - Displays the input value but does not transmit it further. +Standardized Data Node: + - Accepts and transmits values consistently. + - Updates its value based on a global update timer. """ from OdenGraphQt import BaseNode +from Qt import QtCore class DataNode(BaseNode): __identifier__ = 'bunny-lab.io.data_node' @@ -23,103 +15,58 @@ class DataNode(BaseNode): def __init__(self): super(DataNode, self).__init__() - # Add input and output ports. self.add_input('Input') self.add_output('Output') - # Add a text input widget for manual entry. self.add_text_input('value', 'Value', text='') - # Initialize the value from the widget property. self.process_widget_event() - self.set_name(f"Data Node") + self.set_name("Data Node") + # Removed self-contained update timer; global timer now drives updates. def post_create(self): - """ - Called after the node's widget is fully created. - Connect the text input widget's textChanged signal to process_widget_event. - """ text_widget = self.get_widget('value') if text_widget is not None: try: - text_widget.textChanged.connect(self.process_widget_event) + # Removed textChanged signal connection; global timer will call process_input. + pass except Exception as e: print("Error connecting textChanged signal:", e) def process_widget_event(self, event=None): - """ - Reads the current text from the node's property and updates the node's internal value. - """ current_text = self.get_property('value') self.value = current_text + self.transmit_data(current_text) def property_changed(self, property_name): - """ - Called when a node property changes. If the 'value' property changes, - update the internal value. - """ if property_name == 'value': - self.process_widget_event() + # Immediate update removed; relying on global timer. + pass - def update_stream(self): - """ - Updates the node's behavior based on the connection states. - """ + def process_input(self): input_port = self.input(0) output_port = self.output(0) - - if input_port.connected_ports() and output_port.connected_ports(): - # Both input and output are connected; act as passthrough. - self.set_property('value', '') - self.get_widget('value').setEnabled(False) + if input_port.connected_ports(): input_value = input_port.connected_ports()[0].node().get_property('value') self.set_property('value', input_value) + self.transmit_data(input_value) elif output_port.connected_ports(): - # Only output is connected; allow manual input. - self.get_widget('value').setEnabled(True) - elif input_port.connected_ports(): - # Only input is connected; display input value. - self.get_widget('value').setEnabled(False) - input_value = input_port.connected_ports()[0].node().get_property('value') - self.set_property('value', input_value) - else: - # Neither input nor output is connected; allow manual input. - self.get_widget('value').setEnabled(True) + self.transmit_data(self.get_property('value')) def on_input_connected(self, input_port, output_port): - """ - Called when an input port is connected. - """ - self.update_stream() + # Removed immediate update; global timer handles updates. + pass def on_input_disconnected(self, input_port, output_port): - """ - Called when an input port is disconnected. - """ - self.update_stream() - - def on_output_connected(self, output_port, input_port): - """ - Called when an output port is connected. - """ - self.update_stream() - - def on_output_disconnected(self, output_port, input_port): - """ - Called when an output port is disconnected. - """ - self.update_stream() + # Removed immediate update; global timer handles updates. + pass def receive_data(self, data, source_port_name=None): - """ - Receives data from connected nodes and updates the internal value. - """ - self.set_property('value', str(data)) # Ensure it's always stored as a string + self.set_property('value', str(data)) + self.transmit_data(data) - # Transmit data further if there's an output connection + def transmit_data(self, data): 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(data, source_port_name) - - + connected_node.receive_data(data, source_port_name="Output") diff --git a/Nodes/flyff_character_status_node.py b/Nodes/flyff_character_status_node.py index 9275104..3b7ded2 100644 --- a/Nodes/flyff_character_status_node.py +++ b/Nodes/flyff_character_status_node.py @@ -1,47 +1,23 @@ #!/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. +""" + from OdenGraphQt import BaseNode -from Qt import QtCore, QtGui +from Qt import QtCore import requests import traceback -def get_draw_stat_port(color, border_color=None, alpha=127): - """ - Returns a custom port painter function that draws a circular port with a - semi-transparent fill and then draws text (port label and current value) - next to it. - """ - if border_color is None: - border_color = color - - def painter_func(painter, rect, info): - painter.save() - pen = QtGui.QPen(QtGui.QColor(*border_color)) - pen.setWidth(1.8) - painter.setPen(pen) - semi_transparent_color = QtGui.QColor(color[0], color[1], color[2], alpha) - painter.setBrush(semi_transparent_color) - painter.drawEllipse(rect) - port = info.get('port') - if port is not None: - node = port.node() - stat = port.name() - value = node.values.get(stat, "N/A") if hasattr(node, 'values') else "N/A" - text_rect = rect.adjusted(rect.width() + 4, 0, rect.width() + 70, 0) - painter.setPen(QtGui.QColor(0, 0, 0)) - painter.drawText(text_rect, QtCore.Qt.AlignVCenter | QtCore.Qt.AlignLeft, - f"{stat}: {value}") - painter.restore() - return painter_func - -class CharacterStatusNode(BaseNode): +class FlyffCharacterStatusNode(BaseNode): __identifier__ = 'bunny-lab.io.flyff_character_status_node' NODE_NAME = 'Flyff - Character Status' def __init__(self): - super(CharacterStatusNode, self).__init__() - - # Define exact expected keys to avoid transformation mismatches + super(FlyffCharacterStatusNode, self).__init__() self.values = { "HP: Current": "N/A", "HP: Total": "N/A", "MP: Current": "N/A", "MP: Total": "N/A", @@ -49,97 +25,60 @@ class CharacterStatusNode(BaseNode): "EXP": "N/A" } - # Add output ports - self.add_output("HP: Current", painter_func=get_draw_stat_port((217, 36, 78))) - self.add_output("HP: Total", painter_func=get_draw_stat_port((217, 36, 78))) - self.add_output("MP: Current", painter_func=get_draw_stat_port((35, 124, 213))) - self.add_output("MP: Total", painter_func=get_draw_stat_port((35, 124, 213))) - self.add_output("FP: Current", painter_func=get_draw_stat_port((36, 197, 28))) - self.add_output("FP: Total", painter_func=get_draw_stat_port((36, 197, 28))) - self.add_output("EXP", painter_func=get_draw_stat_port((52, 195, 250))) + for stat in self.values.keys(): + self.add_output(stat) self.set_name("Flyff - Character Status (API Disconnected)") + # Removed self-contained polling timer; global timer now drives updates. - # Start polling timer - self.timer = QtCore.QTimer() - self.timer.timeout.connect(self.poll_api) - self.timer.start(500) - - def poll_api(self): - """ - Polls the API endpoint to retrieve the latest character stats and updates - the node's internal values. - """ + def process_input(self): 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): - try: - for key, value in data.items(): - # Ensure the keys match the expected ones exactly - formatted_key = { - "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" - }.get(key, key) # Use mapping or fallback to raw key - - if formatted_key in self.values: + 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) - else: - print(f"[WARNING] Unexpected API key: {key} (not mapped)") - + updated = True + if updated: self.set_name("Flyff - Character Status (API Connected)") - self.update() self.transmit_data() - - except Exception as e: - print("[ERROR] Error processing API response data:", e) - print("[ERROR] Stack Trace:\n", traceback.format_exc()) else: print("[ERROR] Unexpected API response format (not a dict):", data) self.set_name("Flyff - Character Status (API Disconnected)") - else: print(f"[ERROR] API request failed with status code {response.status_code}") self.set_name("Flyff - Character Status (API Disconnected)") - except Exception as e: self.set_name("Flyff - Character Status (API Disconnected)") print("[ERROR] Error polling API in CharacterStatusNode:", str(e)) - print("[ERROR] Stack Trace:\n", traceback.format_exc()) def transmit_data(self): - """ - Sends the updated character stats to connected nodes. - """ for stat, value in self.values.items(): - try: - port = self.get_output(stat) - - if port is None: - print(f"[ERROR] Port '{stat}' not found in node outputs. Skipping...") - continue - - if 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()) - else: - print(f"[WARNING] Connected node {connected_node} does not have receive_data method.") - - except Exception as e: - print(f"[ERROR] Error while handling port {stat}: {e}") - print("[ERROR] Stack Trace:\n", traceback.format_exc()) + 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/flyff_low_health_alert_node.py b/Nodes/flyff_low_health_alert_node.py index 2aa0b1d..699e1bd 100644 --- a/Nodes/flyff_low_health_alert_node.py +++ b/Nodes/flyff_low_health_alert_node.py @@ -1,177 +1,133 @@ -import time -import sys -from OdenGraphQt import BaseNode -from Qt import QtWidgets, QtCore, QtGui +#!/usr/bin/env python3 + +""" +Standardized Flyff Low Health Alert Node: + - Monitors an input value (1 = health alert, 0 = normal). + - Displays a visual alert and plays a sound if enabled. + - Uses a global update timer for processing. + - Automatically processes float, int, and string values. +""" + +import time +from OdenGraphQt import BaseNode +from Qt import QtCore, QtWidgets, QtGui -# Attempt to import winsound (Windows-only) try: import winsound HAS_WINSOUND = True except ImportError: HAS_WINSOUND = False - class OverlayCanvas(QtWidgets.QWidget): """ UI overlay for displaying a red warning box, which can be repositioned by dragging. """ - def __init__(self, parent=None): super().__init__(parent) - - # **Full-screen overlay** screen_geo = QtWidgets.QApplication.primaryScreen().geometry() - self.setGeometry(screen_geo) # Set to full screen - + self.setGeometry(screen_geo) self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint) self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) - self.setAttribute(QtCore.Qt.WA_NoSystemBackground, True) - self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent, False) - self.setAttribute(QtCore.Qt.WA_AlwaysStackOnTop, True) - - # **Draggable Low Health Alert** - self.helper_LowHealthAlert = QtCore.QRect(250, 300, 900, 35) # Default Position + self.setVisible(False) + self.helper_LowHealthAlert = QtCore.QRect(250, 300, 900, 35) self.dragging = False - self.drag_offset = QtCore.QPoint() - - self.setVisible(False) # Initially hidden + self.drag_offset = None def paintEvent(self, event): - """Draw the helper overlay objects.""" if not self.isVisible(): - return # Don't draw anything if invisible - + return painter = QtGui.QPainter(self) painter.setPen(QtCore.Qt.NoPen) - painter.setBrush(QtGui.QColor(255, 0, 0)) # Solid red rectangle + painter.setBrush(QtGui.QColor(255, 0, 0)) painter.drawRect(self.helper_LowHealthAlert) - - # Draw bold white text centered within the rectangle font = QtGui.QFont("Arial", 14, QtGui.QFont.Bold) painter.setFont(font) painter.setPen(QtGui.QColor(255, 255, 255)) - - text = "LOW HEALTH" - metrics = QtGui.QFontMetrics(font) - text_width = metrics.horizontalAdvance(text) - text_height = metrics.height() - text_x = self.helper_LowHealthAlert.center().x() - text_width // 2 - text_y = self.helper_LowHealthAlert.center().y() + text_height // 4 - - painter.drawText(text_x, text_y, text) + text_x = self.helper_LowHealthAlert.center().x() - 50 + text_y = self.helper_LowHealthAlert.center().y() + 5 + painter.drawText(text_x, text_y, "LOW HEALTH") def toggle_alert(self, state): - """ - Show or hide the overlay based on the state (1 = show, 0 = hide). - """ self.setVisible(state == 1) self.update() def mousePressEvent(self, event): - """Detect clicks inside the red box and allow dragging.""" - if event.button() == QtCore.Qt.LeftButton and self.helper_LowHealthAlert.contains(event.pos()): - self.dragging = True - self.drag_offset = event.pos() - self.helper_LowHealthAlert.topLeft() + if event.button() == QtCore.Qt.LeftButton: + if self.helper_LowHealthAlert.contains(event.pos()): + self.dragging = True + self.drag_offset = event.pos() - self.helper_LowHealthAlert.topLeft() + super().mousePressEvent(event) def mouseMoveEvent(self, event): - """Handle dragging movement.""" if self.dragging: - new_x = event.pos().x() - self.drag_offset.x() - new_y = event.pos().y() - self.drag_offset.y() - self.helper_LowHealthAlert.moveTopLeft(QtCore.QPoint(new_x, new_y)) + new_top_left = event.pos() - self.drag_offset + self.helper_LowHealthAlert.moveTo(new_top_left) self.update() + super().mouseMoveEvent(event) def mouseReleaseEvent(self, event): - """Stop dragging when the mouse button is released.""" - self.dragging = False - + if event.button() == QtCore.Qt.LeftButton: + self.dragging = False + super().mouseReleaseEvent(event) class FlyffLowHealthAlertNode(BaseNode): - """ - Custom OdenGraphQt node that toggles a visual alert overlay and plays a beep when health is low. - """ - __identifier__ = 'bunny-lab.io.flyff_low_health_alert_node' NODE_NAME = 'Flyff - Low Health Alert' - overlay_instance = None # Shared overlay instance - last_beep_time = 0 # Time tracking for beep interval - BEEP_INTERVAL_SECONDS = 2 # Beep every 2 seconds + overlay_instance = None + last_beep_time = 0 + BEEP_INTERVAL_SECONDS = 2 def __init__(self): super(FlyffLowHealthAlertNode, self).__init__() - - # Create checkboxes to decide which kind of alert(s) to utilize self.add_checkbox('cb_1', '', 'Sound Alert', True) self.add_checkbox('cb_2', '', 'Visual Alert', True) - - # Create Input Port self.add_input('Toggle (1 = On | 0 = Off)', color=(200, 100, 0)) - - # Add text input widget to display received value self.add_text_input('value', 'Current Value', text='0') + self.add_combo_menu('beep_interval', 'Beep Interval', items=["0.5s", "1.0s", "2.0s"]) - # Ensure only one overlay instance exists if not FlyffLowHealthAlertNode.overlay_instance: FlyffLowHealthAlertNode.overlay_instance = OverlayCanvas() FlyffLowHealthAlertNode.overlay_instance.show() def process_input(self): - """ - This function runs every 500ms (via the global update loop). - It updates the displayed value and toggles the alert if needed. - """ input_port = self.input(0) + value = input_port.connected_ports()[0].node().get_property('value') if input_port.connected_ports() else "0" + self.receive_data(value) - # If there is a connected node, fetch its output value - if input_port.connected_ports(): - connected_node = input_port.connected_ports()[0].node() - if hasattr(connected_node, 'get_property'): - value = connected_node.get_property('value') - else: - value = "0" - else: - value = "0" # Default to zero if nothing is connected - + def receive_data(self, data, source_port_name=None): try: - input_value = int(value) # Ensure we interpret input as an integer (0 or 1) + if isinstance(data, str): + data = float(data) if '.' in data else int(data) + if isinstance(data, (float, int)): + data = 1 if data > 1 else 0 if data <= 0 else int(data) + else: + data = 0 except ValueError: - input_value = 0 # Default to off if the input is not valid + data = 0 - # Update the value display box - self.set_property('value', str(input_value)) - - # Check if the "Visual Alert" checkbox is enabled - visual_alert_enabled = self.get_property('cb_2') - - # Ensure that if "Visual Alert" is unchecked, the overlay is always hidden - if not visual_alert_enabled: - FlyffLowHealthAlertNode.overlay_instance.toggle_alert(0) - else: - FlyffLowHealthAlertNode.overlay_instance.toggle_alert(input_value) - - # Check if "Sound Alert" is enabled and beep if necessary - self.handle_beep(input_value) + self.set_property('value', str(data)) + if self.get_property('cb_2'): + FlyffLowHealthAlertNode.overlay_instance.toggle_alert(data) + self.handle_beep(data) def handle_beep(self, input_value): - """ - Plays a beep sound every 2 seconds when the value is `1` and "Sound Alert" is enabled. - """ - sound_alert_enabled = self.get_property('cb_1') - current_time = time.time() + # Update beep interval from the dropdown property + interval_str = self.get_property('beep_interval') + if interval_str.endswith("s"): + interval_seconds = float(interval_str[:-1]) + else: + interval_seconds = float(interval_str) + self.BEEP_INTERVAL_SECONDS = interval_seconds - if input_value == 1 and sound_alert_enabled: - if (current_time - FlyffLowHealthAlertNode.last_beep_time) >= FlyffLowHealthAlertNode.BEEP_INTERVAL_SECONDS: + if input_value == 1 and self.get_property('cb_1'): + current_time = time.time() + if (current_time - FlyffLowHealthAlertNode.last_beep_time) >= self.BEEP_INTERVAL_SECONDS: FlyffLowHealthAlertNode.last_beep_time = current_time self.play_beep() - else: - FlyffLowHealthAlertNode.last_beep_time = 0 # Reset when health is safe def play_beep(self): - """ - Plays a beep using `winsound.Beep` (Windows) or prints a terminal bell (`\a`). - """ if HAS_WINSOUND: - winsound.Beep(376, 100) # 376 Hz, 100ms duration + winsound.Beep(376, 100) else: - print('\a', end='') # Terminal bell for non-Windows systems + print('\a', end='') diff --git a/Nodes/math_operation_node.py b/Nodes/math_operation_node.py index 149b895..1aea0fa 100644 --- a/Nodes/math_operation_node.py +++ b/Nodes/math_operation_node.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 + """ -Math Operation Node: - - Inputs: Two input ports ("A" and "B"). - - Output: One output port ("Result"). - - Operation: A dropdown (combo menu) to select: - Add, Subtract, Multiply, Divide, Average. - - Displays the computed result in a read-only text box labeled "Result". +Standardized Math Operation Node: + - Performs mathematical operations (+, -, *, /, avg) on two inputs. + - Outputs the computed result. + - Uses a global update timer for processing (defined in borealis.py). + - Ensures it always has a "value" property that the Comparison Node can read. """ from OdenGraphQt import BaseNode +from Qt import QtCore class MathOperationNode(BaseNode): __identifier__ = 'bunny-lab.io.math_node' @@ -16,60 +17,38 @@ class MathOperationNode(BaseNode): def __init__(self): super(MathOperationNode, self).__init__() - - # ---------------------------------------------------------------------- - # Initialization Section: - # - Create two input ports: A, B - # - Create one output port: Result - # - Add a combo box for operator selection - # - Add a text input for displaying the computed result - # ---------------------------------------------------------------------- self.add_input('A') self.add_input('B') self.add_output('Result') - - # Operator combo box (Add, Subtract, Multiply, Divide, Average) + + # Drop-down to choose which operation we do: self.add_combo_menu('operator', 'Operator', items=[ 'Add', 'Subtract', 'Multiply', 'Divide', 'Average' ]) - # Text input for displaying the computed result. - # We'll make it read-only by accessing the underlying QLineEdit. + # A text field for showing the result to the user: self.add_text_input('calc_result', 'Result', text='0') - result_widget = self.get_widget('calc_result') # This is a NodeLineEdit wrapper - if result_widget: - # Get the underlying QLineEdit - line_edit = result_widget.get_custom_widget() - # Make the QLineEdit read-only - line_edit.setReadOnly(True) + # IMPORTANT: define a "value" property that the Comparison Node can read + # We do not necessarily need a text input for it, but adding it ensures + # it becomes an official property recognized by OdenGraphQt. + self.add_text_input('value', 'Internal Value', text='0') + + # Keep a Python-side float of the current computed result: self.value = 0 + + # Give the node a nice name: self.set_name("Math Operation") - self.process_input() - def process_input(self, event=None): - """ - Compute Section: - - For each input port (A, B), if connected, grab the 'value' from - the upstream node; otherwise default to 0.0. - - Convert to float, apply the selected operation from the combo box, - update the "Result" text box, node title, and output port. - """ - # Gather input A + # Removed self-contained timer; global timer calls process_input(). + + def process_input(self): + # Attempt to read "value" from both inputs: input_a = self.input(0) - if input_a and input_a.connected_ports(): - a_raw = input_a.connected_ports()[0].node().get_property('value') - else: - a_raw = 0.0 - - # Gather input B input_b = self.input(1) - if input_b and input_b.connected_ports(): - b_raw = input_b.connected_ports()[0].node().get_property('value') - else: - b_raw = 0.0 + a_raw = input_a.connected_ports()[0].node().get_property('value') if input_a.connected_ports() else "0" + b_raw = input_b.connected_ports()[0].node().get_property('value') if input_b.connected_ports() else "0" - # Convert raw inputs to floats (default 0.0 on failure). try: a_val = float(a_raw) except (ValueError, TypeError): @@ -79,10 +58,7 @@ class MathOperationNode(BaseNode): except (ValueError, TypeError): b_val = 0.0 - # Retrieve the selected operator from the combo box. operator = self.get_property('operator') - result = 0.0 - if operator == 'Add': result = a_val + b_val elif operator == 'Subtract': @@ -93,30 +69,41 @@ class MathOperationNode(BaseNode): result = a_val / b_val if b_val != 0 else 0.0 elif operator == 'Average': result = (a_val + b_val) / 2.0 + else: + result = 0.0 - self.value = result + # If the computed result changed, update our internal properties and transmit + if self.value != result: + self.value = result - # Update the read-only text input and node title. - self.set_property('calc_result', str(result)) + # Update the two text fields so the user sees the numeric result: + self.set_property('calc_result', str(result)) + self.set_property('value', str(result)) # <= This is the critical step - # Transmit the numeric result to any connected output nodes. - output_port = self.output(0) - if output_port and output_port.connected_ports(): - for cp in output_port.connected_ports(): - connected_node = cp.node() - if hasattr(connected_node, 'receive_data'): - connected_node.receive_data(result, source_port_name='Result') + # Let downstream nodes know there's new data: + self.transmit_data(result) def on_input_connected(self, input_port, output_port): - self.process_input() + pass def on_input_disconnected(self, input_port, output_port): - self.process_input() + pass def property_changed(self, property_name): - if property_name in ['operator']: - self.process_input() + pass def receive_data(self, data, source_port_name=None): - self.process_input() + pass + def transmit_data(self, data): + 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'): + try: + # Attempt to convert to int if possible, else float + data_int = int(data) + connected_node.receive_data(data_int, source_port_name='Result') + except ValueError: + connected_node.receive_data(data, source_port_name='Result') diff --git a/borealis.py b/borealis.py index 1f8eb20..8aff9c5 100644 --- a/borealis.py +++ b/borealis.py @@ -1,27 +1,27 @@ #!/usr/bin/env python3 -## --- Patch QGraphicsScene.setSelectionArea to handle selection arguments --- -#from Qt import QtWidgets, QtCore, QtGui -# -#_original_setSelectionArea = QtWidgets.QGraphicsScene.setSelectionArea -# -#def _patched_setSelectionArea(self, painterPath, second_arg, *args, **kwargs): -# try: -# # Try calling the original method with the provided arguments. -# return _original_setSelectionArea(self, painterPath, second_arg, *args, **kwargs) -# except TypeError as e: -# # If a TypeError is raised, assume the call was made with only a QPainterPath -# # and an ItemSelectionMode, and patch it by supplying defaults. -# # Default operation: ReplaceSelection, default transform: QTransform() -# return _original_setSelectionArea(self, painterPath, -# QtCore.Qt.ReplaceSelection, -# second_arg, -# QtGui.QTransform()) -# -## Monkey-patch the setSelectionArea method. -#QtWidgets.QGraphicsScene.setSelectionArea = _patched_setSelectionArea -# -## --- End of patch section --- +# --- Patch QGraphicsScene.setSelectionArea to handle selection arguments --- +from Qt import QtWidgets, QtCore, QtGui + +_original_setSelectionArea = QtWidgets.QGraphicsScene.setSelectionArea + +def _patched_setSelectionArea(self, painterPath, second_arg, *args, **kwargs): + try: + # Try calling the original method with the provided arguments. + return _original_setSelectionArea(self, painterPath, second_arg, *args, **kwargs) + except TypeError as e: + # If a TypeError is raised, assume the call was made with only a QPainterPath + # and an ItemSelectionMode, and patch it by supplying defaults. + # Default operation: ReplaceSelection, default transform: QTransform() + return _original_setSelectionArea(self, painterPath, + QtCore.Qt.ReplaceSelection, + second_arg, + QtGui.QTransform()) + +# Monkey-patch the setSelectionArea method. +QtWidgets.QGraphicsScene.setSelectionArea = _patched_setSelectionArea + +# --- End of patch section --- import sys import pkgutil @@ -92,7 +92,6 @@ if __name__ == '__main__': graph.widget.show() # Global update timer: - # - Call process_input() on every node that implements it. def global_update(): for node in graph.all_nodes(): if hasattr(node, "process_input"): @@ -104,5 +103,4 @@ if __name__ == '__main__': timer.timeout.connect(global_update) timer.start(500) - sys.exit(app.exec()) - + sys.exit(app.exec()) \ No newline at end of file diff --git a/debug_processed.png b/debug_processed.png new file mode 100644 index 0000000..41a544d Binary files /dev/null and b/debug_processed.png differ diff --git a/debug_screenshot.png b/debug_screenshot.png new file mode 100644 index 0000000..fb57647 Binary files /dev/null and b/debug_screenshot.png differ