You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

374 lines
16 KiB

# 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 bmesh
import bpy
from bpy.props import *
from PyHSPlasma import *
from ...exporter import ExportError, ExportAssertionError
from ...helpers import bmesh_from_object
from ... import idprops
from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz
from ..prop_camera import PlasmaCameraProperties
from .physics import bounds_types
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 PlasmaCameraRegion(PlasmaModifierProperties):
pl_id = "camera_rgn"
bl_category = "Region"
bl_label = "Camera Region"
bl_description = "Camera Region"
bl_icon = "CAMERA_DATA"
camera_type = EnumProperty(name="Camera Type",
description="What kind of camera should be used?",
items=[("auto_follow", "Auto Follow Camera", "Automatically generated follow camera"),
("manual", "Manual Camera", "User specified camera object")],
default="manual",
options=set())
camera_object = PointerProperty(name="Camera",
description="Switches to this camera",
type=bpy.types.Object,
poll=idprops.poll_camera_objects,
options=set())
auto_camera = PointerProperty(type=PlasmaCameraProperties, options=set())
def export(self, exporter, bo, so):
if self.camera_type == "manual":
if self.camera_object is None:
raise ExportError("Camera Modifier '{}' does not specify a valid camera object".format(self.id_data.name))
camera_so_key = exporter.mgr.find_create_key(plSceneObject, bl=self.camera_object)
camera_props = self.camera_object.data.plasma_camera.settings
else:
assert self.camera_type[:4] == "auto"
# Wheedoggy! We get to export the doggone camera now.
camera_props = self.auto_camera
camera_type = self.camera_type[5:]
exporter.camera.export_camera(so, bo, camera_type, camera_props)
camera_so_key = so.key
# Setup physical stuff
phys_mod = bo.plasma_modifiers.collision
exporter.physics.generate_physical(bo, so, member_group="kGroupDetector",
report_groups=["kGroupAvatar"],
properties=["kPinned"])
# I don't feel evil enough to make this generate a logic tree...
msg = plCameraMsg()
msg.BCastFlags |= plMessage.kLocalPropagate | plMessage.kBCastByType
msg.setCmd(plCameraMsg.kRegionPushCamera)
msg.setCmd(plCameraMsg.kSetAsPrimary, camera_props.primary_camera)
msg.newCam = camera_so_key
region = exporter.mgr.find_create_object(plCameraRegionDetector, so=so)
region.addMessage(msg)
def harvest_actors(self):
actors = set()
if self.camera_type == "manual":
if self.camera_object is None:
raise ExportError("Camera Modifier '{}' does not specify a valid camera object".format(self.id_data.name))
actors.update(self.camera_object.data.plasma_camera.settings.harvest_actors())
else:
actors.update(self.auto_camera.harvest_actors())
return actors
@property
def requires_actor(self):
return self.camera_type == "auto_follow"
class PlasmaFootstepRegion(PlasmaModifierProperties, PlasmaModifierLogicWiz):
pl_id = "footstep"
bl_category = "Region"
bl_label = "Footstep"
bl_description = "Footstep Region"
surface = EnumProperty(name="Surface",
description="What kind of surface are we walking on?",
items=footstep_surfaces,
default="stone")
bounds = EnumProperty(name="Region Bounds",
description="Physical object's bounds",
items=bounds_types,
default="hull")
def logicwiz(self, bo, tree):
nodes = tree.nodes
# Region Sensor
volsens = nodes.new("PlasmaVolumeSensorNode")
volsens.name = "RegionSensor"
volsens.region_object = bo
volsens.bounds = self.bounds
volsens.find_input_socket("enter").allow = True
volsens.find_input_socket("exit").allow = True
# Responder
respmod = nodes.new("PlasmaResponderNode")
respmod.name = "Resp"
respmod.link_input(volsens, "satisfies", "condition")
respstate = nodes.new("PlasmaResponderStateNode")
respstate.link_input(respmod, "state_refs", "resp")
# ArmatureEffectStateMsg
msg = nodes.new("PlasmaFootstepSoundMsgNode")
msg.link_input(respstate, "msgs", "sender")
msg.surface = self.surface
@property
def key_name(self):
return "{}_FootRgn".format(self.id_data.name)
class PlasmaPanicLinkRegion(PlasmaModifierProperties):
pl_id = "paniclink"
bl_category = "Region"
bl_label = "Panic Link"
bl_description = "Panic Link Region"
play_anim = BoolProperty(name="Play Animation",
description="Play the link-out animation when panic linking",
default=True)
def export(self, exporter, bo, so):
exporter.physics.generate_physical(bo, so, member_group="kGroupDetector",
report_groups=["kGroupAvatar"])
# Finally, the panic link region proper
reg = exporter.mgr.add_object(plPanicLinkRegion, name=self.key_name, so=so)
reg.playLinkOutAnim = self.play_anim
@property
def key_name(self):
return "{}_PanicLinkRgn".format(self.id_data.name)
@property
def requires_actor(self):
return True
class PlasmaSoftVolume(idprops.IDPropMixin, PlasmaModifierProperties):
pl_id = "softvolume"
bl_category = "Region"
bl_label = "Soft Volume"
bl_description = "Soft-Boundary Region"
# Advanced
use_nodes = BoolProperty(name="Use Nodes",
description="Make this a node-based Soft Volume",
default=False)
node_tree = PointerProperty(name="Node Tree",
description="Node Tree detailing soft volume logic",
type=bpy.types.NodeTree)
# Basic
invert = BoolProperty(name="Invert",
description="Invert the soft region")
inside_strength = IntProperty(name="Inside", description="Strength inside the region",
subtype="PERCENTAGE", default=100, min=0, max=100)
outside_strength = IntProperty(name="Outside", description="Strength outside the region",
subtype="PERCENTAGE", default=0, min=0, max=100)
soft_distance = FloatProperty(name="Distance", description="Soft Distance",
default=0.0, min=0.0, max=500.0)
def _apply_settings(self, sv):
sv.insideStrength = self.inside_strength / 100.0
sv.outsideStrength = self.outside_strength / 100.0
def get_key(self, exporter, so=None):
"""Fetches the key appropriate for this Soft Volume"""
if so is None:
so = exporter.mgr.find_create_object(plSceneObject, bl=self.id_data)
if self.use_nodes:
tree = self.get_node_tree()
output = tree.find_output("PlasmaSoftVolumeOutputNode")
if output is None:
raise ExportError("SoftVolume '{}' Node Tree '{}' has no output node!".format(self.key_name, tree.name))
return output.get_key(exporter, so)
else:
pClass = plSoftVolumeInvert if self.invert else plSoftVolumeSimple
return exporter.mgr.find_create_key(pClass, bl=self.id_data, so=so)
def export(self, exporter, bo, so):
if self.use_nodes:
self._export_sv_nodes(exporter, bo, so)
else:
self._export_convex_region(exporter, bo, so)
def _export_convex_region(self, exporter, bo, so):
if bo.type != "MESH":
raise ExportError("SoftVolume '{}': Simple SoftVolumes can only be meshes!".format(bo.name))
# Grab the SoftVolume KO
sv = self.get_key(exporter, so).object
self._apply_settings(sv)
# If "invert" was checked, we got a SoftVolumeInvert, but we need to make a Simple for the
# region data to be exported into..
if isinstance(sv, plSoftVolumeInvert):
svSimple = exporter.mgr.find_create_object(plSoftVolumeSimple, bl=bo, so=so)
self._apply_settings(svSimple)
sv.addSubVolume(svSimple.key)
sv = svSimple
sv.softDist = self.soft_distance
# Initialize the plVolumeIsect. Currently, we only support convex isects. If you want parallel
# isects from empties, be my guest...
with bmesh_from_object(bo) as mesh:
matrix = bo.matrix_world
xform = matrix.inverted()
xform.transpose()
# Ensure the normals always point inward. This is the same thing that
# bpy.ops.normals_make_consistent(inside=True) does, just no need to change
# into the edit mode context (EXPENSIVE!)
bmesh.ops.recalc_face_normals(mesh, faces=mesh.faces)
bmesh.ops.reverse_faces(mesh, faces=mesh.faces, flip_multires=True)
isect = plConvexIsect()
for ngon in mesh.faces:
normal = xform * ngon.normal * -1
normal.normalize()
normal = hsVector3(*normal)
for vertex in ngon.verts:
pos = matrix * vertex.co
isect.addPlane(normal, hsVector3(*pos))
sv.volume = isect
def _export_sv_nodes(self, exporter, bo, so):
tree = self.get_node_tree()
# Stash for later
exporter.want_node_trees.setdefault(tree.name, set()).add((bo, so))
def get_node_tree(self):
if self.node_tree is None:
raise ExportError("SoftVolume '{}' does not specify a valid Node Tree!".format(self.key_name))
return self.node_tree
@classmethod
def _idprop_mapping(cls):
return {"node_tree": "node_tree_name"}
def _idprop_sources(self):
return {"node_tree_name": bpy.data.node_groups}
class PlasmaSubworldRegion(PlasmaModifierProperties):
pl_id = "subworld_rgn"
bl_category = "Region"
bl_label = "Subworld Region"
bl_description = "Subworld transition region"
subworld = PointerProperty(name="Subworld",
description="Subworld to transition into",
type=bpy.types.Object,
poll=idprops.poll_subworld_objects)
transition = EnumProperty(name="Transition",
description="When to transition to the new subworld",
items=[("enter", "On Enter", "Transition when the avatar enters the region"),
("exit", "On Exit", "Transition when the avatar exits the region")],
default="enter",
options=set())
def export(self, exporter, bo, so):
# Due to the fact that our subworld modifier can produce both RidingAnimatedPhysical
# and [HK|PX]Subworlds depending on the situation, this could get hairy, fast.
# Start by surveying the lay of the land.
from_sub, to_sub = bo.plasma_object.subworld, self.subworld
from_isded = exporter.physics.is_dedicated_subworld(from_sub)
to_isded = exporter.physics.is_dedicated_subworld(to_sub)
if 1:
def get_log_text(bo, isded):
main = "[Main World]" if bo is None else bo.name
sub = "Subworld" if isded or bo is None else "RidingAnimatedPhysical"
return main, sub
from_name, from_type = get_log_text(from_sub, from_isded)
to_name, to_type = get_log_text(to_sub, to_isded)
exporter.report.msg("Transition from '{}' ({}) to '{}' ({})",
from_name, from_type, to_name, to_type)
# I think the best solution here is to not worry about the excitement mentioned above.
# If we encounter anything truly interesting, we can fix it in CWE more easily IMO because
# the game actually knows more about the avatar's state than we do here in the exporter.
if to_isded or (from_isded and to_sub is None):
region = exporter.mgr.find_create_object(plSubworldRegionDetector, so=so)
if to_sub is not None:
region.subworld = exporter.mgr.find_create_key(plSceneObject, bl=to_sub)
region.onExit = self.transition == "exit"
else:
msg = plRideAnimatedPhysMsg()
msg.BCastFlags |= plMessage.kLocalPropagate | plMessage.kPropagateToModifiers
msg.sender = so.key
msg.entering = to_sub is not None
# In Cyan's PlasmaMAX RAP detector, it acts as more of a traditional region
# that changes us over to a dynamic character controller on region enter and
# reverts on region exit. We're going for an approach that is backwards compatible
# with subworlds, so our enter/exit regions are separate. Here, enter/exit message
# corresponds with when we should trigger the transition.
region = exporter.mgr.find_create_object(plRidingAnimatedPhysicalDetector, so=so)
if self.transition == "enter":
region.enterMsg = msg
elif self.transition == "exit":
region.exitMsg = msg
else:
raise ExportAssertionError()
# Fancy pants region collider type shit
exporter.physics.generate_physical(bo, so, member_group="kGroupDetector",
report_groups=["kGroupAvatar"])