diff --git a/korman/helpers.py b/korman/helpers.py index a36d138..faaffea 100644 --- a/korman/helpers.py +++ b/korman/helpers.py @@ -62,6 +62,14 @@ def ensure_object_can_bake(bo, toggle): def ensure_power_of_two(value): return pow(2, math.floor(math.log(value, 2))) +def find_modifier(boname, modid): + """Given a Blender Object name, finds a given modifier and returns it or None""" + bo = bpy.data.objects.get(boname, None) + if bo is not None: + # if they give us the wrong modid, it is a bug and an AttributeError + return getattr(bo.plasma_modifiers, modid) + return None + def make_active_selection(bo): """Selects a single Blender Object and makes it active""" for i in bpy.data.objects: diff --git a/korman/nodes/__init__.py b/korman/nodes/__init__.py index d34b022..41e9155 100644 --- a/korman/nodes/__init__.py +++ b/korman/nodes/__init__.py @@ -19,6 +19,7 @@ from nodeitems_utils import NodeCategory, NodeItem import nodeitems_utils # Put all Korman node modules here... +from .node_avatar import * from .node_conditions import * from .node_core import * from .node_messages import * @@ -37,6 +38,7 @@ class PlasmaNodeCategory(NodeCategory): # the class name. Otherwise, absolutely fascinating things will happen. Don't expect for me # to come and rescue you from it, either. _kategory_names = { + "AVATAR": "Avatar", "CONDITIONS": "Conditions", "LOGIC": "Logic", "MSG": "Message", diff --git a/korman/nodes/node_avatar.py b/korman/nodes/node_avatar.py new file mode 100644 index 0000000..69a552b --- /dev/null +++ b/korman/nodes/node_avatar.py @@ -0,0 +1,59 @@ +# This file is part of Korman. +# +# Korman is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Korman is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Korman. If not, see . + +import bpy +from bpy.props import * +from PyHSPlasma import * + +from .node_core import PlasmaNodeBase, PlasmaNodeSocketBase +from ..properties.modifiers.avatar import sitting_approach_flags + +class PlasmaSittingBehaviorNode(PlasmaNodeBase, bpy.types.Node): + bl_category = "AVATAR" + bl_idname = "PlasmaSittingBehaviorNode" + bl_label = "Sitting Behavior" + bl_default_width = 100 + + approach = EnumProperty(name="Approach", + description="Directions an avatar can approach the seat from", + items=sitting_approach_flags, + default={"kApproachFront", "kApproachLeft", "kApproachRight"}, + options={"ENUM_FLAG"}) + + def init(self, context): + self.inputs.new("PlasmaConditionSocket", "Condition", "condition") + # This makes me determined to create and release a whoopee cushion age... + self.outputs.new("PlasmaConditionSocket", "Satisfies", "satisfies") + + def draw_buttons(self, context, layout): + col = layout.column() + col.label("Approach:") + col.prop(self, "approach") + + def draw_buttons_ext(self, context, layout): + layout.prop_menu_enum(self, "approach") + + def get_key(self, exporter, tree, so): + return exporter.mgr.find_create_key(plSittingModifier, name=self.create_key_name(tree), so=so) + + def export(self, exporter, tree, bo, so): + sitmod = self.get_key(exporter, tree, so).object + for flag in self.approach: + sitmod.miscFlags |= getattr(plSittingModifier, flag) + for key in self.find_outputs("satisfies"): + if key is not None: + sitmod.addNotifyKey(key) + else: + exporter.report.warn(" '{}' Node '{}' doesn't expose a key. It won't be triggered by '{}'!".format(i.bl_idname, i.name, self.name), indent=3) diff --git a/korman/nodes/node_conditions.py b/korman/nodes/node_conditions.py index 759164b..1b1abba 100644 --- a/korman/nodes/node_conditions.py +++ b/korman/nodes/node_conditions.py @@ -15,15 +15,223 @@ import bpy from bpy.props import * +import math from PyHSPlasma import * from .node_core import PlasmaNodeBase, PlasmaNodeSocketBase from ..properties.modifiers.physics import bounds_types +class PlasmaClickableNode(PlasmaNodeBase, bpy.types.Node): + bl_category = "CONDITIONS" + bl_idname = "PlasmaClickableNode" + bl_label = "Clickable" + bl_width_default = 160 + + clickable = StringProperty(name="Clickable", + description="Mesh that is clickable") + bounds = EnumProperty(name="Bounds", + description="Clickable's bounds (NOTE: only used if your clickable is not a collider)", + items=bounds_types, + default="hull") + + def init(self, context): + self.inputs.new("PlasmaClickableRegionSocket", "Avatar Inside Region", "region") + self.inputs.new("PlasmaFacingTargetSocket", "Avatar Facing Target", "facing") + self.outputs.new("PlasmaConditionSocket", "Satisfies", "satisfies") + + def draw_buttons(self, context, layout): + layout.prop_search(self, "clickable", bpy.data, "objects", icon="MESH_DATA") + layout.prop(self, "bounds") + + def export(self, exporter, tree, parent_bo, parent_so): + # First: look up the clickable mesh. if it is not specified, then it's this BO. + # We do this because we might be exporting from a BO that is not actually the clickable object. + # Case: sitting modifier (exports from sit position empty) + if self.clickable: + clickable_bo = bpy.data.objects[self.clickable] + clickable_so = exporter.mgr.find_create_key(plSceneObject, bl=clickable_bo).object + else: + clickable_bo = parent_bo + clickable_so = parent_so + + name = self.create_key_name(tree) + interface = exporter.mgr.find_create_key(plInterfaceInfoModifier, name=name, so=clickable_so).object + logicmod = exporter.mgr.find_create_key(plLogicModifier, name=name, so=clickable_so) + interface.addIntfKey(logicmod) + # Matches data seen in Cyan's PRPs... + interface.addIntfKey(logicmod) + logicmod = logicmod.object + + # Try to figure out the appropriate bounds type for the clickable.... + phys_mod = clickable_bo.plasma_modifiers.collision + bounds = phys_mod.bounds if phys_mod.enabled else self.bounds + + # The actual physical object that does the cursor LOS + made_the_phys = (clickable_so.sim is None) + phys_name = "{}_ClickableLOS".format(clickable_bo.name) + simIface, physical = exporter.physics.generate_physical(clickable_bo, clickable_so, bounds, phys_name) + simIface.setProperty(plSimulationInterface.kPinned, True) + if made_the_phys: + # we assume that the collision modifier will do this if they want it to be intangible + physical.memberGroup = plSimDefs.kGroupLOSOnly + physical.LOSDBs |= plSimDefs.kLOSDBUIItems + + # Picking Detector -- detect when the physical is clicked + detector = exporter.mgr.find_create_key(plPickingDetector, name=name, so=clickable_so).object + detector.addReceiver(logicmod.key) + + # Clickable + activator = exporter.mgr.find_create_key(plActivatorConditionalObject, name=name, so=clickable_so).object + activator.addActivator(detector.key) + logicmod.addCondition(activator.key) + logicmod.setLogicFlag(plLogicModifier.kLocalElement, True) + logicmod.cursor = plCursorChangeMsg.kCursorPoised + logicmod.notify = self.generate_notify_msg(exporter, tree, parent_so, "satisfies") + + # If we have a region attached, let it convert. + region = self.find_input("region", "PlasmaClickableRegionNode") + if region is not None: + region.convert_subcondition(exporter, tree, clickable_bo, clickable_so, logicmod) + + # Hand things off to the FaceTarget socket which does things nicely for us + face_target = self.find_input_socket("facing") + face_target.convert_subcondition(exporter, tree, clickable_bo, clickable_so, logicmod) + + +class PlasmaClickableRegionNode(PlasmaNodeBase, bpy.types.Node): + bl_category = "CONDITIONS" + bl_idname = "PlasmaClickableRegionNode" + bl_label = "Clickable Region Settings" + bl_width_default = 200 + + region = StringProperty(name="Region", + description="Object that defines the region mesh") + bounds = EnumProperty(name="Bounds", + description="Physical object's bounds (NOTE: only used if your clickable is not a collider)", + items=bounds_types, + default="hull") + + def init(self, context): + self.outputs.new("PlasmaClickableRegionSocket", "Satisfies", "satisfies") + + def draw_buttons(self, context, layout): + layout.prop_search(self, "region", bpy.data, "objects", icon="MESH_DATA") + layout.prop(self, "bounds") + + def convert_subcondition(self, exporter, tree, parent_bo, parent_so, logicmod): + # REMEMBER: parent_so doesn't have to be the actual region scene object... + region_bo = bpy.data.objects[self.region] + region_so = exporter.mgr.find_create_key(plSceneObject, bl=region_bo).object + + # Try to figure out the appropriate bounds type for the region.... + phys_mod = region_bo.plasma_modifiers.collision + bounds = phys_mod.bounds if phys_mod.enabled else self.bounds + + # Our physical is a detector and it only detects avatars... + phys_name = "{}_ClickableAvRegion".format(region_bo.name) + simIface, physical = exporter.physics.generate_physical(region_bo, region_so, bounds, phys_name) + physical.memberGroup = plSimDefs.kGroupDetector + physical.reportGroup |= 1 << plSimDefs.kGroupAvatar + + # I'm glad this crazy mess made sense to someone at Cyan... + # ObjectInVolumeDetector can notify multiple logic mods. This implies we could share this + # one detector for many unrelated logic mods. However, LogicMods and Conditions appear to + # assume they pwn each other... so we need a unique detector. This detector must be attached + # as a modifier to the region's SO however. + name = self.create_key_name(tree) + detector_key = exporter.mgr.find_create_key(plObjectInVolumeDetector, name=name, so=region_so) + detector = detector_key.object + detector.addReceiver(logicmod.key) + detector.type = plObjectInVolumeDetector.kTypeAny + + # Now, the conditional object. At this point, these seem very silly. At least it's not a plModifier. + # All they really do is hold a satisfied boolean... + objinbox_key = exporter.mgr.find_create_key(plObjectInBoxConditionalObject, name=name, so=parent_so) + objinbox_key.object.satisfied = True + logicmod.addCondition(objinbox_key) + + +class PlasmaClickableRegionSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): + bl_color = (0.412, 0.0, 0.055, 1.0) + + class PlasmaConditionSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): bl_color = (0.188, 0.086, 0.349, 1.0) +class PlasmaFacingTargetNode(PlasmaNodeBase, bpy.types.Node): + bl_category = "CONDITIONS" + bl_idname = "PlasmaFacingTargetNode" + bl_label = "Facing Target" + + 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 init(self, context): + self.outputs.new("PlasmaFacingTargetSocket", "Satisfies", "satisfies") + + def draw_buttons(self, context, layout): + layout.prop(self, "directional") + layout.prop(self, "tolerance") + + +class PlasmaFacingTargetSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): + bl_color = (0.0, 0.267, 0.247, 1.0) + + allow_simple = BoolProperty(name="Facing Target", + description="Avatar must be facing the target object", + default=True) + + def draw(self, context, layout, node, text): + if self.simple_mode: + layout.prop(self, "allow_simple", text="") + layout.label(text) + + def convert_subcondition(self, exporter, tree, bo, so, logicmod): + assert not self.is_output + if not self.enable_condition: + return + + # First, gather the schtuff from the appropriate blah blah blah + if self.simple_mode: + directional = True + tolerance = 45 + name = "{}_SimpleFacing".format(self.node.create_key_name(tree)) + elif self.is_linked: + node = self.links[0].from_node + directional = node.directional + tolerance = node.tolerance + name = node.create_key_name(tree) + else: + # This is a programmer failure, so we need a traceback. + raise RuntimeError("Tried to export an unused PlasmaFacingTargetSocket") + + # Plasma internally depends on a CoordinateInterface. Since we're a node, we don't actually + # flag that in the exporter. Ensure it is generated now. + exporter.export_coordinate_interface(so, bo) + + facing_key = exporter.mgr.find_create_key(plFacingConditionalObject, name=name, so=so) + facing = facing_key.object + facing.directional = directional + facing.satisfied = True + facing.tolerance = math.radians(tolerance) + logicmod.addCondition(facing_key) + + @property + def enable_condition(self): + return ((self.simple_mode and self.allow_simple) or self.is_linked) + + @property + def simple_mode(self): + """Simple mode allows a user to click a button on input sockets to automatically generate a + Facing Target condition""" + return (not self.is_linked and not self.is_output) + + class PlasmaVolumeReportNode(PlasmaNodeBase, bpy.types.Node): bl_category = "CONDITIONS" bl_idname = "PlasmaVoumeReportNode" @@ -121,18 +329,7 @@ class PlasmaVolumeSensorNode(PlasmaNodeBase, bpy.types.Node): logicKey = exporter.mgr.find_create_key(plLogicModifier, name=theName, so=so) logicmod = logicKey.object logicmod.setLogicFlag(plLogicModifier.kMultiTrigger, True) - - # LogicMod notification... This is one of the cases where the linked node needs to match - # exactly one key... - notify = plNotifyMsg() - notify.BCastFlags = (plMessage.kNetPropagate | plMessage.kLocalPropagate) - for i in self.find_outputs("satisfies"): - key = i.get_key(exporter, tree, so) - if key is None: - print(" WARNING: '{}' Node '{}' doesn't expose a key. It won't be triggered!".format(i.bl_idname, i.name)) - else: - notify.addReceiver(key) - logicmod.notify = notify + logicmod.notify = self.generate_notify_msg(exporter, tree, so, "satisfies") # Now, the detector objects print(" [ObjectInVolumeDetector '{}']".format(theName)) @@ -159,7 +356,6 @@ class PlasmaVolumeSensorNode(PlasmaNodeBase, bpy.types.Node): return logicKey - class PlasmaVolumeSettingsSocket(PlasmaNodeSocketBase): bl_color = (43.1, 24.7, 0.0, 1.0) diff --git a/korman/nodes/node_core.py b/korman/nodes/node_core.py index f214310..625b2f8 100644 --- a/korman/nodes/node_core.py +++ b/korman/nodes/node_core.py @@ -15,11 +15,23 @@ import abc import bpy +from PyHSPlasma import plMessage, plNotifyMsg class PlasmaNodeBase: def create_key_name(self, tree): return "{}_{}".format(tree.name, self.name) + def generate_notify_msg(self, exporter, tree, so, socket_id, idname=None): + notify = plNotifyMsg() + notify.BCastFlags = (plMessage.kNetPropagate | plMessage.kLocalPropagate) + for i in self.find_outputs(socket_id, idname): + key = i.get_key(exporter, tree, so) + if key is None: + exporter.report.warn(" '{}' Node '{}' doesn't expose a key. It won't be triggered by '{}'!".format(i.bl_idname, i.name, self.name), indent=3) + else: + notify.addReceiver(key) + return notify + def get_key(self, exporter, tree, so): return None diff --git a/korman/properties/modifiers/__init__.py b/korman/properties/modifiers/__init__.py index 2a1394b..25897be 100644 --- a/korman/properties/modifiers/__init__.py +++ b/korman/properties/modifiers/__init__.py @@ -17,6 +17,7 @@ import bpy import inspect from .base import PlasmaModifierProperties +from .avatar import * from .logic import * from .physics import * from .region import * diff --git a/korman/properties/modifiers/avatar.py b/korman/properties/modifiers/avatar.py new file mode 100644 index 0000000..a35185d --- /dev/null +++ b/korman/properties/modifiers/avatar.py @@ -0,0 +1,104 @@ +# This file is part of Korman. +# +# Korman is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Korman is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Korman. If not, see . + +import bpy +from bpy.props import * +from PyHSPlasma import * + +from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz +from ...exporter.explosions import ExportError +from ...helpers import find_modifier + +sitting_approach_flags = [("kApproachFront", "Front", "Approach from the font"), + ("kApproachLeft", "Left", "Approach from the left"), + ("kApproachRight", "Right", "Approach from the right"), + ("kApproachRear", "Rear", "Approach from the rear guard")] + +class PlasmaSittingBehavior(PlasmaModifierProperties, PlasmaModifierLogicWiz): + pl_id = "sittingmod" + + bl_category = "Avatar" + bl_label = "Sitting Behavior" + bl_description = "Avatar sitting position" + + approach = EnumProperty(name="Approach", + description="Directions an avatar can approach the seat from", + items=sitting_approach_flags, + default={"kApproachFront", "kApproachLeft", "kApproachRight"}, + options={"ENUM_FLAG"}) + + clickable_obj = StringProperty(name="Clickable", + description="Object that defines the clickable area") + region_obj = StringProperty(name="Region", + description="Object that defines the region mesh") + + facing_enabled = BoolProperty(name="Avatar Facing", + description="The avatar must be facing the clickable's Y-axis", + default=True) + facing_degrees = IntProperty(name="Tolerance", + description="How far away we will tolerate the avatar facing the clickable", + min=-180, max=180, default=45) + + def created(self, obj): + self.display_name = "{}_SitBeh".format(obj.name) + + def export(self, exporter, bo, so): + # The user absolutely MUST specify a clickable or this won't export worth crap. + clickable_obj = bpy.data.objects.get(self.clickable_obj, None) + if clickable_obj is None: + raise ExportError("'{}': Sitting Behavior's clickable object is invalid") + + # Generate the logic nodes now + self.logicwiz(bo) + + # Now, export the node tree + self.node_tree.export(exporter, bo, so) + + def logicwiz(self, bo): + tree = self.node_tree + nodes = tree.nodes + nodes.clear() + + # Sitting Modifier + sittingmod = nodes.new("PlasmaSittingBehaviorNode") + sittingmod.approach = self.approach + sittingmod.name = "SittingBeh" + + # Clickable + clickable = nodes.new("PlasmaClickableNode") + clickable.link_output(tree, sittingmod, "satisfies", "condition") + clickable.clickable = self.clickable_obj + clickable.bounds = find_modifier(self.clickable_obj, "collision").bounds + + # Avatar Region (optional) + region_phys = find_modifier(self.region_obj, "collision") + if region_phys is not None: + region = nodes.new("PlasmaClickableRegionNode") + region.link_output(tree, clickable, "satisfies", "region") + region.name = "ClickableAvRegion" + region.region = self.region_obj + region.bounds = region_phys.bounds + + # Facing Target (optional) + if self.facing_enabled: + facing = nodes.new("PlasmaFacingTargetNode") + facing.link_output(tree, clickable, "satisfies", "facing") + facing.name = "FacingClickable" + facing.directional = True + facing.tolerance = self.facing_degrees + else: + # this socket must be explicitly disabled, otherwise it automatically generates a default + # facing target conditional for us. isn't that nice? + clickable.find_input_socket("facing").allow_simple = False diff --git a/korman/ui/modifiers/__init__.py b/korman/ui/modifiers/__init__.py index 06220b9..7dcf614 100644 --- a/korman/ui/modifiers/__init__.py +++ b/korman/ui/modifiers/__init__.py @@ -13,6 +13,7 @@ # You should have received a copy of the GNU General Public License # along with Korman. If not, see . +from .avatar import * from .logic import * from .physics import * from .region import * diff --git a/korman/ui/modifiers/avatar.py b/korman/ui/modifiers/avatar.py new file mode 100644 index 0000000..5e8d010 --- /dev/null +++ b/korman/ui/modifiers/avatar.py @@ -0,0 +1,39 @@ +# This file is part of Korman. +# +# Korman is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Korman is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Korman. If not, see . + +import bpy + +from ...helpers import find_modifier + +def sittingmod(modifier, layout, context): + layout.row().prop(modifier, "approach") + + col = layout.column() + col.prop_search(modifier, "clickable_obj", bpy.data, "objects", icon="MESH_DATA") + clickable = find_modifier(modifier.clickable_obj, "collision") + if clickable is not None: + col.prop(clickable, "bounds") + + col = layout.column() + col.prop_search(modifier, "region_obj", bpy.data, "objects", icon="MESH_DATA") + region = find_modifier(modifier.region_obj, "collision") + if region is not None: + col.prop(region, "bounds") + + split = layout.split() + split.column().prop(modifier, "facing_enabled") + col = split.column() + col.enabled = modifier.facing_enabled + col.prop(modifier, "facing_degrees")