Browse Source

Merge remote-tracking branch 'hoikas/logic-nodes'

pull/8/head
Adam Johnson 10 years ago
parent
commit
dc08e14fe1
  1. 8
      korman/helpers.py
  2. 2
      korman/nodes/__init__.py
  3. 59
      korman/nodes/node_avatar.py
  4. 222
      korman/nodes/node_conditions.py
  5. 12
      korman/nodes/node_core.py
  6. 1
      korman/properties/modifiers/__init__.py
  7. 104
      korman/properties/modifiers/avatar.py
  8. 1
      korman/ui/modifiers/__init__.py
  9. 39
      korman/ui/modifiers/avatar.py

8
korman/helpers.py

@ -62,6 +62,14 @@ def ensure_object_can_bake(bo, toggle):
def ensure_power_of_two(value): def ensure_power_of_two(value):
return pow(2, math.floor(math.log(value, 2))) 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): def make_active_selection(bo):
"""Selects a single Blender Object and makes it active""" """Selects a single Blender Object and makes it active"""
for i in bpy.data.objects: for i in bpy.data.objects:

2
korman/nodes/__init__.py

@ -19,6 +19,7 @@ from nodeitems_utils import NodeCategory, NodeItem
import nodeitems_utils import nodeitems_utils
# Put all Korman node modules here... # Put all Korman node modules here...
from .node_avatar import *
from .node_conditions import * from .node_conditions import *
from .node_core import * from .node_core import *
from .node_messages 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 # the class name. Otherwise, absolutely fascinating things will happen. Don't expect for me
# to come and rescue you from it, either. # to come and rescue you from it, either.
_kategory_names = { _kategory_names = {
"AVATAR": "Avatar",
"CONDITIONS": "Conditions", "CONDITIONS": "Conditions",
"LOGIC": "Logic", "LOGIC": "Logic",
"MSG": "Message", "MSG": "Message",

59
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 <http://www.gnu.org/licenses/>.
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)

222
korman/nodes/node_conditions.py

@ -15,15 +15,223 @@
import bpy import bpy
from bpy.props import * from bpy.props import *
import math
from PyHSPlasma import * from PyHSPlasma import *
from .node_core import PlasmaNodeBase, PlasmaNodeSocketBase from .node_core import PlasmaNodeBase, PlasmaNodeSocketBase
from ..properties.modifiers.physics import bounds_types 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): class PlasmaConditionSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
bl_color = (0.188, 0.086, 0.349, 1.0) 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): class PlasmaVolumeReportNode(PlasmaNodeBase, bpy.types.Node):
bl_category = "CONDITIONS" bl_category = "CONDITIONS"
bl_idname = "PlasmaVoumeReportNode" bl_idname = "PlasmaVoumeReportNode"
@ -121,18 +329,7 @@ class PlasmaVolumeSensorNode(PlasmaNodeBase, bpy.types.Node):
logicKey = exporter.mgr.find_create_key(plLogicModifier, name=theName, so=so) logicKey = exporter.mgr.find_create_key(plLogicModifier, name=theName, so=so)
logicmod = logicKey.object logicmod = logicKey.object
logicmod.setLogicFlag(plLogicModifier.kMultiTrigger, True) logicmod.setLogicFlag(plLogicModifier.kMultiTrigger, True)
logicmod.notify = self.generate_notify_msg(exporter, tree, so, "satisfies")
# 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
# Now, the detector objects # Now, the detector objects
print(" [ObjectInVolumeDetector '{}']".format(theName)) print(" [ObjectInVolumeDetector '{}']".format(theName))
@ -159,7 +356,6 @@ class PlasmaVolumeSensorNode(PlasmaNodeBase, bpy.types.Node):
return logicKey return logicKey
class PlasmaVolumeSettingsSocket(PlasmaNodeSocketBase): class PlasmaVolumeSettingsSocket(PlasmaNodeSocketBase):
bl_color = (43.1, 24.7, 0.0, 1.0) bl_color = (43.1, 24.7, 0.0, 1.0)

12
korman/nodes/node_core.py

@ -15,11 +15,23 @@
import abc import abc
import bpy import bpy
from PyHSPlasma import plMessage, plNotifyMsg
class PlasmaNodeBase: class PlasmaNodeBase:
def create_key_name(self, tree): def create_key_name(self, tree):
return "{}_{}".format(tree.name, self.name) 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): def get_key(self, exporter, tree, so):
return None return None

1
korman/properties/modifiers/__init__.py

@ -17,6 +17,7 @@ import bpy
import inspect import inspect
from .base import PlasmaModifierProperties from .base import PlasmaModifierProperties
from .avatar import *
from .logic import * from .logic import *
from .physics import * from .physics import *
from .region import * from .region import *

104
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 <http://www.gnu.org/licenses/>.
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

1
korman/ui/modifiers/__init__.py

@ -13,6 +13,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Korman. If not, see <http://www.gnu.org/licenses/>. # along with Korman. If not, see <http://www.gnu.org/licenses/>.
from .avatar import *
from .logic import * from .logic import *
from .physics import * from .physics import *
from .region import * from .region import *

39
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 <http://www.gnu.org/licenses/>.
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")
Loading…
Cancel
Save