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:
- 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'

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
"""
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

View File

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

View File

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

View File

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

View File

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

View File

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

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