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