diff --git a/korman/nodes/node_conditions.py b/korman/nodes/node_conditions.py index bb178cb..3071e88 100644 --- a/korman/nodes/node_conditions.py +++ b/korman/nodes/node_conditions.py @@ -13,11 +13,14 @@ # You should have received a copy of the GNU General Public License # along with Korman. If not, see . +from __future__ import annotations + import bpy from bpy.props import * from collections import OrderedDict import math from PyHSPlasma import * +from typing import * from .node_core import * from ..properties.modifiers.physics import bounds_types @@ -226,24 +229,85 @@ class PlasmaFacingTargetNode(PlasmaNodeBase, bpy.types.Node): bl_category = "CONDITIONS" bl_idname = "PlasmaFacingTargetNode" bl_label = "Facing Target" + bl_width_default = 200 - directional = BoolProperty(name="Directional", - description="TODO", - default=True) - tolerance = IntProperty(name="Degrees", - description="How far away from the target the avatar can turn (in degrees)", - min=-180, max=180, default=45) + def _get_directional(self) -> bool: + output_node = self.find_output("satisfies") + if output_node is not None and output_node.bl_idname == "PlasmaVolumeSensorNode": + return True + return self.directional + + def _set_directional(self, value: bool): + self.directional = value + + # Volume Sensors use view ortientation testing, so if we're connected to a volume sensor node, + # then we need to indicate that in the UI. + directional = BoolProperty(default=False, options={"HIDDEN"}) + directional_ui = BoolProperty(name="Directional", + description="Use the object's orientation for facing tests", + get=_get_directional, set=_set_directional) + + def _get_moving_forward(self) -> bool: + output_node = self.find_output("satisfies") + if output_node is not None and output_node.bl_idname != "PlasmaVolumeSensorNode": + return False + return self.moving_forward + + def _set_moving_forward(self, value: bool): + self.moving_forward = value + + # Only volume sensors allow the moving forward setting, so show this as disabled for connections + # that are not volume sensors. + moving_forward = BoolProperty(options={"HIDDEN"}) + moving_forward_ui = BoolProperty(name="Forward Motion", + description="The player must be moving forward (eg walking) for the condition to trigger", + get=_get_moving_forward, set=_set_moving_forward) + + def _get_tolerance(self) -> float: + return math.radians(self.tolerance) + + def _set_tolerance(self, value: float) -> None: + self.tolerance = math.degrees(value) + + # Legacy storage property... this exists as the storage for the tolerance value to prevent + # breaking old blend files. NOTE: This property is stored in degrees. + tolerance = FloatProperty(min=-180.0, max=180.0, default=45.0, options={"HIDDEN"}) + + # New property for usage in the UI. NOTE: This property is stored in radians. + tolerance_ui = FloatProperty(name="Tolerance", + description="How far away from the target the avatar can turn", + min=-180.0, max=180.0, precision=0, + get=_get_tolerance, set=_set_tolerance, + subtype="ANGLE", options=set()) output_sockets = OrderedDict([ ("satisfies", { "text": "Satisfies", "type": "PlasmaFacingTargetSocket", + "link_limit": 1, }), ]) + def _draw_sub_prop(self, layout, prop_name, *, active=True, sidebar=False, **kwargs): + sub = layout.row() if sidebar else layout.column() + sub.enabled = active + sub.prop(self, prop_name, **kwargs) + + def _draw(self, layout, *, sidebar): + output_node = self.find_output("satisfies") + is_regular_condition = output_node is None or output_node.bl_idname != "PlasmaVolumeSensorNode" + is_volume_sensor = output_node is None or output_node.bl_idname == "PlasmaVolumeSensorNode" + + sub = layout if sidebar else layout.split() + self._draw_sub_prop(sub, "directional_ui", active=is_regular_condition) + self._draw_sub_prop(sub, "moving_forward_ui", active=is_volume_sensor, text="Moving") + layout.prop(self, "tolerance_ui") + def draw_buttons(self, context, layout): - layout.prop(self, "directional") - layout.prop(self, "tolerance") + self._draw(layout, sidebar=False) + + def draw_buttons_ext(self, context, layout): + self._draw(layout, sidebar=True) class PlasmaFacingTargetSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): @@ -258,7 +322,7 @@ class PlasmaFacingTargetSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): layout.prop(self, "allow_simple", text="") layout.label(text) - def convert_subcondition(self, exporter, bo, so, logicmod): + def convert_subcondition(self, exporter, bo, so, logicmod: Union[plLogicModifier, plObjectInVolumeAndFacingDetector]): assert not self.is_output if not self.enable_condition: return @@ -266,26 +330,34 @@ class PlasmaFacingTargetSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): # First, gather the schtuff from the appropriate blah blah blah if self.simple_mode: node = self.node - directional = True - tolerance = 45 + directional = False + moving_forward = False + tolerance = math.radians(45.0) elif self.is_linked: node = self.links[0].from_node directional = node.directional + moving_forward = node.moving_forward tolerance = node.tolerance else: # This is a programmer failure, so we need a traceback. raise RuntimeError("Tried to export an unused PlasmaFacingTargetSocket") - facing_key = node._find_create_key(plFacingConditionalObject, exporter, bl=bo, so=so) - facing = facing_key.object - facing.directional = directional - facing.satisfied = True - facing.tolerance = math.radians(tolerance) - logicmod.addCondition(facing_key) + if isinstance(logicmod, plLogicModifier): + facing_key = node._find_create_key(plFacingConditionalObject, exporter, bl=bo, so=so) + facing = facing_key.object + facing.directional = directional + facing.satisfied = True + facing.tolerance = math.radians(tolerance) + logicmod.addCondition(facing_key) + elif isinstance(logicmod, plObjectInVolumeAndFacingDetector): + logicmod.facingTolerance = math.cos(math.radians(tolerance)) + logicmod.needWalkingForward = moving_forward + else: + raise ValueError("logicmod") @property def enable_condition(self): - return ((self.simple_mode and self.allow_simple) or self.is_linked) + return self.enabled and ((self.simple_mode and self.allow_simple) or self.is_linked) @property def simple_mode(self): @@ -330,6 +402,16 @@ class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.type bl_label = "Region Sensor" bl_width_default = 190 + def _update_report_on(self, context): + # Facing target properties only make sense if we trigger on avatars being present + # in the region. Furthermore, the engine explicitly disallows other physicals triggering + # the region sensor if the facing condition is enabled. So, remove the facing option if + # avatars are not selected or if dynamics are selected. NOTE: The socket is hidden + # when it is disabled. + include_avatars = "kGroupAvatar" in self.report_on + include_physicals = "kGroupDynamic" in self.report_on + self.find_input_socket("facing").enabled = include_avatars and not include_physicals + # These are the Python attributes we can fill in pl_attrib = {"ptAttribActivator", "ptAttribActivatorList", "ptAttribNamedActivator"} @@ -348,9 +430,14 @@ class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.type options={"ANIMATABLE", "ENUM_FLAG"}, items=[("kGroupAvatar", "Avatars", "Avatars trigger this region"), ("kGroupDynamic", "Dynamics", "Any non-avatar dynamic physical object (eg kickables)")], - default={"kGroupAvatar"}) + default={"kGroupAvatar"}, + update=_update_report_on) input_sockets = OrderedDict([ + ("facing", { + "text": "Avatar Facing Target", + "type": "PlasmaFacingTargetSocket", + }), ("enter", { "text": "Trigger on Enter", "type": "PlasmaVolumeSettingsSocketIn", @@ -376,6 +463,11 @@ class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.type }), ]) + def init(self, context): + # The default value for the facing socket is a bit silly for this node type. + # Reset it to False. + self.find_input_socket("facing").allow_simple = False + def draw_buttons(self, context, layout): layout.prop(self, "report_on") @@ -448,7 +540,12 @@ class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.type logicmod.notify = self.generate_notify_msg(exporter, parent_so, "satisfies") # Now, the detector objects - det = self._find_create_object(plObjectInVolumeDetector, exporter, suffix=suffix, bl=region_bo, so=region_so) + facing_socket: PlasmaFacingTargetSocket = self.find_input_socket("facing") + if facing_socket.enable_condition: + det = self._find_create_object(plObjectInVolumeAndFacingDetector, exporter, suffix=suffix, bl=region_bo, so=region_so) + facing_socket.convert_subcondition(exporter, region_bo, region_so, det) + else: + det = self._find_create_object(plObjectInVolumeDetector, exporter, suffix=suffix, bl=region_bo, so=region_so) volKey = self._find_create_key(plVolumeSensorConditionalObject, exporter, suffix=suffix, bl=region_bo, so=region_so) volsens = volKey.object @@ -472,6 +569,10 @@ class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.type def export_once(self): return True + def harvest_actors(self): + if self.region_object and self.find_input_socket("facing").enable_condition: + yield self.region_object.name + @classmethod def _idprop_mapping(cls): return {"region_object": "region"} diff --git a/korman/nodes/node_core.py b/korman/nodes/node_core.py index 1b22879..e3825ec 100644 --- a/korman/nodes/node_core.py +++ b/korman/nodes/node_core.py @@ -110,15 +110,19 @@ class PlasmaNodeBase: options = self._socket_defs[0].get(key, {}) spawn_empty = spawn_empty and options.get("spawn_empty", False) - for i in self.inputs: - if i.alias == key: - if spawn_empty and i.is_linked: - continue - return i + matching_sockets = filter(lambda x: x.alias == key, self.inputs) if spawn_empty: + unused_socket = next(filter(lambda x: not x.is_linked, matching_sockets), None) + if unused_socket is not None: + return unused_socket return self._spawn_socket(key, options, self.inputs) - else: - raise KeyError(key) + + matching_socket = next(matching_sockets, None) + if matching_socket is not None: + return matching_socket + if options: + return self._spawn_socket(key, options, self.inputs) + raise KeyError(key) def find_input_sockets(self, key, idname=None): for i in self.inputs: