diff --git a/Assets/Gothic-Core/Resources/DeveloperConfigs/Production.asset b/Assets/Gothic-Core/Resources/DeveloperConfigs/Production.asset index cb9935cc9..5903c4676 100644 --- a/Assets/Gothic-Core/Resources/DeveloperConfigs/Production.asset +++ b/Assets/Gothic-Core/Resources/DeveloperConfigs/Production.asset @@ -51,6 +51,7 @@ MonoBehaviour: ShowVOBMeshCullingGizmos: 0 ShowCapsuleOverlapGizmos: 0 EnableNpcs: 1 + EnableCombatSystem: 1 EnableNpcMeshCulling: 1 NpcCullingDistance: 50 SpawnNpcInstances: @@ -58,6 +59,7 @@ MonoBehaviour: SpawnMonsterInstances: Value: 00000000 EnableNpcEyeBlinking: 0 + EnableNpcLooting: 1 ShowNpcColliders: 0 ShowFreePoints: 0 ShowWayPoints: 0 diff --git a/Assets/Gothic-Core/Scripts/Adapters/Animations/Morph/AbstractMorphAnimation.cs b/Assets/Gothic-Core/Scripts/Adapters/Animations/Morph/AbstractMorphAnimation.cs index c203c4b3d..9cae98670 100644 --- a/Assets/Gothic-Core/Scripts/Adapters/Animations/Morph/AbstractMorphAnimation.cs +++ b/Assets/Gothic-Core/Scripts/Adapters/Animations/Morph/AbstractMorphAnimation.cs @@ -57,6 +57,12 @@ protected void StartAnimation(string morphMeshName, [CanBeNull] string animation var newMorph = new MorphAnimationData(); newMorph.MeshMetadata = _resourceCacheService.TryGetMorphMesh(morphMeshName); + if (newMorph.MeshMetadata == null) + { + Logger.LogWarning($"MorphMesh not found: {morphMeshName}", LogCat.Mesh); + return; + } + newMorph.AnimationMetadata = animationName == null ? newMorph.MeshMetadata.Animations.First() : newMorph.MeshMetadata.Animations.First(anim => anim.Name.EqualsIgnoreCase(animationName)); diff --git a/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcLoader.cs b/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcLoader.cs index e0aebaba0..55e32a039 100644 --- a/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcLoader.cs +++ b/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcLoader.cs @@ -12,3 +12,4 @@ public class NpcLoader : MonoBehaviour public bool IsLoaded; } } + diff --git a/Assets/Gothic-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs b/Assets/Gothic-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs deleted file mode 100644 index a42664a2a..000000000 --- a/Assets/Gothic-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Gothic.Core.Const; -using UnityEngine; - -namespace Gothic.Core.Adapters.Vob.Item -{ - /// - /// The validity check for a hit requires answering: "Is this attack currently active?" - /// That state lives on the attacker (DEF_OPT_FRAME window, DEF_HIT_LIMB, "already connected" flag). - /// If we put the logic on the receiver, we must reach across to the attacker's component to get that state - /// every time any contact happens. If we put it on the attacker, all required state is already local. - /// - public class WeaponAttackAdapter : MonoBehaviour - { - /// - /// TODO - Need to be updated to support fist collider from monsters and player as well - /// Is the other who's hitting me?: - /// 1. A VobItem (aka weapon) - /// 2. Is the attacker in attack window state - /// - private void OnTriggerEnter(Collider other) - { - if (other.gameObject.layer != Constants.VobHitbox) - return; - - Debug.Log("OnTriggerEnter - VobHitbox"); - // if (other.gameObject.layer != Constants.VobItemLayer) - // return; - // - // var vobContainer = other.GetComponentInParent()?.Container; - // - // if (!_vrWeaponService.IsWeaponInAttackWindow(vobContainer)) - // return; - // - // var attacker = _vrWeaponService.GetWeaponOwner(vobContainer); - // if (attacker == null) - // return; - // - // var hitPosition = other.ClosestPoint(transform.position); - // GlobalEventDispatcher.FightHit.Invoke(attacker, _npcContainer, hitPosition); - } - - private void OnTriggerExit(Collider other) - { - if (other.gameObject.layer != Constants.VobHitbox) - return; - - Debug.Log("OnTriggerExit - VobHitbox"); - } - } -} diff --git a/Assets/Gothic-Core/Scripts/Domain/Culling/VobMeshCullingDomain.cs b/Assets/Gothic-Core/Scripts/Domain/Culling/VobMeshCullingDomain.cs index 15814e263..6b94a11a2 100644 --- a/Assets/Gothic-Core/Scripts/Domain/Culling/VobMeshCullingDomain.cs +++ b/Assets/Gothic-Core/Scripts/Domain/Culling/VobMeshCullingDomain.cs @@ -489,8 +489,13 @@ public void StartTrackVobPositionUpdates(GameObject go) } if (index == -1) - Logger.LogError($"Couldn't find object in Culling list {rootGo.name}. Culling updates will break.", + { + // Dynamically spawned items (loot panel, backpack fill) are not registered in the culling lists. + // This is expected — just skip tracking for them. + Logger.LogWarning($"VOB {rootGo.name} not in culling list — skipping position tracking (dynamically spawned).", LogCat.Vob); + return; + } _pausedVobs.Add(rootGo, new Tuple(vobType, index)); } @@ -542,6 +547,11 @@ private IEnumerator StopTrackVobPositionUpdatesDelayed(GameObject rootGo) { yield return new WaitForSeconds(1f); _pausedVobsToReenableCoroutine.Remove(rootGo); + + // GO may have been destroyed (e.g., loot panel closed) during the 1-second delay. + if (rootGo == null) + yield break; + if (!_pausedVobsToReenable.ContainsKey(rootGo)) { _pausedVobsToReenable.Add(rootGo, rootGo.GetComponentInChildren()); @@ -567,9 +577,11 @@ private IEnumerator StopVobTrackingBasedOnVelocity() continue; } - UpdateSpherePosition(key); - rigidBody.isKinematic = true; + // Item may not be in _pausedVobs if it was dynamically spawned and never registered in culling lists. + if (_pausedVobs.ContainsKey(key)) + UpdateSpherePosition(key); + rigidBody.isKinematic = true; _pausedVobs.Remove(key); _pausedVobsToReenable.Remove(key); } @@ -593,6 +605,10 @@ private void UpdateSpherePosition(GameObject go) _ => throw new ArgumentOutOfRangeException() }; + // Index may be stale if other VOBs were removed from the list after this item was grabbed. + if (index >= sphereList.Count) + return; + sphereList[index] = new BoundingSphere(go.transform.position, sphereList[index].radius); } diff --git a/Assets/Gothic-Core/Scripts/Domain/Vobs/VobInitializerDomain.cs b/Assets/Gothic-Core/Scripts/Domain/Vobs/VobInitializerDomain.cs index 6f6273b22..3e37bb0cd 100644 --- a/Assets/Gothic-Core/Scripts/Domain/Vobs/VobInitializerDomain.cs +++ b/Assets/Gothic-Core/Scripts/Domain/Vobs/VobInitializerDomain.cs @@ -466,7 +466,7 @@ private GameObject CreateItemMesh(ItemInstance item, GameObject go, GameObject p if (mrm != null) { - return _meshService.CreateVob(item.Visual, mrm, parent: parent, rootGo: go, useColliderCache: true); + return _meshService.CreateVob(item.Visual, mrm, parent: parent, rootGo: go, useColliderCache: true, useTextureArray: false); } // shortbow (itrw_bow_l_01) has no mrm, but has mmb diff --git a/Assets/Gothic-Core/Scripts/Models/Config/DeveloperConfig.cs b/Assets/Gothic-Core/Scripts/Models/Config/DeveloperConfig.cs index dfac35707..97a281486 100644 --- a/Assets/Gothic-Core/Scripts/Models/Config/DeveloperConfig.cs +++ b/Assets/Gothic-Core/Scripts/Models/Config/DeveloperConfig.cs @@ -175,6 +175,9 @@ public class DebugChannelTypesCollection : CollectionWrapper i.sharedMaterial.SetFloat(shaderProperty, shaderValue)); + entry.Renderers.ForEach(i => { if (i != null) i.sharedMaterial.SetFloat(shaderProperty, shaderValue); }); // And we add the property to the list of "changed" properties. entry.AlteredShaderProperties.Add(shaderProperty); @@ -70,7 +70,7 @@ public void ResetDynamicValue(GameObject go, int shaderProperty, float shaderVal } // Reset values - entry.Renderers.ForEach(i => i.sharedMaterial.SetFloat(shaderProperty, shaderValue)); + entry.Renderers.ForEach(i => { if (i != null) i.sharedMaterial.SetFloat(shaderProperty, shaderValue); }); entry.AlteredShaderProperties.Remove(shaderProperty); if (entry.AlteredShaderProperties.IsEmpty()) @@ -144,6 +144,9 @@ private void ActivateDynamicRenderers(CacheEntry entry) for (var i = 0; i < entry.Renderers.Count; i++) { + if (entry.Renderers[i] == null) + continue; + var dynamicMaterial = entry.DynamicMaterials[i]; // It's a shader we didn't touch. @@ -165,12 +168,15 @@ private void DeactivateDynamicRenderers(CacheEntry entry) for (var i = 0; i < entry.Renderers.Count; i++) { + if (entry.Renderers[i] == null) + continue; + var defaultMaterial = entry.DefaultMaterials[i]; // It's a shader we didn't touch. if (defaultMaterial == null) continue; - + entry.Renderers[i].sharedMaterial = defaultMaterial; } diff --git a/Assets/Gothic-Core/Scripts/Services/Npc/FightService.cs b/Assets/Gothic-Core/Scripts/Services/Npc/FightService.cs index 355b98919..674d5d0a4 100644 --- a/Assets/Gothic-Core/Scripts/Services/Npc/FightService.cs +++ b/Assets/Gothic-Core/Scripts/Services/Npc/FightService.cs @@ -1,13 +1,16 @@ using Gothic.Core.Adapters.UI.StatusBars; using Gothic.Core.Domain.Npc.Actions.AnimationActions; +using Gothic.Core.Logging; using Gothic.Core.Manager; using Gothic.Core.Models.Container; using Gothic.Core.Models.Vm; +using Gothic.Core.Services.Config; using Gothic.Core.Services.Vm; using Gothic.Core.Services.World; using Reflex.Attributes; using UnityEngine; using ZenKit.Daedalus; +using Logger = Gothic.Core.Logging.Logger; namespace Gothic.Core.Services.Npc { @@ -18,21 +21,28 @@ public class FightService [Inject] private AnimationService _animationService; [Inject] private PhysicsService _physicsService; [Inject] private NpcHelperService _npcHelperService; + [Inject] private readonly ConfigService _configService; public void Init() { + if (!_configService.Dev.EnableCombatSystem) + return; + GlobalEventDispatcher.FightHit.AddListener(OnHit); } private void OnHit(NpcContainer attacker, NpcContainer target, Vector3 __) { + Logger.Log($"[FightService.OnHit] *** {attacker.Instance.GetName(NpcNameSlot.Slot0)} HIT {target.Instance.GetName(NpcNameSlot.Slot0)}", LogCat.Npc); if (OnHitUpdateHealth(attacker, target)) { + Logger.Log($"[FightService.OnHit] {target.Instance.GetName(NpcNameSlot.Slot0)} is DEAD", LogCat.Npc); target.Props.BodyState = VmGothicEnums.BodyState.BsDead; OnDyingChangeAnimation(target); } else { + Logger.Log($"[FightService.OnHit] {target.Instance.GetName(NpcNameSlot.Slot0)} took damage, playing hurt animation", LogCat.Npc); OnHitChangeAnimation(target); OnHitPlaySound(target); } @@ -46,17 +56,34 @@ private bool OnHitUpdateHealth(NpcContainer attacker, NpcContainer target) { // FIXME - We need to handle this via power and skill level of attacker, not weapon alone. var hitPoints = target.Vob.GetAttribute((int)NpcAttribute.HitPoints); + var maxHP = target.Vob.GetAttribute((int)NpcAttribute.HitPointsMax); var equippedWeapon = _npcHelperService.ExtNpcGetEquippedMeleeWeapon(attacker.Instance); + Logger.Log($"[FightService.OnHitUpdateHealth] Attacker: {attacker.Instance.GetName(NpcNameSlot.Slot0)}, WeaponName: {(equippedWeapon != null ? equippedWeapon.Name : "None")}, Damage: {(equippedWeapon != null ? equippedWeapon.DamageTotal.ToString() : "N/A")}", LogCat.Npc); // FIXME - Instead of 0, use fist value // FIXME - Instead of DamageTotal, use calculated NPC/Hero value var damage = equippedWeapon?.DamageTotal ?? 0; + if (damage <= 0) + damage = 10; // debug: force minimum 10 until proper damage calculation is implemented + + Logger.Log($"[FightService.OnHitUpdateHealth] {target.Instance.GetName(NpcNameSlot.Slot0)}: {hitPoints} - {damage} dmg", LogCat.Npc); hitPoints -= damage; + Logger.Log($"[FightService.OnHitUpdateHealth] {target.Instance.GetName(NpcNameSlot.Slot0)} HP after: {hitPoints}/{maxHP}", LogCat.Npc); + target.Vob.SetAttribute((int)NpcAttribute.HitPoints, hitPoints); - target.Go.GetComponentInChildren(true)?.SetFillAmount(hitPoints, target.Vob.GetAttribute((int)NpcAttribute.HitPointsMax)); + var statusBar = target.Go.GetComponentInChildren(true); + if (statusBar != null) + { + Logger.Log($"[FightService.OnHitUpdateHealth] Updating HP bar for {target.Instance.GetName(NpcNameSlot.Slot0)}", LogCat.Npc); + statusBar.SetFillAmount(hitPoints, maxHP); + } + else + { + Logger.LogWarning($"[FightService.OnHitUpdateHealth] No StatusBar found for {target.Instance.GetName(NpcNameSlot.Slot0)}", LogCat.Npc); + } return hitPoints <= 0; } diff --git a/Assets/Gothic-Core/Scripts/Services/Npc/NpcInventoryService.cs b/Assets/Gothic-Core/Scripts/Services/Npc/NpcInventoryService.cs index 038d4bd80..ccd9edb66 100644 --- a/Assets/Gothic-Core/Scripts/Services/Npc/NpcInventoryService.cs +++ b/Assets/Gothic-Core/Scripts/Services/Npc/NpcInventoryService.cs @@ -111,6 +111,30 @@ public List GetInventoryItems(NpcInstance npc, VmGothicEnums.InvCat return _vobService.UnpackItems(npcVob.GetPacked((int)category)); } + /// + /// Returns all items across every category. Each InvCats slot only exists in ZenKit if + /// SetPacked was previously called for it — accessing a missing slot throws from native code. + /// This method silently skips slots that were never initialized. + /// + public List GetAllInventoryItems(NpcInstance npc) + { + var items = new List(); + foreach (VmGothicEnums.InvCats cat in System.Enum.GetValues(typeof(VmGothicEnums.InvCats))) + { + if (cat == VmGothicEnums.InvCats.InvCatMax) + continue; + try + { + items.AddRange(GetInventoryItems(npc, cat)); + } + catch + { + // Slot was never initialized for this NPC — expected when a category has no items + } + } + return items; + } + public int ExtNpcHasItems(NpcInstance npc, int itemId) { var npcVob = npc.GetUserData()!.Vob; diff --git a/Assets/Gothic-Core/Scripts/Services/World/WayNetService.cs b/Assets/Gothic-Core/Scripts/Services/World/WayNetService.cs index 758ec0477..a1ce67074 100644 --- a/Assets/Gothic-Core/Scripts/Services/World/WayNetService.cs +++ b/Assets/Gothic-Core/Scripts/Services/World/WayNetService.cs @@ -228,9 +228,16 @@ public FreePoint FindNearestFreePoint(Vector3 lookupPosition, string fpNamePart, public DijkstraWaypoint[] FindFastestPath(string startWaypoint, string endWaypoint) { - // Get the start and end waypoints from the DijkstraWaypoints dictionary - var startDijkstraWaypoint = _gameStateService.DijkstraWaypoints[startWaypoint]; - var endDijkstraWaypoint = _gameStateService.DijkstraWaypoints[endWaypoint]; + if (!_gameStateService.DijkstraWaypoints.ContainsKey(startWaypoint)) + { + Logger.LogWarning($"FindFastestPath: start waypoint '{startWaypoint}' not found in WayNet.", LogCat.Npc); + return null; + } + if (!_gameStateService.DijkstraWaypoints.TryGetValue(endWaypoint, out var endDijkstraWaypoint)) + { + Logger.LogWarning($"FindFastestPath: end waypoint '{endWaypoint}' not found in WayNet.", LogCat.Npc); + return null; + } // Initialize the previousNodes dictionary to keep track of the path var previousNodes = new Dictionary(); diff --git a/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCItem/Weapon.prefab b/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCItem/Weapon.prefab index 0ee845892..d4655c452 100644 --- a/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCItem/Weapon.prefab +++ b/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCItem/Weapon.prefab @@ -34,7 +34,7 @@ Transform: m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!96 &4431916770342467661 TrailRenderer: - serializedVersion: 3 + serializedVersion: 4 m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} @@ -53,6 +53,8 @@ TrailRenderer: m_RayTracingAccelStructBuildFlagsOverride: 0 m_RayTracingAccelStructBuildFlags: 1 m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -74,9 +76,11 @@ TrailRenderer: m_AutoUVMaxDistance: 0.5 m_AutoUVMaxAngle: 89 m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_MaskInteraction: 0 m_Time: 0.77 m_PreviewTimeScale: 1 m_Parameters: @@ -135,7 +139,6 @@ TrailRenderer: shadowBias: 0.5 generateLightingData: 0 m_MinVertexDistance: 0.1 - m_MaskInteraction: 0 m_Autodestruct: 0 m_Emitting: 1 m_ApplyActiveColorSpace: 1 @@ -191,6 +194,10 @@ PrefabInstance: propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} + - target: {fileID: 5406461743051542922, guid: b8c776d1865a6014385673931a399ebc, type: 3} + propertyPath: m_PresetInfoIsWorld + value: 1 + objectReference: {fileID: 0} - target: {fileID: 6503575194566409312, guid: b8c776d1865a6014385673931a399ebc, type: 3} propertyPath: m_Name value: Weapon diff --git a/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab b/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab index f4064916a..f0a06aff6 100644 --- a/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab +++ b/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab @@ -17,6 +17,7 @@ GameObject: - component: {fileID: 3792221975768868209} - component: {fileID: 7657019000195239257} - component: {fileID: 6519483080425844375} + - component: {fileID: 8467261965850404422} m_Layer: 0 m_Name: oCNpc m_TagString: GothicVob @@ -150,6 +151,19 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: _injectionStrategy: 2 +--- !u!114 &8467261965850404422 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2202926895728732233} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9c814ba80f440864495cb75d4515235f, type: 3} + m_Name: + m_EditorClassIdentifier: Gothic.VR::Gothic.VR.Adapters.VRNpcLoot + _socketPrefab: {fileID: 5861164055357080340, guid: 2dd29fe4a7e819740af535267b7cc113, type: 3} --- !u!1 &2815092147823170342 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Gothic-VR/Scripts/Adapters/HVROverrides/VRSocket.cs b/Assets/Gothic-VR/Scripts/Adapters/HVROverrides/VRSocket.cs index fb9260e49..fa22e5deb 100644 --- a/Assets/Gothic-VR/Scripts/Adapters/HVROverrides/VRSocket.cs +++ b/Assets/Gothic-VR/Scripts/Adapters/HVROverrides/VRSocket.cs @@ -56,10 +56,21 @@ protected override void OnGrabbed(HVRGrabArgs args) protected override void OnReleased(HVRGrabbable grabbable) { var tmpPreviousParent = _previousParent; - var itemRoot = grabbable.GetComponentInParent(true).transform; + var vobLoader = grabbable.GetComponentInParent(true); + if (vobLoader == null) + { + base.OnReleased(grabbable); + return; + } + var itemRoot = vobLoader.transform; base.OnReleased(grabbable); - + + // Skip re-parenting when the VobLoader was already deactivated (being destroyed via ClearSocketContents). + // OnGrabbableDestroyed can fire during Destroy() after SetActive(false) — re-parenting a destroying GO throws. + if (!vobLoader.gameObject.activeSelf) + return; + grabbable.transform.parent = itemRoot; itemRoot.parent = tmpPreviousParent; } diff --git a/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs b/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs index 219547f3e..8ad6da4bc 100644 --- a/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs +++ b/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs @@ -267,7 +267,14 @@ private void RefillSockets(List inventory) private void SubtractItemFromHand(List inventory, GameObject handItem) { - var item = handItem?.GetComponentInParent().Container.VobAs(); + if (handItem == null) + return; + + var vobLoader = handItem.GetComponentInParent(); + if (vobLoader == null) + return; + + var item = vobLoader.Container?.VobAs(); if (item == null) return; diff --git a/Assets/Gothic-VR/Scripts/Adapters/Player/VRFistAttackAdapter.cs b/Assets/Gothic-VR/Scripts/Adapters/Player/VRFistAttackAdapter.cs index d4ea646e6..48cefc69c 100644 --- a/Assets/Gothic-VR/Scripts/Adapters/Player/VRFistAttackAdapter.cs +++ b/Assets/Gothic-VR/Scripts/Adapters/Player/VRFistAttackAdapter.cs @@ -1,13 +1,13 @@ #if GOTHIC_HVR_INSTALLED using Gothic.Core.Const; -using UnityEngine; +using Gothic.VR.Adapters.Vob.VobItem; namespace Gothic.Core.Adapters.Vob.Item { /// /// Basically a WeaponAttackAdapter for HVR Hands. But some tweaks are needed to fake the object into being an oCVobItem to fight. /// - public class VRFistAttackAdapter : WeaponAttackAdapter + public class VRFistAttackAdapter : VRWeaponAttackAdapter { private void Awake() { diff --git a/Assets/Gothic-VR/Scripts/Adapters/Player/VRPlayerWeaponInteraction.cs b/Assets/Gothic-VR/Scripts/Adapters/Player/VRPlayerWeaponInteraction.cs index 30fd4c8c5..4bb3622e3 100644 --- a/Assets/Gothic-VR/Scripts/Adapters/Player/VRPlayerWeaponInteraction.cs +++ b/Assets/Gothic-VR/Scripts/Adapters/Player/VRPlayerWeaponInteraction.cs @@ -1,8 +1,10 @@ #if GOTHIC_HVR_INSTALLED using System.Collections.Generic; using Gothic.Core.Adapters.Vob; +using Gothic.Core.Extensions; using Gothic.Core.Models.Marvin; using Gothic.Core.Models.Vm; +using Gothic.Core.Services; using Gothic.VR.Models.Vob; using Gothic.VR.Services; using HurricaneVR.Framework.Core; @@ -16,6 +18,7 @@ namespace Gothic.VR.Adapters.Player public class VRPlayerWeaponInteraction : MonoBehaviour, IMarvinPropertyCollector { [Inject] private readonly VRWeaponService _weaponService; + [Inject] private readonly GameStateService _gameStateService; // FIXME - All of these values will be dynamic in the future. Based on skill level and weapon type. @@ -47,11 +50,14 @@ public void OnGrabbed(HVRGrabberBase hand, HVRGrabbable item) if (vobContainer == null || vobContainer.Vob.Type != VirtualObjectType.oCItem) return; + var itemInstance = vobContainer.GetItemInstance(); + // We currently handle melee weapons only. - if (vobContainer.GetItemInstance()!.MainFlag != (int)VmGothicEnums.ItemFlags.ItemKatNf) + if (itemInstance!.MainFlag != (int)VmGothicEnums.ItemFlags.ItemKatNf) return; _weaponService.OnGrabbed(((HVRHandGrabber)hand).HandSide, vobContainer, GetWeaponPhysicsConfig()); + SyncEquippedWeaponToHero(itemInstance, equip: true); } public void OnReleased(HVRGrabberBase hand, HVRGrabbable item) @@ -63,11 +69,30 @@ public void OnReleased(HVRGrabberBase hand, HVRGrabbable item) if (vobContainer == null || vobContainer.Vob.Type != VirtualObjectType.oCItem) return; + var itemInstance = vobContainer.GetItemInstance(); + // Currently we handle melee weapons only. - if (vobContainer.GetItemInstance()!.MainFlag != (int)VmGothicEnums.ItemFlags.ItemKatNf) + if (itemInstance!.MainFlag != (int)VmGothicEnums.ItemFlags.ItemKatNf) return; _weaponService.OnReleased(((HVRHandGrabber)hand).HandSide, GetWeaponPhysicsConfig()); + SyncEquippedWeaponToHero(itemInstance, equip: false); + } + + private void SyncEquippedWeaponToHero(ZenKit.Daedalus.ItemInstance itemInstance, bool equip) + { + var hero = _gameStateService.GothicVm?.GlobalHero as ZenKit.Daedalus.NpcInstance; + if (hero == null) + return; + + var heroContainer = hero.GetUserData(); + if (heroContainer == null) + return; + + var equippedItems = heroContainer.Props.EquippedItems; + equippedItems.RemoveAll(i => i.MainFlag == itemInstance.MainFlag); + if (equip) + equippedItems.Add(itemInstance); } /// diff --git a/Assets/Gothic-VR/Scripts/Adapters/VRNpc.cs b/Assets/Gothic-VR/Scripts/Adapters/VRNpc.cs index 80dbcd99a..f0e0e1eac 100644 --- a/Assets/Gothic-VR/Scripts/Adapters/VRNpc.cs +++ b/Assets/Gothic-VR/Scripts/Adapters/VRNpc.cs @@ -4,10 +4,10 @@ using Gothic.Core.Models.Container; using Gothic.Core.Models.Vm; using Gothic.Core.Services; +using Gothic.Core.Services.Config; using Gothic.Core.Services.Npc; using Gothic.Core; using Gothic.Core.Extensions; -using Gothic.Core.Const; using HurricaneVR.Framework.Core; using HurricaneVR.Framework.Core.Grabbers; using Reflex.Attributes; @@ -21,16 +21,27 @@ public class VRNpc : MonoBehaviour [Inject] private readonly GameStateService _gameStateService; [Inject] private readonly DialogService _dialogService; [Inject] private readonly NpcAiService _npcAiService; + [Inject] private readonly ConfigService _configService; private NpcContainer _npcData; + private VRNpcLoot _npcLoot; private void Awake() { _npcData = GetComponentInParent().Npc.GetUserData(); + _npcLoot = GetComponent(); } public void OnGrabbed(HVRGrabberBase grabber, HVRGrabbable grabbable) { + var isDead = _npcData.Props.BodyState == VmGothicEnums.BodyState.BsDead; + + if (isDead && _configService.Dev.EnableNpcLooting && _npcLoot != null) + { + _npcLoot.Toggle(_npcData); + return; + } + if (_gameStateService.Dialogs.IsInDialog) { _dialogService.SkipCurrentDialogLine(_npcData.Props); diff --git a/Assets/Gothic-VR/Scripts/Adapters/VRNpcLoot.cs b/Assets/Gothic-VR/Scripts/Adapters/VRNpcLoot.cs new file mode 100644 index 000000000..8181fe115 --- /dev/null +++ b/Assets/Gothic-VR/Scripts/Adapters/VRNpcLoot.cs @@ -0,0 +1,388 @@ +#if GOTHIC_HVR_INSTALLED +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Assets.HurricaneVR.Framework.Shared.Utilities; +using Gothic.Core; +using Gothic.Core.Adapters.Npc; +using Gothic.Core.Adapters.Vob; +using Gothic.Core.Extensions; +using Gothic.Core.Manager; +using Gothic.Core.Models.Container; +using Gothic.Core.Models.Vm; +using Gothic.Core.Models.Vob; +using Gothic.Core.Services; +using Gothic.Core.Services.Caches; +using Gothic.Core.Services.Npc; +using Gothic.Core.Services.Vobs; +using Gothic.VR.Services; +using HurricaneVR.Framework.Core; +using HurricaneVR.Framework.Core.Grabbers; +using HurricaneVR.Framework.Core.Sockets; +using Reflex.Attributes; +using TMPro; +using UnityEngine; +using ZenKit.Daedalus; +using ZenKit.Vobs; + +namespace Gothic.VR.Adapters +{ + public class VRNpcLoot : MonoBehaviour + { + private const int MaxVisibleSlots = 5; + private const float RefreshDelay = 0.6f; + private const float SocketHeight = 0.8f; + private const float SocketSpacing = 0.22f; + + [SerializeField] private GameObject _socketPrefab; + + [Inject] private readonly NpcInventoryService _npcInventoryService; + [Inject] private readonly VobService _vobService; + [Inject] private readonly AudioService _audioService; + [Inject] private readonly VmCacheService _vmCacheService; + [Inject] private readonly VRWeaponService _vrWeaponService; + [Inject] private readonly GameStateService _gameStateService; + + private NpcContainer _npcContainer; + private NpcLoader _npcLoader; + private readonly List _sockets = new(); + private readonly List _socketRoots = new(); + private bool _isOpen; + private bool _tempIgnoreSocketing; + private Coroutine _pendingRefresh; + + private readonly struct LootEntry + { + public readonly ContentItem Item; + public readonly bool IsEquipped; + + public LootEntry(ContentItem item, bool isEquipped) + { + Item = item; + IsEquipped = isEquipped; + } + } + + private void Awake() + { + _npcLoader = GetComponentInParent(); + } + + public void Toggle(NpcContainer npc) + { + if (_isOpen) + Close(); + else + Open(npc); + } + + public void Open(NpcContainer npc) + { + _npcContainer = npc; + _isOpen = true; + PlayOpenSound(); + CreateSockets(); + StartCoroutine(FillSockets()); + } + + public void Close() + { + _isOpen = false; + if (_pendingRefresh != null) + { + StopCoroutine(_pendingRefresh); + _pendingRefresh = null; + } + StartCoroutine(CloseSockets()); + } + + private void PlayOpenSound() + { + var clip = _audioService.CreateAudioClip(_audioService.InvOpen.File); + if (clip == null) + return; + + var audioSource = GetComponentInChildren(); + audioSource?.PlayOneShot(clip); + } + + private void CreateSockets() + { + var totalWidth = (MaxVisibleSlots - 1) * SocketSpacing; + + for (var i = 0; i < MaxVisibleSlots; i++) + { + var socketGo = Instantiate(_socketPrefab, transform); + var xOffset = -totalWidth / 2f + i * SocketSpacing; + socketGo.transform.localPosition = new Vector3(xOffset, SocketHeight, 0f); + socketGo.transform.localRotation = Quaternion.identity; + socketGo.transform.localScale = Vector3.one * 1.6f; + + var socket = socketGo.GetComponentInChildren(); + socket.Released.AddListener(OnItemTakenFromLoot); + + _socketRoots.Add(socketGo); + _sockets.Add(socket); + } + } + + private void DestroySockets() + { + foreach (var root in _socketRoots) + { + if (root != null) + Destroy(root); + } + _socketRoots.Clear(); + _sockets.Clear(); + } + + private List BuildLootList() + { + var result = new List(); + var equippedNames = new HashSet(StringComparer.OrdinalIgnoreCase); + + // Equipped weapons first (melee + ranged) — shown with [E] badge and GO removal on take + foreach (var equipped in _npcContainer.Props.EquippedItems) + { + var mainFlag = (VmGothicEnums.ItemFlags)equipped.MainFlag; + if (mainFlag != VmGothicEnums.ItemFlags.ItemKatNf && mainFlag != VmGothicEnums.ItemFlags.ItemKatFf) + continue; + + var symbolName = _gameStateService.GothicVm.GetSymbolByIndex(equipped.Index)?.Name; + if (symbolName == null) + continue; + + equippedNames.Add(symbolName); + result.Add(new LootEntry(new ContentItem(symbolName, 1), isEquipped: true)); + } + + // All inventory items: skip armor, skip equipped weapons already listed above + foreach (var invItem in _npcInventoryService.GetAllInventoryItems(_npcContainer.Instance)) + { + if (equippedNames.Contains(invItem.Name)) + continue; + + var itemData = _vmCacheService.TryGetItemData(invItem.Name); + if (itemData != null) + { + var cat = ((VmGothicEnums.ItemFlags)itemData.MainFlag).ToInventoryCategory(); + if (cat == VmGothicEnums.InvCats.InvArmor) + continue; + } + + result.Add(new LootEntry(invItem, isEquipped: false)); + } + + return result; + } + + private IEnumerator FillSockets() + { + yield return PopulateSockets(clearFirst: false); + } + + private IEnumerator ClearAndRefill() + { + yield return PopulateSockets(clearFirst: true); + } + + private IEnumerator PopulateSockets(bool clearFirst) + { + _tempIgnoreSocketing = true; + _vrWeaponService.DrawSoundsActive = false; + + if (clearFirst) + { + ClearSocketContents(); + yield return null; + } + + foreach (var entry in BuildLootList().Take(MaxVisibleSlots)) + { + var vobContainer = _vobService.CreateItem(new Item + { + Name = entry.Item.Name, + Visual = new VisualMesh(), + Instance = entry.Item.Name, + Amount = entry.Item.Amount + }); + + vobContainer.Go.GetComponentInChildren().isKinematic = false; + + yield return null; + + var grabbable = vobContainer.Go.GetComponentInChildren(true); + var freeSocket = _sockets.FirstOrDefault(s => !s.IsGrabbing); + if (freeSocket != null) + { + freeSocket.TryGrab(grabbable, true, true); + if (entry.IsEquipped) + AddEquippedLabel(GetSocketRoot(freeSocket)); + } + } + + yield return null; + _tempIgnoreSocketing = false; + _vrWeaponService.DrawSoundsActive = true; + } + + private void ClearSocketContents() + { + // Remove equipped labels before clearing items + foreach (var root in _socketRoots) + { + var label = root.transform.Find("EquippedLabel"); + if (label != null) + Destroy(label.gameObject); + } + + foreach (var socket in _sockets) + { + if (!socket.IsGrabbing) + continue; + + var heldRoot = socket.HeldObject.transform.parent.gameObject; + socket.ForceRelease(); + heldRoot.SetActive(false); + this.ExecuteNextUpdate(() => Destroy(heldRoot)); + } + } + + private IEnumerator CloseSockets() + { + _tempIgnoreSocketing = true; + _vrWeaponService.DrawSoundsActive = false; + + ClearSocketContents(); + yield return null; + + DestroySockets(); + _tempIgnoreSocketing = false; + _vrWeaponService.DrawSoundsActive = true; + } + + public void OnItemTakenFromLoot(HVRGrabberBase grabber, HVRGrabbable grabbable) + { + if (_tempIgnoreSocketing) + return; + + var vobLoader = grabbable.GetComponentInParent(); + if (vobLoader?.Container == null) + return; + + var item = vobLoader.Container.VobAs(); + if (item == null) + return; + + var itemName = vobLoader.Container.Vob.Name; + var itemData = _vmCacheService.TryGetItemData(itemName); + if (itemData == null) + return; + + // If this was an equipped weapon: destroy the mesh from NPC body and remove from equipped list + var equippedMatch = _npcContainer.Props.EquippedItems + .FirstOrDefault(e => string.Equals( + _gameStateService.GothicVm.GetSymbolByIndex(e.Index)?.Name, + itemName, + StringComparison.OrdinalIgnoreCase)); + if (equippedMatch != null) + { + _npcContainer.Props.EquippedItems.Remove(equippedMatch); + var weaponGo = FindEquippedWeaponGo(equippedMatch); + if (weaponGo != null) + Destroy(weaponGo); + } + + _npcInventoryService.ExtRemoveInvItems(_npcContainer.Instance, itemData.Index, item.Amount); + + if (_pendingRefresh != null) + StopCoroutine(_pendingRefresh); + _pendingRefresh = StartCoroutine(RefreshAfterDelay()); + } + + private IEnumerator RefreshAfterDelay() + { + yield return new WaitForSeconds(RefreshDelay); + _pendingRefresh = null; + StartCoroutine(ClearAndRefill()); + } + + /// + /// Finds the weapon GameObject in the NPC skeleton using the same slot mapping as NpcWeaponMeshBuilder. + /// Tries the holster slot first, then ZS_RIGHTHAND as fallback (for NPCs that died mid-draw). + /// + private GameObject FindEquippedWeaponGo(ItemInstance equipped) + { + if (_npcLoader == null) + return null; + + var mainFlag = (VmGothicEnums.ItemFlags)equipped.MainFlag; + var flags = (VmGothicEnums.ItemFlags)equipped.Flags; + var npcRoot = _npcLoader.gameObject; + + string holsterSlot; + switch (mainFlag) + { + case VmGothicEnums.ItemFlags.ItemKatNf: + switch (flags) + { + case VmGothicEnums.ItemFlags.Item2HdAxe: + case VmGothicEnums.ItemFlags.Item2HdSwd: + holsterSlot = "ZS_LONGSWORD"; + break; + default: + holsterSlot = "ZS_SWORD"; + break; + } + break; + case VmGothicEnums.ItemFlags.ItemKatFf: + holsterSlot = flags == VmGothicEnums.ItemFlags.ItemCrossbow ? "ZS_CROSSBOW" : "ZS_BOW"; + break; + default: + return null; + } + + // Try holster slot, fall back to drawn position + foreach (var slotName in new[] { holsterSlot, "ZS_RIGHTHAND" }) + { + var slotGo = npcRoot.FindChildRecursively(slotName); + if (slotGo != null && slotGo.transform.childCount > 0) + return slotGo.transform.GetChild(0).gameObject; + } + + return null; + } + + private GameObject GetSocketRoot(HVRSocket socket) + { + var idx = _sockets.IndexOf(socket); + return idx >= 0 && idx < _socketRoots.Count ? _socketRoots[idx] : null; + } + + private static void AddEquippedLabel(GameObject socketRoot) + { + if (socketRoot == null) + return; + + var labelGo = new GameObject("EquippedLabel"); + labelGo.transform.SetParent(socketRoot.transform, false); + labelGo.transform.localPosition = new Vector3(0f, 0.09f, 0f); + labelGo.transform.localScale = Vector3.one * 0.013f; + + var tmp = labelGo.AddComponent(); + // Assign font immediately after AddComponent to suppress repeated OnPreRenderObject warnings. + var font = Resources.Load("Fonts & Materials/LiberationSans SDF"); + if (font != null) + tmp.font = font; + tmp.text = "[E]"; + tmp.fontSize = 12; + tmp.color = new Color(1f, 0.65f, 0f, 1f); + tmp.alignment = TextAlignmentOptions.Center; + tmp.textWrappingMode = TextWrappingModes.NoWrap; + tmp.fontStyle = FontStyles.Bold; + } + } +} +#endif diff --git a/Assets/Gothic-VR/Scripts/Adapters/VRNpcLoot.cs.meta b/Assets/Gothic-VR/Scripts/Adapters/VRNpcLoot.cs.meta new file mode 100644 index 000000000..40fe5bc38 --- /dev/null +++ b/Assets/Gothic-VR/Scripts/Adapters/VRNpcLoot.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9c814ba80f440864495cb75d4515235f \ No newline at end of file diff --git a/Assets/Gothic-VR/Scripts/Adapters/Vob/Container/VRVobContainerSocketInventory.cs b/Assets/Gothic-VR/Scripts/Adapters/Vob/Container/VRVobContainerSocketInventory.cs index 2f3f7c72d..5c43a6669 100644 --- a/Assets/Gothic-VR/Scripts/Adapters/Vob/Container/VRVobContainerSocketInventory.cs +++ b/Assets/Gothic-VR/Scripts/Adapters/Vob/Container/VRVobContainerSocketInventory.cs @@ -1,5 +1,7 @@ #if GOTHIC_HVR_INSTALLED using Gothic.Core.Adapters.Vob; +using Gothic.Core.Models.Container; +using Gothic.Core.Models.Vm; using Gothic.VR.Services; using HurricaneVR.Framework.Core; using HurricaneVR.Framework.Core.Grabbers; @@ -14,17 +16,29 @@ namespace Gothic.VR.Adapters.Vob.Container public class VRVobContainerSocketInventory : MonoBehaviour { [Inject] private VRWeaponService _vrWeaponService; - + public void OnBeforeGrabbed(HVRGrabberBase grabber, HVRGrabbable grabbable) { - Debug.Log("Undraw"); - _vrWeaponService.PlayUndrawSound(grabbable.GetComponentInParent().Container); + var container = grabbable.GetComponentInParent()?.Container; + if (!IsWeapon(container)) + return; + _vrWeaponService.PlayUndrawSound(container); } public void OnReleased(HVRGrabberBase grabber, HVRGrabbable grabbable) { - Debug.Log("Draw"); - _vrWeaponService.PlayDrawSound(grabbable.GetComponentInParent().Container); + var container = grabbable.GetComponentInParent()?.Container; + if (!IsWeapon(container)) + return; + _vrWeaponService.PlayDrawSound(container); + } + + private bool IsWeapon(VobContainer container) + { + var itemInstance = container?.GetItemInstance(); + if (itemInstance == null) return false; + var flag = (VmGothicEnums.ItemFlags)itemInstance.MainFlag; + return flag is VmGothicEnums.ItemFlags.ItemKatNf or VmGothicEnums.ItemFlags.ItemKatFf; } } } diff --git a/Assets/Gothic-VR/Scripts/Adapters/Vob/LockPicking/VRContainerDoorPickingInteraction.cs b/Assets/Gothic-VR/Scripts/Adapters/Vob/LockPicking/VRContainerDoorPickingInteraction.cs index b4b5710d0..528706556 100644 --- a/Assets/Gothic-VR/Scripts/Adapters/Vob/LockPicking/VRContainerDoorPickingInteraction.cs +++ b/Assets/Gothic-VR/Scripts/Adapters/Vob/LockPicking/VRContainerDoorPickingInteraction.cs @@ -67,7 +67,10 @@ private void Start() // Stop this handler if the object is already unlocked. if (!_isLocked) + { gameObject.SetActive(false); + return; + } StartCoroutine(StartDelayed()); } diff --git a/Assets/Gothic-VR/Scripts/Adapters/Vob/VobItem/VRMouth.cs b/Assets/Gothic-VR/Scripts/Adapters/Vob/VobItem/VRMouth.cs index 9cd5ce671..91e1d688b 100644 --- a/Assets/Gothic-VR/Scripts/Adapters/Vob/VobItem/VRMouth.cs +++ b/Assets/Gothic-VR/Scripts/Adapters/Vob/VobItem/VRMouth.cs @@ -14,6 +14,7 @@ using UnityEngine; using ZenKit; using ZenKit.Daedalus; +using ZenKit.Vobs; using Logger = Gothic.Core.Logging.Logger; namespace Gothic.VR.Adapters.Vob.VobItem @@ -60,8 +61,8 @@ private void OnTriggerEnter(Collider other) GameObject rootGo = go; var vobLoaderComp = go.GetComponentInParent(); - // LAB - Fallback as objects aren't LazyLoaded in here. - if (vobLoaderComp != null) + // Only use the VobLoader root if it belongs to this specific item (not a parent chest/container). + if (vobLoaderComp != null && vobLoaderComp.Container.Vob.Type == VirtualObjectType.oCItem) rootGo = vobLoaderComp.gameObject; StartCoroutine(ConsumeObject(rootGo, clip, destroyTime)); diff --git a/Assets/Gothic-VR/Scripts/Adapters/Vob/VobItem/VRWeaponAttackAdapter.cs b/Assets/Gothic-VR/Scripts/Adapters/Vob/VobItem/VRWeaponAttackAdapter.cs new file mode 100644 index 000000000..62d6ef39f --- /dev/null +++ b/Assets/Gothic-VR/Scripts/Adapters/Vob/VobItem/VRWeaponAttackAdapter.cs @@ -0,0 +1,119 @@ +#if GOTHIC_HVR_INSTALLED +using Gothic.Core; +using Gothic.Core.Adapters.Npc; +using Gothic.Core.Adapters.Vob; +using Gothic.Core.Const; +using Gothic.Core.Logging; +using Gothic.Core.Models.Container; +using Gothic.VR.Services; +using Reflex.Attributes; +using UnityEngine; +using ZenKit.Daedalus; +using Logger = Gothic.Core.Logging.Logger; + +namespace Gothic.VR.Adapters.Vob.VobItem +{ + /// + /// The validity check for a hit requires answering: "Is this attack currently active?" + /// That state lives on the attacker (DEF_OPT_FRAME window, DEF_HIT_LIMB, "already connected" flag). + /// If we put the logic on the receiver, we must reach across to the attacker's component to get that state + /// every time any contact happens. If we put it on the attacker, all required state is already local. + /// + public class VRWeaponAttackAdapter : MonoBehaviour + { + [Inject] private readonly VRWeaponService _vrWeaponService; + + private VobContainer _weaponVobContainer; + private NpcContainer _targetNpcContainer; + + private void Start() + { + // Get reference to this weapon's VobContainer if it exists + var vobLoader = GetComponentInParent(); + if (vobLoader != null) + { + _weaponVobContainer = vobLoader.Container; + } + } + + /// + /// TODO - We need to handle multiple hitboxes on the same target (e.g. head vs body) and ensure we don't apply multiple hits from one attack. + /// TODO - figure out how to do fists + /// TODO - figure out how to do NPC-to-NPC hits (e.g. monster attacking hero or monster attacking monster) + /// Handles collision between weapon and potential targets (NPCs/Monsters). + /// Validates the hit and fires FightHit event if conditions are met. + /// + private void OnTriggerEnter(Collider other) + { + if (other.gameObject.layer != Constants.VobHitbox) + { + Logger.LogWarning($"[WeaponAttackAdapter] Wrong layer: {LayerMask.LayerToName(other.gameObject.layer)}", LogCat.Fight); + return; + } + + Logger.Log($"[WeaponAttackAdapter] Collision with VobHitbox: {other.gameObject.name}", LogCat.Fight); + + // Try to get the target NPC/Monster from the hitbox + var targetNpcLoader = other.GetComponentInParent(); + if (targetNpcLoader == null) + { + Logger.LogWarning($"[WeaponAttackAdapter] No NpcLoader found", LogCat.Fight); + return; + } + + var targetNpcContainer = targetNpcLoader.Container; + if (targetNpcContainer == null) + { + Logger.LogWarning("[WeaponAttackAdapter] No NpcContainer found", LogCat.Fight); + return; + } + + Logger.Log($"[WeaponAttackAdapter] Target: {targetNpcContainer.Instance.GetName(NpcNameSlot.Slot0)}", LogCat.Fight); + + // Try to fire the hit through VR weapon service if available + if (TryFireHitViaVRWeaponService(targetNpcContainer)) + { + Logger.Log($"[WeaponAttackAdapter] *** HIT FIRED (VR)", LogCat.Fight); + return; + } + + Logger.LogWarning("[WeaponAttackAdapter] Failed to fire hit (not in attack window or VR unavailable)", LogCat.Fight); + // TODO - Add support for flat-screen weapon hits and NPC-to-NPC hits here + } + + private void OnTriggerExit(Collider other) + { + if (other.gameObject.layer != Constants.VobHitbox) + return; + } + + private bool TryFireHitViaVRWeaponService(NpcContainer targetNpcContainer) + { + if (_vrWeaponService == null) + { + Logger.LogWarning("[WeaponAttackAdapter] VRWeaponService not injected (flat-screen mode?)", LogCat.Fight); + return false; + } + + var isInAttackWindow = _vrWeaponService.IsWeaponInAttackWindow(_weaponVobContainer); + Logger.Log($"[WeaponAttackAdapter] IsInAttackWindow: {isInAttackWindow}", LogCat.Fight); + if (!isInAttackWindow) + { + Logger.LogWarning("[WeaponAttackAdapter] Weapon not in attack window", LogCat.Fight); + return false; + } + + var attacker = _vrWeaponService.GetWeaponOwner(_weaponVobContainer); + if (attacker == null) + { + Logger.LogWarning("[WeaponAttackAdapter] No attacker found for weapon", LogCat.Fight); + return false; + } + + Logger.Log($"[WeaponAttackAdapter] FightHit event: {attacker.Instance.GetName(NpcNameSlot.Slot0)} → {targetNpcContainer.Instance.GetName(NpcNameSlot.Slot0)}", LogCat.Fight); + GlobalEventDispatcher.FightHit.Invoke(attacker, targetNpcContainer, transform.position); + return true; + } + } +} +#endif diff --git a/Assets/Gothic-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs.meta b/Assets/Gothic-VR/Scripts/Adapters/Vob/VobItem/VRWeaponAttackAdapter.cs.meta similarity index 100% rename from Assets/Gothic-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs.meta rename to Assets/Gothic-VR/Scripts/Adapters/Vob/VobItem/VRWeaponAttackAdapter.cs.meta diff --git a/Assets/Gothic-VR/Scripts/Services/VRPlayerService.cs b/Assets/Gothic-VR/Scripts/Services/VRPlayerService.cs index b9ba5237c..754090e60 100644 --- a/Assets/Gothic-VR/Scripts/Services/VRPlayerService.cs +++ b/Assets/Gothic-VR/Scripts/Services/VRPlayerService.cs @@ -45,6 +45,9 @@ public void SetGrab(HVRGrabberBase grabber, HVRGrabbable grabbable) else handGrabber = grabber as HVRHandGrabber; + if (handGrabber == null) + return; + if (handGrabber.IsLeftHand) { // If we did remote grabbing, this function is called twice (remote grabber+hand grabber). @@ -70,7 +73,8 @@ public void SetGrab(HVRGrabberBase grabber, HVRGrabbable grabbable) // Otherwise alter inventory count var vobItem = grabbable.GetComponentInParent().Container.VobAs(); - _playerService.AddItem(vobItem.Instance, vobItem.Amount); + var instanceName = !string.IsNullOrEmpty(vobItem.Instance) ? vobItem.Instance : vobItem.Name; + _playerService.AddItem(instanceName, vobItem.Amount); } public void UnsetGrab(HVRGrabberBase grabber, HVRGrabbable grabbable) @@ -83,6 +87,9 @@ public void UnsetGrab(HVRGrabberBase grabber, HVRGrabbable grabbable) else handGrabber = grabber as HVRHandGrabber; + if (handGrabber == null) + return; + if (handGrabber.IsLeftHand) { // If we did remote grabbing, this function is called twice (remote grabber+hand grabber). @@ -108,7 +115,8 @@ public void UnsetGrab(HVRGrabberBase grabber, HVRGrabbable grabbable) // Otherwise alter inventory count var vobItem = grabbable.GetComponentInParent().Container.VobAs(); - _playerService.RemoveItem(vobItem.Instance, vobItem.Amount); + var instanceName = !string.IsNullOrEmpty(vobItem.Instance) ? vobItem.Instance : vobItem.Name; + _playerService.RemoveItem(instanceName, vobItem.Amount); } public HVRController GetHand(HVRHandSide side) diff --git a/ProjectSettings/DynamicsManager.asset b/ProjectSettings/DynamicsManager.asset index b55c6c701..c81ca078f 100644 --- a/ProjectSettings/DynamicsManager.asset +++ b/ProjectSettings/DynamicsManager.asset @@ -17,7 +17,7 @@ PhysicsManager: m_EnableAdaptiveForce: 0 m_ClothInterCollisionDistance: 0 m_ClothInterCollisionStiffness: 0 - m_LayerCollisionMatrix: 6fefffff09e0cfffedefffffffffffff08e0cfff0de0cfff4dfaffff0ce2efff0de6cfffcdefffff0de7ffff4deaffff48e0cfffffffffffffffffffffffffffffffffffffffffffffffffffffffffff4deeffffcdeeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + m_LayerCollisionMatrix: 6fefffffc9e0cfffedefffffffffffff08e0cfff0de0cfff4ffaffff0ee2efff0de6cfffcdefffff0de7ffff4deaffff48e0cfffffffffffffffffffffffffffffffffffffffffffffffffffffffffff4deeffffcdeeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff m_SimulationMode: 0 m_AutoSyncTransforms: 0 m_ReuseCollisionCallbacks: 1