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/Resources/FontAsset/LiberationSans SDF empty.asset b/Assets/Gothic-Core/Resources/FontAsset/LiberationSans SDF empty.asset index 858a10bdb..c111263b5 100644 --- a/Assets/Gothic-Core/Resources/FontAsset/LiberationSans SDF empty.asset +++ b/Assets/Gothic-Core/Resources/FontAsset/LiberationSans SDF empty.asset @@ -37,6 +37,25 @@ MonoBehaviour: m_TabWidth: 29 m_Material: {fileID: 5152873236376385906} m_SourceFontFileGUID: e3265ab4bf004d28a9537516768c1c75 + m_CreationSettings: + sourceFontFileName: + sourceFontFileGUID: e3265ab4bf004d28a9537516768c1c75 + faceIndex: 0 + pointSizeSamplingMode: 0 + pointSize: 104 + padding: 8 + paddingMode: 1 + packingMode: 0 + atlasWidth: 512 + atlasHeight: 256 + characterSetSelectionMode: 5 + characterSequence: + referencedFontAssetGUID: + referencedTextAssetGUID: + fontStyle: 0 + fontStyleModifier: 0 + renderMode: 4165 + includeFontFeatures: 0 m_SourceFontFile: {fileID: 0} m_SourceFontFilePath: m_AtlasPopulationMode: 0 @@ -67,25 +86,6 @@ MonoBehaviour: m_MarkToMarkAdjustmentRecords: [] m_ShouldReimportFontFeatures: 0 m_FallbackFontAssetTable: [] - m_CreationSettings: - sourceFontFileName: - sourceFontFileGUID: e3265ab4bf004d28a9537516768c1c75 - faceIndex: 0 - pointSizeSamplingMode: 0 - pointSize: 104 - padding: 8 - paddingMode: 1 - packingMode: 0 - atlasWidth: 512 - atlasHeight: 256 - characterSetSelectionMode: 5 - characterSequence: - referencedFontAssetGUID: - referencedTextAssetGUID: - fontStyle: 0 - fontStyleModifier: 0 - renderMode: 4165 - includeFontFeatures: 0 m_FontWeightTable: - regularTypeface: {fileID: 0} italicTypeface: {fileID: 0} @@ -151,17 +151,15 @@ Texture2D: m_ImageContentsHash: serializedVersion: 2 Hash: 00000000000000000000000000000000 - m_ForcedFallbackFormat: 4 - m_DownscaleFallback: 0 m_IsAlphaChannelOptional: 0 - serializedVersion: 2 + serializedVersion: 4 m_Width: 512 m_Height: 256 m_CompleteImageSize: 131072 m_MipsStripped: 0 m_TextureFormat: 1 m_MipCount: 1 - m_IsReadable: 0 + m_IsReadable: 1 m_IsPreProcessed: 0 m_IgnoreMipmapLimit: 0 m_MipmapLimitGroupName: @@ -297,3 +295,4 @@ Material: - _SpecularColor: {r: 1, g: 1, b: 1, a: 1} - _UnderlayColor: {r: 0, g: 0, b: 0, a: 0.5} m_BuildTextureStacks: [] + m_AllowLocking: 1 diff --git a/Assets/Gothic-Core/Resources/Prefabs/UI/StatusBars/StatusBar.prefab b/Assets/Gothic-Core/Resources/Prefabs/UI/StatusBars/StatusBar.prefab index b7cdcf879..8a7cc1894 100644 --- a/Assets/Gothic-Core/Resources/Prefabs/UI/StatusBars/StatusBar.prefab +++ b/Assets/Gothic-Core/Resources/Prefabs/UI/StatusBars/StatusBar.prefab @@ -225,7 +225,7 @@ Canvas: m_OverridePixelPerfect: 0 m_SortingBucketNormalizedSize: 0 m_VertexColorAlwaysGammaSpace: 1 - m_AdditionalShaderChannelsFlag: 0 + m_AdditionalShaderChannelsFlag: 25 m_UpdateRectTransformForStandalone: 0 m_SortingLayerID: 0 m_SortingOrder: 0 diff --git a/Assets/Gothic-Core/Scenes/MainMenu.unity b/Assets/Gothic-Core/Scenes/MainMenu.unity index 23dcdfba3..e682b6026 100644 --- a/Assets/Gothic-Core/Scenes/MainMenu.unity +++ b/Assets/Gothic-Core/Scenes/MainMenu.unity @@ -287,7 +287,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 775636288} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -300,6 +300,8 @@ MeshRenderer: m_RayTracingAccelStructBuildFlagsOverride: 0 m_RayTracingAccelStructBuildFlags: 1 m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -321,9 +323,11 @@ MeshRenderer: 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_AdditionalVertexStreams: {fileID: 0} --- !u!33 &775636291 MeshFilter: @@ -388,7 +392,7 @@ Transform: m_GameObject: {fileID: 783007466} serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: -15.407947, y: 2.073237, z: -29.998268} + m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: @@ -467,6 +471,8 @@ MeshRenderer: m_RayTracingAccelStructBuildFlagsOverride: 0 m_RayTracingAccelStructBuildFlags: 1 m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -488,9 +494,11 @@ MeshRenderer: 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_AdditionalVertexStreams: {fileID: 0} --- !u!33 &791865609 MeshFilter: @@ -565,17 +573,23 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 474bcb49853aa07438625e644c072ee6, type: 3} m_Name: m_EditorClassIdentifier: - m_Version: 3 m_UsePipelineSettings: 1 m_AdditionalLightsShadowResolutionTier: 2 - m_LightLayerMask: 1 - m_RenderingLayers: 1 m_CustomShadowLayers: 0 - m_ShadowLayerMask: 1 - m_ShadowRenderingLayers: 1 m_LightCookieSize: {x: 1, y: 1} m_LightCookieOffset: {x: 0, y: 0} m_SoftShadowQuality: 0 + m_RenderingLayersMask: + serializedVersion: 0 + m_Bits: 1 + m_ShadowRenderingLayersMask: + serializedVersion: 0 + m_Bits: 1 + m_Version: 4 + m_LightLayerMask: 1 + m_ShadowLayerMask: 1 + m_RenderingLayers: 1 + m_ShadowRenderingLayers: 1 --- !u!108 &1308095012 Light: m_ObjectHideFlags: 0 @@ -584,14 +598,14 @@ Light: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1308095009} m_Enabled: 1 - serializedVersion: 11 + serializedVersion: 12 m_Type: 1 m_Color: {r: 1, g: 1, b: 1, a: 1} m_Intensity: 1 m_Range: 10 m_SpotAngle: 30 m_InnerSpotAngle: 21.80208 - m_CookieSize: 10 + m_CookieSize2D: {x: 10, y: 10} m_Shadows: m_Type: 0 m_Resolution: -1 @@ -667,13 +681,13 @@ RectTransform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 2031667426} - m_LocalRotation: {x: -0.7071068, y: -0, z: -0, w: 0.7071068} - m_LocalPosition: {x: 0, y: 0, z: 0.02} - m_LocalScale: {x: -40, y: 8, z: 32} + m_LocalRotation: {x: 0, y: 0.7071068, z: 0.7071068, w: 0} + m_LocalPosition: {x: 0, y: 0, z: 10} + m_LocalScale: {x: 2, y: 1, z: 2} m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 783007468} - m_LocalEulerAnglesHint: {x: -90, y: 0, z: 0} + m_LocalEulerAnglesHint: {x: -90, y: 180, z: 0} m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5} m_AnchoredPosition: {x: 0, y: 0} @@ -721,6 +735,8 @@ MeshRenderer: m_RayTracingAccelStructBuildFlagsOverride: 0 m_RayTracingAccelStructBuildFlags: 1 m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: [] @@ -741,9 +757,11 @@ MeshRenderer: 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_AdditionalVertexStreams: {fileID: 0} --- !u!33 &2031667430 MeshFilter: @@ -761,10 +779,38 @@ PrefabInstance: serializedVersion: 3 m_TransformParent: {fileID: 505140342} m_Modifications: + - target: {fileID: 514510020229674838, guid: a0af29d97abd24d4c8382b6da4017660, type: 3} + propertyPath: m_fontSize + value: 44.75 + objectReference: {fileID: 0} + - target: {fileID: 819469628458212509, guid: a0af29d97abd24d4c8382b6da4017660, type: 3} + propertyPath: m_fontSize + value: 44.75 + objectReference: {fileID: 0} + - target: {fileID: 1099075632631060353, guid: a0af29d97abd24d4c8382b6da4017660, type: 3} + propertyPath: m_fontSize + value: 44.75 + objectReference: {fileID: 0} + - target: {fileID: 1395391690268023265, guid: a0af29d97abd24d4c8382b6da4017660, type: 3} + propertyPath: m_fontSize + value: 44.75 + objectReference: {fileID: 0} + - target: {fileID: 1684002802907956876, guid: a0af29d97abd24d4c8382b6da4017660, type: 3} + propertyPath: m_fontSize + value: 44.75 + objectReference: {fileID: 0} - target: {fileID: 1879303798920566507, guid: a0af29d97abd24d4c8382b6da4017660, type: 3} propertyPath: m_Name value: Tutorials objectReference: {fileID: 0} + - target: {fileID: 1894576811620697740, guid: a0af29d97abd24d4c8382b6da4017660, type: 3} + propertyPath: m_fontSize + value: 44.75 + objectReference: {fileID: 0} + - target: {fileID: 1988018937390540162, guid: a0af29d97abd24d4c8382b6da4017660, type: 3} + propertyPath: m_fontSize + value: 22.9 + objectReference: {fileID: 0} - target: {fileID: 2283093842980008316, guid: a0af29d97abd24d4c8382b6da4017660, type: 3} propertyPath: m_LocalPosition.x value: 0 @@ -805,6 +851,58 @@ PrefabInstance: propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} + - target: {fileID: 3406562529197016604, guid: a0af29d97abd24d4c8382b6da4017660, type: 3} + propertyPath: m_fontSize + value: 44.75 + objectReference: {fileID: 0} + - target: {fileID: 3521149265392391147, guid: a0af29d97abd24d4c8382b6da4017660, type: 3} + propertyPath: m_fontSize + value: 22.35 + objectReference: {fileID: 0} + - target: {fileID: 4641005291164401446, guid: a0af29d97abd24d4c8382b6da4017660, type: 3} + propertyPath: m_fontSize + value: 44.75 + objectReference: {fileID: 0} + - target: {fileID: 6503434739734083609, guid: a0af29d97abd24d4c8382b6da4017660, type: 3} + propertyPath: m_fontSize + value: 44.75 + objectReference: {fileID: 0} + - target: {fileID: 6916374624597502023, guid: a0af29d97abd24d4c8382b6da4017660, type: 3} + propertyPath: m_fontSize + value: 36.75 + objectReference: {fileID: 0} + - target: {fileID: 6924097052499452501, guid: a0af29d97abd24d4c8382b6da4017660, type: 3} + propertyPath: m_fontSize + value: 44.75 + objectReference: {fileID: 0} + - target: {fileID: 7063008752886569165, guid: a0af29d97abd24d4c8382b6da4017660, type: 3} + propertyPath: m_fontSize + value: 44.75 + objectReference: {fileID: 0} + - target: {fileID: 7255685370734901281, guid: a0af29d97abd24d4c8382b6da4017660, type: 3} + propertyPath: m_fontSize + value: 44.75 + objectReference: {fileID: 0} + - target: {fileID: 7407273248831804694, guid: a0af29d97abd24d4c8382b6da4017660, type: 3} + propertyPath: m_fontSize + value: 22.35 + objectReference: {fileID: 0} + - target: {fileID: 8256774338473347927, guid: a0af29d97abd24d4c8382b6da4017660, type: 3} + propertyPath: m_fontSize + value: 44.75 + objectReference: {fileID: 0} + - target: {fileID: 8591534907977924294, guid: a0af29d97abd24d4c8382b6da4017660, type: 3} + propertyPath: m_fontSize + value: 44.75 + objectReference: {fileID: 0} + - target: {fileID: 9011431278706922558, guid: a0af29d97abd24d4c8382b6da4017660, type: 3} + propertyPath: m_fontSize + value: 44.75 + objectReference: {fileID: 0} + - target: {fileID: 9118953017428058395, guid: a0af29d97abd24d4c8382b6da4017660, type: 3} + propertyPath: m_fontSize + value: 44.75 + objectReference: {fileID: 0} m_RemovedComponents: [] m_RemovedGameObjects: [] m_AddedGameObjects: [] @@ -828,11 +926,11 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 7389606469834037353, guid: 962bda2b335a56f48a6f7159a9e824d8, type: 3} propertyPath: m_LocalScale.x - value: 0.7 + value: 1 objectReference: {fileID: 0} - target: {fileID: 7389606469834037353, guid: 962bda2b335a56f48a6f7159a9e824d8, type: 3} propertyPath: m_LocalScale.y - value: 0.7 + value: 1 objectReference: {fileID: 0} - target: {fileID: 7389606469834037353, guid: 962bda2b335a56f48a6f7159a9e824d8, type: 3} propertyPath: m_LocalPosition.x @@ -840,7 +938,7 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 7389606469834037353, guid: 962bda2b335a56f48a6f7159a9e824d8, type: 3} propertyPath: m_LocalPosition.y - value: 1.6 + value: 1.5 objectReference: {fileID: 0} - target: {fileID: 7389606469834037353, guid: 962bda2b335a56f48a6f7159a9e824d8, type: 3} propertyPath: m_LocalPosition.z diff --git a/Assets/Gothic-Core/Scripts/Adapters/Animations/AnimationSystem.cs b/Assets/Gothic-Core/Scripts/Adapters/Animations/AnimationSystem.cs index 357f76a9e..3ca3351ec 100644 --- a/Assets/Gothic-Core/Scripts/Adapters/Animations/AnimationSystem.cs +++ b/Assets/Gothic-Core/Scripts/Adapters/Animations/AnimationSystem.cs @@ -565,13 +565,13 @@ private void ApplyEventTags(AnimationTrackInstance trackInstance) AttackAnimation = trackInstance.AnimationName; break; case EventType.OptimalFrame: - AttackOptFrame = eventTag.Slots.Item1.Split(' ').Select(i => Convert.ToInt32(i)).ToList(); + AttackOptFrame = eventTag.Slots.Item1.Split(' ').Where(s => !string.IsNullOrEmpty(s)).Select(i => Convert.ToInt32(i)).ToList(); break; case EventType.HitEnd: - AttackHitEnd = eventTag.Slots.Item1.Split(' ').Select(i => Convert.ToInt32(i)).ToList(); + AttackHitEnd = eventTag.Slots.Item1.Split(' ').Where(s => !string.IsNullOrEmpty(s)).Select(i => Convert.ToInt32(i)).ToList(); break; case EventType.ComboWindow: - AttackWindowFrames = eventTag.Slots.Item1.Split(' ').Select(i => Convert.ToInt32(i)).ToList(); + AttackWindowFrames = eventTag.Slots.Item1.Split(' ').Where(s => !string.IsNullOrEmpty(s)).Select(i => Convert.ToInt32(i)).ToList(); break; // Unused. @see: https://gothic-modding-community.github.io/gmc/zengin/anims/events/#def_dir case EventType.HitDirection: 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/AiHandler.cs b/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs index e35458390..9a683c22e 100644 --- a/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs +++ b/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs @@ -79,9 +79,12 @@ private void Update() if (Properties.AnimationQueue.Count == 0) { // We always need to set "self" before executing any Daedalus function. + // "other" defaults to hero here so routine states (ZS_*_Loop) have a sensible fallback. + // Perception calls (ExecutePerception) override GlobalOther themselves with their own save/restore. if (NpcInstance != null) { Vm.GlobalSelf = NpcInstance; + Vm.GlobalOther = Vm.GlobalHero; } DaedalusSymbol loopSymbol; @@ -254,14 +257,6 @@ public void StartRoutine(int action, string wayPointName) public void StartRoutine(int action) { - // End original loop first - // TODO - Calling ClearState(false) was buggy when e.g. Diego dialog "END" was clicked. Then the dialog lines were skipped. - // if (Properties.CurrentLoopState == NpcProperties.LoopState.Loop) - // { - // // We reuse this function as it is doing what we need. - // ClearState(false); - // } - var didRoutineChange = Vob.CurrentStateIndex != action; Vob.LastAiState = Vob.CurrentStateIndex; @@ -295,6 +290,11 @@ public void StartRoutine(int action) // When we reached end of ZS_*_END, we also call this method. Check if we really altered the routine action or just restarted it. if (didRoutineChange) { + if (Properties.CurrentFreePoint != null) + { + Properties.CurrentFreePoint.IsLocked = false; + Properties.CurrentFreePoint = null; + } Logger.Log($"Start new routine >{routineSymbol.Name}< on >{Go.transform.parent.name}<", LogCat.Ai); Properties.StateTime = 0; } @@ -337,8 +337,11 @@ public void ReEnableNpc() var currentRoutine = Properties.RoutineCurrent; if (currentRoutine != null) { - var wpPos = _wayNetService.GetWayNetPoint(currentRoutine.Waypoint).Position; - gameObject.transform.position = _npcService.GetFreeAreaAtSpawnPoint(wpPos); + var wp = _wayNetService.GetWayNetPoint(currentRoutine.Waypoint); + if (wp != null) + gameObject.transform.position = _npcService.GetFreeAreaAtSpawnPoint(wp.Position); + else + Logger.LogWarning($"ReEnableNpc: waypoint '{currentRoutine.Waypoint}' not found for {gameObject.name} — NPC will re-enable at current position.", LogCat.Npc); } // Animation state handling 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/Audio/MusicDomain.cs b/Assets/Gothic-Core/Scripts/Domain/Audio/MusicDomain.cs index 34490efb1..4187d0038 100644 --- a/Assets/Gothic-Core/Scripts/Domain/Audio/MusicDomain.cs +++ b/Assets/Gothic-Core/Scripts/Domain/Audio/MusicDomain.cs @@ -148,9 +148,9 @@ private void PlayInternal(MusicThemeInstance theme) // DMusic crashes for some unfinished small themes (<3kB). Therefore, skipping them now with a warning until fixed. // Issue report: https://github.com/GothicKit/dmusic-cs/issues/1 - if (_resourceCacheService.Vfs.Find(theme.File)?.Buffer.Bytes.Length < 3000) + if (_resourceCacheService.Vfs.Find(theme.File)?.Buffer.Bytes.Length < 1000) { - Logger.LogWarning($"Music theme >{theme.File}< might be broken with less than 3kB size. Safety skip", LogCat.Audio); + Logger.LogWarning($"Music theme >{theme.File}< might be broken with less than 1kB size. Safety skip", LogCat.Audio); return; } diff --git a/Assets/Gothic-Core/Scripts/Domain/Culling/VobMeshCullingDomain.cs b/Assets/Gothic-Core/Scripts/Domain/Culling/VobMeshCullingDomain.cs index 15814e263..c7c726a92 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()); @@ -562,14 +572,21 @@ private IEnumerator StopVobTrackingBasedOnVelocity() { var key = _pausedVobsToReenable.Keys.ElementAt(i); var rigidBody = _pausedVobsToReenable[key]; + if (rigidBody == null) + { + _pausedVobsToReenable.Remove(key); + continue; + } if (rigidBody.linearVelocity != Vector3.zero) { 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 +610,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/Meshes/Builder/NpcHeadMeshBuilder.cs b/Assets/Gothic-Core/Scripts/Domain/Meshes/Builder/NpcHeadMeshBuilder.cs index 5b0e7e89d..920fe944e 100644 --- a/Assets/Gothic-Core/Scripts/Domain/Meshes/Builder/NpcHeadMeshBuilder.cs +++ b/Assets/Gothic-Core/Scripts/Domain/Meshes/Builder/NpcHeadMeshBuilder.cs @@ -20,9 +20,14 @@ public override GameObject Build() return RootGo; } - var npcContainer = RootGo.GetComponentInParent().Npc.GetUserData(); + var npcContainer = RootGo.GetComponentInParent()?.Npc?.GetUserData(); + if (npcContainer == null) + { + Logger.LogWarning($"NpcContainer not available during head build for {RootGo.name} — skipping head component setup.", LogCat.Mesh); + return RootGo; + } - // Cache it f1or faster use during runtime + // Cache it for faster use during runtime npcContainer.PrefabProps.Head = headGo.transform; npcContainer.PrefabProps.HeadMorph = headGo.AddComponent().Inject(); npcContainer.PrefabProps.HeadMorph.HeadName = npcContainer.Props.BodyData.Head; diff --git a/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/Attack.cs b/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/Attack.cs index 0c133159d..32da7ff9f 100644 --- a/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/Attack.cs +++ b/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/Attack.cs @@ -32,6 +32,13 @@ public Attack(AnimationAction action, NpcContainer npcData) : base(action, npcDa public override void Start() { + if (Vob.GuildTrue < (int)VmGothicEnums.Guild.GIL_SEPERATOR_HUM) + { + Logger.Log($"AI_Attack() on human NPC (guild={Vob.GuildTrue}) — not yet implemented, skipping.", LogCat.Ai); + IsFinishedFlag = true; + return; + } + var aiFunctionTemplate = FindAiFunctionTemplate(); _move = VmCacheService.TryGetFightAiData(aiFunctionTemplate, Vob.FightTactic).GetRandomMove(); StartAttackAction(); diff --git a/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/GoToNpc.cs b/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/GoToNpc.cs index 29b970bc6..d2a0e2cc5 100644 --- a/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/GoToNpc.cs +++ b/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/GoToNpc.cs @@ -6,6 +6,8 @@ namespace Gothic.Core.Domain.Npc.Actions.AnimationActions { public class GoToNpc : AbstractWalkAnimationAction { + private const float ConversationDistance = 1.5f; + private Transform _destinationTransform; public GoToNpc(AnimationAction action, NpcContainer npcContainer) : base(action, npcContainer) @@ -21,7 +23,11 @@ public override void Start() protected override Vector3 GetWalkDestination() { - return _destinationTransform.position; + var targetPos = _destinationTransform.position; + var toTarget = targetPos - NpcGo.transform.position; + if (toTarget.sqrMagnitude < 0.001f) + return targetPos; + return targetPos + toTarget.normalized * -ConversationDistance; } diff --git a/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/GoToWp.cs b/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/GoToWp.cs index e889e8861..2cf9ab28e 100644 --- a/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/GoToWp.cs +++ b/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/GoToWp.cs @@ -33,8 +33,14 @@ public override void Start() } // We need to set the route now to ensure base.Start() can check if NPC is already _on_ the final destination. - _route = new Stack(WayNetService.FindFastestPath(currentWaypoint.Name, - destinationWaypoint.Name)); + var path = WayNetService.FindFastestPath(currentWaypoint.Name, destinationWaypoint.Name); + if (path == null) + { + IsFinishedFlag = true; + return; + } + + _route = new Stack(path); base.Start(); } diff --git a/Assets/Gothic-Core/Scripts/Domain/Vobs/VobInitializerDomain.cs b/Assets/Gothic-Core/Scripts/Domain/Vobs/VobInitializerDomain.cs index 6f6273b22..1a1a9110d 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 @@ -581,14 +581,18 @@ public AudioClip GetSoundClip(string soundName) if (sfxContainer == null) return null; + var firstSound = sfxContainer.GetFirstSound(); + if (firstSound == null) + return null; + // Instead of decoding nosound.wav which might be decoded incorrectly, just return null. - if (sfxContainer.GetFirstSound().File.EqualsIgnoreCase(AudioService.NoSoundName)) + if (firstSound.File.EqualsIgnoreCase(AudioService.NoSoundName)) return null; if (sfxContainer.Count > 1) - Logger.LogWarning($"Multiple random elements exist for >{sfxContainer.GetFirstSound().File}< but only first is selected.", LogCat.Audio); + Logger.LogWarning($"Multiple random elements exist for >{firstSound.File}< but only first is selected.", LogCat.Audio); - clip = _audioService.CreateAudioClip(sfxContainer.GetFirstSound().File); + clip = _audioService.CreateAudioClip(firstSound.File); } return clip; diff --git a/Assets/Gothic-Core/Scripts/Models/Audio/SfxModel.cs b/Assets/Gothic-Core/Scripts/Models/Audio/SfxModel.cs index c02327a76..b2285618b 100644 --- a/Assets/Gothic-Core/Scripts/Models/Audio/SfxModel.cs +++ b/Assets/Gothic-Core/Scripts/Models/Audio/SfxModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Gothic.Core.Logging; using Gothic.Core.Services; using Gothic.Core.Const; using Gothic.Core.Extensions; @@ -8,6 +9,7 @@ using MyBox; using Reflex.Attributes; using ZenKit.Daedalus; +using Logger = Gothic.Core.Logging.Logger; namespace Gothic.Core.Models.Audio { @@ -64,9 +66,10 @@ private void LoadSoundEffects() var firstSound = _gameStateService.SfxVm.InitInstance(soundKey); sounds.Add(firstSound); } - catch (Exception e) + catch (Exception) { - // If the key itself doesn't exist, then we don't need to look further. + // SFX symbol missing from Gothic's SFX scripts — expected for some broken VOBs (e.g. Wood_Night2 was never defined). + Logger.LogWarning($"SFX symbol not found in VM: '{soundKey}' — no sound will play.", LogCat.Audio); _soundEffects = Array.Empty(); return; } 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..163910d20 100644 --- a/Assets/Gothic-Core/Scripts/Services/Npc/FightService.cs +++ b/Assets/Gothic-Core/Scripts/Services/Npc/FightService.cs @@ -1,38 +1,48 @@ 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.Vm; +using Gothic.Core.Services.Config; using Gothic.Core.Services.World; using Reflex.Attributes; using UnityEngine; using ZenKit.Daedalus; +using Logger = Gothic.Core.Logging.Logger; namespace Gothic.Core.Services.Npc { public class FightService { [Inject] private AudioService _audioService; - [Inject] private VmService _vmService; [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 __) { + if (target.Props.BodyState == VmGothicEnums.BodyState.BsDead) + return; + + 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,38 +56,55 @@ 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; } private void OnDyingChangeAnimation(NpcContainer target) { - // Stop current (attack) animation. - target.Props.CurrentAction.StopImmediately(); + // Clear pending AI queue and stop all running animations (e.g. s_walk still looping). + // Death takes priority over everything — bypass the queue and play directly. + target.Props.AnimationQueue.Clear(); + target.PrefabProps.AnimationSystem.StopAllAnimations(); _physicsService.DisablePhysicsForNpc(target.PrefabProps); var animName = _animationService.GetAnimationName(VmGothicEnums.AnimationType.DeadB, target); - target.Props.AnimationQueue.Enqueue(new PlayAni(new(animName), target)); + target.PrefabProps.AnimationSystem.PlayAnimation(animName); } private void OnHitChangeAnimation(NpcContainer target) { - // Stop current (attack) animation. - target.Props.CurrentAction.StopImmediately(); - + // Play hurt on top of whatever is currently running — don't interrupt the current action. var animName = _animationService.GetAnimationName(VmGothicEnums.AnimationType.StumbleA, target); - target.Props.AnimationQueue.Enqueue(new PlayAni(new(animName), target)); + target.PrefabProps.AnimationSystem.PlayAnimation(animName); } private void OnHitPlaySound(NpcContainer target) diff --git a/Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs b/Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs index 971fd2c8e..0adbe470f 100644 --- a/Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs +++ b/Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs @@ -50,12 +50,12 @@ public void ExecutePerception(VmGothicEnums.PerceptionType type, NpcProperties p } var oldSelf = _gameStateService.GothicVm.GlobalSelf; + var oldOther = _gameStateService.GothicVm.GlobalOther; var oldVictim = _gameStateService.GothicVm.GlobalVictim; - var oldOther = _gameStateService.GothicVm.GlobalVictim; _gameStateService.GothicVm.GlobalSelf = self; - - if(other != null) + + if(other != null) { _gameStateService.GothicVm.GlobalOther = other; } @@ -66,10 +66,10 @@ public void ExecutePerception(VmGothicEnums.PerceptionType type, NpcProperties p } _gameStateService.GothicVm.Call(perceptionFunction); - + _gameStateService.GothicVm.GlobalSelf = oldSelf; + _gameStateService.GothicVm.GlobalOther = oldOther; _gameStateService.GothicVm.GlobalVictim = oldVictim; - _gameStateService.GothicVm.GlobalVictim = oldOther; } public void ExtNpcSetPerceptionTime(NpcInstance npc, float time) @@ -341,8 +341,8 @@ public int ExtNpcGetDistToNpc(NpcInstance npc1, NpcInstance npc2) var npc1Pos = npc1.GetUserData().Go.transform.position; Vector3 npc2Pos; - // If hero - if (npc2.Id == 0) + // If hero: use camera position (VR head position is most accurate) + if (npc2.Index == _gameStateService.GothicVm.GlobalHero?.Index) { npc2Pos = Camera.main!.transform.position; } @@ -388,14 +388,14 @@ public void ExtAiDrawWeapon(NpcInstance npc) public bool ExtNpcIsDead(NpcInstance npcInstance) { - // FIXME - We need to implement it properly. Just fixing NPEs for now! - // FIXME - e.g. used for PC_Thief_AFTERTROLL_Condition() from Daedalus. - return false; + // FIXME - BodyState is runtime-only and lost on NPC reload (e.g. world reload respawns the NPC alive). + // A permanent death flag needs to be persisted in SaveGame state and checked here instead. + return npcInstance.GetUserData()?.Props.BodyState == VmGothicEnums.BodyState.BsDead; } public bool ExtNpcIsInState(NpcInstance npc, int state) { - return npc.GetUserData().Vob.CurrentStateIndex == state; + return npc.GetUserData()?.Vob.CurrentStateIndex == state; } public bool ExtNpcIsPlayer(NpcInstance npc) diff --git a/Assets/Gothic-Core/Scripts/Services/Npc/NpcHelperService.cs b/Assets/Gothic-Core/Scripts/Services/Npc/NpcHelperService.cs index ac0354fe6..b3a41e31f 100644 --- a/Assets/Gothic-Core/Scripts/Services/Npc/NpcHelperService.cs +++ b/Assets/Gothic-Core/Scripts/Services/Npc/NpcHelperService.cs @@ -152,7 +152,7 @@ public bool ExtWldDetectNpcEx(NpcInstance npcInstance, int specificNpcIndex, int _gameStateService.GothicVm.GlobalHero!.Index) // if we don't detect player, then skip it .Where(i => specificNpcIndex < 0 || specificNpcIndex == i.Instance.Index) // Specific NPC is found right now? - .Where(i => aiState < 0 || npcVob.CurrentStateIndex == i.Vob.CurrentStateIndex) + .Where(i => aiState < 0 || aiState == i.Vob.CurrentStateIndex) .Where(i => guild < 0 || i.Instance.Guild == guild) // check guild .OrderBy(i => Vector3.Distance(i.Go.transform.position, npcPos)) // get nearest .FirstOrDefault(); diff --git a/Assets/Gothic-Core/Scripts/Services/Npc/NpcInventoryService.cs b/Assets/Gothic-Core/Scripts/Services/Npc/NpcInventoryService.cs index 038d4bd80..0017f8d19 100644 --- a/Assets/Gothic-Core/Scripts/Services/Npc/NpcInventoryService.cs +++ b/Assets/Gothic-Core/Scripts/Services/Npc/NpcInventoryService.cs @@ -111,17 +111,52 @@ 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; var itemInstanceName = _gameStateService.GothicVm.GetSymbolByIndex(itemId)!.Name; - - for (var i = 0; i < npcVob.ItemCount; i++) + + foreach (InvCats cat in System.Enum.GetValues(typeof(InvCats))) { - if (npcVob.GetItem(i).Name == itemInstanceName) - return npcVob.GetItem(i).Amount; + if (cat == InvCats.InvCatMax) + continue; + try + { + foreach (var item in GetInventoryItems(npc, cat)) + { + if (string.Equals(item.Name, itemInstanceName, System.StringComparison.OrdinalIgnoreCase)) + return item.Amount; + } + } + catch + { + // Category slot was never initialized for this NPC + } } - + return 0; } diff --git a/Assets/Gothic-Core/Scripts/Services/Player/DialogService.cs b/Assets/Gothic-Core/Scripts/Services/Player/DialogService.cs index 63cdc66e0..6635f89e6 100644 --- a/Assets/Gothic-Core/Scripts/Services/Player/DialogService.cs +++ b/Assets/Gothic-Core/Scripts/Services/Player/DialogService.cs @@ -99,9 +99,12 @@ public void StartDialog(NpcContainer npcContainer, bool initialDialogStarting) // TODO - Should be outsourced to some VmManager.Call function which sets and resets values. var oldSelf = _gameStateService.GothicVm.GlobalSelf; + var oldOther = _gameStateService.GothicVm.GlobalOther; _gameStateService.GothicVm.GlobalSelf = npcContainer.Instance; + _gameStateService.GothicVm.GlobalOther = _gameStateService.GothicVm.GlobalHero; var conditionResult = _gameStateService.GothicVm.Call(dialog.Condition); _gameStateService.GothicVm.GlobalSelf = oldSelf; + _gameStateService.GothicVm.GlobalOther = oldOther; // Dialog condition is false if (conditionResult == 0) @@ -146,9 +149,12 @@ private bool TryGetImportant(NpcContainer npcContainer, out InfoInstance item) // TODO - Should be outsourced to some VmManager.Call function which sets and resets values. var oldSelf = _gameStateService.GothicVm.GlobalSelf; + var oldOther = _gameStateService.GothicVm.GlobalOther; _gameStateService.GothicVm.GlobalSelf = npcContainer.Instance; + _gameStateService.GothicVm.GlobalOther = _gameStateService.GothicVm.GlobalHero; var conditionResult = _gameStateService.GothicVm.Call(dialog.Condition); _gameStateService.GothicVm.GlobalSelf = oldSelf; + _gameStateService.GothicVm.GlobalOther = oldOther; if (conditionResult == 0) { diff --git a/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs b/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs index caa1709cb..6796a4104 100644 --- a/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs +++ b/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs @@ -1,4 +1,3 @@ -using System; using System.Reflection; using Gothic.Core.Const; using Gothic.Core.Logging; @@ -58,8 +57,6 @@ public TMP_SpriteAsset TryGetFont(string fontName) var y = font.Glyphs[i].topLeft.Y * fontTexture.height; var w = font.Glyphs[i].width; var h = font.Height; - var newSprite = Sprite.Create(fontTexture, new Rect(x, y, w, h), - new Vector2(font.Glyphs[i].topLeft.X, font.Glyphs[i].bottomRight.Y)); var spriteGlyph = new TMP_SpriteGlyph { @@ -79,11 +76,10 @@ public TMP_SpriteAsset TryGetFont(string fontName) horizontalAdvance = w }, index = (uint)i, - sprite = newSprite, + sprite = null, scale = -1 }; - // Convert the glyph index (treated as a codepage-byte) to its Unicode equivalent var unicodeChars = _gameStateService.Encoding.GetChars(new[]{(byte)i}); var unicodeValue = (uint)unicodeChars[0]; // Return the Unicode character's code point diff --git a/Assets/Gothic-Core/Scripts/Services/Vobs/VobService.cs b/Assets/Gothic-Core/Scripts/Services/Vobs/VobService.cs index 28b608a1e..c6ea15d55 100644 --- a/Assets/Gothic-Core/Scripts/Services/Vobs/VobService.cs +++ b/Assets/Gothic-Core/Scripts/Services/Vobs/VobService.cs @@ -176,12 +176,19 @@ private IEnumerator InitVobCoroutine() // } var item = _objectsToInitQueue.Dequeue(); - + item.IsLoaded = true; // We assume that each loaded VOB is centered at parent=0,0,0. // Should work smoothly until we start lazy loading sub-vobs ;-) - _initializerDomain.InitVob(item.Container.Vob, item.gameObject, default, true); + try + { + _initializerDomain.InitVob(item.Container.Vob, item.gameObject, default, true); + } + catch (Exception e) + { + Logger.LogError($"Failed to init VOB {item.name}: {e}", LogCat.Vob); + } yield return _frameSkipperService.TrySkipToNextFrameCoroutine(); } 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/Player-Elements/BackPack.prefab b/Assets/Gothic-VR/Resources/VR/Prefabs/Player-Elements/BackPack.prefab index b6b86c804..07854330c 100644 --- a/Assets/Gothic-VR/Resources/VR/Prefabs/Player-Elements/BackPack.prefab +++ b/Assets/Gothic-VR/Resources/VR/Prefabs/Player-Elements/BackPack.prefab @@ -1293,15 +1293,15 @@ PrefabInstance: m_Modifications: - target: {fileID: 6401121504383089494, guid: 4293d805a850fb84885c55f47b7299bd, type: 3} propertyPath: m_text - value: x/y + value: '' objectReference: {fileID: 0} - target: {fileID: 6401121504383089494, guid: 4293d805a850fb84885c55f47b7299bd, type: 3} propertyPath: m_fontAsset - value: + value: objectReference: {fileID: 11400000, guid: ed34b05229166f3469c21b7208879736, type: 2} - target: {fileID: 6401121504383089494, guid: 4293d805a850fb84885c55f47b7299bd, type: 3} propertyPath: m_sharedMaterial - value: + value: objectReference: {fileID: 5152873236376385906, guid: ed34b05229166f3469c21b7208879736, type: 2} - target: {fileID: 6401121504383089494, guid: 4293d805a850fb84885c55f47b7299bd, type: 3} propertyPath: m_VerticalAlignment @@ -3692,7 +3692,7 @@ PrefabInstance: m_Modifications: - target: {fileID: 6401121504383089494, guid: 4293d805a850fb84885c55f47b7299bd, type: 3} propertyPath: m_text - value: '{{CATEGORY}}' + value: '' objectReference: {fileID: 0} - target: {fileID: 6401121504383089494, guid: 4293d805a850fb84885c55f47b7299bd, type: 3} propertyPath: m_fontSize diff --git a/Assets/Gothic-VR/Resources/VR/Prefabs/Story/IntroduceChapter.prefab b/Assets/Gothic-VR/Resources/VR/Prefabs/Story/IntroduceChapter.prefab index 39dcc335f..e5a60535f 100644 --- a/Assets/Gothic-VR/Resources/VR/Prefabs/Story/IntroduceChapter.prefab +++ b/Assets/Gothic-VR/Resources/VR/Prefabs/Story/IntroduceChapter.prefab @@ -103,6 +103,7 @@ MonoBehaviour: m_VerticalAlignment: 512 m_textAlignment: 65535 m_characterSpacing: 0 + m_characterHorizontalScale: 1 m_wordSpacing: 0 m_lineSpacing: 0 m_lineSpacingMax: 0 @@ -239,6 +240,7 @@ MonoBehaviour: m_VerticalAlignment: 512 m_textAlignment: 65535 m_characterSpacing: 0 + m_characterHorizontalScale: 1 m_wordSpacing: 0 m_lineSpacing: 0 m_lineSpacingMax: 0 @@ -444,6 +446,7 @@ GameObject: - component: {fileID: 7369722077890996068} - component: {fileID: 390066666418957288} - component: {fileID: 5615940322375626677} + - component: {fileID: 1602513700271543697} m_Layer: 5 m_Name: IntroduceChapter m_TagString: Untagged @@ -495,6 +498,7 @@ AudioSource: serializedVersion: 4 OutputAudioMixerGroup: {fileID: 0} m_audioClip: {fileID: 0} + m_Resource: {fileID: 0} m_PlayOnAwake: 1 m_Volume: 1 m_Pitch: 1 @@ -580,3 +584,16 @@ AudioSource: m_PreInfinity: 2 m_PostInfinity: 2 m_RotationOrder: 4 +--- !u!114 &1602513700271543697 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7339275886835513150} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 20620ae30fdd0164c9bb5bdc717d3ada, type: 3} + m_Name: + m_EditorClassIdentifier: Reflex::Reflex.Components.GameObjectSelfInjector + _injectionStrategy: 2 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..3ea4fa907 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 @@ -750,6 +764,10 @@ PrefabInstance: propertyPath: m_SizeDelta.y value: 2.8 objectReference: {fileID: 0} + - target: {fileID: 2805224566998289421, guid: 6ce7e724ba52cae47a499f87c612cd47, type: 3} + propertyPath: m_LocalPosition.z + value: 0.02 + objectReference: {fileID: 0} - target: {fileID: 8230739814078363531, guid: 6ce7e724ba52cae47a499f87c612cd47, type: 3} propertyPath: m_Pivot.x value: 0.5 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..fc5275e31 100644 --- a/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs +++ b/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs @@ -87,7 +87,7 @@ public void OnItemPutIntoHolster(HVRGrabberBase grabber, HVRGrabbable grabbable) var vobLoader = grabbable.GetComponentInParent(); var vobContainer = vobLoader.Container; - _playerService.AddItem(vobContainer.Vob.Name, vobContainer.VobAs().Amount); + _playerService.AddItem(vobContainer.Vob.Name, Mathf.Max(1, vobContainer.VobAs().Amount)); } @@ -102,8 +102,8 @@ public void OnItemPutIntoBackpack(HVRGrabberBase grabber, HVRGrabbable grabbable _vobMeshCullingService.RemoveCullingEntry(vobContainer); _saveGameService.CurrentWorldData.Vobs.Remove(vobContainer.Vob); - _playerService.AddItem(vobContainer.Vob.Name, vobContainer.VobAs().Amount); - + _playerService.AddItem(vobContainer.Vob.Name, Mathf.Max(1, vobContainer.VobAs().Amount)); + UpdateInventoryView(); } @@ -114,23 +114,25 @@ public void OnItemPutIntoBackpack(HVRGrabberBase grabber, HVRGrabbable grabbable public void OnItemPutOutOfHolster(HVRGrabberBase grabber, HVRGrabbable grabbable) { var vobLoader = grabbable.GetComponentInParent(); + if (vobLoader == null) + return; var vobContainer = vobLoader.Container; - _playerService.RemoveItem(vobContainer.Vob.Name, vobContainer.VobAs().Amount); + _playerService.RemoveItem(vobContainer.Vob.Name, Mathf.Max(1, vobContainer.VobAs().Amount)); } public void OnItemPutOutOfBackpack(HVRGrabberBase grabber, HVRGrabbable grabbable) { if (_tempIgnoreSocketing) return; - + var vobLoader = grabbable.GetComponentInParent(); var vobContainer = vobLoader.Container; _vobMeshCullingService.AddCullingEntry(vobContainer); _saveGameService.CurrentWorldData.Vobs.Add(vobContainer.Vob); - _playerService.RemoveItem(vobContainer.Vob.Name, vobContainer.VobAs().Amount); + _playerService.RemoveItem(vobContainer.Vob.Name, Mathf.Max(1, vobContainer.VobAs().Amount)); UpdateInventoryView(); } @@ -267,7 +269,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..8664f2289 100644 --- a/Assets/Gothic-VR/Scripts/Adapters/VRNpc.cs +++ b/Assets/Gothic-VR/Scripts/Adapters/VRNpc.cs @@ -4,10 +4,11 @@ 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.Services.World; 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 +22,31 @@ public class VRNpc : MonoBehaviour [Inject] private readonly GameStateService _gameStateService; [Inject] private readonly DialogService _dialogService; [Inject] private readonly NpcAiService _npcAiService; + [Inject] private readonly ConfigService _configService; + [Inject] private readonly PhysicsService _physicsService; 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); + // HVR sets isKinematic=false during grab; release immediately and freeze the corpse + grabber.ForceRelease(); + _physicsService.DisablePhysicsForNpc(_npcData.PrefabProps); + 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