From 4e449e7bd0804c48cae6071c114357cf84e3d6d5 Mon Sep 17 00:00:00 2001 From: JaXt0r Date: Sat, 3 Jan 2026 15:27:46 +0100 Subject: [PATCH 1/3] feat: Remove Fist Fight again. It will be handled by range and attack window alone in the future. e.g., Molerat is biting, not fisting... --- .../Scripts/Adapters/Npc/FistFightAdapter.cs | 75 ------------------- .../Adapters/Npc/FistFightAdapter.cs.meta | 3 - .../Domain/Meshes/Builder/NpcMeshBuilder.cs | 13 ---- 3 files changed, 91 deletions(-) delete mode 100644 Assets/UnZENity-Core/Scripts/Adapters/Npc/FistFightAdapter.cs delete mode 100644 Assets/UnZENity-Core/Scripts/Adapters/Npc/FistFightAdapter.cs.meta 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 1f3506b0..e4a7bc12 100644 --- a/Assets/UnZENity-Core/Scripts/Domain/Meshes/Builder/NpcMeshBuilder.cs +++ b/Assets/UnZENity-Core/Scripts/Domain/Meshes/Builder/NpcMeshBuilder.cs @@ -35,22 +35,9 @@ protected override GameObject[] BuildViaMdmAndMdh() { var nodeObjects = base.BuildViaMdmAndMdh(); - AddFistCollider(nodeObjects); - return nodeObjects; } - private void AddFistCollider(GameObject[] nodeObjects) - { - foreach (var nodeObject in nodeObjects) - { - if (nodeObject.name == "BIP01 L HAND" || nodeObject.name == "BIP01 R HAND") - { - var capsuleCollider = nodeObject.AddComponent(); - } - } - } - /// /// Change texture name based on VisualBodyData. /// From 58cda2e7ed6099fb1986177e0be111c8c6c037f5 Mon Sep 17 00:00:00 2001 From: JaXt0r Date: Sun, 4 Jan 2026 09:05:08 +0100 Subject: [PATCH 2/3] feat: Implement Bone Colliders for hit detection. --- .../Meshes/Builder/AbstractMeshBuilder.cs | 4 +- .../Domain/Meshes/Builder/NpcMeshBuilder.cs | 79 +++++++++++++++++-- 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/Assets/UnZENity-Core/Scripts/Domain/Meshes/Builder/AbstractMeshBuilder.cs b/Assets/UnZENity-Core/Scripts/Domain/Meshes/Builder/AbstractMeshBuilder.cs index 192dcc34..cd9b2f12 100644 --- a/Assets/UnZENity-Core/Scripts/Domain/Meshes/Builder/AbstractMeshBuilder.cs +++ b/Assets/UnZENity-Core/Scripts/Domain/Meshes/Builder/AbstractMeshBuilder.cs @@ -215,7 +215,7 @@ protected void BuildViaMrm() SetPosAndRot(RootGo, RootPosition, RootRotation); } - protected virtual GameObject[] BuildViaMdmAndMdh() + protected void BuildViaMdmAndMdh() { CheckPreconditions(); @@ -333,8 +333,6 @@ protected virtual GameObject[] BuildViaMdmAndMdh() // We need to reset the rootBones position to zero. Otherwise Vobs won't be placed right. // Due to Unity's parent-child transformation magic, we need to do it at the end. ╰(*°▽°*)╯ nodeObjects[0].transform.localPosition = Vector3.zero; - - return nodeObjects; } protected GameObject BuildViaMmb() diff --git a/Assets/UnZENity-Core/Scripts/Domain/Meshes/Builder/NpcMeshBuilder.cs b/Assets/UnZENity-Core/Scripts/Domain/Meshes/Builder/NpcMeshBuilder.cs index e4a7bc12..b105ac23 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,17 +27,11 @@ public virtual void SetBodyData(ExtSetVisualBodyData body) public override GameObject Build() { BuildViaMdmAndMdh(); + CreateBoneColliders(); return RootGo; } - protected override GameObject[] BuildViaMdmAndMdh() - { - var nodeObjects = base.BuildViaMdmAndMdh(); - - return nodeObjects; - } - /// /// Change texture name based on VisualBodyData. /// @@ -74,5 +68,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. + } + } } } From aad934a9241117b814b7def2748b074b91dd6734 Mon Sep 17 00:00:00 2001 From: JaXt0r Date: Thu, 15 Jan 2026 08:05:11 +0100 Subject: [PATCH 3/3] feat: Track attack information in AnimationSystem. Activate/Deactivate with animation play. --- .../Adapters/Animations/AnimationSystem.cs | 54 +++++++++++++++++-- 1 file changed, 49 insertions(+), 5 deletions(-) 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;