diff --git a/korman/__init__.py b/korman/__init__.py
index bc52614..cd51a9c 100644
--- a/korman/__init__.py
+++ b/korman/__init__.py
@@ -16,6 +16,7 @@
import bpy
from . import exporter, render
from . import properties, ui
+from . import nodes
from . import operators
bl_info = {
@@ -37,6 +38,7 @@ def register():
bpy.utils.register_module(__name__)
# Sigh... Blender isn't totally automated.
+ nodes.register()
operators.register()
properties.register()
@@ -44,6 +46,7 @@ def register():
def unregister():
"""Unregisters all Blender operators and GUI items"""
bpy.utils.unregister_module(__name__)
+ nodes.unregister()
operators.unregister()
diff --git a/korman/nodes/__init__.py b/korman/nodes/__init__.py
new file mode 100644
index 0000000..869f1b0
--- /dev/null
+++ b/korman/nodes/__init__.py
@@ -0,0 +1,68 @@
+# 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
+import inspect
+from nodeitems_utils import NodeCategory, NodeItem
+import nodeitems_utils
+
+# Put all Korman node modules here...
+from .node_conditions import *
+from .node_core import *
+from .node_logic import *
+from .node_messages import *
+from .node_responder import *
+
+class PlasmaNodeCategory(NodeCategory):
+ """Plasma Node Category"""
+
+ @classmethod
+ def poll(cls, context):
+ return (context.space_data.tree_type == "PlasmaNodeTree")
+
+# Here's what you need to know about this...
+# If you add a new category, put the pretty name here!
+# If you're making a new Node, ensure that your bl_idname attribute is present AND matches
+# the class name. Otherwise, absolutely fascinating things will happen. Don't expect for me
+# to come and rescue you from it, either.
+_kategory_names = {
+ "CONDITIONS": "Conditions",
+ "LOGIC": "Logic",
+ "MSG": "Message",
+}
+
+# Now, generate the categories as best we can...
+_kategories = {}
+for cls in dict(globals()).values():
+ if inspect.isclass(cls):
+ if not issubclass(cls, PlasmaNodeBase) or not issubclass(cls, bpy.types.Node):
+ continue
+ else:
+ continue
+ try:
+ _kategories[cls.bl_category].append(cls)
+ except LookupError:
+ _kategories[cls.bl_category] = [cls,]
+_actual_kategories = []
+for i in sorted(_kategories.keys(), key=lambda x: _kategory_names[x]):
+ # Note that even though we're sorting the category names, Blender appears to not care...
+ _kat_items = [NodeItem(j.bl_idname) for j in sorted(_kategories[i], key=lambda x: x.bl_label)]
+ _actual_kategories.append(PlasmaNodeCategory(i, _kategory_names[i], items=_kat_items))
+
+def register():
+ nodeitems_utils.register_node_categories("PLASMA_NODES", _actual_kategories)
+
+def unregister():
+ nodeitems_utils.unregister_node_categories("PLASMA_NODES")
diff --git a/korman/nodes/node_conditions.py b/korman/nodes/node_conditions.py
new file mode 100644
index 0000000..1ac9574
--- /dev/null
+++ b/korman/nodes/node_conditions.py
@@ -0,0 +1,98 @@
+# 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 .node_core import PlasmaNodeBase, PlasmaNodeSocketBase
+from ..properties.modifiers.physics import bounds_types
+
+class PlasmaConditionSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
+ bl_color = (0.188, 0.086, 0.349, 1.0)
+
+
+class PlasmaVolumeReportNode(PlasmaNodeBase, bpy.types.Node):
+ bl_category = "CONDITIONS"
+ bl_idname = "PlasmaVoumeReportNode"
+ bl_label = "Region Trigger Settings"
+
+ report_when = EnumProperty(name="When",
+ description="When the region should trigger",
+ items=[("each", "Each Event", "The region will trigger on every enter/exit"),
+ ("count", "Population", "When the region has a certain number of objects inside it")])
+ threshold = IntProperty(name="Threshold",
+ description="How many objects should be in the region for it to trigger",
+ min=1)
+
+ def init(self, context):
+ self.outputs.new("PlasmaVolumeSettingsSocketOut", "Trigger Settings")
+
+ def draw_buttons(self, context, layout):
+ layout.prop(self, "report_when")
+ if self.report_when == "count":
+ row = layout.row()
+ row.label("Threshold: ")
+ row.prop(self, "threshold", text="")
+
+
+class PlasmaVolumeSensorNode(PlasmaNodeBase, bpy.types.Node):
+ bl_category = "CONDITIONS"
+ bl_idname = "PlasmaVolumeSensorNode"
+ bl_label = "Region Sensor"
+ bl_width_default = 190
+
+ # Region Mesh
+ region = StringProperty(name="Region",
+ description="Object that defines the region mesh")
+ bounds = EnumProperty(name="Bounds",
+ description="Physical object's bounds",
+ items=bounds_types)
+
+ # Detector Properties
+ report_on = EnumProperty(name="Triggerers",
+ description="What triggers this region?",
+ options={"ANIMATABLE", "ENUM_FLAG"},
+ items=[("avatar", "Avatars", "Avatars trigger this region"),
+ ("dynamics", "Dynamics", "Any non-avatar dynamic physical object (eg kickables)")],
+ default={"avatar"})
+
+ def init(self, context):
+ self.inputs.new("PlasmaVolumeSettingsSocketIn", "Trigger on Enter", "enter")
+ self.inputs.new("PlasmaVolumeSettingsSocketIn", "Trigger on Exit", "exit")
+ self.outputs.new("PlasmaConditionSocket", "Satisfies", "satisfies")
+
+ def draw_buttons(self, context, layout):
+ layout.prop(self, "report_on")
+
+ # Okay, if they changed the name of the ObData, that's THEIR problem...
+ layout.prop_search(self, "region", bpy.data, "meshes", icon="MESH_DATA")
+ layout.prop(self, "bounds")
+
+
+class PlasmaVolumeSettingsSocket(PlasmaNodeSocketBase):
+ bl_color = (43.1, 24.7, 0.0, 1.0)
+
+
+class PlasmaVolumeSettingsSocketIn(PlasmaVolumeSettingsSocket, bpy.types.NodeSocket):
+ allow = BoolProperty()
+
+ def draw(self, context, layout, node, text):
+ if not self.is_linked:
+ layout.prop(self, "allow", text="")
+ layout.label(text)
+
+
+class PlasmaVolumeSettingsSocketOut(PlasmaVolumeSettingsSocket, bpy.types.NodeSocket):
+ pass
diff --git a/korman/nodes/node_core.py b/korman/nodes/node_core.py
new file mode 100644
index 0000000..f00e389
--- /dev/null
+++ b/korman/nodes/node_core.py
@@ -0,0 +1,87 @@
+# 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
+
+class PlasmaNodeBase:
+ def find_input(self, key, idname=None):
+ for i in self.inputs:
+ if i.identifier == key:
+ if i.links:
+ node = i.links[0].from_node
+ if idname is not None and idname != node.bl_idname:
+ return None
+ return node
+ else:
+ return None
+ raise KeyError(key)
+
+ def find_input_socket(self, key):
+ for i in self.inputs:
+ if i.identifier == key:
+ return i
+ raise KeyError(key)
+
+ def find_outputs(self, key, idname=None):
+ for i in self.outputs:
+ if i.identifier == key:
+ for j in i.links:
+ node = j.to_node
+ if idname is not None and idname != node.bl_idname:
+ continue
+ yield node
+
+ def find_output_socket(self, key):
+ for i in self.outputs:
+ if i.identifier == key:
+ return i
+ raise KeyError(key)
+
+ def link_input(self, tree, node, out_key, in_key):
+ """Links a given Node's output socket to a given input socket on this Node"""
+ in_socket = self.find_input_socket(in_key)
+ out_socket = node.find_output_socket(out_key)
+ link = tree.links.new(in_socket, out_socket)
+
+ def link_output(self, tree, node, out_key, in_key):
+ """Links a given Node's input socket to a given output socket on this Node"""
+ in_socket = node.find_input_socket(in_key)
+ out_socket = self.find_output_socket(out_key)
+ link = tree.links.new(in_socket, out_socket)
+
+ @classmethod
+ def poll(cls, context):
+ return (context.bl_idname == "PlasmaNodeTree")
+
+
+class PlasmaNodeSocketBase:
+ def draw(self, context, layout, node, text):
+ layout.label(text)
+
+ def draw_color(self, context, node):
+ # It's so tempting to just do RGB sometimes... Let's be nice.
+ if len(self.bl_color) == 3:
+ return tuple(self.bl_color[0], self.bl_color[1], self.bl_color[2], 1.0)
+ return self.bl_color
+
+
+class PlasmaNodeTree(bpy.types.NodeTree):
+ bl_idname = "PlasmaNodeTree"
+ bl_label = "Plasma"
+ bl_icon = "NODETREE"
+
+ @classmethod
+ def poll(cls, context):
+ return (context.scene.render.engine == "PLASMA_GAME")
diff --git a/korman/nodes/node_logic.py b/korman/nodes/node_logic.py
new file mode 100644
index 0000000..48971cc
--- /dev/null
+++ b/korman/nodes/node_logic.py
@@ -0,0 +1,31 @@
+# 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 .node_core import *
+
+class PlasmaLogicTriggerNode(PlasmaNodeBase, bpy.types.Node):
+ bl_category = "LOGIC"
+ bl_idname = "PlasmaLogicTriggerNode"
+ bl_label = "Logic Trigger"
+
+ def init(self, context):
+ self.inputs.new("PlasmaConditionSocket", "Condition", "condition")
+ self.outputs.new("PlasmaRespTriggerSocket", "Trigger", "trigger")
+
+
+class PlasmaRespTriggerSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
+ bl_color = (0.384, 0.239, 0.239, 1.0)
diff --git a/korman/nodes/node_messages.py b/korman/nodes/node_messages.py
new file mode 100644
index 0000000..bec5981
--- /dev/null
+++ b/korman/nodes/node_messages.py
@@ -0,0 +1,40 @@
+# 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 .node_core import *
+from ..properties.modifiers.region import footstep_surfaces, footstep_surface_ids
+
+class PlasmaMessageSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
+ bl_color = (0.004, 0.282, 0.349, 1.0)
+
+
+class PlasmaFootstepSoundMsgNode(PlasmaNodeBase, 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 init(self, context):
+ self.inputs.new("PlasmaMessageSocket", "Sender", "sender")
+
+ def draw_buttons(self, context, layout):
+ layout.prop(self, "surface")
diff --git a/korman/nodes/node_responder.py b/korman/nodes/node_responder.py
new file mode 100644
index 0000000..47e6811
--- /dev/null
+++ b/korman/nodes/node_responder.py
@@ -0,0 +1,108 @@
+# 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 *
+import uuid
+
+from .node_core import *
+
+class PlasmaResponderNode(PlasmaNodeBase, bpy.types.Node):
+ bl_category = "LOGIC"
+ bl_idname = "PlasmaResponderNode"
+ bl_label = "Responder"
+
+ def init(self, context):
+ self.inputs.new("PlasmaRespTriggerSocket", "Trigger", "whodoneit")
+ self.outputs.new("PlasmaRespStateSocket", "States", "states")
+
+
+class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node):
+ bl_category = "LOGIC"
+ bl_idname = "PlasmaResponderStateNode"
+ bl_label = "Responder State"
+
+ def init(self, context):
+ self.inputs.new("PlasmaRespStateSocket", "Condition", "whodoneit")
+ self.outputs.new("PlasmaRespCommandSocket", "Commands", "cmds")
+ self.outputs.new("PlasmaRespStateSocket", "Trigger", "gotostate").link_limit = 1
+
+
+class PlasmaRespStateSocketBase(PlasmaNodeSocketBase):
+ bl_color = (0.388, 0.78, 0.388, 1.0)
+
+
+class PlasmaRespStateSocket(PlasmaRespStateSocketBase, bpy.types.NodeSocket):
+ default_state = BoolProperty(name="Default State",
+ description="This state is the Responder's default",
+ default=False)
+
+ def draw(self, context, layout, node, text):
+ # If this is a RespoderState node and the parent is a Responder, offer the user the
+ # ability to make this the default state.
+ if self.is_linked and not self.is_output:
+ # Before we do anything, see if we need to do a delayed update...
+ if node.bl_idname == "PlasmaResponderStateNode":
+ parent = node.find_input("whodoneit", "PlasmaResponderNode")
+ if parent is not None:
+ layout.prop(self, "default_state")
+ return
+
+ # Still here? Draw the text.
+ layout.label(text)
+
+
+class PlasmaResponderStateListNode(PlasmaNodeBase, bpy.types.Node):
+ bl_category = "LOGIC"
+ bl_idname = "PlasmaResponderStateListNode"
+ bl_label = "Responder State List"
+
+ def add_state_input(self):
+ self.inputs.new("PlasmaRespStateListSocket", str(uuid.uuid4()))
+
+ def init(self, context):
+ # Inputs will be added by the user
+ self.outputs.new("PlasmaRespStateSocket", "Go To State", "gotostate")
+
+ def draw_buttons(self, context, layout):
+ # This will allow us to add input states on the fly.
+ # Caveat: We're only showing this operator in the properties because we need the node
+ # to be active in the operator...
+ op = layout.operator("node.plasma_add_responder_state", text="Add State", icon="ZOOMIN")
+ op.node_name = self.name
+
+
+class PlasmaRespStateListSocket(PlasmaRespStateSocketBase, bpy.types.NodeSocket):
+ def draw(self, context, layout, node, text):
+ # We'll allow them to delete all their inputs if they want to be stupid...
+ props = layout.operator("node.plasma_remove_responder_state", text="", icon="X")
+ props.node_name = node.name
+ props.socket_name = self.name
+
+
+class PlasmaResponderCommandNode(PlasmaNodeBase, bpy.types.Node):
+ bl_category = "LOGIC"
+ bl_idname = "PlasmaResponderCommandNode"
+ bl_label = "Responder Command"
+
+ def init(self, context):
+ self.inputs.new("PlasmaRespCommandSocket", "Condition", "whodoneit")
+ self.outputs.new("PlasmaMessageSocket", "Message", "msg")
+ self.outputs.new("PlasmaRespCommandSocket", "Trigger", "trigger")
+
+
+class PlasmaRespCommandSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
+ bl_color = (0.451, 0.0, 0.263, 1.0)
+
diff --git a/korman/operators/__init__.py b/korman/operators/__init__.py
index f0c38fe..bc307e9 100644
--- a/korman/operators/__init__.py
+++ b/korman/operators/__init__.py
@@ -16,6 +16,7 @@
from . import op_export as exporter
from . import op_lightmap as lightmap
from . import op_modifier as modifier
+from . import op_nodes as node
from . import op_world as world
def register():
diff --git a/korman/operators/op_nodes.py b/korman/operators/op_nodes.py
new file mode 100644
index 0000000..6840467
--- /dev/null
+++ b/korman/operators/op_nodes.py
@@ -0,0 +1,55 @@
+# 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 *
+
+class NodeOperator:
+ def get_node_tree(self, context):
+ space = context.space_data
+ if space.type != "NODE_EDITOR":
+ raise RuntimeError("Operator '{}' should only be used in the node editor".format(self.bl_idname))
+ return space.node_tree
+
+ @classmethod
+ def poll(cls, context):
+ return context.scene.render.engine == "PLASMA_GAME"
+
+
+class ResponderStateAddOperator(NodeOperator, bpy.types.Operator):
+ bl_idname = "node.plasma_add_responder_state"
+ bl_label = "Add Responder State Socket"
+
+ node_name = StringProperty(name="Node's name", options={"HIDDEN"})
+
+ def execute(self, context):
+ tree = self.get_node_tree(context)
+ tree.nodes[self.node_name].add_state_input()
+ return {"FINISHED"}
+
+
+class ResponderStateRemoveOperator(NodeOperator, bpy.types.Operator):
+ bl_idname = "node.plasma_remove_responder_state"
+ bl_label = "Remove Responder State Socket"
+
+ node_name = StringProperty(name="Node's name", options={"HIDDEN"})
+ socket_name = StringProperty(name="Socket name to remove", options={"HIDDEN"})
+
+ def execute(self, context):
+ tree = self.get_node_tree(context)
+ node = tree.nodes[self.node_name]
+ socket = node.inputs[self.socket_name]
+ node.inputs.remove(socket)
+ return {"FINISHED"}
diff --git a/korman/properties/modifiers/region.py b/korman/properties/modifiers/region.py
index 899a6c5..708b55e 100644
--- a/korman/properties/modifiers/region.py
+++ b/korman/properties/modifiers/region.py
@@ -19,6 +19,41 @@ from PyHSPlasma import *
from .base import PlasmaModifierProperties
+footstep_surface_ids = {
+ "dirt": 0,
+ # 1 = NULL
+ "puddle": 2,
+ # 3 = tile (NULL in MOUL)
+ "metal": 4,
+ "woodbridge": 5,
+ "rope": 6,
+ "grass": 7,
+ # 8 = NULL
+ "woodfloor": 9,
+ "rug": 10,
+ "stone": 11,
+ # 12 = NULL
+ # 13 = metal ladder (dupe of metal)
+ "woodladder": 14,
+ "water": 15,
+ # 16 = maintainer's glass (NULL in PotS)
+ # 17 = maintainer's metal grating (NULL in PotS)
+ # 18 = swimming (why would you want this?)
+}
+
+footstep_surfaces = [("dirt", "Dirt", "Dirt"),
+ ("grass", "Grass", "Grass"),
+ ("metal", "Metal", "Metal Catwalk"),
+ ("puddle", "Puddle", "Shallow Water"),
+ ("rope", "Rope", "Rope Ladder"),
+ ("rug", "Rug", "Carpet Rug"),
+ ("stone", "Stone", "Stone Tile"),
+ ("water", "Water", "Deep Water"),
+ ("woodbridge", "Wood Bridge", "Wood Bridge"),
+ ("woodfloor", "Wood Floor", "Wood Floor"),
+ ("woodladder", "Wood Ladder", "Wood Ladder")]
+
+
class PlasmaPanicLinkRegion(PlasmaModifierProperties):
pl_id = "paniclink"