diff --git a/korman/properties/modifiers/game_gui.py b/korman/properties/modifiers/game_gui.py index a8a6c9a..4ebd050 100644 --- a/korman/properties/modifiers/game_gui.py +++ b/korman/properties/modifiers/game_gui.py @@ -191,6 +191,122 @@ class PlasmaGameGuiControlModifier(PlasmaModifierProperties, _GameGuiMixin): 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"} @@ -217,6 +333,10 @@ class PlasmaGameGuiButtonModifier(PlasmaModifierProperties, _GameGuiMixin): 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", @@ -266,6 +386,9 @@ class PlasmaGameGuiButtonModifier(PlasmaModifierProperties, _GameGuiMixin): 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" diff --git a/korman/ui/modifiers/game_gui.py b/korman/ui/modifiers/game_gui.py index bf17c1e..6be792f 100644 --- a/korman/ui/modifiers/game_gui.py +++ b/korman/ui/modifiers/game_gui.py @@ -13,19 +13,86 @@ # You should have received a copy of the GNU General Public License # along with Korman. If not, see . +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") - soundemit = modifier.id_data.plasma_modifiers.soundemit - col = layout.column() - col.label("Sound Effects:") - 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") + _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()