Browse Source

Merge pull request #373 from Hoikas/game_gui

Experiemental: Game GUIs
pull/214/merge
Adam Johnson 7 months ago committed by GitHub
parent
commit
2e43e0c1f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      korman/exporter/convert.py
  2. 225
      korman/exporter/gui.py
  3. 4
      korman/exporter/manager.py
  4. 31
      korman/exporter/utils.py
  5. 13
      korman/helpers.py
  6. 11
      korman/nodes/node_python.py
  7. 1
      korman/operators/__init__.py
  8. 119
      korman/operators/op_camera.py
  9. 45
      korman/operators/op_modifier.py
  10. 22
      korman/properties/modifiers/__init__.py
  11. 4
      korman/properties/modifiers/anim.py
  12. 15
      korman/properties/modifiers/base.py
  13. 508
      korman/properties/modifiers/game_gui.py
  14. 192
      korman/properties/modifiers/gui.py
  15. 1
      korman/properties/modifiers/logic.py
  16. 1
      korman/properties/modifiers/physics.py
  17. 6
      korman/properties/modifiers/region.py
  18. 15
      korman/properties/modifiers/render.py
  19. 1
      korman/properties/modifiers/sound.py
  20. 4
      korman/properties/modifiers/water.py
  21. 1
      korman/properties/prop_world.py
  22. 1
      korman/ui/modifiers/__init__.py
  23. 120
      korman/ui/modifiers/game_gui.py
  24. 13
      korman/ui/modifiers/gui.py

10
korman/exporter/convert.py

@ -32,6 +32,7 @@ from .camera import CameraConverter
from .decal import DecalConverter
from . import explosions
from .etlight import LightBaker
from .gui import GuiConverter
from .image import ImageCache
from .locman import LocalizationConverter
from . import logger
@ -45,6 +46,7 @@ from . import utils
class Exporter:
if TYPE_CHECKING:
_objects: List[bpy.types.Object] = ...
actors: Set[str] = ...
want_node_trees: defaultdict[Set[str]] = ...
report: logger._ExportLogger = ...
@ -60,6 +62,7 @@ class Exporter:
locman: LocalizationConverter = ...
decal: DecalConverter = ...
oven: LightBaker = ...
gui: GuiConverter
def __init__(self, op):
self._op = op # Blender export operator
@ -83,6 +86,7 @@ class Exporter:
self.locman = LocalizationConverter(self)
self.decal = DecalConverter(self)
self.oven = LightBaker(mesh=self.mesh, report=self.report)
self.gui = GuiConverter(self)
# Step 0.8: Init the progress mgr
self.mesh.add_progress_presteps(self.report)
@ -371,6 +375,12 @@ class Exporter:
tree.export(self, bo, so)
inc_progress()
def get_objects(self, page: Optional[str]) -> Iterator[bpy.types.Object]:
yield from filter(
lambda x: x.plasma_object.page == page,
self._objects
)
def _harvest_actors(self):
self.report.progress_advance()
self.report.progress_range = len(self._objects) + len(bpy.data.textures)

225
korman/exporter/gui.py

@ -0,0 +1,225 @@
# 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/>.
from __future__ import annotations
import bpy
import mathutils
from contextlib import contextmanager, ExitStack
import itertools
import math
from PyHSPlasma import *
from typing import *
import weakref
from .explosions import ExportError
from .. import helpers
from . import utils
if TYPE_CHECKING:
from .convert import Exporter
from .logger import _ExportLogger as ExportLogger
class Clipping(NamedTuple):
hither: float
yonder: float
class PostEffectModMatrices(NamedTuple):
c2w: hsMatrix44
w2c: hsMatrix44
class GuiConverter:
if TYPE_CHECKING:
_parent: weakref.ref[Exporter] = ...
def __init__(self, parent: Optional[Exporter] = None):
self._parent = weakref.ref(parent) if parent is not None else None
# Go ahead and prepare the GUI transparent material for future use.
if parent is not None:
self._transp_material = parent.exit_stack.enter_context(
helpers.TemporaryObject(
bpy.data.materials.new("GUITransparent"),
bpy.data.materials.remove
)
)
self._transp_material.diffuse_color = mathutils.Vector((1.0, 1.0, 0.0))
self._transp_material.use_mist = False
# Cyan's transparent GUI materials just set an opacity of 0%
tex_slot = self._transp_material.texture_slots.add()
tex_slot.texture = parent.exit_stack.enter_context(
helpers.TemporaryObject(
bpy.data.textures.new("AutoTransparentLayer", "NONE"),
bpy.data.textures.remove
)
)
tex_slot.texture.plasma_layer.opacity = 0.0
else:
self._transp_material = None
def calc_camera_matrix(
self,
scene: bpy.types.Scene,
objects: Sequence[bpy.types.Object],
fov: float,
scale: float = 0.75
) -> mathutils.Matrix:
if not objects:
raise ExportError("No objects specified for GUI Camera generation.")
# Generally, GUIs are flat planes. However, we are not Cyan, so artists cannot walk down
# the hallway to get smacked on the knuckles by programmers. This means that they might
# give us some three dimensional crap as a GUI. Therefore, to come up with a camera matrix,
# we'll use the average area-weighted inverse normal of all the polygons they give us. That
# way, the camera *always* should face the GUI as would be expected.
remove_mesh = bpy.data.meshes.remove
avg_normal = mathutils.Vector()
for i in objects:
mesh = i.to_mesh(bpy.context.scene, True, "RENDER", calc_tessface=False)
with helpers.TemporaryObject(mesh, remove_mesh):
utils.transform_mesh(mesh, i.matrix_world)
for polygon in mesh.polygons:
avg_normal += (polygon.normal * polygon.area)
avg_normal.normalize()
avg_normal *= -1.0
# From the inverse area weighted normal we found above, get the rotation from the up axis
# (that is to say, the +Z axis) and create our rotation matrix.
axis = mathutils.Vector((avg_normal.x, avg_normal.y, 0.0))
axis.normalize()
angle = math.acos(avg_normal.z)
mat = mathutils.Matrix.Rotation(angle, 3, axis)
# Now, we know the rotation of the camera. Great! What we need to do now is ensure that all
# of the objects in question fit within the view of a 4:3 camera rotated as above. Blender
# helpfully provides us with the localspace bounding boxes of all the objects and an API to
# fit points into the camera view.
with ExitStack() as stack:
stack.enter_context(self.generate_camera_render_settings(scene))
# Create a TEMPORARY camera object so we can use a certain Blender API.
camera = stack.enter_context(utils.temporary_camera_object(scene, "GUICameraTemplate"))
camera.matrix_world = mat.to_4x4()
camera.data.angle = fov
camera.data.lens_unit = "FOV"
# Get all of the bounding points and make sure they all fit inside the camera's view frame.
bound_boxes = [
obj.matrix_world * mathutils.Vector(bbox)
for obj in objects for bbox in obj.bound_box
]
co, _ = camera.camera_fit_coords(
scene,
# bound_box is a list of vectors of each corner of all the objects' bounding boxes;
# however, Blender's API wants a sequence of individual channel positions. Therefore,
# we need to flatten the vectors.
list(itertools.chain.from_iterable(bound_boxes))
)
# This generates a list of 6 faces per bounding box, which we then flatten out and pass
# into the BVHTree constructor. This is to calculate the distance from the camera to the
# "entire GUI" - which we can then use to apply the scale given to us.
if scale != 1.0:
bvh = mathutils.bvhtree.BVHTree.FromPolygons(
bound_boxes,
list(itertools.chain.from_iterable(
[(i + 0, i + 1, i + 5, i + 4),
(i + 1, i + 2, i + 5, i + 6),
(i + 3, i + 2, i + 6, i + 7),
(i + 0, i + 1, i + 2, i + 3),
(i + 0, i + 3, i + 7, i + 4),
(i + 4, i + 5, i + 6, i + 7),
] for i in range(0, len(bound_boxes), 8)
))
)
loc, normal, index, distance = bvh.find_nearest(co)
co += normal * distance * (scale - 1.0)
# ...
mat.resize_4x4()
mat.translation = co
return mat
def calc_clipping(
self,
pose: mathutils.Matrix,
scene: bpy.types.Scene,
objects: Sequence[bpy.types.Object],
fov: float
) -> Clipping:
with ExitStack() as stack:
stack.enter_context(self.generate_camera_render_settings(scene))
camera = stack.enter_context(utils.temporary_camera_object(scene, "GUICameraTemplate"))
camera.matrix_world = pose
camera.data.angle = fov
camera.data.lens_unit = "FOV"
# Determine the camera plane's normal so we can do a distance check against the
# bounding boxes of the objects shown in the GUI.
view_frame = [i * pose for i in camera.data.view_frame(scene)]
cam_plane = mathutils.geometry.normal(view_frame)
bound_boxes = (
obj.matrix_world * mathutils.Vector(bbox)
for obj in objects for bbox in obj.bound_box
)
pos = pose.to_translation()
bounds_dists = [
abs(mathutils.geometry.distance_point_to_plane(i, pos, cam_plane))
for i in bound_boxes
]
# Offset them by some epsilon to ensure the objects are rendered.
hither, yonder = min(bounds_dists), max(bounds_dists)
if yonder - 0.5 < hither:
hither -= 0.25
yonder += 0.25
return Clipping(hither, yonder)
def convert_post_effect_matrices(self, camera_matrix: mathutils.Matrix) -> PostEffectModMatrices:
# PostEffectMod matrices face *away* from the GUI... For some reason.
# See plPostEffectMod::SetWorldToCamera()
c2w = utils.matrix44(camera_matrix)
w2c = utils.matrix44(camera_matrix.inverted())
for i in range(4):
c2w[i, 2] *= -1.0
w2c[2, i] *= -1.0
return PostEffectModMatrices(c2w, w2c)
@contextmanager
def generate_camera_render_settings(self, scene: bpy.types.Scene) -> Iterator[None]:
# Set the render info to basically TV NTSC 4:3, which will set Blender's camera
# viewport up as a 4:3 thingy to match Plasma.
with helpers.GoodNeighbor() as toggle:
toggle.track(scene.render, "resolution_x", 720)
toggle.track(scene.render, "resolution_y", 486)
toggle.track(scene.render, "pixel_aspect_x", 10.0)
toggle.track(scene.render, "pixel_aspect_y", 11.0)
yield
@property
def _report(self) -> ExportLogger:
return self._parent().report
@property
def transparent_material(self) -> bpy.types.Material:
assert self._transp_material is not None
return self._transp_material

