Fixed Timing / Data Update Logic / Low Health Overlay Issues / Comparison Node and Math Node issues.

This commit is contained in:
Nicole Rappe 2025-02-15 23:18:58 -07:00
parent d9623193d8
commit 6bac303dea
16 changed files with 292 additions and 557 deletions

View File

@ -5,8 +5,8 @@ class ArrayNode(BaseNode):
Array Node: Array Node:
- Inputs: 'in' (value to store), 'ArraySize' (defines maximum length) - Inputs: 'in' (value to store), 'ArraySize' (defines maximum length)
- Output: 'Array' (the current array as a string) - Output: 'Array' (the current array as a string)
- Stores incoming values in an array with size defined by ArraySize. - Stores incoming values in an array with a size defined by ArraySize.
When full, it removes the oldest value. - Updates are now handled via a global update timer.
""" """
__identifier__ = 'bunny-lab.io.array_node' __identifier__ = 'bunny-lab.io.array_node'
NODE_NAME = 'Array' NODE_NAME = 'Array'

View File

@ -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)

View File

@ -1,15 +1,15 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Comparison Node: Standardized Comparison Node:
- Inputs: Two input ports ("A" and "B"). - Compares two input values using a selected operator (==, !=, >, <, >=, <=).
- Output: One output port ("Result"). - Outputs a result of 1 (True) or 0 (False).
- Operation: A dropdown (combo menu) to select: - Uses a global update timer for processing.
Equal (==), Not Equal (!=), Greater Than (>), Less Than (<), - Supports an additional 'Input Type' dropdown to choose between 'Number' and 'String'.
Greater Than or Equal (>=), Less Than or Equal (<=).
- Displays the computed result in a read-only text box labeled "Result".
""" """
from OdenGraphQt import BaseNode from OdenGraphQt import BaseNode
from Qt import QtCore
class ComparisonNode(BaseNode): class ComparisonNode(BaseNode):
__identifier__ = 'bunny-lab.io.comparison_node' __identifier__ = 'bunny-lab.io.comparison_node'
@ -17,112 +17,106 @@ class ComparisonNode(BaseNode):
def __init__(self): def __init__(self):
super(ComparisonNode, self).__init__() 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('A')
self.add_input('B') self.add_input('B')
self.add_output('Result') 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=[ self.add_combo_menu('operator', 'Operator', items=[
'Equal (==)', 'Equal (==)', 'Not Equal (!=)', 'Greater Than (>)',
'Not Equal (!=)', 'Less Than (<)', 'Greater Than or Equal (>=)', 'Less Than or Equal (<=)'
'Greater Than (>)',
'Less Than (<)',
'Greater Than or Equal (>=)',
'Less Than or Equal (<=)'
]) ])
# Replace calc_result with a standardized "value" text input.
# Text input for displaying the computed result. self.add_text_input('value', 'Value', text='0')
# 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)
self.value = 0 self.value = 0
self.set_name("Comparison Node") self.set_name("Comparison Node")
self.process_input() self.processing = False # Guard for process_input
def process_input(self, event=None): # Set default properties explicitly
""" self.set_property('input_type', 'Number')
Compute Section: self.set_property('operator', 'Equal (==)')
- For each input port (A, B), if connected, grab the 'value' from
the upstream node; otherwise default to 0.0. def process_input(self):
- Convert to float when possible, apply the selected comparison operator, if self.processing:
update the "Result" text box, node title, and output port. return
""" self.processing = True
# Gather input A
# Retrieve input values; if no connection or None, default to "0"
input_a = self.input(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) input_b = self.input(1)
if input_b and input_b.connected_ports(): a_raw = (input_a.connected_ports()[0].node().get_property('value')
b_raw = input_b.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: 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') operator = self.get_property('operator')
result = False
# Perform the comparison
if operator == 'Equal (==)': result = {
result = a_val == b_val 'Equal (==)': a_val == b_val,
elif operator == 'Not Equal (!=)': 'Not Equal (!=)': a_val != b_val,
result = a_val != b_val 'Greater Than (>)': a_val > b_val,
elif operator == 'Greater Than (>)': 'Less Than (<)': a_val < b_val,
result = a_val > b_val 'Greater Than or Equal (>=)': a_val >= b_val,
elif operator == 'Less Than (<)': 'Less Than or Equal (<=)': a_val <= b_val
result = a_val < b_val }.get(operator, False)
elif operator == 'Greater Than or Equal (>=)':
result = a_val >= b_val new_value = 1 if result else 0
elif operator == 'Less Than or Equal (<=)': self.value = new_value
result = a_val <= b_val self.set_property('value', str(self.value))
self.transmit_data(self.value)
# Convert boolean result to integer (1 for True, 0 for False)
self.value = 1 if result else 0 self.processing = False
# 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')
def on_input_connected(self, input_port, output_port): def on_input_connected(self, input_port, output_port):
self.process_input() pass
def on_input_disconnected(self, input_port, output_port): def on_input_disconnected(self, input_port, output_port):
self.process_input() pass
def property_changed(self, property_name): def property_changed(self, property_name):
if property_name in ['operator']: pass
self.process_input()
def receive_data(self, data, source_port_name=None): 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

View File

@ -1,21 +1,13 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Data Node: Standardized Data Node:
- Input: Accepts a value (string, integer, or float) from an input port. - Accepts and transmits values consistently.
- Output: Outputs the current value, either from the input port or set manually via a text box. - Updates its value based on a global update timer.
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.
""" """
from OdenGraphQt import BaseNode from OdenGraphQt import BaseNode
from Qt import QtCore
class DataNode(BaseNode): class DataNode(BaseNode):
__identifier__ = 'bunny-lab.io.data_node' __identifier__ = 'bunny-lab.io.data_node'
@ -23,103 +15,58 @@ class DataNode(BaseNode):
def __init__(self): def __init__(self):
super(DataNode, self).__init__() super(DataNode, self).__init__()
# Add input and output ports.
self.add_input('Input') self.add_input('Input')
self.add_output('Output') self.add_output('Output')
# Add a text input widget for manual entry.
self.add_text_input('value', 'Value', text='') self.add_text_input('value', 'Value', text='')
# Initialize the value from the widget property.
self.process_widget_event() 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): 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') text_widget = self.get_widget('value')
if text_widget is not None: if text_widget is not None:
try: try:
text_widget.textChanged.connect(self.process_widget_event) # Removed textChanged signal connection; global timer will call process_input.
pass
except Exception as e: except Exception as e:
print("Error connecting textChanged signal:", e) print("Error connecting textChanged signal:", e)
def process_widget_event(self, event=None): 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') current_text = self.get_property('value')
self.value = current_text self.value = current_text
self.transmit_data(current_text)
def property_changed(self, property_name): 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': if property_name == 'value':
self.process_widget_event() # Immediate update removed; relying on global timer.
pass
def update_stream(self): def process_input(self):
"""
Updates the node's behavior based on the connection states.
"""
input_port = self.input(0) input_port = self.input(0)
output_port = self.output(0) output_port = self.output(0)
if input_port.connected_ports():
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)
input_value = input_port.connected_ports()[0].node().get_property('value') input_value = input_port.connected_ports()[0].node().get_property('value')
self.set_property('value', input_value) self.set_property('value', input_value)
self.transmit_data(input_value)
elif output_port.connected_ports(): elif output_port.connected_ports():
# Only output is connected; allow manual input. self.transmit_data(self.get_property('value'))
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)
def on_input_connected(self, input_port, output_port): def on_input_connected(self, input_port, output_port):
""" # Removed immediate update; global timer handles updates.
Called when an input port is connected. pass
"""
self.update_stream()
def on_input_disconnected(self, input_port, output_port): def on_input_disconnected(self, input_port, output_port):
""" # Removed immediate update; global timer handles updates.
Called when an input port is disconnected. pass
"""
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()
def receive_data(self, data, source_port_name=None): def receive_data(self, data, source_port_name=None):
""" self.set_property('value', str(data))
Receives data from connected nodes and updates the internal value. self.transmit_data(data)
"""
self.set_property('value', str(data)) # Ensure it's always stored as a string
# Transmit data further if there's an output connection def transmit_data(self, data):
output_port = self.output(0) output_port = self.output(0)
if output_port and output_port.connected_ports(): if output_port and output_port.connected_ports():
for connected_port in output_port.connected_ports(): for connected_port in output_port.connected_ports():
connected_node = connected_port.node() connected_node = connected_port.node()
if hasattr(connected_node, 'receive_data'): if hasattr(connected_node, 'receive_data'):
connected_node.receive_data(data, source_port_name) connected_node.receive_data(data, source_port_name="Output")

