diff --git a/korman/exporter/utils.py b/korman/exporter/utils.py index 4235d81..a5258a0 100644 --- a/korman/exporter/utils.py +++ b/korman/exporter/utils.py @@ -20,6 +20,7 @@ import bpy import mathutils from contextlib import contextmanager +import enum from typing import * from PyHSPlasma import * @@ -130,21 +131,54 @@ def create_camera_object(name: str) -> bpy.types.Object: bpy.context.scene.objects.link(cam_obj) return cam_obj -def create_cube_region(name: str, size: float, owner_object: bpy.types.Object) -> bpy.types.Object: +class RegionOrigin(enum.Enum): + center = enum.auto() + bottom = enum.auto() + + +def create_cube_region(name: str, size: float, owner_object: bpy.types.Object, origin: RegionOrigin = RegionOrigin.center) -> bpy.types.Object: """Create a cube shaped region object""" + # Proxy over to the generic rectangular prism implementation to + # ensure that it is well tested. + return create_box_region(name, (size, size, size), owner_object, origin) + +def create_box_region( + name: str, size: Union[Tuple[float, float, float], mathutils.Vector], + owner_object: bpy.types.Object, + origin: RegionOrigin = RegionOrigin.center +) -> bpy.types.Object: + """Create a rectangular prism region object""" + if not isinstance(size, mathutils.Vector): + size = mathutils.Vector(size) + region_object = BMeshObject(name) region_object.plasma_object.enabled = True region_object.plasma_object.page = owner_object.plasma_object.page region_object.hide_render = True with region_object as bm: - bmesh.ops.create_cube(bm, size=(size)) - bmesh.ops.transform( - bm, - matrix=mathutils.Matrix.Translation( - owner_object.matrix_world.translation - region_object.matrix_world.translation - ), - space=region_object.matrix_world, verts=bm.verts - ) + center = owner_object.matrix_world.translation + if origin == RegionOrigin.bottom: + center = center.copy() + center.z += size.z * 0.5 + vert_src = [ + (center.x + size.x * 0.5, center.y + size.y * 0.5, center.z + size.z * 0.5), + (center.x + size.x * 0.5, center.y + size.y * 0.5, center.z - size.z * 0.5), + (center.x + size.x * 0.5, center.y - size.y * 0.5, center.z + size.z * 0.5), + (center.x + size.x * 0.5, center.y - size.y * 0.5, center.z - size.z * 0.5), + (center.x - size.x * 0.5, center.y + size.y * 0.5, center.z + size.z * 0.5), + (center.x - size.x * 0.5, center.y + size.y * 0.5, center.z - size.z * 0.5), + (center.x - size.x * 0.5, center.y - size.y * 0.5, center.z + size.z * 0.5), + (center.x - size.x * 0.5, center.y - size.y * 0.5, center.z - size.z * 0.5), + ] + verts = [bm.verts.new(i) for i in vert_src] + + new_face = bm.faces.new + new_face((verts[0], verts[1], verts[3], verts[2])) # X+ + new_face((verts[4], verts[5], verts[7], verts[6])) # X- + new_face((verts[0], verts[1], verts[5], verts[4])) # Y+ + new_face((verts[2], verts[3], verts[7], verts[6])) # Y- + new_face((verts[0], verts[2], verts[6], verts[4])) # Z+ + new_face((verts[1], verts[3], verts[7], verts[5])) # Z- return region_object.release() @contextmanager diff --git a/korman/properties/modifiers/base.py b/korman/properties/modifiers/base.py index ed2b76e..2a984a2 100644 --- a/korman/properties/modifiers/base.py +++ b/korman/properties/modifiers/base.py @@ -13,12 +13,17 @@ # You should have received a copy of the GNU General Public License # along with Korman. If not, see . +from __future__ import annotations + import bpy from bpy.props import * import abc import itertools -from typing import Any, Dict, FrozenSet, Optional +from typing import * + +if TYPE_CHECKING: + from ...nodes.node_python import * from ... import helpers @@ -202,24 +207,23 @@ class PlasmaModifierLogicWiz: pfm_node.update() return pfm_node - def _create_python_attribute(self, pfm_node, attribute_name: str, attribute_type: Optional[str] = None, **kwargs): + def _create_python_attribute(self, pfm_node: PlasmaPythonFileNode, attribute_name: str, **kwargs): """Creates and links a Python Attribute Node to the Python File Node given by `pfm_node`. - This will automatically handle simple attribute types such as numbers and strings, however, - for object linkage, you should specify the optional `attribute_type` to ensure the proper - attribute type is found. For attribute nodes that require multiple values, the `value` may - be set to None and handled in your code.""" - from ...nodes.node_python import PlasmaAttribute, PlasmaAttribNodeBase - if attribute_type is None: - assert len(kwargs) == 1 and "value" in kwargs, \ - "In order to deduce the attribute_type, exactly one attribute value must be passed as a kw named `value`" - attribute_type = PlasmaAttribute.type_LUT.get(kwargs["value"].__class__) + For attribute nodes that require multiple values, the `value` may be set to None and + handled in your code.""" + attribute_defn = next((i for i in pfm_node.attributes if i.attribute_name == attribute_name), None) + if attribute_defn is None: + raise KeyError(attribute_name) + + from ...nodes.node_python import PlasmaAttribNodeBase + attribute_type = attribute_defn.attribute_type node_cls = next((i for i in PlasmaAttribNodeBase.__subclasses__() if attribute_type in i.pl_attrib), None) - assert node_cls is not None, "'{}': Unable to find attribute node type for '{}' ('{}')".format( - self.id_data.name, attribute_name, attribute_type - ) + assert node_cls is not None, \ + f"'{self.id_data.name}': Unable to find attribute node type for '{attribute_name}' ('{attribute_type}')" node = pfm_node.id_data.nodes.new(node_cls.bl_idname) node.link_output(pfm_node, "pfm", attribute_name) + assert kwargs for i, j in kwargs.items(): setattr(node, i, j) return node diff --git a/korman/properties/modifiers/logic.py b/korman/properties/modifiers/logic.py index 675b4fe..4cb4fd9 100644 --- a/korman/properties/modifiers/logic.py +++ b/korman/properties/modifiers/logic.py @@ -13,16 +13,35 @@ # You should have received a copy of the GNU General Public License # along with Korman. If not, see . +from __future__ import annotations + import bmesh import bpy from bpy.props import * import mathutils from PyHSPlasma import * +from typing import * + +if TYPE_CHECKING: + from ...exporter import Exporter + from ...nodes.node_conditions import * + from ...nodes.node_messages import * + from ...nodes.node_responder import * from ...addon_prefs import game_versions from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz from ...exporter import ExportError, utils from ... import idprops +from .physics import bounds_type_index, bounds_type_str, bounds_types + +entry_cam_pfm = { + "filename": "xEntryCam.py", + "attribs": ( + { 'id': 1, 'type': "ptAttribActivator", 'name': "actRegionSensor" }, + { 'id': 2, 'type': "ptAttribSceneobject", 'name': "camera" }, + { 'id': 3, 'type': "ptAttribBoolean", 'name': "undoFirstPerson" }, + ) +} class PlasmaVersionedNodeTree(idprops.IDPropMixin, bpy.types.PropertyGroup): version = EnumProperty(name="Version", @@ -76,7 +95,7 @@ class PlasmaAdvancedLogic(PlasmaModifierProperties): return any((i.node_tree.requires_actor for i in self.logic_groups if i.node_tree)) -class PlasmaSpawnPoint(PlasmaModifierProperties): +class PlasmaSpawnPoint(PlasmaModifierProperties, PlasmaModifierLogicWiz): pl_id = "spawnpoint" bl_category = "Logic" @@ -84,10 +103,73 @@ class PlasmaSpawnPoint(PlasmaModifierProperties): bl_description = "Point at which avatars link into the Age" bl_object_types = {"EMPTY"} + def _get_bounds(self) -> int: + if self.exit_region is not None: + return bounds_type_index(self.exit_region.plasma_modifiers.collision.bounds_type) + return bounds_type_index("hull") + + def _set_bounds(self, value: int) -> None: + if self.exit_region is not None: + self.exit_region.plasma_modifiers.collision.bounds_type = bounds_type_str(value) + + entry_camera = PointerProperty( + name="Entry Camera", + description="Camera to use when the player spawns at this location", + type=bpy.types.Object, + poll=idprops.poll_camera_objects + ) + + exit_region = PointerProperty( + name="Exit Region", + description="Pop the camera when the player exits this region", + type=bpy.types.Object, + poll=idprops.poll_mesh_objects + ) + + bounds_type = EnumProperty( + name="Bounds", + description="", + items=bounds_types, + get=_get_bounds, + set=_set_bounds, + default="hull" + ) + + def pre_export(self, exporter: Exporter, bo: bpy.types.Object) -> None: + if self.entry_camera is None: + return + + if self.exit_region is None: + self.exit_region = yield utils.create_box_region( + f"{self.key_name}_ExitRgn", (2.0, 2.0, 6.0), + bo, utils.RegionOrigin.bottom + ) + + yield self.convert_logic(bo) + + def logicwiz(self, bo, tree): + pfm_node = self._create_python_file_node( + tree, + entry_cam_pfm["filename"], + entry_cam_pfm["attribs"] + ) + + volume_sensor: PlasmaVolumeSensorNode = tree.nodes.new("PlasmaVolumeSensorNode") + volume_sensor.find_input_socket("enter").allow = True + volume_sensor.find_input_socket("exit").allow = True + volume_sensor.region_object = self.exit_region + volume_sensor.bounds = self.bounds_type + volume_sensor.link_output(pfm_node, "satisfies", "actRegionSensor") + + self._create_python_attribute( + pfm_node, + "camera", + target_object=self.entry_camera + ) + + def export(self, exporter, bo, so): - # Not much to this modifier... It's basically a flag that tells the engine, "hey, this is a - # place the avatar can show up." Nice to have a simple one to get started with. - spawn = exporter.mgr.add_object(pl=plSpawnModifier, so=so, name=self.key_name) + exporter.mgr.add_object(pl=plSpawnModifier, so=so, name=self.key_name) @property def requires_actor(self): diff --git a/korman/ui/modifiers/logic.py b/korman/ui/modifiers/logic.py index c2da1fd..a99b169 100644 --- a/korman/ui/modifiers/logic.py +++ b/korman/ui/modifiers/logic.py @@ -13,8 +13,15 @@ # You should have received a copy of the GNU General Public License # along with Korman. If not, see . +from __future__ import annotations + import bpy +from typing import * + +if TYPE_CHECKING: + from ...properties.modifiers.logic import * + from .. import ui_list class LogicListUI(bpy.types.UIList): @@ -36,8 +43,18 @@ def advanced_logic(modifier, layout, context): layout.row().prop_menu_enum(logic, "version") layout.prop(logic, "node_tree", icon="NODETREE") -def spawnpoint(modifier, layout, context): +def spawnpoint(modifier: PlasmaSpawnPoint, layout, context): layout.label(text="Avatar faces negative Y.") + layout.separator() + + col = layout.column() + col.prop(modifier, "entry_camera", icon="CAMERA_DATA") + sub = col.row() + sub.active = modifier.entry_camera is not None + sub.prop(modifier, "exit_region", icon="MESH_DATA") + sub = col.row() + sub.active = modifier.entry_camera is not None and modifier.exit_region is not None + sub.prop(modifier, "bounds_type") def maintainersmarker(modifier, layout, context): layout.label(text="Positive Y is North, positive Z is up.")