4
korman/exporter/manager.py

@ -265,8 +265,8 @@ class ExportManager:
return self.add_object(pl=pClass, name=name, bl=bl, so=so)
return key.object
def find_object(self, pClass: Type[KeyedT], bl=None, name=None, so=None) -> Optional[KeyedT]:
key = self.find_key(pClass, bl, name, so)
def find_object(self, pClass: Type[KeyedT], bl=None, name=None, so=None, loc=None) -> Optional[KeyedT]:
key = self.find_key(pClass, bl, name, so, loc)
if key is not None:
return key.object
return None

31
korman/exporter/utils.py

@ -24,6 +24,8 @@ from typing import *
from PyHSPlasma import *
from ..import helpers
def affine_parts(xform):
# Decompose the matrix into the 90s-era 3ds max affine parts sillyness
# All that's missing now is something like "(c) 1998 HeadSpin" oh wait...
@ -106,6 +108,20 @@ class BMeshObject:
return self._obj
def create_empty_object(name: str, owner_object: Optional[bpy.types.Object] = None) -> bpy.types.Object:
empty_object = bpy.data.objects.new(name, None)
if owner_object is not None:
empty_object.plasma_object.enabled = owner_object.plasma_object.enabled
empty_object.plasma_object.page = owner_object.plasma_object.page
bpy.context.scene.objects.link(empty_object)
return empty_object
def create_camera_object(name: str) -> bpy.types.Object:
cam_data = bpy.data.cameras.new(name)
cam_obj = bpy.data.objects.new(name, cam_data)
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:
"""Create a cube shaped region object"""
region_object = BMeshObject(name)
@ -136,6 +152,21 @@ def pre_export_optional_cube_region(source, attr: str, name: str, size: float, o
# contextlib.contextmanager requires for us to yield. Sad.
yield
@contextmanager
def temporary_camera_object(scene: bpy.types.Scene, name: str) -> bpy.types.Object:
try:
cam_data = bpy.data.cameras.new(name)
cam_obj = bpy.data.objects.new(name, cam_data)
scene.objects.link(cam_obj)
yield cam_obj
finally:
cam_obj = locals().get("cam_obj")
if cam_obj is not None:
bpy.data.objects.remove(cam_obj)
cam_data = locals().get("cam_data")
if cam_data is not None:
bpy.data.cameras.remove(cam_data)
@contextmanager
def temporary_mesh_object(source : bpy.types.Object) -> bpy.types.Object:
"""Creates a temporary mesh object from a nonmesh object that will only exist for the duration

13
korman/helpers.py

@ -137,3 +137,16 @@ def find_modifier(bo, modid):
# if they give us the wrong modid, it is a bug and an AttributeError
return getattr(bo.plasma_modifiers, modid)
return None
def get_page_type(page: str) -> str:
all_pages = bpy.context.scene.world.plasma_age.pages
if page:
page_type = next((i.page_type for i in all_pages if i.name == page), None)
if page_type is None:
raise LookupError(page)
return page_type
else:
# A falsey page name is likely a request for the default page, so look for Page ID 0.
# If it doesn't exist, that's an implicit default page (a "room" type).
page_type = next((i.page_type for i in all_pages if i.seq_suffix == 0), None)
return page_type if page_type is not None else "room"

11
korman/nodes/node_python.py

@ -734,7 +734,8 @@ class PlasmaAttribObjectNode(idprops.IDPropObjectMixin, PlasmaAttribNodeBase, bp
bl_label = "Object Attribute"
pl_attrib = ("ptAttribSceneobject", "ptAttribSceneobjectList", "ptAttribAnimation",
"ptAttribSwimCurrent", "ptAttribWaveSet", "ptAttribGrassShader")
"ptAttribSwimCurrent", "ptAttribWaveSet", "ptAttribGrassShader",
"ptAttribGUIDialog")
target_object = PointerProperty(name="Object",
description="Object containing the required data",
@ -781,7 +782,13 @@ class PlasmaAttribObjectNode(idprops.IDPropObjectMixin, PlasmaAttribNodeBase, bp
return None
return [exporter.mgr.find_create_key(plGrassShaderMod, so=ref_so, name=i.name)
for i in exporter.mesh.material.get_materials(bo)]
elif attrib == "ptAttribGUIDialog":
gui_dialog = bo.plasma_modifiers.gui_dialog
if not gui_dialog.enabled:
self.raise_error(f"GUI Dialog modifier not enabled on '{self.object_name}'")
dialog_mod = exporter.mgr.find_create_object(pfGUIDialogMod, so=ref_so, bl=bo)
dialog_mod.procReceiver = attrib.node.get_key(exporter, so)
return dialog_mod.key
@classmethod
def _idprop_mapping(cls):

1
korman/operators/__init__.py

@ -13,6 +13,7 @@
# You should have received a copy of the GNU General Public License
# along with Korman. If not, see <http://www.gnu.org/licenses/>.
from . import op_camera as camera
from . import op_export as exporter
from . import op_image as image
from . import op_lightmap as lightmap

119
korman/operators/op_camera.py

@ -0,0 +1,119 @@
# 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/>.
from __future__ import annotations
import bpy
from bpy.props import *
import math
from ..exporter.explosions import ExportError
from ..exporter.gui import GuiConverter
class CameraOperator:
@classmethod
def poll(cls, context):
return context.scene.render.engine == "PLASMA_GAME"
class PlasmaGameGuiCameraOperator(CameraOperator, bpy.types.Operator):
bl_idname = "camera.plasma_create_game_gui_camera"
bl_label = "Create Game GUI Camera"
bl_description = "Create a camera looking at all of the objects in this GUI page with a custom scale factor"
fov: float = FloatProperty(
name="Field of View",
description="Camera Field of View angle",
subtype="ANGLE",
default=math.radians(90.0),
min=0.0,
max=math.radians(360.0),
precision=1,
options=set()
)
gui_page: str = StringProperty(
name="GUI Page",
description="",
options={"HIDDEN"}
)
scale: float = FloatProperty(
name="GUI Scale",
description="GUI Camera distance scale factor.",
default=75.0,
min=0.1,
soft_max=100.0,
precision=1,
subtype="PERCENTAGE",
options=set()
)
mod_id: str = StringProperty(options={"HIDDEN"})
cam_prop_name: str = StringProperty(options={"HIDDEN"})
def invoke(self, context, event):
context.window_manager.invoke_props_dialog(self)
return {"RUNNING_MODAL"}
def execute(self, context):
# If the modifier has been given to us, select all of the objects in the
# given GUI page.
if self.gui_page:
for i in context.scene.objects:
i.select = i.plasma_object.page == self.gui_page
context.scene.update()
visible_objects = [
i for i in context.selected_objects
if i.type in {"MESH", "FONT"}
]
gui = GuiConverter()
try:
cam_matrix = gui.calc_camera_matrix(
context.scene,
visible_objects,
self.fov,
self.scale / 100.0
)
except ExportError as e:
self.report({"ERROR"}, str(e))
return {"CANCELLED"}
if self.mod_id and self.cam_prop_name:
modifier = getattr(context.object.plasma_modifiers, self.mod_id)
cam_obj = getattr(modifier, self.cam_prop_name)
else:
cam_obj = None
if cam_obj is None:
if self.gui_page:
name = f"{self.gui_page}_GUICamera"
else:
name = f"{context.object.name}_GUICamera"
cam_data = bpy.data.cameras.new(name)
cam_obj = bpy.data.objects.new(name, cam_data)
context.scene.objects.link(cam_obj)
cam_obj.matrix_world = cam_matrix
cam_obj.data.angle = self.fov
cam_obj.data.lens_unit = "FOV"
for i in context.scene.objects:
i.select = i == cam_obj
if self.mod_id and self.cam_prop_name:
modifier = getattr(context.object.plasma_modifiers, self.mod_id)
setattr(modifier, self.cam_prop_name, cam_obj)
return {"FINISHED"}

45
korman/operators/op_modifier.py

@ -23,17 +23,7 @@ import time
from typing import *
from ..properties import modifiers
def _fetch_modifiers():
items = []
mapping = modifiers.modifier_mapping()
for i in sorted(mapping.keys()):
items.append(("", i, ""))
items.extend(mapping[i])
#yield ("", i, "")
#yield mapping[i]
return items
from ..helpers import find_modifier
class ModifierOperator:
def _get_modifier(self, context) -> modifiers.PlasmaModifierProperties:
@ -55,10 +45,41 @@ class ModifierAddOperator(ModifierOperator, bpy.types.Operator):
bl_label = "Add Modifier"
bl_description = "Adds a Plasma Modifier"
def _fetch_modifiers(self, context):
items = []
def filter_mod_name(mod):
# The modifier might include the cateogry name in its name, so we'll strip that.
if mod.bl_label != mod.bl_category:
if mod.bl_label.startswith(mod.bl_category):
return mod.bl_label[len(mod.bl_category)+1:]
if mod.bl_label.endswith(mod.bl_category):
return mod.bl_label[:-len(mod.bl_category)-1]
return mod.bl_label
sorted_modifiers = sorted(
modifiers.PlasmaModifierProperties.__subclasses__(),
key=lambda x: f"{x.bl_category} - {filter_mod_name(x)}"
)
last_category = None
for i, mod in enumerate(sorted_modifiers):
# Some modifiers aren't permissible in certain situations. Hide them.
if not find_modifier(context.object, mod.pl_id).allowed:
continue
if mod.bl_category != last_category:
items.append(("", mod.bl_category, ""))
last_category = mod.bl_category
items.append(
(mod.pl_id, filter_mod_name(mod), mod.bl_description,
getattr(mod, "bl_icon", ""), i)
)
return items
types = EnumProperty(
name="Modifier Type",
description="The type of modifier we add to the list",
items=_fetch_modifiers()
items=_fetch_modifiers
)
def execute(self, context):

22
korman/properties/modifiers/__init__.py

@ -18,6 +18,7 @@ import bpy
from .base import PlasmaModifierProperties
from .anim import *
from .avatar import *
from .game_gui import *
from .gui import *
from .logic import *
from .physics import *
@ -74,24 +75,3 @@ class PlasmaModifiers(bpy.types.PropertyGroup):
class PlasmaModifierSpec(bpy.types.PropertyGroup):
pass
def modifier_mapping():
"""This returns a dict mapping Plasma Modifier categories to names"""
d = {}
sorted_modifiers = sorted(PlasmaModifierProperties.__subclasses__(), key=lambda x: x.bl_label)
for i, mod in enumerate(sorted_modifiers):
pl_id, category, label, description = mod.pl_id, mod.bl_category, mod.bl_label, mod.bl_description
icon = getattr(mod, "bl_icon", "")
# The modifier might include the cateogry name in its name, so we'll strip that.
if label != category:
if label.startswith(category):
label = label[len(category)+1:]
if label.endswith(category):
label = label[:-len(category)-1]
tup = (pl_id, label, description, icon, i)
d_cat = d.setdefault(category, [])
d_cat.append(tup)
return d

4
korman/properties/modifiers/anim.py

@ -53,6 +53,7 @@ class ActionModifier:
class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties):
pl_id = "animation"
pl_page_types = {"gui", "room"}
bl_category = "Animation"
bl_label = "Animation"
@ -170,6 +171,7 @@ class AnimGroupObject(idprops.IDPropObjectMixin, bpy.types.PropertyGroup):
class PlasmaAnimationFilterModifier(PlasmaModifierProperties):
pl_id = "animation_filter"
pl_page_types = {"gui", "room"}
bl_category = "Animation"
bl_label = "Filter Transform"
@ -214,6 +216,7 @@ class PlasmaAnimationFilterModifier(PlasmaModifierProperties):
class PlasmaAnimationGroupModifier(ActionModifier, PlasmaModifierProperties):
pl_id = "animation_group"
pl_depends = {"animation"}
pl_page_types = {"gui", "room"}
bl_category = "Animation"
bl_label = "Group Master"
@ -272,6 +275,7 @@ class LoopMarker(bpy.types.PropertyGroup):
class PlasmaAnimationLoopModifier(ActionModifier, PlasmaModifierProperties):
pl_id = "animation_loop"
pl_depends = {"animation"}
pl_page_types = {"gui", "room"}
bl_category = "Animation"
bl_label = "Loop Markers"

15
korman/properties/modifiers/base.py

@ -20,7 +20,20 @@ import abc
import itertools
from typing import Any, Dict, FrozenSet, Optional
from ... import helpers
class PlasmaModifierProperties(bpy.types.PropertyGroup):
@property
def allowed(self) -> bool:
"""Returns if this modifier is allowed to be enabled on the owning Object"""
allowed_page_types = getattr(self, "pl_page_types", {"room"})
allowed_object_types = getattr(self, "bl_object_types", set())
page_name = self.id_data.plasma_object.page
if not allowed_object_types or self.id_data.type in allowed_object_types:
if helpers.get_page_type(page_name) in allowed_page_types:
return True
return False
@property
def copy_material(self):
"""Materials MUST be single-user"""
@ -53,7 +66,7 @@ class PlasmaModifierProperties(bpy.types.PropertyGroup):
@property
def enabled(self) -> bool:
return self.display_order >= 0
return self.display_order >= 0 and self.allowed
@enabled.setter
def enabled(self, value: bool) -> None:

508
korman/properties/modifiers/game_gui.py

@ -0,0 +1,508 @@
# 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/>.
from __future__ import annotations
import bpy
from bpy.props import *
import itertools
import math
from typing import *
from PyHSPlasma import *
from ...exporter import ExportError
from .base import PlasmaModifierProperties
from ... import idprops
if TYPE_CHECKING:
from ...exporter import Exporter
from ..prop_world import PlasmaAge, PlasmaPage
class _GameGuiMixin:
@property
def gui_sounds(self) -> Iterable[Tuple[str, int]]:
"""Overload to automatically export GUI sounds on the control. This should return an iterable
of tuple attribute name and sound index.
"""
return []
def get_control(self, exporter: Exporter, bo: Optional[bpy.types.Object] = None, so: Optional[plSceneObject] = None) -> Optional[pfGUIControlMod]:
return None
@property
def has_gui_proc(self) -> bool:
return True
def iterate_control_modifiers(self) -> Iterator[_GameGuiMixin]:
pl_mods = self.id_data.plasma_modifiers
yield from (
getattr(pl_mods, i.pl_id)
for i in self.iterate_control_subclasses()
if getattr(pl_mods, i.pl_id).enabled
)
@classmethod
def iterate_control_subclasses(cls) -> Iterator[_GameGuiMixin]:
yield from filter(
lambda x: x.is_game_gui_control(),
_GameGuiMixin.__subclasses__()
)
@classmethod
def is_game_gui_control(cls) -> bool:
return True
@property
def requires_dyntext(self) -> bool:
return False
def sanity_check(self):
age: PlasmaAge = bpy.context.scene.world.plasma_age
# Game GUI modifiers must be attached to objects in a GUI page, ONLY
page_name: str = self.id_data.plasma_object.page
our_page: Optional[PlasmaPage] = next(
(i for i in age.pages if i.name == page_name)
)
if our_page is None or our_page.page_type != "gui":
raise ExportError(f"'{self.id_data.name}': {self.bl_label} Modifier must be in a GUI page!")
# Only one Game GUI Control per object. Continuously check this because objects can be
# generated/mutated during the pre-export phase.
modifiers = self.id_data.plasma_modifiers
controls = [i for i in self.iterate_control_subclasses() if getattr(modifiers, i.pl_id).enabled]
num_controls = len(controls)
if num_controls > 1:
raise ExportError(f"'{self.id_data.name}': Only 1 GUI Control modifier is allowed per object. We found {num_controls}.")
# Blow up on invalid sounds
soundemit = self.id_data.plasma_modifiers.soundemit
for attr_name, _ in self.gui_sounds:
sound_name = getattr(self, attr_name)
if not sound_name:
continue
sound = next((i for i in soundemit.sounds if i.name == sound_name), None)
if sound is None:
raise ExportError(f"'{self.id_data.name}': Invalid '{attr_name}' GUI Sound '{sound_name}'")
class PlasmaGameGuiControlModifier(PlasmaModifierProperties, _GameGuiMixin):
pl_id = "gui_control"
pl_page_types = {"gui"}
bl_category = "GUI"
bl_label = "GUI Control (ex)"
bl_description = "XXX"
bl_object_types = {"FONT", "MESH"}
tag_id = IntProperty(
name="Tag ID",
description="",
min=0,
options=set()
)
visible = BoolProperty(
name="Visible",
description="",
default=True,
options=set()
)
proc = EnumProperty(
name="Notification Procedure",
description="",
items=[
("default", "[Default]", "Send notifications to the owner's notification procedure."),
("close_dialog", "Close Dialog", "Close the current Game GUI Dialog."),
("console_command", "Run Console Command", "Run a Plasma Console command.")
],
options=set()
)
console_command = StringProperty(
name="Command",
description="",
options=set()
)
def convert_gui_control(self, exporter: Exporter, ctrl: pfGUIControlMod, bo: bpy.types.Object, so: plSceneObject):
ctrl.tagID = self.tag_id
ctrl.visible = self.visible
if self.proc == "default":
ctrl.setFlag(pfGUIControlMod.kInheritProcFromDlg, True)
elif self.proc == "close_dialog":
ctrl.handler = pfGUICloseDlgProc()
elif self.proc == "console_command":
handler = pfGUIConsoleCmdProc()
handler.command = self.console_command
ctrl.handler = handler
def convert_gui_sounds(self, exporter: Exporter, ctrl: pfGUIControlMod, ctrl_mod: _GameGuiMixin):
soundemit = ctrl_mod.id_data.plasma_modifiers.soundemit
if not ctrl_mod.gui_sounds or not soundemit.enabled:
return
# This is a lot like the plPhysicalSndGroup where we have a vector behaving as a lookup table.
# NOTE that zero is a special value here meaning no sound, so we need to offset the sounds
# that we get from the emitter modifier by +1.
sound_indices = {}
for attr_name, gui_sound_idx in ctrl_mod.gui_sounds:
sound_name = getattr(ctrl_mod, attr_name)
if not sound_name:
continue
sound_keys = soundemit.get_sound_keys(exporter, sound_name)
sound_key, soundemit_index = next(sound_keys, (None, -1))
if sound_key is not None:
sound_indices[gui_sound_idx] = soundemit_index + 1
# Compress the list to include only the highest entry we need.
if sound_indices:
ctrl.soundIndices = [sound_indices.get(i, 0) for i in range(max(sound_indices) + 1)]
def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject):
ctrl_mods = list(self.iterate_control_modifiers())
if not ctrl_mods:
exporter.report.msg(str(list(self.iterate_control_subclasses())))
exporter.report.warn("This modifier has no effect because no GUI control modifiers are present!")
for ctrl_mod in ctrl_mods:
ctrl_obj = ctrl_mod.get_control(exporter, bo, so)
self.convert_gui_control(exporter, ctrl_obj, bo, so)
self.convert_gui_sounds(exporter, ctrl_obj, ctrl_mod)
@property
def has_gui_proc(self) -> bool:
return any((i.has_gui_proc for i in self.iterate_control_modifiers()))
@classmethod
def is_game_gui_control(cls) -> bool:
# How is a control not a control, you ask? Because, grasshopper, this modifier does not
# actually export a GUI control itself. Instead, it holds common properties that may
# or may not be used by other controls. This just helps fill out the other modifiers.
return False
class GameGuiAnimation(bpy.types.PropertyGroup):
def _poll_target_object(self, value):
# Only allow targetting things that are in our GUI page.
if value.plasma_object.page != self.id_data.plasma_object.page:
return False
if self.anim_type == "OBJECT":
return idprops.poll_animated_objects(self, value)
else:
return idprops.poll_drawable_objects(self, value)
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
else:
target_object = self.target_object if self.target_object is not None else self.id_data
for i in (slot.material for slot in 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
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.
target_object = self.target_object if self.target_object is not None else self.id_data
object_materials = (slot.material for slot in target_object.material_slots if slot and slot.material)
return value in object_materials
anim_type: str = EnumProperty(
name="Type",
description="Animation type to affect",
items=[
("OBJECT", "Object", "Object Animation"),
("TEXTURE", "Texture", "Texture Animation"),
],
default="OBJECT",
options=set()
)
target_object: bpy.types.Object = PointerProperty(
name="Object",
description="Target object",
poll=_poll_target_object,
type=bpy.types.Object
)
target_material: bpy.types.Material = PointerProperty(
name="Material",
description="Target material",
type=bpy.types.Material,
poll=_poll_material
)
target_texture: bpy.types.Texture = PointerProperty(
name="Texture",
description="Target texture",
type=bpy.types.Texture,
poll=_poll_texture
)
class GameGuiAnimationGroup(bpy.types.PropertyGroup):
def _update_animation_name(self, context) -> None:
if not self.animation_name:
self.animation_name = "(Entire Animation)"
animations = CollectionProperty(
name="Animations",
description="",
type=GameGuiAnimation,
options=set()
)
animation_name: str = StringProperty(
name="Animation Name",
description="Name of the animation to play",
default="(Entire Animation)",
update=_update_animation_name,
options=set()
)
active_anim_index: int = IntProperty(options={"HIDDEN"})
show_expanded: bool = BoolProperty(options={"HIDDEN"})
def export(
self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject,
ctrl_obj: pfGUIControlMod, add_func: Callable[[plKey], None],
anim_name_attr: str
):
keys = set()
for anim in self.animations:
target_object = anim.target_object if anim.target_object is not None else bo
if anim.anim_type == "OBJECT":
keys.add(exporter.animation.get_animation_key(target_object))
elif anim.anim_type == "TEXTURE":
# Layer animations don't respect the name field, so we need to grab exactly the
# layer animation key that is requested. Cyan's Max plugin does not allow specifying
# layer animations here as best I can tell, but I don't see why we shouldn't.
keys.update(
exporter.mesh.material.get_texture_animation_key(
target_object,
anim.target_material,
anim.target_texture,
self.animation_name
)
)
else:
raise RuntimeError()
# This is to make sure that we only waste space in the PRP file with the animation
# name if we actually have some doggone animations.
if keys:
setattr(ctrl_obj, anim_name_attr, self.animation_name)
for i in keys:
add_func(i)
class PlasmaGameGuiButtonModifier(PlasmaModifierProperties, _GameGuiMixin):
pl_id = "gui_button"
pl_depends = {"gui_control"}
pl_page_types = {"gui"}
bl_category = "GUI"
bl_label = "GUI Button (ex)"
bl_description = "XXX"
bl_object_types = {"FONT", "MESH"}
def _update_notify_type(self, context):
# It doesn't make sense to have no notify type at all selected, so
# default to at least one option.
if not self.notify_type:
self.notify_type = {"DOWN"}
notify_type = EnumProperty(
name="Notify On",
description="When the button should perform its action",
items=[
("UP", "Up", "When the mouse button is down over the GUI button."),
("DOWN", "Down", "When the mouse button is released over the GUI button."),
],
default={"UP"},
options={"ENUM_FLAG"},
update=_update_notify_type
)
mouse_over_anims: GameGuiAnimationGroup = PointerProperty(type=GameGuiAnimationGroup)
mouse_click_anims: GameGuiAnimationGroup = PointerProperty(type=GameGuiAnimationGroup)
show_expanded_sounds: bool = BoolProperty(options={"HIDDEN"})
mouse_down_sound: str = StringProperty(
name="Mouse Down SFX",
description="Sound played when the mouse button is down",
options=set()
)
mouse_up_sound: str = StringProperty(
name="Mouse Up SFX",
description="Sound played when the mouse button is released",
options=set()
)
mouse_over_sound: str = StringProperty(
name="Mouse Over SFX",
description="Sound played when the mouse moves over the GUI button",
options=set()
)
mouse_off_sound: str = StringProperty(
name="Mouse Off SFX",
description="Sound played when the mouse moves off of the GUI button",
options=set()
)
@property
def gui_sounds(self):
return (
("mouse_down_sound", pfGUIButtonMod.kMouseDown),
("mouse_up_sound", pfGUIButtonMod.kMouseUp),
("mouse_over_sound", pfGUIButtonMod.kMouseOver),
("mouse_off_sound", pfGUIButtonMod.kMouseOff),
)
def get_control(self, exporter: Exporter, bo: Optional[bpy.types.Object] = None, so: Optional[plSceneObject] = None) -> pfGUIButtonMod:
return exporter.mgr.find_create_object(pfGUIButtonMod, bl=bo, so=so)
def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject):
ctrl = self.get_control(exporter, bo, so)
ctrl.setFlag(pfGUIControlMod.kWantsInterest, True)
if self.notify_type == {"UP"}:
ctrl.notifyType = pfGUIButtonMod.kNotifyOnUp
elif self.notify_type == {"DOWN"}:
ctrl.notifyType = pfGUIButtonMod.kNotifyOnDown
elif self.notify_type == {"UP", "DOWN"}:
ctrl.notifyType = pfGUIButtonMod.kNotifyOnUpAndDown
else:
raise ValueError(self.notify_type)
self.mouse_over_anims.export(exporter, bo, so, ctrl, ctrl.addMouseOverKey, "mouseOverAnimName")
self.mouse_click_anims.export(exporter, bo, so, ctrl, ctrl.addAnimationKey, "animName")
class PlasmaGameGuiDialogModifier(PlasmaModifierProperties, _GameGuiMixin):
pl_id = "gui_dialog"
pl_page_types = {"gui"}
bl_category = "GUI"
bl_label = "GUI Dialog (ex)"
bl_description = "XXX"
bl_object_types = {"FONT", "MESH"}
camera_object: bpy.types.Object = PointerProperty(
name="GUI Camera",
description="Camera used to project the GUI to screenspace.",
type=bpy.types.Object,
poll=idprops.poll_camera_objects,
options=set()
)
is_modal = BoolProperty(
name="Modal",
description="",
default=True,
options=set()
)
def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject):
# Find all of the visible objects in the GUI page for use in hither/yon raycast and
# camera matrix calculations.
visible_objects = [
i for i in exporter.get_objects(bo.plasma_object.page)
if i.type == "MESH" and i.data.materials
]
camera_object = self.id_data if self.id_data.type == "CAMERA" else self.camera_object
if camera_object:
exporter.report.msg(f"Using camera matrix from camera '{camera_object.name}'")
if camera_object != self.id_data and camera_object.plasma_object.enabled:
with exporter.report.indent():
exporter.report.warn("The camera object should NOT be a Plasma Object!")
camera_matrix = camera_object.matrix_world
# Save the clipping info from the camera for later use.
cam_data = camera_object.data
fov, hither, yonder = cam_data.angle, cam_data.clip_start, cam_data.clip_end
else:
exporter.report.msg(f"Building a camera matrix to view: {', '.join((i.name for i in visible_objects))}")
fov = math.radians(45.0)
camera_matrix = exporter.gui.calc_camera_matrix(
bpy.context.scene,
visible_objects,
fov
)
# There isn't a real camera, so just pretend like the user didn't set the clipping info.
hither, yonder = 0.0, 0.0
with exporter.report.indent():
exporter.report.msg(str(camera_matrix))
# If no hither or yonder was specified on the camera, then we need to determine that ourselves.
if not hither or not yonder:
exporter.report.msg(f"Incomplete clipping: H:{hither:.02f} Y:{yonder:.02f}; calculating new...")
with exporter.report.indent():
clipping = exporter.gui.calc_clipping(
camera_matrix,
bpy.context.scene,
visible_objects,
fov
)
exporter.report.msg(f"Calculated: H:{clipping.hither:.02f} Y:{clipping.yonder:.02f}")
if not hither:
hither = clipping.hither
if not yonder:
yonder = clipping.yonder
exporter.report.msg(f"Corrected clipping: H:{hither:.02f} Y:{yonder:.02f}")
# Both of the objects we export go into the pool.
scene_node_key = exporter.mgr.get_scene_node(bl=bo)
post_effect = exporter.mgr.find_create_object(plPostEffectMod, bl=bo)
post_effect.defaultC2W, post_effect.defaultW2C = exporter.gui.convert_post_effect_matrices(camera_matrix)
post_effect.fovX = math.degrees(fov)
post_effect.fovY = math.degrees(fov * (3.0 / 4.0))
post_effect.hither = min((hither, yonder))
post_effect.yon = max((hither, yonder))
post_effect.nodeKey = scene_node_key
dialog_mod = exporter.mgr.find_create_object(pfGUIDialogMod, bl=bo)
dialog_mod.name = bo.plasma_object.page
dialog_mod.setFlag(pfGUIDialogMod.kModal, self.is_modal)
dialog_mod.renderMod = post_effect.key
dialog_mod.sceneNode = scene_node_key
@property
def has_gui_proc(self) -> bool:
return False
@classmethod
def is_game_gui_control(cls) -> bool:
return False
def post_export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject):
# All objects have been exported. Now, we can establish linkage to all controls that
# have been exported.
dialog = exporter.mgr.find_object(pfGUIDialogMod, bl=bo, so=so)
control_modifiers: Iterable[_GameGuiMixin] = itertools.chain.from_iterable(
obj.plasma_modifiers.gui_control.iterate_control_modifiers()
for obj in exporter.get_objects(bo.plasma_object.page)
if obj.plasma_modifiers.gui_control.enabled
)
for control_modifier in control_modifiers:
control = control_modifier.get_control(exporter, control_modifier.id_data)
ctrl_key = control.key
exporter.report.msg(f"GUIDialog '{bo.name}': [{control.ClassName()}] '{ctrl_key.name}'")
dialog.addControl(ctrl_key)

192
korman/properties/modifiers/gui.py

@ -13,20 +13,28 @@
# You should have received a copy of the GNU General Public License
# along with Korman. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bpy
import bmesh
from bpy.props import *
import mathutils
from contextlib import ExitStack
import itertools
import math
from pathlib import Path
from typing import *
from PyHSPlasma import *
from ...addon_prefs import game_versions
from ...exporter import ExportError, utils
from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz, PlasmaModifierUpgradable
from ... import idprops
from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz
from ... import helpers, idprops
if TYPE_CHECKING:
from ...exporter import Exporter
from .game_gui import PlasmaGameGuiDialogModifier
journal_pfms = {
pvPots : {
@ -665,3 +673,179 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
raise ExportError("{}: Linking Book modifier requires a clickable!", self.id_data.name)
if self.seek_point is None:
raise ExportError("{}: Linking Book modifier requires a seek point!", self.id_data.name)
dialog_toggle = {
"filename": "xDialogToggle.py",
"attribs": (
{ 'id': 1, 'type': "ptAttribActivator", 'name': "Activate" },
{ 'id': 4, 'type': "ptAttribString", 'name': "Vignette" },
)
}
class PlasmaNotePopupModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz):
pl_id = "note_popup"
bl_category = "GUI"
bl_label = "Note Popup (ex)"
bl_description = "XXX"
bl_icon = "MATPLANE"
def _get_gui_pages(self, context):
scene = context.scene if context is not None else bpy.context.scene
return [
(i.name, i.name, i.name)
for i in scene.world.plasma_age.pages
if i.page_type == "gui"
]
clickable: bpy.types.Object = PointerProperty(
name="Clickable",
description="The object the player will click on to activate the GUI",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects
)
clickable_region: bpy.types.Object = PointerProperty(
name="Clickable Region",
description="The region in which the avatar must be standing before they can click on the note",
type=bpy.types.Object,
poll=idprops.poll_mesh_objects
)
gui_page: str = EnumProperty(
name="GUI Page",
description="Page containing all of the objects to display",
items=_get_gui_pages,
options=set()
)
gui_camera: bpy.types.Object = PointerProperty(
name="GUI Camera",
description="",
poll=idprops.poll_camera_objects,
type=bpy.types.Object,
options=set()
)
@property
def clickable_object(self) -> Optional[bpy.types.Object]:
if self.clickable is not None:
return self.clickable
if self.id_data.type == "MESH":
return self.id_data
def sanity_check(self):
page_type = helpers.get_page_type(self.id_data.plasma_object.page)
if page_type != "room":
raise ExportError(f"Note Popup modifiers should be in a 'room' page, not a '{page_type}' page!")
def pre_export(self, exporter: Exporter, bo: bpy.types.Object):
guidialog_object = utils.create_empty_object(f"{self.gui_page}_NoteDialog")
guidialog_object.plasma_object.enabled = True
guidialog_object.plasma_object.page = self.gui_page
yield guidialog_object
guidialog_mod: PlasmaGameGuiDialogModifier = guidialog_object.plasma_modifiers.gui_dialog
guidialog_mod.enabled = True
guidialog_mod.is_modal = True
if self.gui_camera:
guidialog_mod.camera_object = self.gui_camera
else:
# Abuse the GUI Dialog's lookat caLculation to make us a camera that looks at everything
# the artist has placed into the GUI page. We want to do this NOW because we will very
# soon be adding more objects into the GUI page.
camera_object = yield utils.create_camera_object(f"{self.key_name}_GUICamera")
camera_object.data.angle = math.radians(45.0)
camera_object.data.lens_unit = "FOV"
visible_objects = [
i for i in exporter.get_objects(self.gui_page)
if i.type == "MESH" and i.data.materials
]
camera_object.matrix_world = exporter.gui.calc_camera_matrix(
bpy.context.scene,
visible_objects,
camera_object.data.angle
)
clipping = exporter.gui.calc_clipping(
camera_object.matrix_world,
bpy.context.scene,
visible_objects,
camera_object.data.angle
)
camera_object.data.clip_start = clipping.hither
camera_object.data.clip_end = clipping.yonder
guidialog_mod.camera_object = camera_object
# Begin creating the object for the clickoff plane. We want to yield it immediately
# to the exporter in case something goes wrong during the export, allowing the stale
# object to be cleaned up.
click_plane_object = utils.BMeshObject(f"{self.key_name}_Exit")
click_plane_object.matrix_world = guidialog_mod.camera_object.matrix_world
click_plane_object.plasma_object.enabled = True
click_plane_object.plasma_object.page = self.gui_page
yield click_plane_object
# We have a camera on guidialog_mod.camera_object. We will now use it to generate the
# points for the click-off plane button.
# TODO: Allow this to be configurable to 4:3, 16:9, or 21:9?
with ExitStack() as stack:
stack.enter_context(exporter.gui.generate_camera_render_settings(bpy.context.scene))
toggle = stack.enter_context(helpers.GoodNeighbor())
# Temporarily adjust the clipping plane out to the farthest point we can find to ensure
# that the click-off button ecompasses everything. This is a bit heavy-handed, but if
# you want more refined control, you won't be using this helper.
clipping = max((guidialog_mod.camera_object.data.clip_start, guidialog_mod.camera_object.data.clip_end))
toggle.track(guidialog_mod.camera_object.data, "clip_start", clipping - 0.1)
view_frame = guidialog_mod.camera_object.data.view_frame(bpy.context.scene)
click_plane_object.data.materials.append(exporter.gui.transparent_material)
with click_plane_object as click_plane_mesh:
verts = [click_plane_mesh.verts.new(i) for i in view_frame]
face = click_plane_mesh.faces.new(verts)
# TODO: Ensure the face is pointing toward the camera!
# I feel like we should be fine by assuming that Blender returns the viewframe
# verts in the correct order, but this is Blender... So test that assumption carefully.
# TODO: Apparently not!
face.normal_flip()
# We've now created the mesh object - handle the GUI Button stuff
click_plane_object.plasma_modifiers.gui_button.enabled = True
# NOTE: We will be using xDialogToggle.py, so we use a special tag ID instead of the
# close dialog procedure.
click_plane_object.plasma_modifiers.gui_control.tag_id = 99
# Auto-generate a six-foot cube region around the clickable if none was provided.
yield utils.pre_export_optional_cube_region(
self, "clickable_region",
f"{self.key_name}_DialogToggle_ClkRgn", 6.0,
self.clickable_object
)
# Everything is ready now - create an xDialogToggle.py in the room page to display the GUI.
yield self.convert_logic(bo)
def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject):
# You're not hallucinating... Everything is done in the pre-export phase.
pass
def logicwiz(self, bo, tree):
nodes = tree.nodes
# xDialogToggle.py PythonFile Node
dialog_node = self._create_python_file_node(
tree,
dialog_toggle["filename"],
dialog_toggle["attribs"]
)
self._create_python_attribute(dialog_node, "Vignette", value=self.gui_page)
# Clickable
clickable_region = nodes.new("PlasmaClickableRegionNode")
clickable_region.region_object = self.clickable_region
clickable = nodes.new("PlasmaClickableNode")
clickable.find_input_socket("facing").allow_simple = False
clickable.clickable_object = self.clickable_object
clickable.link_input(clickable_region, "satisfies", "region")
clickable.link_output(dialog_node, "satisfies", "Activate")

1
korman/properties/modifiers/logic.py

@ -82,6 +82,7 @@ class PlasmaSpawnPoint(PlasmaModifierProperties):
bl_category = "Logic"
bl_label = "Spawn Point"
bl_description = "Point at which avatars link into the Age"
bl_object_types = {"EMPTY"}
def export(self, exporter, bo, so):
# Not much to this modifier... It's basically a flag that tells the engine, "hey, this is a

1
korman/properties/modifiers/physics.py

@ -74,6 +74,7 @@ class PlasmaCollider(PlasmaModifierProperties):
bl_label = "Collision"
bl_icon = "MOD_PHYSICS"
bl_description = "Simple physical collider"
bl_object_types = {"MESH", "FONT"}
bounds = EnumProperty(name="Bounds Type",
description="",

6
korman/properties/modifiers/region.py

@ -67,6 +67,7 @@ class PlasmaCameraRegion(PlasmaModifierProperties):
bl_label = "Camera Region"
bl_description = "Camera Region"
bl_icon = "CAMERA_DATA"
bl_object_types = {"MESH"}
camera_type = EnumProperty(name="Camera Type",
description="What kind of camera should be used?",
@ -133,6 +134,7 @@ class PlasmaFootstepRegion(PlasmaModifierProperties, PlasmaModifierLogicWiz):
bl_category = "Region"
bl_label = "Footstep"
bl_description = "Footstep Region"
bl_object_types = {"MESH"}
surface = EnumProperty(name="Surface",
description="What kind of surface are we walking on?",
@ -177,6 +179,7 @@ class PlasmaPanicLinkRegion(PlasmaModifierProperties):
bl_category = "Region"
bl_label = "Panic Link"
bl_description = "Panic Link Region"
bl_object_types = {"MESH"}
play_anim = BoolProperty(name="Play Animation",
description="Play the link-out animation when panic linking",
@ -313,6 +316,7 @@ class PlasmaSubworldRegion(PlasmaModifierProperties):
bl_category = "Region"
bl_label = "Subworld Region"
bl_description = "Subworld transition region"
bl_object_types = {"MESH"}
subworld = PointerProperty(name="Subworld",
description="Subworld to transition into",
@ -327,7 +331,7 @@ class PlasmaSubworldRegion(PlasmaModifierProperties):
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.
# 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)

15
korman/properties/modifiers/render.py

@ -39,10 +39,12 @@ class PlasmaBlendOntoObject(bpy.types.PropertyGroup):
class PlasmaBlendMod(PlasmaModifierProperties):
pl_id = "blend"
pl_page_types = {"gui", "room"}
bl_category = "Render"
bl_label = "Blending"
bl_description = "Advanced Blending Options"
bl_object_types = {"MESH", "FONT"}
render_level = EnumProperty(name="Render Pass",
description="Suggested render pass for this object.",
@ -150,6 +152,7 @@ class PlasmaDecalPrintMod(PlasmaDecalMod, PlasmaModifierProperties):
bl_category = "Render"
bl_label = "Print Decal"
bl_description = "Prints a decal onto an object"
bl_object_types = {"MESH", "FONT"}
decal_type = EnumProperty(name="Decal Type",
description="Type of decal to print onto another object",
@ -201,6 +204,7 @@ class PlasmaDecalReceiveMod(PlasmaDecalMod, PlasmaModifierProperties):
bl_category = "Render"
bl_label = "Receive Decal"
bl_description = "Allows this object to receive dynamic decals"
bl_object_types = {"MESH", "FONT"}
managers = CollectionProperty(type=PlasmaDecalManagerRef)
active_manager_index = IntProperty(options={"HIDDEN"})
@ -220,6 +224,7 @@ class PlasmaFadeMod(PlasmaModifierProperties):
bl_category = "Render"
bl_label = "Opacity Fader"
bl_description = "Fades an object based on distance or line-of-sight"
bl_object_types = {"MESH", "FONT"}
fader_type = EnumProperty(name="Fader Type",
description="Type of opacity fade",
@ -281,6 +286,7 @@ class PlasmaFollowMod(idprops.IDPropObjectMixin, PlasmaModifierProperties):
bl_category = "Render"
bl_label = "Follow"
bl_description = "Follow the movement of the camera, player, or another object"
bl_object_types = {"MESH", "FONT"}
follow_mode = EnumProperty(name="Mode",
description="Leader's movement to follow",
@ -357,6 +363,7 @@ class PlasmaGrassShaderMod(PlasmaModifierProperties):
bl_category = "Render"
bl_label = "Grass Shader"
bl_description = "Applies waving grass effect at run-time"
bl_object_types = {"MESH", "FONT"}
wave1 = PointerProperty(type=PlasmaGrassWave)
wave2 = PointerProperty(type=PlasmaGrassWave)
@ -404,6 +411,7 @@ class PlasmaLightMapGen(idprops.IDPropMixin, PlasmaModifierProperties, PlasmaMod
bl_category = "Render"
bl_label = "Bake Lighting"
bl_description = "Auto-Bake Static Lighting"
bl_object_types = {"MESH", "FONT"}
deprecated_properties = {"render_layers"}
@ -551,10 +559,12 @@ class PlasmaLightMapGen(idprops.IDPropMixin, PlasmaModifierProperties, PlasmaMod
class PlasmaLightingMod(PlasmaModifierProperties):
pl_id = "lighting"
pl_page_types = {"gui", "room"}
bl_category = "Render"
bl_label = "Lighting Info"
bl_description = "Fine tune Plasma lighting settings"
bl_object_types = {"MESH", "FONT"}
force_rt_lights = BoolProperty(name="Force RT Lighting",
description="Unleashes satan by forcing the engine to dynamically light this object",
@ -640,11 +650,13 @@ _LOCALIZED_TEXT_PFM = (
class PlasmaLocalizedTextModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz, TranslationMixin):
pl_id = "dynatext"
pl_page_types = {"gui", "room"}
bl_category = "Render"
bl_label = "Localized Text"
bl_description = ""
bl_icon = "TEXT"
bl_object_types = {"MESH", "FONT"}
translations = CollectionProperty(name="Translations",
type=PlasmaJournalTranslation,
@ -769,6 +781,7 @@ class PlasmaShadowCasterMod(PlasmaModifierProperties):
bl_category = "Render"
bl_label = "Cast RT Shadow"
bl_description = "Cast runtime shadows"
bl_object_types = {"MESH", "FONT"}
blur = IntProperty(name="Blur",
description="Blur factor for the shadow map",
@ -809,6 +822,7 @@ class PlasmaViewFaceMod(idprops.IDPropObjectMixin, PlasmaModifierProperties):
bl_category = "Render"
bl_label = "Swivel"
bl_description = "Swivel object to face the camera, player, or another object"
bl_object_types = {"MESH", "FONT"}
preset_options = EnumProperty(name="Type",
description="Type of Facing",
@ -952,6 +966,7 @@ class PlasmaVisibilitySet(PlasmaModifierProperties):
bl_category = "Render"
bl_label = "Visibility Set"
bl_description = "Defines areas where this object is visible"
bl_object_types = {"MESH", "LAMP"}
regions = CollectionProperty(name="Visibility Regions",
type=VisRegion)

1
korman/properties/modifiers/sound.py

@ -515,6 +515,7 @@ class PlasmaSound(idprops.IDPropMixin, bpy.types.PropertyGroup):
class PlasmaSoundEmitter(PlasmaModifierProperties):
pl_id = "soundemit"
pl_page_types = {"gui", "room"}
bl_category = "Logic"
bl_label = "Sound Emitter"

4
korman/properties/modifiers/water.py

@ -29,6 +29,7 @@ class PlasmaSwimRegion(idprops.IDPropObjectMixin, PlasmaModifierProperties, bpy.
bl_label = "Swimming Surface"
bl_description = "Surface that the avatar can swim on"
bl_icon = "MOD_WAVE"
bl_object_types = {"MESH"}
_CURRENTS = {
"NONE": plSwimRegionInterface,
@ -179,6 +180,7 @@ class PlasmaWaterModifier(idprops.IDPropMixin, PlasmaModifierProperties, bpy.typ
bl_category = "Water"
bl_label = "Basic Water"
bl_description = "Basic water properties"
bl_object_types = {"MESH"}
wind_object = PointerProperty(name="Wind Object",
description="Object whose Y axis represents the wind direction",
@ -327,6 +329,7 @@ class PlasmaWaterShoreModifier(PlasmaModifierProperties):
bl_category = "Water"
bl_label = "Water Shore"
bl_description = ""
bl_object_types = {"MESH"}
# The basic modifier may want to export a default copy of us
_shore_tint_default = (0.2, 0.4, 0.4)
@ -395,6 +398,7 @@ class PlasmaWaterShoreModifier(PlasmaModifierProperties):
class PlasmaWaveState:
pl_depends = {"water_basic"}
bl_object_types = {"MESH"}
def convert_wavestate(self, state):
state.minLength = self.min_length

1
korman/properties/prop_world.py

@ -141,6 +141,7 @@ class PlasmaPage(bpy.types.PropertyGroup):
items=[
("room", "Room", "A standard Plasma Room containing scene objects"),
("external", "External", "A page generated by an external process. Korman will not write to this page, ever."),
("gui", "Game GUI", "A page containing Game GUI objects.")
],
default="room",
options=set())

1
korman/ui/modifiers/__init__.py

@ -15,6 +15,7 @@
from .anim import *
from .avatar import *
from .game_gui import *
from .gui import *
from .logic import *
from .physics import *

120
korman/ui/modifiers/game_gui.py

@ -0,0 +1,120 @@
# 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/>.
from __future__ import annotations
from typing import *
import bpy.types
from .. import ui_list
if TYPE_CHECKING:
from ...properties.modifiers.game_gui import GameGuiAnimation, GameGuiAnimationGroup
class GuiAnimListUI(bpy.types.UIList):
def _iter_target_names(self, item: GameGuiAnimation):
if item.target_object is not None:
yield item.target_object.name
else:
yield item.id_data.name
if item.target_material is not None:
yield item.target_material.name
if item.target_texture is not None:
yield item.target_texture.name
def draw_item(
self, context, layout, data, item: GameGuiAnimation, icon, active_data,
active_property, index=0, flt_flag=0
):
if item.anim_type == "OBJECT":
name = item.target_object.name if item.target_object is not None else item.id_data.name
layout.label(name, icon="OBJECT_DATA")
elif item.anim_type == "TEXTURE":
name_seq = list(self._iter_target_names(item))
layout.label(" / ".join(name_seq), icon="TEXTURE")
else:
raise RuntimeError()
def _gui_anim(name: str, group: GameGuiAnimationGroup, layout, context):
box = layout.box()
row = box.row(align=True)
exicon = "TRIA_DOWN" if group.show_expanded else "TRIA_RIGHT"
row.prop(group, "show_expanded", text="", icon=exicon, emboss=False)
row.prop(group, "animation_name", text=name, icon="ANIM")
if not group.show_expanded:
return
ui_list.draw_modifier_list(box, "GuiAnimListUI", group, "animations", "active_anim_index", rows=2)
try:
anim: GameGuiAnimation = group.animations[group.active_anim_index]
except:
pass
else:
col = box.column()
col.prop(anim, "anim_type")
col.prop(anim, "target_object")
if anim.anim_type == "TEXTURE":
col.prop(anim, "target_material")
col.prop(anim, "target_texture")
def gui_button(modifier, layout, context):
row = layout.row()
row.label("Notify On:")
row.prop(modifier, "notify_type")
_gui_anim("Mouse Click", modifier.mouse_click_anims, layout, context)
_gui_anim("Mouse Over", modifier.mouse_over_anims, layout, context)
box = layout.box()
row = box.row(align=True)
exicon = "TRIA_DOWN" if modifier.show_expanded_sounds else "TRIA_RIGHT"
row.prop(modifier, "show_expanded_sounds", text="", icon=exicon, emboss=False)
row.label("Sound Effects")
if modifier.show_expanded_sounds:
col = box.column()
soundemit = modifier.id_data.plasma_modifiers.soundemit
col.active = soundemit.enabled
col.prop_search(modifier, "mouse_down_sound", soundemit, "sounds", text="Mouse Down", icon="SPEAKER")
col.prop_search(modifier, "mouse_up_sound", soundemit, "sounds", text="Mouse Up", icon="SPEAKER")
col.prop_search(modifier, "mouse_over_sound", soundemit, "sounds", text="Mouse Over", icon="SPEAKER")
col.prop_search(modifier, "mouse_off_sound", soundemit, "sounds", text="Mouse Off", icon="SPEAKER")
def gui_control(modifier, layout, context):
split = layout.split()
col = split.column()
col.prop(modifier, "visible")
col = split.column()
col.prop(modifier, "tag_id")
col = layout.column()
col.active = modifier.has_gui_proc
col.prop(modifier, "proc")
row = col.row()
row.active = col.active and modifier.proc == "console_command"
row.prop(modifier, "console_command")
def gui_dialog(modifier, layout, context):
row = layout.row(align=True)
row.prop(modifier, "camera_object")
op = row.operator("camera.plasma_create_game_gui_camera", text="", icon="CAMERA_DATA")
op.mod_id = modifier.pl_id
op.cam_prop_name = "camera_object"
op.gui_page = modifier.id_data.plasma_object.page
layout.prop(modifier, "is_modal")

13
korman/ui/modifiers/gui.py

@ -116,3 +116,16 @@ def linkingbookmod(modifier, layout, context):
row.label("Stamp Position:")
row.prop(modifier, "stamp_x", text="X")
row.prop(modifier, "stamp_y", text="Y")
def note_popup(modifier, layout, context):
layout.prop(modifier, "gui_page")
row = layout.row(align=True)
row.prop(modifier, "gui_camera")
op = row.operator("camera.plasma_create_game_gui_camera", text="", icon="CAMERA_DATA")
op.mod_id = modifier.pl_id
op.cam_prop_name = "gui_camera"
op.gui_page = modifier.gui_page
layout.prop(modifier, "clickable")
layout.prop(modifier, "clickable_region")

Loading…
Cancel
Save