diff --git a/Assets/UnZENity-Core/Scripts/Adapters/Animations/AnimationSystem.cs b/Assets/UnZENity-Core/Scripts/Adapters/Animations/AnimationSystem.cs index 5ac5b223..0a791084 100644 --- a/Assets/UnZENity-Core/Scripts/Adapters/Animations/AnimationSystem.cs +++ b/Assets/UnZENity-Core/Scripts/Adapters/Animations/AnimationSystem.cs @@ -43,7 +43,6 @@ public class AnimationSystem : BasePlayerBehaviour [Inject] private readonly NpcService _npcService; - public Transform RootBone; // Caching bone Transforms makes it faster to apply them to animations later. @@ -55,7 +54,16 @@ public class AnimationSystem : BasePlayerBehaviour private bool _isSittingInverted; // Some sitting animations are rotated wrong. They need to be inverted in y-axis. - private string[] _animationsToInvertYAxis = new[] { "S_BENCH_S1", "S_THRONE_S1" }; + private string[] _animationsToInvertYAxis = { "S_BENCH_S1", "S_THRONE_S1" }; + + + // Attack information + private bool IsAttack => AttackAnimation.NotNullOrEmpty(); + private string AttackAnimation; + private string AttackHitLimb; + private List AttackOptFrame; + private List AttackHitEnd; + private List AttackWindowFrames; protected override void Awake() @@ -93,6 +101,7 @@ public void DisableObject() _bones[i].SetLocalPositionAndRotation(_initialMeshBonePos[i], _initialMeshBoneRot[i]); } + DisableAttack(); _trackInstances.Clear(); } @@ -220,8 +229,7 @@ public void StopAnimation(string animationName) } #endif - var newTrack = _animationService.GetTrack(animationName, Properties.MdsNameBase, Properties.MdsNameOverlay); - + var trackToStop = _animationService.GetTrack(animationName, Properties.MdsNameBase, Properties.MdsNameOverlay); Logger.LogEditor($"Stopping animation: {animationName}", LogCat.Animation); AnimationTrackInstance instanceToStop = null; @@ -231,10 +239,13 @@ public void StopAnimation(string animationName) { var instance = _trackInstances[i]; // If animation is found, then mark it as "BlendOut" - if (instance.Track.Name.EqualsIgnoreCase(newTrack.Name)) + if (instance.Track.Name.EqualsIgnoreCase(trackToStop.Name)) { instanceToStop = instance; instance.BlendOutTrack(instance.Track.BlendOut); + + if (AttackAnimation == trackToStop.Name) + AttackAnimation = null; // Do not break. We could potentially need to stop multiple instances of the same animation. } } @@ -400,6 +411,23 @@ private void PreStopAnimation(AnimationTrackInstance instance) { if (_animationsToInvertYAxis.Contains(instance.Track.Name.ToUpper())) _isSittingInverted = false; + + if (AttackAnimation.EqualsIgnoreCase(instance.AnimationName)) + DisableAttack(); + } + + private void DisableAttack() + { + if (AttackAnimation == null) + return; + + AttackAnimation = null; + AttackHitLimb = null; + AttackOptFrame = null; + AttackHitEnd = null; + AttackWindowFrames = null; + + // FIXME - We need to disable all limbs, if they are still active from current attack window. } private void ApplyFinalPose() @@ -532,6 +560,22 @@ private void ApplyEventTags(AnimationTrackInstance trackInstance) case EventType.TorchInventory: // TODO - I assume this means: if torch is in inventory, then put it out. But not really sure. Need a NPC with real usage of it to predict right. break; + case EventType.HitLimb: + AttackHitLimb = eventTag.Slots.Item1; + AttackAnimation = trackInstance.AnimationName; + break; + case EventType.OptimalFrame: + AttackOptFrame = eventTag.Slots.Item1.Split(' ').Select(i => Convert.ToInt32(i)).ToList(); + break; + case EventType.HitEnd: + AttackHitEnd = eventTag.Slots.Item1.Split(' ').Select(i => Convert.ToInt32(i)).ToList(); + break; + case EventType.ComboWindow: + AttackWindowFrames = eventTag.Slots.Item1.Split(' ').Select(i => Convert.ToInt32(i)).ToList(); + break; + // Unused. @see: https://gothic-modding-community.github.io/gmc/zengin/anims/events/#def_dir + case EventType.HitDirection: + break; default: Logger.LogWarning($"EventType.type {eventTag.Type} not yet supported.", LogCat.Animation); break; diff --git a/Assets/UnZENity-Core/Scripts/Adapters/Npc/FistFightAdapter.cs b/Assets/UnZENity-Core/Scripts/Adapters/Npc/FistFightAdapter.cs deleted file mode 100644 index 70bf3ad6..00000000 --- a/Assets/UnZENity-Core/Scripts/Adapters/Npc/FistFightAdapter.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Collections.Generic; -using UnityEngine; - -namespace GUZ.Core.Adapters.Npc -{ - /// - /// Hint: This Component is attached to "BIP01 L/R HAND" which is parent of fingers. - /// - [RequireComponent(typeof(SphereCollider))] - public class FistFightAdapter : MonoBehaviour - { - private SphereCollider _sphereCollider; - private readonly List _allFingerTransforms = new(); - - // TODO - Checked with G1 Zombie. Check how it behaves with a troll hand etc. - private const float _radiusMultiplier = 0.5f; - - - private void Awake() - { - _sphereCollider = GetComponent(); - _sphereCollider.isTrigger = true; - - GetAllChildrenRecursively(transform); - } - - private void GetAllChildrenRecursively(Transform parent) - { - foreach (Transform child in parent) - { - _allFingerTransforms.Add(child); - GetAllChildrenRecursively(child); - } - } - - private void Update() - { - var farthestFingerTransform = GetFarthestTransform(_allFingerTransforms); - - // The local position of the finger relative to the hand start - var fingerLocalPos = transform.InverseTransformPoint(farthestFingerTransform.position); - var handLength = fingerLocalPos.magnitude; - - // Place the sphere center between the hand and the fingertip - _sphereCollider.center = fingerLocalPos / 2f; - - // Scale the radius based on the hand length - // This ensures (e.g.) a Troll gets a massive sphere and a Human gets a small one - _sphereCollider.radius = handLength * _radiusMultiplier; - } - - /// - /// During animations, the position of fingers related to the arm can change. (e.g. by a troll opening his hands). - /// We therefore need to calculate the farthest finger each frame to span the bounds of fist hitbox collider correctly. - /// - private Transform GetFarthestTransform(List transforms) - { - Transform farthest = null; - var maxDistanceSqr = 0f; - var rootPosition = transform.position; - - foreach (var t in transforms) - { - var distanceSqr = (t.position - rootPosition).sqrMagnitude; - if (distanceSqr > maxDistanceSqr) - { - maxDistanceSqr = distanceSqr; - farthest = t; - } - } - - return farthest; - } - } -} diff --git a/Assets/UnZENity-Core/Scripts/Adapters/Npc/FistFightAdapter.cs.meta b/Assets/UnZENity-Core/Scripts/Adapters/Npc/FistFightAdapter.cs.meta deleted file mode 100644 index fe5b8c17..00000000 --- a/Assets/UnZENity-Core/Scripts/Adapters/Npc/FistFightAdapter.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: aac74ba613e04b79a3f63087aef76a06 -timeCreated: 1766759606 \ No newline at end of file diff --git a/Assets/UnZENity-Core/Scripts/Domain/Meshes/Builder/NpcMeshBuilder.cs b/Assets/UnZENity-Core/Scripts/Domain/Meshes/Builder/NpcMeshBuilder.cs index 9fedacb1..33169851 100644 --- a/Assets/UnZENity-Core/Scripts/Domain/Meshes/Builder/NpcMeshBuilder.cs +++ b/Assets/UnZENity-Core/Scripts/Domain/Meshes/Builder/NpcMeshBuilder.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Text.RegularExpressions; -using GUZ.Core.Adapters.Npc; using GUZ.Core.Logging; using GUZ.Core.Models.Vm; using GUZ.Core.Services.Caches; @@ -8,6 +7,7 @@ using UnityEngine; using ZenKit; using Logger = GUZ.Core.Logging.Logger; +using Mesh = UnityEngine.Mesh; using Vector3 = System.Numerics.Vector3; namespace GUZ.Core.Domain.Meshes.Builder @@ -27,6 +27,7 @@ public virtual void SetBodyData(ExtSetVisualBodyData body) public override GameObject Build() { BuildViaMdmAndMdh(); + CreateBoneColliders(); return RootGo; } @@ -87,5 +88,74 @@ protected override List GetSoftSkinMeshPositions(ISoftSkinMesh softSkin { return _npcArmorCacheService.TryGetPositions(softSkinMesh, Mdh); } + + /// + /// During fight situations, the bones are checked for physical collision via e.g. *eventTag(0 "DEF_HIT_LIMB" "BIP01 R HAND") + /// We therefore calculate a box collider for all of the limbs/bones and disable it until its needed at fight time. + /// + /// Hint: We assume that the bounding boxes of the bones will stay stable and no long stretches will happen + /// (which would force a recalculation). + /// + private void CreateBoneColliders() + { + var renderers = RootGo.GetComponentsInChildren(); + var boneBoundsMap = new Dictionary(); + + foreach (var renderer in renderers) + { + var mesh = renderer.sharedMesh; + if (mesh == null) + continue; + + var vertices = mesh.vertices; + var weights = mesh.boneWeights; + var smrBones = renderer.bones; + var bindPoses = mesh.bindposes; + + for (var i = 0; i < vertices.Length; i++) + { + var weight = weights[i]; + var boneIdx = weight.boneIndex0; + + // Use vertices with more than 10% weight. + if (weight.weight0 > 0.1f) + { + var boneTransform = smrBones[boneIdx]; + + // DIRECT CALCULATION: + // Multiply the vertex by the bind pose matrix to get the + // position relative to the bone at the time of rigging. + var localPt = bindPoses[boneIdx].MultiplyPoint3x4(vertices[i]); + + if (!boneBoundsMap.ContainsKey(boneTransform)) + { + boneBoundsMap[boneTransform] = new Bounds(localPt, UnityEngine.Vector3.zero); + } + else + { + var bounds = boneBoundsMap[boneTransform]; + bounds.Encapsulate(localPt); + boneBoundsMap[boneTransform] = bounds; + } + } + } + } + + // Apply to Colliders + foreach (var boneBound in boneBoundsMap) + { + var boneTransform = boneBound.Key; + var finalBounds = boneBound.Value; + + if (finalBounds.size.sqrMagnitude < 0.0001f) + continue; + + var col = boneTransform.gameObject.AddComponent(); + col.center = finalBounds.center; + col.size = finalBounds.size; + col.isTrigger = true; // We want to calculate Triggering only, not pushing/colliding. + col.enabled = false; // Will be enabled at runtime during fights when DEF_HIT_LIMB is set. + } + } } }