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"