View File

@ -1,47 +1,23 @@
#!/usr/bin/env python3 #!/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 OdenGraphQt import BaseNode
from Qt import QtCore, QtGui from Qt import QtCore
import requests import requests
import traceback import traceback
def get_draw_stat_port(color, border_color=None, alpha=127): class FlyffCharacterStatusNode(BaseNode):
"""
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):
__identifier__ = 'bunny-lab.io.flyff_character_status_node' __identifier__ = 'bunny-lab.io.flyff_character_status_node'
NODE_NAME = 'Flyff - Character Status' NODE_NAME = 'Flyff - Character Status'
def __init__(self): def __init__(self):
super(CharacterStatusNode, self).__init__() super(FlyffCharacterStatusNode, self).__init__()
# Define exact expected keys to avoid transformation mismatches
self.values = { self.values = {
"HP: Current": "N/A", "HP: Total": "N/A", "HP: Current": "N/A", "HP: Total": "N/A",
"MP: Current": "N/A", "MP: Total": "N/A", "MP: Current": "N/A", "MP: Total": "N/A",
@ -49,97 +25,60 @@ class CharacterStatusNode(BaseNode):
"EXP": "N/A" "EXP": "N/A"
} }
# Add output ports for stat in self.values.keys():
self.add_output("HP: Current", painter_func=get_draw_stat_port((217, 36, 78))) self.add_output(stat)
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)))
self.set_name("Flyff - Character Status (API Disconnected)") self.set_name("Flyff - Character Status (API Disconnected)")
# Removed self-contained polling timer; global timer now drives updates.
# Start polling timer def process_input(self):
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.
"""
try: try:
response = requests.get("http://127.0.0.1:5000/data", timeout=1) response = requests.get("http://127.0.0.1:5000/data", timeout=1)
if response.status_code == 200: if response.status_code == 200:
data = response.json() data = response.json()
if isinstance(data, dict): if isinstance(data, dict):
try: mapping = {
for key, value in data.items(): "hp_current": "HP: Current",
# Ensure the keys match the expected ones exactly "hp_total": "HP: Total",
formatted_key = { "mp_current": "MP: Current",
"hp_current": "HP: Current", "mp_total": "MP: Total",
"hp_total": "HP: Total", "fp_current": "FP: Current",
"mp_current": "MP: Current", "fp_total": "FP: Total",
"mp_total": "MP: Total", "exp": "EXP"
"fp_current": "FP: Current", }
"fp_total": "FP: Total", updated = False
"exp": "EXP" for key, value in data.items():
}.get(key, key) # Use mapping or fallback to raw key if key in mapping:
formatted_key = mapping[key]
if formatted_key in self.values: if str(value) != self.values.get(formatted_key, None):
self.values[formatted_key] = str(value) self.values[formatted_key] = str(value)
else: updated = True
print(f"[WARNING] Unexpected API key: {key} (not mapped)") if updated:
self.set_name("Flyff - Character Status (API Connected)") self.set_name("Flyff - Character Status (API Connected)")
self.update()
self.transmit_data() self.transmit_data()
except Exception as e:
print("[ERROR] Error processing API response data:", e)
print("[ERROR] Stack Trace:\n", traceback.format_exc())
else: else:
print("[ERROR] Unexpected API response format (not a dict):", data) print("[ERROR] Unexpected API response format (not a dict):", data)
self.set_name("Flyff - Character Status (API Disconnected)") self.set_name("Flyff - Character Status (API Disconnected)")
else: else:
print(f"[ERROR] API request failed with status code {response.status_code}") print(f"[ERROR] API request failed with status code {response.status_code}")
self.set_name("Flyff - Character Status (API Disconnected)") self.set_name("Flyff - Character Status (API Disconnected)")
except Exception as e: except Exception as e:
self.set_name("Flyff - Character Status (API Disconnected)") self.set_name("Flyff - Character Status (API Disconnected)")
print("[ERROR] Error polling API in CharacterStatusNode:", str(e)) print("[ERROR] Error polling API in CharacterStatusNode:", str(e))
print("[ERROR] Stack Trace:\n", traceback.format_exc())
def transmit_data(self): def transmit_data(self):
"""
Sends the updated character stats to connected nodes.
"""
for stat, value in self.values.items(): for stat, value in self.values.items():
try: port = self.get_output(stat)
port = self.get_output(stat) if port and port.connected_ports():
for connected_port in port.connected_ports():
if port is None: connected_node = connected_port.node()
print(f"[ERROR] Port '{stat}' not found in node outputs. Skipping...") if hasattr(connected_node, 'receive_data'):
continue try:
connected_node.receive_data(value, stat)
if port.connected_ports(): except Exception as e:
for connected_port in port.connected_ports(): print(f"[ERROR] Error transmitting data to {connected_node}: {e}")
connected_node = connected_port.node() print("[ERROR] Stack Trace:\n", traceback.format_exc())
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())
def receive_data(self, data, source_port_name=None):
# This node only transmits data; it does not receive external data.
pass

