|
|
|
# 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 collections import OrderedDict
|
|
|
|
from PyHSPlasma import *
|
|
|
|
|
|
|
|
from .node_core import *
|
|
|
|
from ..properties.modifiers.region import footstep_surfaces, footstep_surface_ids
|
|
|
|
from ..exporter import ExportError
|
|
|
|
from .. import idprops
|
|
|
|
|
|
|
|
class PlasmaMessageSocketBase(PlasmaNodeSocketBase):
|
|
|
|
bl_color = (0.004, 0.282, 0.349, 1.0)
|
|
|
|
class PlasmaMessageSocket(PlasmaMessageSocketBase, bpy.types.NodeSocket):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class PlasmaMessageNode(PlasmaNodeBase):
|
|
|
|
input_sockets = OrderedDict([
|
|
|
|
("sender", {
|
|
|
|
"text": "Sender",
|
|
|
|
"type": "PlasmaMessageSocket",
|
|
|
|
"valid_link_sockets": "PlasmaMessageSocket",
|
|
|
|
"spawn_empty": True,
|
|
|
|
}),
|
|
|
|
])
|
|
|
|
|
|
|
|
@property
|
|
|
|
def has_callbacks(self):
|
|
|
|
"""This message does not have callbacks that can be waited on by a Responder"""
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
class PlasmaMessageWithCallbacksNode(PlasmaMessageNode):
|
|
|
|
output_sockets = OrderedDict([
|
|
|
|
("msgs", {
|
|
|
|
"can_link": "can_link_callback",
|
|
|
|
"text": "Send On Completion",
|
|
|
|
"type": "PlasmaMessageSocket",
|
|
|
|
"valid_link_sockets": "PlasmaMessageSocket",
|
|
|
|
}),
|
|
|
|
])
|
|
|
|
|
|
|
|
@property
|
|
|
|
def can_link_callback(self):
|
|
|
|
"""Determines if a callback message can be linked to this socket"""
|
|
|
|
|
|
|
|
# Node Graphs enable us to draw lots of fancy logic, unfortunately, not
|
|
|
|
# everything that can potentially be represented in a node tree can be
|
|
|
|
# exported to URU in a way that will actually work. Responder commands can
|
|
|
|
# wait on other responder commands, but the way they are executed in Plasma is
|
|
|
|
# serialized. It's really a list of commands that are executed until a wait
|
|
|
|
# is encountered. At that time, Plasma waits and resumes running the list when
|
|
|
|
# the wait callback is received.
|
|
|
|
# So what does this mean???
|
|
|
|
# It means that only one "branch" of message nodes can have waits.
|
|
|
|
def check_for_callbacks(parent_node, child_node):
|
|
|
|
for sibling_node in parent_node.find_outputs("msgs"):
|
|
|
|
if sibling_node == child_node:
|
|
|
|
continue
|
|
|
|
if getattr(sibling_node, "has_linked_callbacks", False):
|
|
|
|
return True
|
|
|
|
for grandparent_node in parent_node.find_inputs("sender"):
|
|
|
|
return check_for_callbacks(grandparent_node, parent_node)
|
|
|
|
return False
|
|
|
|
|
|
|
|
for sender_node in self.find_inputs("sender"):
|
|
|
|
if check_for_callbacks(sender_node, self):
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
@property
|
|
|
|
def has_callbacks(self):
|
|
|
|
"""This message has callbacks that can be waited on by a Responder"""
|
|
|
|
return True
|
|
|
|
|
|
|
|
@property
|
|
|
|
def has_linked_callbacks(self):
|
|
|
|
return self.find_output("msgs") is not None
|
|
|
|
|
|
|
|
|
|
|
|
class PlasmaAnimCmdMsgNode(idprops.IDPropMixin, PlasmaMessageWithCallbacksNode, bpy.types.Node):
|
|
|
|
bl_category = "MSG"
|
|
|
|
bl_idname = "PlasmaAnimCmdMsgNode"
|
|
|
|
bl_label = "Animation Command"
|
|
|
|
bl_width_default = 190
|
|
|
|
|
|
|
|
anim_type = EnumProperty(name="Type",
|
|
|
|
description="Animation type to affect",
|
|
|
|
items=[("OBJECT", "Object", "Mesh Action"),
|
|
|
|
("TEXTURE", "Texture", "Texture Action")],
|
|
|
|
default="OBJECT")
|
|
|
|
|
|
|
|
def _poll_texture(self, value):
|
|
|
|
# must be a legal option... but is it a member of this material... or, if no material,
|
|
|
|
# any of the materials attached to the object?
|
|
|
|
if self.target_material is not None:
|
|
|
|
return value.name in self.target_material.texture_slots
|
|
|
|
elif self.target_object is not None:
|
|
|
|
for i in (slot.material for slot in self.target_object.material_slots if slot and slot.material):
|
|
|
|
if value in (slot.texture for slot in i.texture_slots if slot and slot.texture):
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
return True
|
|
|
|
|
|
|
|
def _poll_material(self, value):
|
|
|
|
# Don't filter materials by texture - this would (potentially) result in surprising UX
|
|
|
|
# in that you would have to clear the texture selection before being able to select
|
|
|
|
# certain materials.
|
|
|
|
if self.target_object is not None:
|
|
|
|
object_materials = (slot.material for slot in self.target_object.material_slots if slot and slot.material)
|
|
|
|
return value in object_materials
|
|
|
|
return True
|
|
|
|
|
|
|
|
target_object = PointerProperty(name="Object",
|
|
|
|
description="Target object",
|
|
|
|
type=bpy.types.Object)
|
|
|
|
target_material = PointerProperty(name="Material",
|
|
|
|
description="Target material",
|
|
|
|
type=bpy.types.Material,
|
|
|
|
poll=_poll_material)
|
|
|
|
target_texture = PointerProperty(name="Texture",
|
|
|
|
description="Target texture",
|
|
|
|
type=bpy.types.Texture,
|
|
|
|
poll=_poll_texture)
|
|
|
|
|
|
|
|
go_to = EnumProperty(name="Go To",
|
|
|
|
description="Where should the animation start?",
|
|
|
|
items=[("kGoToBegin", "Beginning", "The beginning"),
|
|
|
|
("kGoToLoopBegin", "Loop Beginning", "The beginning of the active loop"),
|
|
|
|
("CURRENT", "(Don't Change)", "The current position"),
|
|
|
|
("kGoToEnd", "Ending", "The end"),
|
|
|
|
("kGoToLoopEnd", "Loop Ending", "The end of the active loop")],
|
|
|
|
default="CURRENT")
|
|
|
|
action = EnumProperty(name="Action",
|
|
|
|
description="What do you want the animation to do?",
|
|
|
|
items=[("kContinue", "Play", "Plays the animation"),
|
|
|
|
("kPlayToPercent", "Play to Percent", "Plays the animation until a given percent is complete"),
|
|
|
|
("kPlayToTime", "Play to Frame", "Plays the animation up to a given frame number"),
|
|
|
|
("kStop", "Stop", "Stops the animation",),
|
|
|
|
("kToggleState", "Toggle", "Toggles between Play and Stop"),
|
|
|
|
("CURRENT", "(Don't Change)", "Don't change the animation's playing state")],
|
|
|
|
default="CURRENT")
|
|
|
|
play_direction = EnumProperty(name="Direction",
|
|
|
|
description="Which direction do you want to play from?",
|
|
|
|
items=[("kSetForwards", "Forward", "Play forwards"),
|
|
|
|
("kSetBackwards", "Backwards", "Play backwards"),
|
|
|
|
("CURRENT", "(Don't Change)", "Don't change the play direction")],
|
|
|
|
default="CURRENT")
|
|
|
|
play_to_percent = IntProperty(name="Play To",
|
|
|
|
description="Percentage at which to stop the animation",
|
|
|
|
subtype="PERCENTAGE",
|
|
|
|
min=0, max=100, default=50)
|
|
|
|
play_to_frame = IntProperty(name="Play To",
|
|
|
|
description="Frame at which to stop the animation",
|
|
|
|
min=0)
|
|
|
|
|
|
|
|
def _set_loop_name(self, context):
|
|
|
|
"""Updates loop_begin and loop_end when the loop name is changed"""
|
|
|
|
pass
|
|
|
|
|
|
|
|
looping = EnumProperty(name="Looping",
|
|
|
|
description="Is the animation looping?",
|
|
|
|
items=[("kSetLooping", "Yes", "The animation is looping",),
|
|
|
|
("CURRENT", "(Don't Change)", "Don't change the loop status"),
|
|
|
|
("kSetUnLooping", "No", "The animation is NOT looping")],
|
|
|
|
default="CURRENT")
|
|
|
|
loop_name = StringProperty(name="Active Loop",
|
|
|
|
description="Name of the active loop",
|
|
|
|
update=_set_loop_name)
|
|
|
|
loop_begin = IntProperty(name="Loop Begin",
|
|
|
|
description="Frame number at which the loop begins",
|
|
|
|
min=0)
|
|
|
|
loop_end = IntProperty(name="Loop End",
|
|
|
|
description="Frame number at which the loop ends",
|
|
|
|
min=0)
|
|
|
|
|
|
|
|
event = EnumProperty(name="Callback",
|
|
|
|
description="Event upon which to callback the Responder",
|
|
|
|
items=[("kEnd", "End", "When the action ends"),
|
|
|
|
("NONE", "(None)", "Don't notify the Responder at all"),
|
|
|
|
("kStop", "Stop", "When the action is stopped by a message")],
|
|
|
|
default="kEnd")
|
|
|
|
|
|
|
|
# Blender memory workaround
|
|
|
|
_ENTIRE_ANIMATION = "(Entire Animation)"
|
|
|
|
def _get_anim_names(self, context):
|
|
|
|
if self.anim_type == "OBJECT":
|
|
|
|
items = [(anim.animation_name, anim.animation_name, "")
|
|
|
|
for anim in self.target_object.plasma_modifiers.animation.subanimations]
|
|
|
|
elif self.anim_type == "TEXTURE":
|
|
|
|
if self.target_texture is not None:
|
|
|
|
items = [(anim.animation_name, anim.animation_name, "")
|
|
|
|
for anim in self.target_texture.plasma_layer.subanimations]
|
|
|
|
elif self.target_material is not None or self.target_object is not None:
|
|
|
|
if self.target_material is None:
|
|
|
|
materials = (i.material for i in self.target_object.material_slots if i and i.material)
|
|
|
|
else:
|
|
|
|
materials = (self.target_material,)
|
|
|
|
layer_props = (i.texture.plasma_layer for mat in materials for i in mat.texture_slots if i and i.texture)
|
|
|
|
all_anims = frozenset((anim.animation_name for i in layer_props for anim in i.subanimations))
|
|
|
|
items = [(i, i, "") for i in all_anims]
|
|
|
|
else:
|
|
|
|
items = [(PlasmaAnimCmdMsgNode._ENTIRE_ANIMATION, PlasmaAnimCmdMsgNode._ENTIRE_ANIMATION, "")]
|
|
|
|
else:
|
|
|
|
raise RuntimeError()
|
|
|
|
|
|
|
|
# We always want "(Entire Animation)", if it exists, to be the first item.
|
|
|
|
entire = items.index((PlasmaAnimCmdMsgNode._ENTIRE_ANIMATION, PlasmaAnimCmdMsgNode._ENTIRE_ANIMATION, ""))
|
|
|
|
if entire not in (-1, 0):
|
|
|
|
items.pop(entire)
|
|
|
|
items.insert(0, (PlasmaAnimCmdMsgNode._ENTIRE_ANIMATION, PlasmaAnimCmdMsgNode._ENTIRE_ANIMATION, ""))
|
|
|
|
|
|
|
|
return items
|
|
|
|
|
|
|
|
anim_name = EnumProperty(name="Animation",
|
|
|
|
description="Name of the animation to control",
|
|
|
|
items=_get_anim_names,
|
|
|
|
options=set())
|
|
|
|
|
|
|
|
def draw_buttons(self, context, layout):
|
|
|
|
layout.prop(self, "anim_type")
|
|
|
|
|
|
|
|
col = layout.column()
|
|
|
|
if self.anim_type == "OBJECT":
|
|
|
|
col.alert = self.target_object is None
|
|
|
|
else:
|
|
|
|
col.alert = not any((self.target_object, self.target_material, self.target_texture))
|
|
|
|
col.prop(self, "target_object")
|
|
|
|
if self.anim_type != "OBJECT":
|
|
|
|
col.prop(self, "target_material")
|
|
|
|
col.prop(self, "target_texture")
|
|
|
|
col.prop(self, "anim_name")
|
|
|
|
|
|
|
|
layout.prop(self, "go_to")
|
|
|
|
layout.prop(self, "action")
|
|
|
|
layout.prop(self, "play_direction")
|
|
|
|
if self.action == "kPlayToPercent":
|
|
|
|
layout.prop(self, "play_to_percent")
|
|
|
|
elif self.action == "kPlayToTime":
|
|
|
|
layout.prop(self, "play_to_frame")
|
|
|
|
|
|
|
|
layout.prop(self, "looping")
|
|
|
|
col = layout.column()
|
|
|
|
col.enabled = self.looping != "CURRENT"
|
|
|
|
if self.anim_type != "OBJECT":
|
|
|
|
loops = None
|
|
|
|
else:
|
|
|
|
loops = None if self.target_object is None else self.target_object.plasma_modifiers.animation_loop
|
|
|
|
if loops is not None and loops.enabled:
|
|
|
|
layout.prop_search(self, "loop_name", loops, "loops", icon="PMARKER_ACT")
|
|
|
|
else:
|
|
|
|
layout.prop(self, "loop_begin")
|
|
|
|
layout.prop(self, "loop_end")
|
|
|
|
|
|
|
|
layout.prop(self, "event")
|
|
|
|
|
|
|
|
def convert_callback_message(self, exporter, so, msg, target, wait):
|
|
|
|
cb = plEventCallbackMsg()
|
|
|
|
cb.addReceiver(target)
|
|
|
|
cb.event = globals()[self.event]
|
|
|
|
cb.user = wait
|
|
|
|
msg.addCallback(cb)
|
|
|
|
msg.setCmd(plAnimCmdMsg.kAddCallbacks, True)
|
|
|
|
|
|
|
|
def convert_message(self, exporter, so):
|
|
|
|
msg = plAnimCmdMsg()
|
|
|
|
|
|
|
|
# We're either sending this off to an AGMasterMod or a LayerAnim
|
|
|
|
obj = self.target_object
|
|
|
|
if self.anim_type == "OBJECT":
|
|
|
|
if obj is None:
|
|
|
|
self.raise_error("target object must be specified")
|
|
|
|
if not obj.plasma_object.has_animation_data:
|
|
|
|
self.raise_error("invalid animation")
|
|
|
|
target = (exporter.animation.get_animation_key(obj),)
|
|
|
|
else:
|
|
|
|
material = self.target_material
|
|
|
|
texture = self.target_texture
|
|
|
|
if obj is None and material is None and texture is None:
|
|
|
|
self.raise_error("At least one of: target object, material, texture MUST be specified")
|
|
|
|
target = exporter.mesh.material.get_texture_animation_key(obj, material, texture, self.anim_name)
|
|
|
|
|
|
|
|
target = [i for i in target if not isinstance(i.object, (plAgeGlobalAnim, plLayerSDLAnimation))]
|
|
|
|
if not target:
|
|
|
|
self.raise_error("No controllable animations were found.")
|
|
|
|
for i in target:
|
|
|
|
msg.addReceiver(i)
|
|
|
|
|
|
|
|
# Check the enum properties to see what commands we need to add
|
|
|
|
for prop in (self.go_to, self.action, self.play_direction, self.looping):
|
|
|
|
cmd = getattr(plAnimCmdMsg, prop, None)
|
|
|
|
if cmd is not None:
|
|
|
|
msg.setCmd(cmd, True)
|
|
|
|
|
|
|
|
# Easier part starts here???
|
|
|
|
msg.animName = self.anim_name
|
|
|
|
if self.action == "kPlayToPercent":
|
|
|
|
msg.time = self.play_to_percent
|
|
|
|
elif self.action == "kPlayToTime":
|
|
|
|
msg.time = exporter.animation.convert_frame_time(self.play_to_frame)
|
|
|
|
|
|
|
|
# Implicit s better than explicit, I guess...
|
|
|
|
if self.loop_begin != self.loop_end:
|
|
|
|
# NOTE: loop name is not used in the engine AFAICT
|
|
|
|
msg.setCmd(plAnimCmdMsg.kSetLoopBegin, True)
|
|
|
|
msg.setCmd(plAnimCmdMsg.kSetLoopEnd, True)
|
|
|
|
msg.loopBegin = exporter.animation.convert_frame_time(self.loop_begin)
|
|
|
|
msg.loopEnd = exporter.animation.convert_frame_time(self.loop_end)
|
|
|
|
|
|
|
|
# Whew, this was crazy
|
|
|
|
return msg
|
|
|
|
|
|
|
|
@property
|
|
|
|
def has_callbacks(self):
|
|
|
|
return self.event != "NONE"
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def _idprop_mapping(cls):
|
|
|
|
return {"target_object": "object_name",
|
|
|
|
"target_material": "material_name",
|
|
|
|
"target_texture": "texture_name"}
|
|
|
|
|
|
|
|
def _idprop_sources(self):
|
|
|
|
return {"object_name": bpy.data.objects,
|
|
|
|
"material_name": bpy.data.materials,
|
|
|
|
"texture_name": bpy.data.textures}
|
|
|
|
|
|
|
|
|
|
|
|
class PlasmaCameraMsgNode(PlasmaMessageNode, bpy.types.Node):
|
|
|
|
bl_category = "MSG"
|
|
|
|
bl_idname = "PlasmaCameraMsgNode"
|
|
|
|
bl_label = "Camera"
|
|
|
|
bl_width_default = 200
|
|
|
|
|
|
|
|
cmd = EnumProperty(name="Command",
|
|
|
|
description="Command to send to the camera system",
|
|
|
|
items=[("push", "Push Camera", "Pushes a new camera onto the camera stack and transitions to it"),
|
|
|
|
("pop", "Pop Camera", "Pops the camera off the camera stack"),
|
|
|
|
("disablefp", "Disable First Person", "Forces the camera into third person if it is currently in first person and disables first person mode"),
|
|
|
|
("enablefp", "Enable First Person", "Reenables the first person camera and switches back to it if the player was in first person previously")],
|
|
|
|
options=set())
|
|
|
|
camera = PointerProperty(name="Camera",
|
|
|
|
type=bpy.types.Object,
|
|
|
|
poll=idprops.poll_camera_objects,
|
|
|
|
options=set())
|
|
|
|
cut = BoolProperty(name="Cut Transition",
|
|
|
|
description="Immediately swap over to the new camera without a transition animation",
|
|
|
|
options=set())
|
|
|
|
|
|
|
|
def convert_message(self, exporter, so):
|
|
|
|
msg = plCameraMsg()
|
|
|
|
msg.BCastFlags |= plMessage.kLocalPropagate | plMessage.kBCastByType
|
|
|
|
if self.cmd in {"push", "pop"}:
|
|
|
|
if self.camera is not None:
|
|
|
|
msg.newCam = exporter.mgr.find_create_key(plSceneObject, bl=self.camera)
|
|
|
|
# It appears that kRegionPopCamera is unused. pushing is controlled by observing
|
|
|
|
# the presence of the kResponderTrigger command.
|
|
|
|
msg.setCmd(plCameraMsg.kResponderTrigger, self.cmd == "push")
|
|
|
|
msg.setCmd(plCameraMsg.kRegionPushCamera, True)
|
|
|
|
msg.setCmd(plCameraMsg.kSetAsPrimary, self.camera is None
|
|
|
|
or self.camera.data.plasma_camera.settings.primary_camera)
|
|
|
|
msg.setCmd(plCameraMsg.kCut, self.cut)
|
|
|
|
elif self.cmd == "disablefp":
|
|
|
|
msg.setCmd(plCameraMsg.kResponderSetThirdPerson)
|
|
|
|
elif self.cmd == "enablefp":
|
|
|
|
msg.setCmd(plCameraMsg.kResponderUndoThirdPerson)
|
|
|
|
else:
|
|
|
|
raise RuntimeError()
|
|
|
|
return msg
|
|
|
|
|
|
|
|
def draw_buttons(self, context, layout):
|
|
|
|
layout.prop(self, "cmd")
|
|
|
|
if self.cmd in {"push", "pop"}:
|
|
|
|
layout.prop(self, "camera")
|
|
|
|
layout.prop(self, "cut")
|
|
|
|
|
|
|
|
|
|
|
|
class PlasmaEnableMsgNode(PlasmaMessageNode, bpy.types.Node):
|
|
|
|
bl_category = "MSG"
|
|
|
|
bl_idname = "PlasmaEnableMsgNode"
|
|
|
|
bl_label = "Enable/Disable"
|
|
|
|
|
|
|
|
output_sockets = OrderedDict([
|
|
|
|
("receivers", {
|
|
|
|
"text": "Send To",
|
|
|
|
"type": "PlasmaEnableMessageSocket",
|
|
|
|
"valid_link_sockets": {"PlasmaEnableMessageSocket", "PlasmaNodeSocketInputGeneral"},
|
|
|
|
}),
|
|
|
|
])
|
|
|
|
|
|
|
|
cmd = EnumProperty(name="Command",
|
|
|
|
description="How should we affect the object's state?",
|
|
|
|
items=[("kDisable", "Disable", "Deactivate the object"),
|
|
|
|
("kEnable", "Enable", "Activate the object")],
|
|
|
|
default="kEnable")
|
|
|
|
settings = EnumProperty(name="Affects",
|
|
|
|
description="Which attributes should we change",
|
|
|
|
items=[("kAudible", "Audio", "Sounds played by this object"),
|
|
|
|
("kPhysical", "Physics", "Physical simulation of the object"),
|
|
|
|
("kDrawable", "Visibility", "Visible geometry/light of the object"),
|
|
|
|
("kModifiers", "Modifiers", "Modifiers attached to the object")],
|
|
|
|
options={"ENUM_FLAG"},
|
|
|
|
default={"kAudible", "kDrawable", "kPhysical", "kModifiers"})
|
|
|
|
bcast_to_children = BoolProperty(name="Send to Children",
|
|
|
|
description="Send the message to objects parented to the object",
|
|
|
|
default=False,
|
|
|
|
options=set())
|
|
|
|
|
|
|
|
def convert_message(self, exporter, so):
|
|
|
|
settings = self.settings
|
|
|
|
if not settings:
|
|
|
|
self.raise_error("Nothing set to enable/disable")
|
|
|
|
|
|
|
|
receivers = []
|
|
|
|
for i in self.find_outputs("receivers"):
|
|
|
|
key = i.get_key(exporter, so)
|
|
|
|
if isinstance(key, tuple):
|
|
|
|
for j in key:
|
|
|
|
receivers.append(j)
|
|
|
|
else:
|
|
|
|
receivers.append(key)
|
|
|
|
|
|
|
|
# OK, so, bad news old bean... In versions of the game using Havok physics, plEnableMsg
|
|
|
|
# does not actually affect the physics. So we have to potentially generate a new message
|
|
|
|
# for that.
|
|
|
|
if exporter.mgr.getVer() <= pvPots:
|
|
|
|
if "kPhysical" in settings:
|
|
|
|
msg = plSimSuppressMsg()
|
|
|
|
for i in receivers:
|
|
|
|
msg.addReceiver(i)
|
|
|
|
if self.bcast_to_children:
|
|
|
|
msg.BCastFlags |= plMessage.kPropagateToChildren
|
|
|
|
msg.suppress = self.cmd == "kDisable"
|
|
|
|
yield msg
|
|
|
|
|
|
|
|
msg = plEnableMsg()
|
|
|
|
for i in receivers:
|
|
|
|
msg.addReceiver(i)
|
|
|
|
msg.setCmd(getattr(plEnableMsg, self.cmd), True)
|
|
|
|
|
|
|
|
# If we have a full house, let's send it to all the SO's generic modifiers as by compressing
|
|
|
|
# to kAll :) -- And no, this is not a bug. We do put the named types in commands. The types
|
|
|
|
# bit vector is for raw Plasma class IDs listing which modifier types we prop to if "kByType"
|
|
|
|
# is a command. Nice flexibility--I have no idea where that's used in Uru though...
|
|
|
|
# NOTE: kAll will never be set for PotS because enable/disable physicals seems to do nothing.
|
|
|
|
if settings >= {"kAudible", "kPhysical", "kDrawable"}:
|
|
|
|
msg.setCmd(plEnableMsg.kAll, True)
|
|
|
|
else:
|
|
|
|
for i in settings:
|
|
|
|
bit = getattr(plEnableMsg, i, None)
|
|
|
|
if bit is not None:
|
|
|
|
msg.setCmd(bit, True)
|
|
|
|
|
|
|
|
# Propagation to modifiers for, for exmple, ladders
|
|
|
|
if "kModifiers" in settings:
|
|
|
|
msg.BCastFlags |= plMessage.kPropagateToModifiers
|
|
|
|
if self.bcast_to_children:
|
|
|
|
msg.BCastFlags |= plMessage.kPropagateToChildren
|
|
|
|
yield msg
|
|
|
|
|
|
|
|
def draw_buttons(self, context, layout):
|
|
|
|
layout.row(align=True).prop(self, "cmd", expand=True)
|
|
|
|
layout.prop(self, "bcast_to_children")
|
|
|
|
layout.separator()
|
|
|
|
layout.label("Affects:")
|
|
|
|
layout.column(align=True).prop(self, "settings")
|
|
|
|
|
|
|
|
|
|
|
|
class PlasmaEnableMessageSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
|
|
|
|
bl_color = (0.427, 0.196, 0.0, 1.0)
|
|
|
|
|
|
|
|
|
|
|
|
class PlasmaExcludeRegionMsg(PlasmaMessageNode, bpy.types.Node):
|
|
|
|
bl_category = "MSG"
|
|
|
|
bl_idname = "PlasmaExcludeRegionMsg"
|
|
|
|
bl_label = "Exclude Region"
|
|
|
|
|
|
|
|
output_sockets = OrderedDict([
|
|
|
|
("region", {
|
|
|
|
"text": "Region",
|
|
|
|
"type": "PlasmaExcludeMessageSocket"
|
|
|
|
}),
|
|
|
|
])
|
|
|
|
|
|
|
|
cmd = EnumProperty(name="Command",
|
|
|
|
description="Exclude Region State",
|
|
|
|
items=[("kClear", "Clear", "Clear all avatars from the region"),
|
|
|
|
("kRelease", "Release", "Allow avatars to enter the region")],
|
|
|
|
default="kClear")
|
|
|
|
|
|
|
|
def convert_message(self, exporter, so):
|
|
|
|
msg = plExcludeRegionMsg()
|
|
|
|
for i in self.find_outputs("region"):
|
|
|
|
msg.addReceiver(i.get_key(exporter, so))
|
|
|
|
msg.cmd = getattr(plExcludeRegionMsg, self.cmd)
|
|
|
|
return msg
|
|
|
|
|
|
|
|
def draw_buttons(self, context, layout):
|
|
|
|
layout.prop(self, "cmd", text="Cmd")
|
|
|
|
|
|
|
|
|
|
|
|
class PlasmaLinkToAgeMsg(PlasmaMessageNode, bpy.types.Node):
|
|
|
|
bl_category = "MSG"
|
|
|
|
bl_idname = "PlasmaLinkToAgeMsg"
|
|
|
|
bl_label = "Link to Age"
|
|
|
|
bl_width_default = 280
|
|
|
|
|
|
|
|
rules = EnumProperty(name="Rules",
|
|
|
|
description="Rules describing which age instance to link to",
|
|
|
|
items=[("kOriginalBook", "Original Age", "Links to a personally owned instance, creating if none exists"),
|
|
|
|
("kOwnedBook", "Owned Age", "Links to a personally owned instance, fails if none exists"),
|
|
|
|
("kChildAgeBook", "Child Age", "Links to an age instance parented to another personal age"),
|
|
|
|
("kSubAgeBook", "Sub Age", "Links to an age instance owned by the current age instance"),
|
|
|
|
("kBasicLink", "Basic", "Links to a specific age instance")])
|
|
|
|
parent_filename = StringProperty(name="Parent Age",
|
|
|
|
description="Filename of the age that owns the age instance we're linking to")
|
|
|
|
|
|
|
|
age_filename = StringProperty(name="Age Filename",
|
|
|
|
description="Filename of the age to link to (eg 'Garden'")
|
|
|
|
age_instance = StringProperty(name="Age Instance",
|
|
|
|
description="Instance name of the age to link to (eg 'Eder Kemo')")
|
|
|
|
age_uuid = StringProperty(name="Age Guid",
|
|
|
|
description="Instance GUID to link to (eg 'ea489821-6c35-4bd0-9dae-bb17c585e680')")
|
|
|
|
|
|
|
|
spawn_title = StringProperty(name="Spawn Title",
|
|
|
|
description="Title of the Spawn Point to use",
|
|
|
|
default="Default")
|
|
|
|
spawn_point = StringProperty(name="Spawn Point",
|
|
|
|
description="Name of the Spawn Point's Plasma Object",
|
|
|
|
default="LinkInPointDefault")
|
|
|
|
|
|
|
|
def convert_message(self, exporter, so):
|
|
|
|
msg = plLinkToAgeMsg()
|
|
|
|
als = msg.ageLink
|
|
|
|
ais, spi = als.ageInfo, als.spawnPoint
|
|
|
|
|
|
|
|
als.linkingRules = getattr(plAgeLinkStruct, self.rules)
|
|
|
|
if self.rules == "kChildAgeBook":
|
|
|
|
als.parentAgeFilename = self.parent_filename
|
|
|
|
ais.ageFilename = self.age_filename
|
|
|
|
ais.ageInstanceName = self.age_instance if self.age_instance else self.age_filename
|
|
|
|
if self.rules == "kBasicLink":
|
|
|
|
try:
|
|
|
|
ais.ageInstanceGuid = self.age_uuid
|
|
|
|
except ValueError:
|
|
|
|
self.raise_error("Age Instance GUID is not a valid UUID")
|
|
|
|
spi.title = self.spawn_title
|
|
|
|
spi.spawnPt = self.spawn_point
|
|
|
|
|
|
|
|
link_oneshot = self._find_link_oneshot(self)
|
|
|
|
if link_oneshot is not None:
|
|
|
|
msg.linkEffects.linkInAnimName = link_oneshot.animation
|
|
|
|
|
|
|
|
return msg
|
|
|
|
|
|
|
|
def _find_link_oneshot(self, node, state=None):
|
|
|
|
if state is None:
|
|
|
|
state = set()
|
|
|
|
state.add(node)
|
|
|
|
|
|
|
|
# Recursively search the responder tree for what avatar animation (OneShot) we are blocking
|
|
|
|
# on when linking. We'll continue playing that when the link-in completes.
|
|
|
|
for child_node in node.find_inputs("sender"):
|
|
|
|
if child_node in state:
|
|
|
|
continue
|
|
|
|
if isinstance(child_node, PlasmaOneShotMsgNode):
|
|
|
|
if child_node.has_callbacks:
|
|
|
|
return child_node
|
|
|
|
elif isinstance(child_node, PlasmaMessageNode) and child_node:
|
|
|
|
return self._find_link_oneshot(child_node, state)
|
|
|
|
return None
|
|
|
|
|
|
|
|
def draw_buttons(self, context, layout):
|
|
|
|
layout.prop(self, "rules")
|
|
|
|
if self.rules == "kChildAgeBook":
|
|
|
|
layout.prop(self, "parent_filename")
|
|
|
|
layout.separator()
|
|
|
|
|
|
|
|
layout.prop(self, "age_filename")
|
|
|
|
layout.prop(self, "age_instance")
|
|
|
|
if self.rules == "kBasicLink":
|
|
|
|
layout.prop(self, "age_uuid")
|
|
|
|
layout.separator()
|
|
|
|
|
|
|
|
layout.prop(self, "spawn_title")
|
|
|
|
layout.prop(self, "spawn_point")
|
|
|
|
|
|
|
|
|
|
|
|
class PlasmaOneShotMsgNode(idprops.IDPropObjectMixin, PlasmaMessageWithCallbacksNode, bpy.types.Node):
|
|
|
|
bl_category = "MSG"
|
|
|
|
bl_idname = "PlasmaOneShotMsgNode"
|
|
|
|
bl_label = "One Shot"
|
|
|
|
bl_width_default = 210
|
|
|
|
|
|
|
|
pos_object = PointerProperty(name="Position",
|
|
|
|
description="Object defining the OneShot position",
|
|
|
|
type=bpy.types.Object)
|
|
|
|
seek = EnumProperty(name="Seek",
|
|
|
|
description="How the avatar should approach the OneShot position",
|
|
|
|
items=[("SMART", "Smart Seek", "Let the engine figure out the best path"),
|
|
|
|
("DUMB", "Seek", "Shuffle to the OneShot position"),
|
|
|
|
("NONE", "Warp", "Warp the avatar to the OneShot position")],
|
|
|
|
default="SMART")
|
|
|
|
|
|
|
|
animation = StringProperty(name="Animation",
|
|
|
|
description="Name of the animation the avatar should execute")
|
|
|
|
marker = StringProperty(name="Marker",
|
|
|
|
description="Name of the marker specifying when to notify the Responder")
|
|
|
|
drivable = BoolProperty(name="Drivable",
|
|
|
|
description="Player retains control of the avatar during the OneShot",
|
|
|
|
default=False)
|
|
|
|
reversable = BoolProperty(name="Reversable",
|
|
|
|
description="Player can reverse the OneShot",
|
|
|
|
default=False)
|
|
|
|
|
|
|
|
def convert_callback_message(self, exporter, so, msg, target, wait):
|
|
|
|
msg.addCallback(self.marker, target, wait)
|
|
|
|
|
|
|
|
def convert_message(self, exporter, so):
|
|
|
|
msg = plOneShotMsg()
|
|
|
|
msg.addReceiver(self.get_key(exporter, so))
|
|
|
|
return msg
|
|
|
|
|
|
|
|
def draw_buttons(self, context, layout):
|
|
|
|
layout.prop(self, "animation", text="Anim")
|
|
|
|
layout.prop(self, "marker")
|
|
|
|
row = layout.row()
|
|
|
|
row.prop(self, "drivable")
|
|
|
|
row.prop(self, "reversable")
|
|
|
|
layout.prop(self, "pos_object", icon="EMPTY_DATA")
|
|
|
|
layout.prop(self, "seek")
|
|
|
|
|
|
|
|
def export(self, exporter, bo, so):
|
|
|
|
# Note: we purposefully allow this to proceed because plOneShotMod is a MultiMod, so we
|
|
|
|
# want all referencing SOs to get a copy of the modifier.
|
|
|
|
oneshotmod = self.get_key(exporter, so).object
|
|
|
|
oneshotmod.animName = self.animation
|
|
|
|
oneshotmod.drivable = self.drivable
|
|
|
|
oneshotmod.reversable = self.reversable
|
|
|
|
oneshotmod.smartSeek = self.seek == "SMART"
|
|
|
|
oneshotmod.noSeek = self.seek == "NONE"
|
|
|
|
oneshotmod.seekDuration = 1.0
|
|
|
|
|
|
|
|
def get_key(self, exporter, so):
|
|
|
|
if self.pos_object is not None:
|
|
|
|
pos_so = exporter.mgr.find_create_object(plSceneObject, bl=self.pos_object)
|
|
|
|
return self._find_create_key(plOneShotMod, exporter, so=pos_so)
|
|
|
|
else:
|
|
|
|
return self._find_create_key(plOneShotMod, exporter, so=so)
|
|
|
|
|
|
|
|
def harvest_actors(self):
|
|
|
|
if self.pos_object:
|
|
|
|
yield self.pos_object.name
|
|
|
|
|
|
|
|
@property
|
|
|
|
def has_callbacks(self):
|
|
|
|
return bool(self.marker)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def requires_actor(self):
|
|
|
|
return self.pos_object is None
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def _idprop_mapping(cls):
|
|
|
|
return {"pos_object": "pos"}
|
|
|
|
|
|
|
|
|
|
|
|
class PlasmaOneShotCallbackSocket(PlasmaMessageSocketBase, bpy.types.NodeSocket):
|
|
|
|
marker = StringProperty(name="Marker",
|
|
|
|
description="Marker specifying the time at which to send a callback to this Responder")
|
|
|
|
|
|
|
|
def draw(self, context, layout, node, text):
|
|
|
|
layout.prop(self, "marker")
|
|
|
|
|
|
|
|
|
|
|
|
class PlasmaSceneObjectMsgRcvrNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.Node):
|
|
|
|
bl_category = "MSG"
|
|
|
|
bl_idname = "PlasmaSceneObjectMsgRcvrNode"
|
|
|
|
bl_label = "Send To Object"
|
|
|
|
bl_width_default = 190
|
|
|
|
|
|
|
|
input_sockets = OrderedDict([
|
|
|
|
("message", {
|
|
|
|
"text": "Message",
|
|
|
|
"type": "PlasmaNodeSocketInputGeneral",
|
|
|
|
"valid_link_sockets": {"PlasmaEnableMessageSocket"},
|
|
|
|
"spawn_empty": True,
|
|
|
|
}),
|
|
|
|
])
|
|
|
|
|
|
|
|
target_object = PointerProperty(name="Object",
|
|
|
|
description="Object to send the message to",
|
|
|
|
type=bpy.types.Object)
|
|
|
|
|
|
|
|
def draw_buttons(self, context, layout):
|
|
|
|
layout.prop(self, "target_object")
|
|
|
|
|
|
|
|
def get_key(self, exporter, so):
|
|
|
|
bo = self.target_object
|
|
|
|
if bo is None:
|
|
|
|
self.raise_error("target object must be specified")
|
|
|
|
ref_so_key = exporter.mgr.find_create_key(plSceneObject, bl=bo)
|
|
|
|
return ref_so_key
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def _idprop_mapping(cls):
|
|
|
|
return {"target_object": "object_name"}
|
|
|
|
|
|
|
|
|
|
|
|
class PlasmaSoundMsgNode(idprops.IDPropObjectMixin, PlasmaMessageWithCallbacksNode, bpy.types.Node):
|
|
|
|
bl_category = "MSG"
|
|
|
|
bl_idname = "PlasmaSoundMsgNode"
|
|
|
|
bl_label = "Sound"
|
|
|
|
bl_width_default = 190
|
|
|
|
|
|
|
|
def _poll_sound_emitters(self, value):
|
|
|
|
return value.plasma_modifiers.soundemit.enabled
|
|
|
|
|
|
|
|
emitter_object = PointerProperty(name="Object",
|
|
|
|
description="Sound emitter object",
|
|
|
|
type=bpy.types.Object,
|
|
|
|
poll=_poll_sound_emitters)
|
|
|
|
sound_name = StringProperty(name="Sound",
|
|
|
|
description="Sound datablock")
|
|
|
|
|
|
|
|
go_to = EnumProperty(name="Go To",
|
|
|
|
description="Where should the sound start?",
|
|
|
|
items=[("BEGIN", "Beginning", "The beginning"),
|
|
|
|
("CURRENT", "(Don't Change)", "The current position"),
|
|
|
|
("TIME", "Time", "The time specified in seconds")],
|
|
|
|
default="CURRENT")
|
|
|
|
looping = EnumProperty(name="Looping",
|
|
|
|
description="Is the sound looping?",
|
|
|
|
items=[("kSetLooping", "Yes", "The sound is looping",),
|
|
|
|
("CURRENT", "(Don't Change)", "Don't change the loop status"),
|
|
|
|
("kUnSetLooping", "No", "The sound is NOT looping")],
|
|
|
|
default="CURRENT")
|
|
|
|
action = EnumProperty(name="Action",
|
|
|
|
description="What do you want the sound to do?",
|
|
|
|
items=[("kPlay", "Play", "Plays the sound"),
|
|
|
|
("kStop", "Stop", "Stops the sound",),
|
|
|
|
("kToggleState", "Toggle", "Toggles between Play and Stop"),
|
|
|
|
("CURRENT", "(Don't Change)", "Don't change the sound's playing state")],
|
|
|
|
default="CURRENT")
|
|
|
|
volume = EnumProperty(name="Volume",
|
|
|
|
description="What should happen to the volume?",
|
|
|
|
items=[("MUTE", "Mute", "Mutes the volume"),
|
|
|
|
("CURRENT", "(Don't Change)", "Don't change the volume"),
|
|
|
|
("CUSTOM", "Custom", "Manually specify the volume")],
|
|
|
|
default="CURRENT")
|
|
|
|
|
|
|
|
time = FloatProperty(name="Time",
|
|
|
|
description="Time in seconds to begin playing from",
|
|
|
|
min=0.0, default=0.0,
|
|
|
|
options=set(), subtype="TIME", unit="TIME")
|
|
|
|
volume_pct = IntProperty(name="Volume Level",
|
|
|
|
description="Volume to play the sound",
|
|
|
|
min=0, max=100, default=100,
|
|
|
|
options=set(),
|
|
|
|
subtype="PERCENTAGE")
|
|
|
|
event = EnumProperty(name="Callback",
|
|
|
|
description="Event upon which to callback the Responder",
|
|
|
|
items=[("kEnd", "End", "When the sound ends"),
|
|
|
|
("NONE", "(None)", "Don't notify the Responder at all"),
|
|
|
|
("kStop", "Stop", "When the sound is stopped by a message")],
|
|
|
|
default="NONE")
|
|
|
|
|
|
|
|
def convert_callback_message(self, exporter, so, msg, target, wait):
|
|
|
|
assert not self.is_random_sound, "Callbacks are not available for random sounds"
|
|
|
|
cb = plEventCallbackMsg()
|
|
|
|
cb.addReceiver(target)
|
|
|
|
cb.event = globals()[self.event]
|
|
|
|
cb.user = wait
|
|
|
|
msg.addCallback(cb)
|
|
|
|
msg.setCmd(plSoundMsg.kAddCallbacks)
|
|
|
|
|
|
|
|
def convert_message(self, exporter, so):
|
|
|
|
if self.emitter_object is None:
|
|
|
|
self.raise_error("Sound emitter must be set")
|
|
|
|
soundemit = self.emitter_object.plasma_modifiers.soundemit
|
|
|
|
if not soundemit.enabled:
|
|
|
|
self.raise_error("'{}' is not a valid Sound Emitter".format(self.emitter_object.name))
|
|
|
|
|
|
|
|
if self.is_random_sound:
|
|
|
|
yield from self._convert_random_sound_msg(exporter, so)
|
|
|
|
else:
|
|
|
|
yield from self._convert_sound_emitter_msg(exporter, so)
|
|
|
|
|
|
|
|
def _convert_random_sound_msg(self, exporter, so):
|
|
|
|
# Yas, plAnimCmdMsg
|
|
|
|
msg = plAnimCmdMsg()
|
|
|
|
msg.addReceiver(exporter.mgr.find_key(plRandomSoundMod, bl=self.emitter_object))
|
|
|
|
|
|
|
|
if self.action == "kPlay":
|
|
|
|
msg.setCmd(plAnimCmdMsg.kContinue, True)
|
|
|
|
elif self.action == "kStop":
|
|
|
|
msg.setCmd(plAnimCmdMsg.kStop, True)
|
|
|
|
elif self.action == "kToggleState":
|
|
|
|
msg.setCmd(plAnimCmdMsg.kToggleState, True)
|
|
|
|
|
|
|
|
if self.volume != "CURRENT":
|
|
|
|
# No, you are not imagining things...
|
|
|
|
msg.setCmd(plAnimCmdMsg.kSetSpeed, True)
|
|
|
|
msg.speed = self.volume_pct / 100.0 if self.volume == "CUSTOM" else 0.0
|
|
|
|
|
|
|
|
yield msg
|
|
|
|
|
|
|
|
def _convert_sound_emitter_msg(self, exporter, so):
|
|
|
|
soundemit = self.emitter_object.plasma_modifiers.soundemit
|
|
|
|
|
|
|
|
# Always test the specified audible for validity
|
|
|
|
if self.sound_name and soundemit.sounds.get(self.sound_name, None) is None:
|
|
|
|
self.raise_error("Invalid Sound '{}' requested from Sound Emitter '{}'".format(self.sound_name, self.emitter_object.name))
|
|
|
|
|
|
|
|
# Remember that 3D stereo sounds are exported as two emitters...
|
|
|
|
# But, if we only have one sound attached, who cares, we can just address the message to all
|
|
|
|
audible_key = exporter.mgr.find_create_key(plAudioInterface, bl=self.emitter_object)
|
|
|
|
indices = (-1,) if not self.sound_name or len(soundemit.sounds) == 1 else soundemit.get_sound_indices(self.sound_name)
|
|
|
|
for idx in indices:
|
|
|
|
msg = plSoundMsg()
|
|
|
|
msg.addReceiver(audible_key)
|
|
|
|
msg.index = idx
|
|
|
|
|
|
|
|
# NOTE: There are a number of commands in Plasma's enumeration that do nothing.
|
|
|
|
# This is what I determine to be the most useful and functional subset...
|
|
|
|
# Please see plAudioInterface::MsgReceive for more details.
|
|
|
|
if self.go_to == "BEGIN":
|
|
|
|
msg.setCmd(plSoundMsg.kGoToTime)
|
|
|
|
msg.time = 0.0
|
|
|
|
elif self.go_to == "TIME":
|
|
|
|
msg.setCmd(plSoundMsg.kGoToTime)
|
|
|
|
msg.time = self.time
|
|
|
|
|
|
|
|
if self.volume == "MUTE":
|
|
|
|
msg.setCmd(plSoundMsg.kSetVolume)
|
|
|
|
msg.volume = 0.0
|
|
|
|
elif self.volume == "CUSTOM":
|
|
|
|
msg.setCmd(plSoundMsg.kSetVolume)
|
|
|
|
msg.volume = self.volume_pct / 100.0
|
|
|
|
|
|
|
|
if self.looping != "CURRENT":
|
|
|
|
msg.setCmd(getattr(plSoundMsg, self.looping))
|
|
|
|
if self.action != "CURRENT":
|
|
|
|
msg.setCmd(getattr(plSoundMsg, self.action))
|
|
|
|
|
|
|
|
# Because we might be giving two messages here...
|
|
|
|
yield msg
|
|
|
|
|
|
|
|
def draw_buttons(self, context, layout):
|
|
|
|
layout.prop(self, "emitter_object")
|
|
|
|
|
|
|
|
# Random Sound emitters can only control the entire emitter object, not the
|
|
|
|
# individual sounds.
|
|
|
|
random = self.is_random_sound
|
|
|
|
if not random:
|
|
|
|
if self.emitter_object is not None:
|
|
|
|
soundemit = self.emitter_object.plasma_modifiers.soundemit
|
|
|
|
if soundemit.enabled:
|
|
|
|
layout.prop_search(self, "sound_name", soundemit, "sounds", icon="SOUND")
|
|
|
|
else:
|
|
|
|
layout.label("Not a Sound Emitter", icon="ERROR")
|
|
|
|
|
|
|
|
layout.prop(self, "go_to")
|
|
|
|
if self.go_to == "TIME":
|
|
|
|
layout.prop(self, "time")
|
|
|
|
|
|
|
|
layout.prop(self, "action")
|
|
|
|
if self.volume == "CUSTOM":
|
|
|
|
layout.prop(self, "volume_pct")
|
|
|
|
if not random:
|
|
|
|
layout.prop(self, "looping")
|
|
|
|
layout.prop(self, "volume")
|
|
|
|
if not random:
|
|
|
|
layout.prop(self, "event")
|
|
|
|
|
|
|
|
@property
|
|
|
|
def has_callbacks(self):
|
|
|
|
if not self.is_random_sound:
|
|
|
|
return self.event != "NONE"
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def _idprop_mapping(cls):
|
|
|
|
return {"emitter_object": "object_name"}
|
|
|
|
|
|
|
|
@property
|
|
|
|
def is_random_sound(self):
|
|
|
|
if self.emitter_object is not None:
|
|
|
|
return self.emitter_object.plasma_modifiers.random_sound.enabled
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
class PlasmaTimerCallbackMsgNode(PlasmaMessageWithCallbacksNode, bpy.types.Node):
|
|
|
|
bl_category = "MSG"
|
|
|
|
bl_idname = "PlasmaTimerCallbackMsgNode"
|
|
|
|
bl_label = "Timed Callback"
|
|
|
|
|
|
|
|
delay = FloatProperty(name="Delay",
|
|
|
|
description="Time (in seconds) to wait until continuing",
|
|
|
|
min=0.1,
|
|
|
|
default=1.0)
|
|
|
|
|
|
|
|
def draw_buttons(self, context, layout):
|
|
|
|
layout.prop(self, "delay")
|
|
|
|
|
|
|
|
def convert_callback_message(self, exporter, so, msg, target, wait):
|
|
|
|
msg.addReceiver(target)
|
|
|
|
msg.ID = wait
|
|
|
|
|
|
|
|
def convert_message(self, exporter, so):
|
|
|
|
msg = plTimerCallbackMsg()
|
|
|
|
msg.time = self.delay
|
|
|
|
return msg
|
|
|
|
|
|
|
|
|
|
|
|
class PlasmaTriggerMultiStageMsgNode(PlasmaMessageNode, bpy.types.Node):
|
|
|
|
bl_category = "MSG"
|
|
|
|
bl_idname = "PlasmaTriggerMultiStageMsgNode"
|
|
|
|
bl_label = "Trigger MultiStage"
|
|
|
|
|
|
|
|
output_sockets = OrderedDict([
|
|
|
|
("satisfies", {
|
|
|
|
"text": "Trigger",
|
|
|
|
"type": "PlasmaConditionSocket",
|
|
|
|
"valid_link_nodes": "PlasmaMultiStageBehaviorNode",
|
|
|
|
"valid_link_sockets": "PlasmaConditionSocket",
|
|
|
|
"link_limit": 1,
|
|
|
|
})
|
|
|
|
])
|
|
|
|
|
|
|
|
def convert_message(self, exporter, so):
|
|
|
|
# Yeah, this is not a REAL Plasma message, but the Korman way is to try to hide these little
|
|
|
|
# low-level notifications behind higher level abstractions, so here you go. A notify message
|
|
|
|
# that only targets plMultiStageBehMod. You're welcome!
|
|
|
|
msg = self.generate_notify_msg(exporter, so, "satisfies")
|
|
|
|
|
|
|
|
# The MultiStageBehMod needs to receive the avatar key that whatdonetriggeredit. We don't know
|
|
|
|
# this information at export-time, but plResponderModifier::IContinueSending will interpret
|
|
|
|
# a collision event as "ohey, let's add the avatar key for MSBs" - nice.
|
|
|
|
msg.addEvent(proCollisionEventData())
|
|
|
|
return msg
|
|
|
|
|
|
|
|
|
|
|
|
class PlasmaFootstepSoundMsgNode(PlasmaMessageNode, bpy.types.Node):
|
|
|
|
bl_category = "MSG"
|
|
|
|
bl_idname = "PlasmaFootstepSoundMsgNode"
|
|
|
|
bl_label = "Footstep Sound"
|
|
|
|
|
|
|
|
surface = EnumProperty(name="Surface",
|
|
|
|
description="What kind of surface are we walking on?",
|
|
|
|
items=footstep_surfaces,
|
|
|
|
default="stone")
|
|
|
|
|
|
|
|
def draw_buttons(self, context, layout):
|
|
|
|
layout.prop(self, "surface")
|
|
|
|
|
|
|
|
def convert_message(self, exporter, so):
|
|
|
|
msg = plArmatureEffectStateMsg()
|
|
|
|
msg.BCastFlags |= (plMessage.kPropagateToModifiers | plMessage.kNetPropagate)
|
|
|
|
msg.surface = footstep_surface_ids[self.surface]
|
|
|
|
return msg
|