View File

@ -1,177 +1,133 @@
import time #!/usr/bin/env python3
import sys
from OdenGraphQt import BaseNode """
from Qt import QtWidgets, QtCore, QtGui 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: try:
import winsound import winsound
HAS_WINSOUND = True HAS_WINSOUND = True
except ImportError: except ImportError:
HAS_WINSOUND = False HAS_WINSOUND = False
class OverlayCanvas(QtWidgets.QWidget): class OverlayCanvas(QtWidgets.QWidget):
""" """
UI overlay for displaying a red warning box, which can be repositioned by dragging. UI overlay for displaying a red warning box, which can be repositioned by dragging.
""" """
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
# **Full-screen overlay**
screen_geo = QtWidgets.QApplication.primaryScreen().geometry() 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.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True)
self.setAttribute(QtCore.Qt.WA_NoSystemBackground, True) self.setVisible(False)
self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent, False) self.helper_LowHealthAlert = QtCore.QRect(250, 300, 900, 35)
self.setAttribute(QtCore.Qt.WA_AlwaysStackOnTop, True)
# **Draggable Low Health Alert**
self.helper_LowHealthAlert = QtCore.QRect(250, 300, 900, 35) # Default Position
self.dragging = False self.dragging = False
self.drag_offset = QtCore.QPoint() self.drag_offset = None
self.setVisible(False) # Initially hidden
def paintEvent(self, event): def paintEvent(self, event):
"""Draw the helper overlay objects."""
if not self.isVisible(): if not self.isVisible():
return # Don't draw anything if invisible return
painter = QtGui.QPainter(self) painter = QtGui.QPainter(self)
painter.setPen(QtCore.Qt.NoPen) 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) painter.drawRect(self.helper_LowHealthAlert)
# Draw bold white text centered within the rectangle
font = QtGui.QFont("Arial", 14, QtGui.QFont.Bold) font = QtGui.QFont("Arial", 14, QtGui.QFont.Bold)
painter.setFont(font) painter.setFont(font)
painter.setPen(QtGui.QColor(255, 255, 255)) painter.setPen(QtGui.QColor(255, 255, 255))
text_x = self.helper_LowHealthAlert.center().x() - 50
text = "LOW HEALTH" text_y = self.helper_LowHealthAlert.center().y() + 5
metrics = QtGui.QFontMetrics(font) painter.drawText(text_x, text_y, "LOW HEALTH")
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)
def toggle_alert(self, state): def toggle_alert(self, state):
"""
Show or hide the overlay based on the state (1 = show, 0 = hide).
"""
self.setVisible(state == 1) self.setVisible(state == 1)
self.update() self.update()
def mousePressEvent(self, event): def mousePressEvent(self, event):
"""Detect clicks inside the red box and allow dragging.""" if event.button() == QtCore.Qt.LeftButton:
if event.button() == QtCore.Qt.LeftButton and self.helper_LowHealthAlert.contains(event.pos()): if self.helper_LowHealthAlert.contains(event.pos()):
self.dragging = True self.dragging = True
self.drag_offset = event.pos() - self.helper_LowHealthAlert.topLeft() self.drag_offset = event.pos() - self.helper_LowHealthAlert.topLeft()
super().mousePressEvent(event)
def mouseMoveEvent(self, event): def mouseMoveEvent(self, event):
"""Handle dragging movement."""
if self.dragging: if self.dragging:
new_x = event.pos().x() - self.drag_offset.x() new_top_left = event.pos() - self.drag_offset
new_y = event.pos().y() - self.drag_offset.y() self.helper_LowHealthAlert.moveTo(new_top_left)
self.helper_LowHealthAlert.moveTopLeft(QtCore.QPoint(new_x, new_y))
self.update() self.update()
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event): def mouseReleaseEvent(self, event):
"""Stop dragging when the mouse button is released.""" if event.button() == QtCore.Qt.LeftButton:
self.dragging = False self.dragging = False
super().mouseReleaseEvent(event)
class FlyffLowHealthAlertNode(BaseNode): 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' __identifier__ = 'bunny-lab.io.flyff_low_health_alert_node'
NODE_NAME = 'Flyff - Low Health Alert' NODE_NAME = 'Flyff - Low Health Alert'
overlay_instance = None # Shared overlay instance overlay_instance = None
last_beep_time = 0 # Time tracking for beep interval last_beep_time = 0
BEEP_INTERVAL_SECONDS = 2 # Beep every 2 seconds BEEP_INTERVAL_SECONDS = 2
def __init__(self): def __init__(self):
super(FlyffLowHealthAlertNode, self).__init__() 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_1', '', 'Sound Alert', True)
self.add_checkbox('cb_2', '', 'Visual 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)) 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_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: if not FlyffLowHealthAlertNode.overlay_instance:
FlyffLowHealthAlertNode.overlay_instance = OverlayCanvas() FlyffLowHealthAlertNode.overlay_instance = OverlayCanvas()
FlyffLowHealthAlertNode.overlay_instance.show() FlyffLowHealthAlertNode.overlay_instance.show()
def process_input(self): 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) 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 def receive_data(self, data, source_port_name=None):
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
try: 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: 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(data))
self.set_property('value', str(input_value)) if self.get_property('cb_2'):
FlyffLowHealthAlertNode.overlay_instance.toggle_alert(data)
# Check if the "Visual Alert" checkbox is enabled self.handle_beep(data)
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)
def handle_beep(self, input_value): def handle_beep(self, input_value):
""" # Update beep interval from the dropdown property
Plays a beep sound every 2 seconds when the value is `1` and "Sound Alert" is enabled. interval_str = self.get_property('beep_interval')
""" if interval_str.endswith("s"):
sound_alert_enabled = self.get_property('cb_1') interval_seconds = float(interval_str[:-1])
current_time = time.time() else:
interval_seconds = float(interval_str)
self.BEEP_INTERVAL_SECONDS = interval_seconds
if input_value == 1 and sound_alert_enabled: if input_value == 1 and self.get_property('cb_1'):
if (current_time - FlyffLowHealthAlertNode.last_beep_time) >= FlyffLowHealthAlertNode.BEEP_INTERVAL_SECONDS: current_time = time.time()
if (current_time - FlyffLowHealthAlertNode.last_beep_time) >= self.BEEP_INTERVAL_SECONDS:
FlyffLowHealthAlertNode.last_beep_time = current_time FlyffLowHealthAlertNode.last_beep_time = current_time
self.play_beep() self.play_beep()
else:
FlyffLowHealthAlertNode.last_beep_time = 0 # Reset when health is safe
def play_beep(self): def play_beep(self):
"""
Plays a beep using `winsound.Beep` (Windows) or prints a terminal bell (`\a`).
"""
if HAS_WINSOUND: if HAS_WINSOUND:
winsound.Beep(376, 100) # 376 Hz, 100ms duration winsound.Beep(376, 100)
else: else:
print('\a', end='') # Terminal bell for non-Windows systems print('\a', end='')

View File

@ -1,14 +1,15 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Math Operation Node: Standardized Math Operation Node:
- Inputs: Two input ports ("A" and "B"). - Performs mathematical operations (+, -, *, /, avg) on two inputs.
- Output: One output port ("Result"). - Outputs the computed result.
- Operation: A dropdown (combo menu) to select: - Uses a global update timer for processing (defined in borealis.py).
Add, Subtract, Multiply, Divide, Average. - Ensures it always has a "value" property that the Comparison Node can read.
- Displays the computed result in a read-only text box labeled "Result".
""" """
from OdenGraphQt import BaseNode from OdenGraphQt import BaseNode
from Qt import QtCore
class MathOperationNode(BaseNode): class MathOperationNode(BaseNode):
__identifier__ = 'bunny-lab.io.math_node' __identifier__ = 'bunny-lab.io.math_node'
@ -16,60 +17,38 @@ class MathOperationNode(BaseNode):
def __init__(self): def __init__(self):
super(MathOperationNode, self).__init__() 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('A')
self.add_input('B') self.add_input('B')
self.add_output('Result') 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=[ self.add_combo_menu('operator', 'Operator', items=[
'Add', 'Subtract', 'Multiply', 'Divide', 'Average' 'Add', 'Subtract', 'Multiply', 'Divide', 'Average'
]) ])
# Text input for displaying the computed result. # A text field for showing the result to the user:
# We'll make it read-only by accessing the underlying QLineEdit.
self.add_text_input('calc_result', 'Result', text='0') 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 self.value = 0
# Give the node a nice name:
self.set_name("Math Operation") self.set_name("Math Operation")
self.process_input()
def process_input(self, event=None): # Removed self-contained timer; global timer calls process_input().
"""
Compute Section: def process_input(self):
- For each input port (A, B), if connected, grab the 'value' from # Attempt to read "value" from both inputs:
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
input_a = self.input(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) input_b = self.input(1)
if input_b and input_b.connected_ports(): 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') b_raw = input_b.connected_ports()[0].node().get_property('value') if input_b.connected_ports() else "0"
else:
b_raw = 0.0
# Convert raw inputs to floats (default 0.0 on failure).
try: try:
a_val = float(a_raw) a_val = float(a_raw)
except (ValueError, TypeError): except (ValueError, TypeError):
@ -79,10 +58,7 @@ class MathOperationNode(BaseNode):
except (ValueError, TypeError): except (ValueError, TypeError):
b_val = 0.0 b_val = 0.0
# Retrieve the selected operator from the combo box.
operator = self.get_property('operator') operator = self.get_property('operator')
result = 0.0
if operator == 'Add': if operator == 'Add':
result = a_val + b_val result = a_val + b_val
elif operator == 'Subtract': elif operator == 'Subtract':
@ -93,30 +69,41 @@ class MathOperationNode(BaseNode):
result = a_val / b_val if b_val != 0 else 0.0 result = a_val / b_val if b_val != 0 else 0.0
elif operator == 'Average': elif operator == 'Average':
result = (a_val + b_val) / 2.0 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. # Update the two text fields so the user sees the numeric result:
self.set_property('calc_result', str(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. # Let downstream nodes know there's new data:
output_port = self.output(0) self.transmit_data(result)
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')
def on_input_connected(self, input_port, output_port): def on_input_connected(self, input_port, output_port):
self.process_input() pass
def on_input_disconnected(self, input_port, output_port): def on_input_disconnected(self, input_port, output_port):
self.process_input() pass
def property_changed(self, property_name): def property_changed(self, property_name):
if property_name in ['operator']: pass
self.process_input()
def receive_data(self, data, source_port_name=None): 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')

View File

@ -1,27 +1,27 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
## --- Patch QGraphicsScene.setSelectionArea to handle selection arguments --- # --- Patch QGraphicsScene.setSelectionArea to handle selection arguments ---
#from Qt import QtWidgets, QtCore, QtGui from Qt import QtWidgets, QtCore, QtGui
#
#_original_setSelectionArea = QtWidgets.QGraphicsScene.setSelectionArea _original_setSelectionArea = QtWidgets.QGraphicsScene.setSelectionArea
#
#def _patched_setSelectionArea(self, painterPath, second_arg, *args, **kwargs): def _patched_setSelectionArea(self, painterPath, second_arg, *args, **kwargs):
# try: try:
# # Try calling the original method with the provided arguments. # Try calling the original method with the provided arguments.
# return _original_setSelectionArea(self, painterPath, second_arg, *args, **kwargs) return _original_setSelectionArea(self, painterPath, second_arg, *args, **kwargs)
# except TypeError as e: except TypeError as e:
# # If a TypeError is raised, assume the call was made with only a QPainterPath # If a TypeError is raised, assume the call was made with only a QPainterPath
# # and an ItemSelectionMode, and patch it by supplying defaults. # and an ItemSelectionMode, and patch it by supplying defaults.
# # Default operation: ReplaceSelection, default transform: QTransform() # Default operation: ReplaceSelection, default transform: QTransform()
# return _original_setSelectionArea(self, painterPath, return _original_setSelectionArea(self, painterPath,
# QtCore.Qt.ReplaceSelection, QtCore.Qt.ReplaceSelection,
# second_arg, second_arg,
# QtGui.QTransform()) QtGui.QTransform())
#
## Monkey-patch the setSelectionArea method. # Monkey-patch the setSelectionArea method.
#QtWidgets.QGraphicsScene.setSelectionArea = _patched_setSelectionArea QtWidgets.QGraphicsScene.setSelectionArea = _patched_setSelectionArea
#
## --- End of patch section --- # --- End of patch section ---
import sys import sys
import pkgutil import pkgutil
@ -92,7 +92,6 @@ if __name__ == '__main__':
graph.widget.show() graph.widget.show()
# Global update timer: # Global update timer:
# - Call process_input() on every node that implements it.
def global_update(): def global_update():
for node in graph.all_nodes(): for node in graph.all_nodes():
if hasattr(node, "process_input"): if hasattr(node, "process_input"):
@ -104,5 +103,4 @@ if __name__ == '__main__':
timer.timeout.connect(global_update) timer.timeout.connect(global_update)
timer.start(500) timer.start(500)
sys.exit(app.exec()) sys.exit(app.exec())

BIN
debug_processed.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
debug_screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB