From 91816c6f8ce45b58dd23d95843f8810b3e9cce5a Mon Sep 17 00:00:00 2001 From: JaXt0r Date: Sat, 6 Jun 2026 09:24:27 +0200 Subject: [PATCH 01/39] fix: Allow main menu music again (lower disable threshold) --- Assets/Gothic-Core/Scripts/Domain/Audio/MusicDomain.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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; } From a69c6521e176139d6baa4ccafabb18b13fb0156a Mon Sep 17 00:00:00 2001 From: JaXt0r Date: Sat, 6 Jun 2026 09:58:28 +0200 Subject: [PATCH 02/39] fix: Move main menu background behind menu, disable floor, and change scale of background. --- Assets/Gothic-Core/Scenes/MainMenu.unity | 130 ++++++++++++++++++++--- 1 file changed, 114 insertions(+), 16 deletions(-) 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 From efdb27fe1a2c3315938982adb38fd4a5c847582e Mon Sep 17 00:00:00 2001 From: JaXt0r Date: Sat, 6 Jun 2026 10:04:11 +0200 Subject: [PATCH 03/39] fix: VrIntroduceChapter now injects Services. --- .../VR/Prefabs/Story/IntroduceChapter.prefab | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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 From 11430f9b53f0d42362a4e95511e1c1ee019a8dd9 Mon Sep 17 00:00:00 2001 From: JaXt0r Date: Tue, 9 Jun 2026 21:53:01 +0200 Subject: [PATCH 04/39] fix: Broken font by removing sprite with initial race condition (not needed for glyphs). --- Assets/Gothic-Core/Scripts/Services/UI/FontService.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs b/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs index caa1709cb..327456552 100644 --- a/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs +++ b/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs @@ -58,8 +58,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 +77,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 From 649bfc7a19fa3d240eed46f6ad172fe41feeafdb Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Tue, 9 Jun 2026 21:12:33 +0100 Subject: [PATCH 05/39] =?UTF-8?q?fix:=20Restore=20VRMouth=20food=20detecti?= =?UTF-8?q?on=20=E2=80=94=20add=20VobItem/VobItemNoWorldCollision=20to=20T?= =?UTF-8?q?ransparentFX=20collision=20matrix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ProjectSettings/DynamicsManager.asset | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 2218d632d1178c51e4f09e92e0c902d3421a504c Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Tue, 9 Jun 2026 23:10:17 +0100 Subject: [PATCH 06/39] feat: player -> NPC melee combat --- .../Adapters/Vob/Item/WeaponAttackAdapter.cs | 139 +++++++++++++++--- .../Scripts/Models/Config/DeveloperConfig.cs | 2 + .../Scripts/Services/Npc/FightService.cs | 27 +++- .../Player/VRPlayerWeaponInteraction.cs | 29 +++- 4 files changed, 174 insertions(+), 23 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs b/Assets/Gothic-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs index a42664a2a..f32dbd600 100644 --- a/Assets/Gothic-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs +++ b/Assets/Gothic-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs @@ -1,5 +1,9 @@ +using System; using Gothic.Core.Const; +using Gothic.Core.Adapters.Npc; +using Gothic.Core.Models.Container; using UnityEngine; +using ZenKit.Daedalus; namespace Gothic.Core.Adapters.Vob.Item { @@ -11,40 +15,135 @@ namespace Gothic.Core.Adapters.Vob.Item /// public class WeaponAttackAdapter : MonoBehaviour { + 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 - 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 + /// 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) + { + Debug.LogWarning($"[WeaponAttackAdapter] Wrong layer: {LayerMask.LayerToName(other.gameObject.layer)}"); + return; + } + + Debug.Log($"[WeaponAttackAdapter] Collision with VobHitbox: {other.gameObject.name}"); + + // Try to get the target NPC/Monster from the hitbox + var targetNpcLoader = other.GetComponentInParent(); + if (targetNpcLoader == null) + { + Debug.LogWarning($"[WeaponAttackAdapter] No NpcLoader found"); + return; + } + + var targetNpcContainer = targetNpcLoader.Container; + if (targetNpcContainer == null) + { + Debug.LogWarning($"[WeaponAttackAdapter] No NpcContainer found"); + return; + } + + Debug.Log($"[WeaponAttackAdapter] Target: {targetNpcContainer.Instance.GetName(NpcNameSlot.Slot0)}"); + + // Try to fire the hit through VR weapon service if available + if (TryFireHitViaVRWeaponService(targetNpcContainer)) + { + Debug.Log($"[WeaponAttackAdapter] *** HIT FIRED (VR)"); 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); + Debug.LogWarning($"[WeaponAttackAdapter] Failed to fire hit (not in attack window or VR unavailable)"); + // 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; + } + + /// + /// Attempts to validate and fire a hit using VRWeaponService (VR mode only). + /// Returns true if the hit was processed, false otherwise. + /// + private bool TryFireHitViaVRWeaponService(NpcContainer targetNpcContainer) + { + try + { + // Try to dynamically get the VRWeaponService through the Reflex DI container + // Using reflection to avoid direct assembly dependency on Gothic.VR + var vrWeaponServiceType = Type.GetType("Gothic.VR.Services.VRWeaponService, Gothic.VR"); + if (vrWeaponServiceType == null) + { + Debug.LogWarning($"[WeaponAttackAdapter] VRWeaponService not found (flat-screen mode?)"); + return false; + } + + var vrWeaponService = ReflexProjectInstaller.DIContainer.Resolve(vrWeaponServiceType); + if (vrWeaponService == null) + { + Debug.LogWarning($"[WeaponAttackAdapter] VRWeaponService could not be resolved"); + return false; + } + + // Use reflection to call the methods + var isInAttackWindowMethod = vrWeaponServiceType.GetMethod("IsWeaponInAttackWindow"); + var getWeaponOwnerMethod = vrWeaponServiceType.GetMethod("GetWeaponOwner"); + + if (isInAttackWindowMethod == null || getWeaponOwnerMethod == null) + { + Debug.LogWarning($"[WeaponAttackAdapter] Required methods not found on VRWeaponService"); + return false; + } + + // Check if this weapon is currently in an active attack window + var isInAttackWindow = (bool)isInAttackWindowMethod.Invoke(vrWeaponService, new object[] { _weaponVobContainer }); + Debug.Log($"[WeaponAttackAdapter] IsInAttackWindow: {isInAttackWindow}"); + if (!isInAttackWindow) + { + Debug.LogWarning($"[WeaponAttackAdapter] Weapon not in attack window"); + return false; + } + + // Get who is attacking with this weapon + var attacker = (NpcContainer)getWeaponOwnerMethod.Invoke(vrWeaponService, new object[] { _weaponVobContainer }); + if (attacker == null) + { + Debug.LogWarning($"[WeaponAttackAdapter] No attacker found for weapon"); + return false; + } + + // Get the hit position for effects/knockback later + var hitPosition = transform.position; - Debug.Log("OnTriggerExit - VobHitbox"); + // Fire the combat event + Debug.Log($"[WeaponAttackAdapter] FightHit event: {attacker.Instance.GetName(NpcNameSlot.Slot0)} → {targetNpcContainer.Instance.GetName(NpcNameSlot.Slot0)}"); + GlobalEventDispatcher.FightHit.Invoke(attacker, targetNpcContainer, hitPosition); + return true; + } + catch (Exception ex) + { + // VRWeaponService not available or DI resolution failed + Debug.LogError($"[WeaponAttackAdapter] Exception in TryFireHitViaVRWeaponService: {ex.Message}"); + return false; + } } } } diff --git a/Assets/Gothic-Core/Scripts/Models/Config/DeveloperConfig.cs b/Assets/Gothic-Core/Scripts/Models/Config/DeveloperConfig.cs index dfac35707..19a0d6c08 100644 --- a/Assets/Gothic-Core/Scripts/Models/Config/DeveloperConfig.cs +++ b/Assets/Gothic-Core/Scripts/Models/Config/DeveloperConfig.cs @@ -283,6 +283,8 @@ public class DebugChannelTypesCollection : CollectionWrapper(true)?.SetFillAmount(hitPoints, target.Vob.GetAttribute((int)NpcAttribute.HitPointsMax)); + var statusBar = target.Go.GetComponentInChildren(true); + if (statusBar != null) + { + Debug.Log($"[FightService.OnHitUpdateHealth] Updating HP bar for {target.Instance.GetName(NpcNameSlot.Slot0)}"); + statusBar.SetFillAmount(hitPoints, maxHP); + } + else + { + Debug.LogWarning($"[FightService.OnHitUpdateHealth] No StatusBar found for {target.Instance.GetName(NpcNameSlot.Slot0)}"); + } return hitPoints <= 0; } 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); } /// From df877d18be881c30f83f6683aca4604952b0d2ee Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Tue, 9 Jun 2026 23:11:46 +0100 Subject: [PATCH 07/39] NPC -> Player attack foundation --- .../Scripts/Adapters/Npc/NpcAttackAdapter.cs | 161 ++++++++++++++++++ .../Adapters/Npc/NpcAttackAdapter.cs.meta | 2 + .../Scripts/Adapters/Npc/NpcLoader.cs | 10 ++ 3 files changed, 173 insertions(+) create mode 100644 Assets/Gothic-Core/Scripts/Adapters/Npc/NpcAttackAdapter.cs create mode 100644 Assets/Gothic-Core/Scripts/Adapters/Npc/NpcAttackAdapter.cs.meta diff --git a/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcAttackAdapter.cs b/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcAttackAdapter.cs new file mode 100644 index 000000000..273df6901 --- /dev/null +++ b/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcAttackAdapter.cs @@ -0,0 +1,161 @@ +using System.Linq; +using Gothic.Core.Const; +using Gothic.Core.Models.Container; +using Gothic.Core.Domain.Npc.Actions.AnimationActions; +using MyBox; +using UnityEngine; +using ZenKit.Daedalus; + +namespace Gothic.Core.Adapters.Npc +{ + /// + /// Attached to NPC/Monster root collider to allow them to deal melee damage via unarmed attacks. + /// Fires FightHit events when the NPC's collider hits an opponent during attack animations. + /// + public class NpcAttackAdapter : MonoBehaviour + { + private NpcContainer _npcContainer; + private BoxCollider _collider; + private float _attackHitCooldown; + + private void Start() + { + var npcLoader = GetComponentInParent(); + if (npcLoader != null) + { + _npcContainer = npcLoader.Container; + } + + _collider = GetComponent(); + } + + private void Update() + { + // Reduce cooldown timer each frame + if (_attackHitCooldown > 0) + { + _attackHitCooldown -= Time.deltaTime; + } + } + + /// + /// Called when the NPC's collider hits something during combat. + /// Checks if the NPC is actively attacking and if the target is valid. + /// + private void OnTriggerEnter(Collider other) + { + // Can't hit without a valid container + if (_npcContainer == null) + { + Debug.LogWarning($"[NpcAttackAdapter] No NPC container"); + return; + } + + // Check if this is within attack hit cooldown to prevent multiple hits per attack + if (_attackHitCooldown > 0) + { + Debug.LogWarning($"[NpcAttackAdapter] Hit in cooldown (remaining: {_attackHitCooldown:F2}s)"); + return; + } + + // The target must have a hitbox layer + if (other.gameObject.layer != Constants.VobHitbox) + { + Debug.LogWarning($"[NpcAttackAdapter] Wrong layer: {LayerMask.LayerToName(other.gameObject.layer)}"); + return; + } + + Debug.Log($"[NpcAttackAdapter] Collision with hitbox layer: {other.gameObject.name}"); + + // Try to get the target NPC/Player + var targetNpcLoader = other.GetComponentInParent(); + if (targetNpcLoader == null) + { + Debug.LogWarning($"[NpcAttackAdapter] No target NpcLoader found"); + return; + } + + var targetNpcContainer = targetNpcLoader.Container; + if (targetNpcContainer == null) + { + Debug.LogWarning($"[NpcAttackAdapter] No target NpcContainer found"); + return; + } + + Debug.Log($"[NpcAttackAdapter] Target NPC: {targetNpcContainer.Instance.GetName(NpcNameSlot.Slot0)}"); + + // Don't let NPCs hit themselves + if (targetNpcContainer == _npcContainer) + { + Debug.LogWarning($"[NpcAttackAdapter] Target is self, ignoring"); + return; + } + + Debug.Log($"[NpcAttackAdapter] Checking if {_npcContainer.Instance.GetName(NpcNameSlot.Slot0)} is attacking..."); + + // Check if the NPC is currently attacking + if (!IsNpcCurrentlyAttacking()) + { + Debug.LogWarning($"[NpcAttackAdapter] Not in attack state"); + return; + } + + // Fire the hit event + var hitPosition = transform.position; + Debug.Log($"[NpcAttackAdapter] *** HIT FIRED! {_npcContainer.Instance.GetName(NpcNameSlot.Slot0)} → {targetNpcContainer.Instance.GetName(NpcNameSlot.Slot0)} at {hitPosition}"); + GlobalEventDispatcher.FightHit.Invoke(_npcContainer, targetNpcContainer, hitPosition); + + // Set cooldown to prevent multiple hits in rapid succession (0.5 seconds) + _attackHitCooldown = 0.5f; + } + + /// + /// Determines if the NPC is currently in an attack animation. + /// Checks if the current animation action is an attack-related action. + /// + private bool IsNpcCurrentlyAttacking() + { + if (_npcContainer?.Props?.CurrentAction == null) + { + Debug.LogWarning($"[NpcAttackAdapter] No CurrentAction"); + return false; + } + + // Check if the current action is an attack-based action + // AttackPlayAni is used for all NPC melee attacks + var currentAction = _npcContainer.Props.CurrentAction; + var actionTypeName = currentAction.GetType().Name; + + Debug.Log($"[NpcAttackAdapter] CurrentAction type: {actionTypeName}"); + + // Direct check for AttackPlayAni type (preferred, most reliable) + if (actionTypeName == "AttackPlayAni") + { + Debug.Log($"[NpcAttackAdapter] IsAttacking=TRUE (AttackPlayAni)"); + return true; + } + + // Also support PlayAni with attack animations by checking the animation name + if (actionTypeName == "PlayAni") + { + // PlayAni stores animation name in Action.String0 + // Animation names for attacks typically contain "attack" or follow pattern like "s_1hAttack", "s_2hAttack" + if (currentAction.Action?.String0 != null) + { + var aniName = currentAction.Action.String0.ToLower(); + Debug.Log($"[NpcAttackAdapter] PlayAni animation: {aniName}"); + var isAttack = aniName.Contains("attack"); + Debug.Log($"[NpcAttackAdapter] IsAttacking={isAttack}"); + return isAttack; + } + else + { + Debug.LogWarning($"[NpcAttackAdapter] PlayAni but no animation name"); + } + } + + Debug.LogWarning($"[NpcAttackAdapter] IsAttacking=FALSE (type: {actionTypeName})"); + return false; + } + } +} diff --git a/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcAttackAdapter.cs.meta b/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcAttackAdapter.cs.meta new file mode 100644 index 000000000..0652e3be5 --- /dev/null +++ b/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcAttackAdapter.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f9fe78d5279a6924e9ab8671790275d8 \ No newline at end of file diff --git a/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcLoader.cs b/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcLoader.cs index e0aebaba0..33aceb740 100644 --- a/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcLoader.cs +++ b/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcLoader.cs @@ -10,5 +10,15 @@ public class NpcLoader : MonoBehaviour public NpcInstance Npc; public NpcContainer Container => Npc.GetUserData(); public bool IsLoaded; + + private void Start() + { + // Ensure NPC has an attack adapter for melee combat + if (GetComponent() == null) + { + gameObject.AddComponent(); + } + } } } + From b5477f3bc36eb759fc181f99c7467ae30f04ff7b Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Wed, 10 Jun 2026 01:03:28 +0100 Subject: [PATCH 08/39] fix: guard remote grabbers and restrict draw sounds and mouth detection to items only(prevents 'eating' chests) --- .../VRVobContainerSocketInventory.cs | 24 +++++++++++++++---- .../Scripts/Adapters/Vob/VobItem/VRMouth.cs | 5 ++-- .../Scripts/Services/VRPlayerService.cs | 12 ++++++++-- 3 files changed, 32 insertions(+), 9 deletions(-) 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/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/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) From a15392909160ea9599ca1bcf74a5bc439601a3f5 Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Wed, 10 Jun 2026 01:06:57 +0100 Subject: [PATCH 09/39] catching null references --- .../Animations/Morph/AbstractMorphAnimation.cs | 6 ++++++ .../Scripts/Domain/Culling/VobMeshCullingDomain.cs | 3 +++ .../Scripts/Services/World/WayNetService.cs | 13 ++++++++++--- .../Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs | 9 ++++++++- .../VRContainerDoorPickingInteraction.cs | 3 +++ 5 files changed, 30 insertions(+), 4 deletions(-) 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/Domain/Culling/VobMeshCullingDomain.cs b/Assets/Gothic-Core/Scripts/Domain/Culling/VobMeshCullingDomain.cs index 15814e263..8612330b7 100644 --- a/Assets/Gothic-Core/Scripts/Domain/Culling/VobMeshCullingDomain.cs +++ b/Assets/Gothic-Core/Scripts/Domain/Culling/VobMeshCullingDomain.cs @@ -489,8 +489,11 @@ public void StartTrackVobPositionUpdates(GameObject go) } if (index == -1) + { Logger.LogError($"Couldn't find object in Culling list {rootGo.name}. Culling updates will break.", LogCat.Vob); + return; + } _pausedVobs.Add(rootGo, new Tuple(vobType, index)); } 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/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/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()); } From f3977a2f00b63eec0f54136a451400a8b2b4ef60 Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Wed, 10 Jun 2026 15:07:40 +0100 Subject: [PATCH 10/39] added NpcAttackAdapter through the prefab --- .../Gothic-Core/Scripts/Adapters/Npc/NpcLoader.cs | 9 --------- .../Resources/VR/Prefabs/Vobs/oCNpc.prefab | 13 +++++++++++++ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcLoader.cs b/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcLoader.cs index 33aceb740..55e32a039 100644 --- a/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcLoader.cs +++ b/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcLoader.cs @@ -10,15 +10,6 @@ public class NpcLoader : MonoBehaviour public NpcInstance Npc; public NpcContainer Container => Npc.GetUserData(); public bool IsLoaded; - - private void Start() - { - // Ensure NPC has an attack adapter for melee combat - if (GetComponent() == null) - { - gameObject.AddComponent(); - } - } } } diff --git a/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab b/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab index f4064916a..86901c493 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: 1346089012454031838} m_Layer: 0 m_Name: oCNpc m_TagString: GothicVob @@ -150,6 +151,18 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: _injectionStrategy: 2 +--- !u!114 &1346089012454031838 +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: f9fe78d5279a6924e9ab8671790275d8, type: 3} + m_Name: + m_EditorClassIdentifier: Gothic.Core::Gothic.Core.Adapters.Npc.NpcAttackAdapter --- !u!1 &2815092147823170342 GameObject: m_ObjectHideFlags: 0 From ffbf5d6252480a62788cf0fbec198b74a9476fe5 Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Wed, 10 Jun 2026 19:53:19 +0100 Subject: [PATCH 11/39] =?UTF-8?q?feat:=20NPC=20loot=20panel=20=E2=80=94=20?= =?UTF-8?q?grab=20dead=20NPCs=20to=20open=20inventory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Scripts/Models/Config/DeveloperConfig.cs | 2 + .../Services/Npc/NpcInventoryService.cs | 24 + .../Resources/VR/Prefabs/Vobs/oCNpc.prefab | 14 + Assets/Gothic-VR/Scripts/Adapters/VRNpc.cs | 13 +- .../Gothic-VR/Scripts/Adapters/VRNpcLoot.cs | 409 ++++++++++++++++++ .../Scripts/Adapters/VRNpcLoot.cs.meta | 2 + 6 files changed, 463 insertions(+), 1 deletion(-) create mode 100644 Assets/Gothic-VR/Scripts/Adapters/VRNpcLoot.cs create mode 100644 Assets/Gothic-VR/Scripts/Adapters/VRNpcLoot.cs.meta diff --git a/Assets/Gothic-Core/Scripts/Models/Config/DeveloperConfig.cs b/Assets/Gothic-Core/Scripts/Models/Config/DeveloperConfig.cs index 19a0d6c08..1c445237a 100644 --- a/Assets/Gothic-Core/Scripts/Models/Config/DeveloperConfig.cs +++ b/Assets/Gothic-Core/Scripts/Models/Config/DeveloperConfig.cs @@ -285,6 +285,8 @@ public class DebugChannelTypesCollection : CollectionWrapper 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-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab b/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab index 86901c493..2ce7f32c1 100644 --- a/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab +++ b/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab @@ -18,6 +18,7 @@ GameObject: - component: {fileID: 7657019000195239257} - component: {fileID: 6519483080425844375} - component: {fileID: 1346089012454031838} + - component: {fileID: 8467261965850404422} m_Layer: 0 m_Name: oCNpc m_TagString: GothicVob @@ -163,6 +164,19 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: f9fe78d5279a6924e9ab8671790275d8, type: 3} m_Name: m_EditorClassIdentifier: Gothic.Core::Gothic.Core.Adapters.Npc.NpcAttackAdapter +--- !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/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..3069e1733 --- /dev/null +++ b/Assets/Gothic-VR/Scripts/Adapters/VRNpcLoot.cs @@ -0,0 +1,409 @@ +#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() + { + _tempIgnoreSocketing = true; + _vrWeaponService.DrawSoundsActive = false; + + 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 IEnumerator ClearAndRefill() + { + _tempIgnoreSocketing = true; + _vrWeaponService.DrawSoundsActive = false; + + 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 From b833c9cc7a5104a3e2facd7c88a79254e6929ea9 Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Wed, 10 Jun 2026 19:54:14 +0100 Subject: [PATCH 12/39] =?UTF-8?q?fix:=20VobMeshCullingDomain=20=E2=80=94?= =?UTF-8?q?=20guard=20dynamically-spawned=20items=20against=20culling=20cr?= =?UTF-8?q?ashes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Domain/Culling/VobMeshCullingDomain.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Domain/Culling/VobMeshCullingDomain.cs b/Assets/Gothic-Core/Scripts/Domain/Culling/VobMeshCullingDomain.cs index 8612330b7..6b94a11a2 100644 --- a/Assets/Gothic-Core/Scripts/Domain/Culling/VobMeshCullingDomain.cs +++ b/Assets/Gothic-Core/Scripts/Domain/Culling/VobMeshCullingDomain.cs @@ -490,7 +490,9 @@ 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; } @@ -545,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()); @@ -570,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); } @@ -596,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); } From da2b7ee9904dfe0914f6d120b06e4327fcb0e7ff Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Wed, 10 Jun 2026 19:55:04 +0100 Subject: [PATCH 13/39] null-check destroyed renderers to prevent stuck ghost shader --- .../Services/Meshes/DynamicMaterialService.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Services/Meshes/DynamicMaterialService.cs b/Assets/Gothic-Core/Scripts/Services/Meshes/DynamicMaterialService.cs index c5249028c..f63681c2a 100644 --- a/Assets/Gothic-Core/Scripts/Services/Meshes/DynamicMaterialService.cs +++ b/Assets/Gothic-Core/Scripts/Services/Meshes/DynamicMaterialService.cs @@ -50,7 +50,7 @@ public void SetDynamicValue(GameObject go, int shaderProperty, float shaderValue ActivateDynamicRenderers(entry); // Finally set the new values. - entry.Renderers.ForEach(i => 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; } From d6a1eff76d3014ce5fdaceda8635b7e091c0976d Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Wed, 10 Jun 2026 19:56:53 +0100 Subject: [PATCH 14/39] fix: loot item texture corruption and VRSocket destroy-time re-parenting error --- .../Scripts/Domain/Vobs/VobInitializerDomain.cs | 2 +- .../Scripts/Adapters/HVROverrides/VRSocket.cs | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) 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-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; } From c17aa93045d9aa681867cdcc08ded5426c225f5f Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Thu, 11 Jun 2026 13:17:19 +0100 Subject: [PATCH 15/39] swapped Debug.Log to our own Logger service, using LogCat.Npc --- .../Scripts/Adapters/Npc/NpcAttackAdapter.cs | 40 ++++++++++--------- .../Adapters/Vob/Item/WeaponAttackAdapter.cs | 34 ++++++++-------- .../Scripts/Services/Npc/FightService.cs | 18 +++++---- 3 files changed, 49 insertions(+), 43 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcAttackAdapter.cs b/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcAttackAdapter.cs index 273df6901..24c789678 100644 --- a/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcAttackAdapter.cs +++ b/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcAttackAdapter.cs @@ -1,10 +1,12 @@ using System.Linq; using Gothic.Core.Const; -using Gothic.Core.Models.Container; using Gothic.Core.Domain.Npc.Actions.AnimationActions; +using Gothic.Core.Logging; +using Gothic.Core.Models.Container; using MyBox; using UnityEngine; using ZenKit.Daedalus; +using Logger = Gothic.Core.Logging.Logger; namespace Gothic.Core.Adapters.Npc { @@ -47,62 +49,62 @@ private void OnTriggerEnter(Collider other) // Can't hit without a valid container if (_npcContainer == null) { - Debug.LogWarning($"[NpcAttackAdapter] No NPC container"); + Logger.LogWarning($"[NpcAttackAdapter] No NPC container", LogCat.Npc); return; } // Check if this is within attack hit cooldown to prevent multiple hits per attack if (_attackHitCooldown > 0) { - Debug.LogWarning($"[NpcAttackAdapter] Hit in cooldown (remaining: {_attackHitCooldown:F2}s)"); + Logger.LogWarning($"[NpcAttackAdapter] Hit in cooldown (remaining: {_attackHitCooldown:F2}s)", LogCat.Npc); return; } // The target must have a hitbox layer if (other.gameObject.layer != Constants.VobHitbox) { - Debug.LogWarning($"[NpcAttackAdapter] Wrong layer: {LayerMask.LayerToName(other.gameObject.layer)}"); + Logger.LogWarning($"[NpcAttackAdapter] Wrong layer: {LayerMask.LayerToName(other.gameObject.layer)}", LogCat.Npc); return; } - Debug.Log($"[NpcAttackAdapter] Collision with hitbox layer: {other.gameObject.name}"); + Logger.Log($"[NpcAttackAdapter] Collision with hitbox layer: {other.gameObject.name}", LogCat.Npc); // Try to get the target NPC/Player var targetNpcLoader = other.GetComponentInParent(); if (targetNpcLoader == null) { - Debug.LogWarning($"[NpcAttackAdapter] No target NpcLoader found"); + Logger.LogWarning($"[NpcAttackAdapter] No target NpcLoader found", LogCat.Npc); return; } var targetNpcContainer = targetNpcLoader.Container; if (targetNpcContainer == null) { - Debug.LogWarning($"[NpcAttackAdapter] No target NpcContainer found"); + Logger.LogWarning($"[NpcAttackAdapter] No target NpcContainer found", LogCat.Npc); return; } - Debug.Log($"[NpcAttackAdapter] Target NPC: {targetNpcContainer.Instance.GetName(NpcNameSlot.Slot0)}"); + Logger.Log($"[NpcAttackAdapter] Target NPC: {targetNpcContainer.Instance.GetName(NpcNameSlot.Slot0)}", LogCat.Npc); // Don't let NPCs hit themselves if (targetNpcContainer == _npcContainer) { - Debug.LogWarning($"[NpcAttackAdapter] Target is self, ignoring"); + Logger.LogWarning($"[NpcAttackAdapter] Target is self, ignoring", LogCat.Npc); return; } - Debug.Log($"[NpcAttackAdapter] Checking if {_npcContainer.Instance.GetName(NpcNameSlot.Slot0)} is attacking..."); + Logger.Log($"[NpcAttackAdapter] Checking if {_npcContainer.Instance.GetName(NpcNameSlot.Slot0)} is attacking...", LogCat.Npc); // Check if the NPC is currently attacking if (!IsNpcCurrentlyAttacking()) { - Debug.LogWarning($"[NpcAttackAdapter] Not in attack state"); + Logger.LogWarning($"[NpcAttackAdapter] Not in attack state", LogCat.Npc); return; } // Fire the hit event var hitPosition = transform.position; - Debug.Log($"[NpcAttackAdapter] *** HIT FIRED! {_npcContainer.Instance.GetName(NpcNameSlot.Slot0)} → {targetNpcContainer.Instance.GetName(NpcNameSlot.Slot0)} at {hitPosition}"); + Logger.Log($"[NpcAttackAdapter] *** HIT FIRED! {_npcContainer.Instance.GetName(NpcNameSlot.Slot0)} → {targetNpcContainer.Instance.GetName(NpcNameSlot.Slot0)} at {hitPosition}", LogCat.Npc); GlobalEventDispatcher.FightHit.Invoke(_npcContainer, targetNpcContainer, hitPosition); // Set cooldown to prevent multiple hits in rapid succession (0.5 seconds) @@ -117,7 +119,7 @@ private bool IsNpcCurrentlyAttacking() { if (_npcContainer?.Props?.CurrentAction == null) { - Debug.LogWarning($"[NpcAttackAdapter] No CurrentAction"); + Logger.LogWarning($"[NpcAttackAdapter] No CurrentAction", LogCat.Npc); return false; } @@ -126,12 +128,12 @@ private bool IsNpcCurrentlyAttacking() var currentAction = _npcContainer.Props.CurrentAction; var actionTypeName = currentAction.GetType().Name; - Debug.Log($"[NpcAttackAdapter] CurrentAction type: {actionTypeName}"); + Logger.Log($"[NpcAttackAdapter] CurrentAction type: {actionTypeName}", LogCat.Npc); // Direct check for AttackPlayAni type (preferred, most reliable) if (actionTypeName == "AttackPlayAni") { - Debug.Log($"[NpcAttackAdapter] IsAttacking=TRUE (AttackPlayAni)"); + Logger.Log($"[NpcAttackAdapter] IsAttacking=TRUE (AttackPlayAni)", LogCat.Npc); return true; } @@ -143,18 +145,18 @@ private bool IsNpcCurrentlyAttacking() if (currentAction.Action?.String0 != null) { var aniName = currentAction.Action.String0.ToLower(); - Debug.Log($"[NpcAttackAdapter] PlayAni animation: {aniName}"); + Logger.Log($"[NpcAttackAdapter] PlayAni animation: {aniName}", LogCat.Npc); var isAttack = aniName.Contains("attack"); - Debug.Log($"[NpcAttackAdapter] IsAttacking={isAttack}"); + Logger.Log($"[NpcAttackAdapter] IsAttacking={isAttack}", LogCat.Npc); return isAttack; } else { - Debug.LogWarning($"[NpcAttackAdapter] PlayAni but no animation name"); + Logger.LogWarning($"[NpcAttackAdapter] PlayAni but no animation name", LogCat.Npc); } } - Debug.LogWarning($"[NpcAttackAdapter] IsAttacking=FALSE (type: {actionTypeName})"); + Logger.LogWarning($"[NpcAttackAdapter] IsAttacking=FALSE (type: {actionTypeName})", LogCat.Npc); return false; } } diff --git a/Assets/Gothic-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs b/Assets/Gothic-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs index f32dbd600..351dfb979 100644 --- a/Assets/Gothic-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs +++ b/Assets/Gothic-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs @@ -1,9 +1,11 @@ using System; -using Gothic.Core.Const; using Gothic.Core.Adapters.Npc; +using Gothic.Core.Const; +using Gothic.Core.Logging; using Gothic.Core.Models.Container; using UnityEngine; using ZenKit.Daedalus; +using Logger = Gothic.Core.Logging.Logger; namespace Gothic.Core.Adapters.Vob.Item { @@ -39,37 +41,37 @@ private void OnTriggerEnter(Collider other) { if (other.gameObject.layer != Constants.VobHitbox) { - Debug.LogWarning($"[WeaponAttackAdapter] Wrong layer: {LayerMask.LayerToName(other.gameObject.layer)}"); + Logger.LogWarning($"[WeaponAttackAdapter] Wrong layer: {LayerMask.LayerToName(other.gameObject.layer)}", LogCat.Npc); return; } - Debug.Log($"[WeaponAttackAdapter] Collision with VobHitbox: {other.gameObject.name}"); + Logger.Log($"[WeaponAttackAdapter] Collision with VobHitbox: {other.gameObject.name}", LogCat.Npc); // Try to get the target NPC/Monster from the hitbox var targetNpcLoader = other.GetComponentInParent(); if (targetNpcLoader == null) { - Debug.LogWarning($"[WeaponAttackAdapter] No NpcLoader found"); + Logger.LogWarning($"[WeaponAttackAdapter] No NpcLoader found", LogCat.Npc); return; } var targetNpcContainer = targetNpcLoader.Container; if (targetNpcContainer == null) { - Debug.LogWarning($"[WeaponAttackAdapter] No NpcContainer found"); + Logger.LogWarning($"[WeaponAttackAdapter] No NpcContainer found", LogCat.Npc); return; } - Debug.Log($"[WeaponAttackAdapter] Target: {targetNpcContainer.Instance.GetName(NpcNameSlot.Slot0)}"); + Logger.Log($"[WeaponAttackAdapter] Target: {targetNpcContainer.Instance.GetName(NpcNameSlot.Slot0)}", LogCat.Npc); // Try to fire the hit through VR weapon service if available if (TryFireHitViaVRWeaponService(targetNpcContainer)) { - Debug.Log($"[WeaponAttackAdapter] *** HIT FIRED (VR)"); + Logger.Log($"[WeaponAttackAdapter] *** HIT FIRED (VR)", LogCat.Npc); return; } - Debug.LogWarning($"[WeaponAttackAdapter] Failed to fire hit (not in attack window or VR unavailable)"); + Logger.LogWarning($"[WeaponAttackAdapter] Failed to fire hit (not in attack window or VR unavailable)", LogCat.Npc); // TODO - Add support for flat-screen weapon hits and NPC-to-NPC hits here } @@ -92,14 +94,14 @@ private bool TryFireHitViaVRWeaponService(NpcContainer targetNpcContainer) var vrWeaponServiceType = Type.GetType("Gothic.VR.Services.VRWeaponService, Gothic.VR"); if (vrWeaponServiceType == null) { - Debug.LogWarning($"[WeaponAttackAdapter] VRWeaponService not found (flat-screen mode?)"); + Logger.LogWarning($"[WeaponAttackAdapter] VRWeaponService not found (flat-screen mode?)", LogCat.Npc); return false; } var vrWeaponService = ReflexProjectInstaller.DIContainer.Resolve(vrWeaponServiceType); if (vrWeaponService == null) { - Debug.LogWarning($"[WeaponAttackAdapter] VRWeaponService could not be resolved"); + Logger.LogWarning($"[WeaponAttackAdapter] VRWeaponService could not be resolved", LogCat.Npc); return false; } @@ -109,16 +111,16 @@ private bool TryFireHitViaVRWeaponService(NpcContainer targetNpcContainer) if (isInAttackWindowMethod == null || getWeaponOwnerMethod == null) { - Debug.LogWarning($"[WeaponAttackAdapter] Required methods not found on VRWeaponService"); + Logger.LogWarning($"[WeaponAttackAdapter] Required methods not found on VRWeaponService", LogCat.Npc); return false; } // Check if this weapon is currently in an active attack window var isInAttackWindow = (bool)isInAttackWindowMethod.Invoke(vrWeaponService, new object[] { _weaponVobContainer }); - Debug.Log($"[WeaponAttackAdapter] IsInAttackWindow: {isInAttackWindow}"); + Logger.Log($"[WeaponAttackAdapter] IsInAttackWindow: {isInAttackWindow}", LogCat.Npc); if (!isInAttackWindow) { - Debug.LogWarning($"[WeaponAttackAdapter] Weapon not in attack window"); + Logger.LogWarning($"[WeaponAttackAdapter] Weapon not in attack window", LogCat.Npc); return false; } @@ -126,7 +128,7 @@ private bool TryFireHitViaVRWeaponService(NpcContainer targetNpcContainer) var attacker = (NpcContainer)getWeaponOwnerMethod.Invoke(vrWeaponService, new object[] { _weaponVobContainer }); if (attacker == null) { - Debug.LogWarning($"[WeaponAttackAdapter] No attacker found for weapon"); + Logger.LogWarning($"[WeaponAttackAdapter] No attacker found for weapon", LogCat.Npc); return false; } @@ -134,14 +136,14 @@ private bool TryFireHitViaVRWeaponService(NpcContainer targetNpcContainer) var hitPosition = transform.position; // Fire the combat event - Debug.Log($"[WeaponAttackAdapter] FightHit event: {attacker.Instance.GetName(NpcNameSlot.Slot0)} → {targetNpcContainer.Instance.GetName(NpcNameSlot.Slot0)}"); + Logger.Log($"[WeaponAttackAdapter] FightHit event: {attacker.Instance.GetName(NpcNameSlot.Slot0)} → {targetNpcContainer.Instance.GetName(NpcNameSlot.Slot0)}", LogCat.Npc); GlobalEventDispatcher.FightHit.Invoke(attacker, targetNpcContainer, hitPosition); return true; } catch (Exception ex) { // VRWeaponService not available or DI resolution failed - Debug.LogError($"[WeaponAttackAdapter] Exception in TryFireHitViaVRWeaponService: {ex.Message}"); + Logger.LogError($"[WeaponAttackAdapter] Exception in TryFireHitViaVRWeaponService: {ex.Message}", LogCat.Npc); return false; } } diff --git a/Assets/Gothic-Core/Scripts/Services/Npc/FightService.cs b/Assets/Gothic-Core/Scripts/Services/Npc/FightService.cs index 912cfb6a5..674d5d0a4 100644 --- a/Assets/Gothic-Core/Scripts/Services/Npc/FightService.cs +++ b/Assets/Gothic-Core/Scripts/Services/Npc/FightService.cs @@ -1,5 +1,6 @@ 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; @@ -9,6 +10,7 @@ using Reflex.Attributes; using UnityEngine; using ZenKit.Daedalus; +using Logger = Gothic.Core.Logging.Logger; namespace Gothic.Core.Services.Npc { @@ -31,16 +33,16 @@ public void Init() private void OnHit(NpcContainer attacker, NpcContainer target, Vector3 __) { - Debug.Log($"[FightService.OnHit] *** {attacker.Instance.GetName(NpcNameSlot.Slot0)} HIT {target.Instance.GetName(NpcNameSlot.Slot0)}"); + Logger.Log($"[FightService.OnHit] *** {attacker.Instance.GetName(NpcNameSlot.Slot0)} HIT {target.Instance.GetName(NpcNameSlot.Slot0)}", LogCat.Npc); if (OnHitUpdateHealth(attacker, target)) { - Debug.Log($"[FightService.OnHit] {target.Instance.GetName(NpcNameSlot.Slot0)} is DEAD"); + Logger.Log($"[FightService.OnHit] {target.Instance.GetName(NpcNameSlot.Slot0)} is DEAD", LogCat.Npc); target.Props.BodyState = VmGothicEnums.BodyState.BsDead; OnDyingChangeAnimation(target); } else { - Debug.Log($"[FightService.OnHit] {target.Instance.GetName(NpcNameSlot.Slot0)} took damage, playing hurt animation"); + Logger.Log($"[FightService.OnHit] {target.Instance.GetName(NpcNameSlot.Slot0)} took damage, playing hurt animation", LogCat.Npc); OnHitChangeAnimation(target); OnHitPlaySound(target); } @@ -57,30 +59,30 @@ private bool OnHitUpdateHealth(NpcContainer attacker, NpcContainer target) var maxHP = target.Vob.GetAttribute((int)NpcAttribute.HitPointsMax); var equippedWeapon = _npcHelperService.ExtNpcGetEquippedMeleeWeapon(attacker.Instance); - Debug.Log($"[FightService.OnHitUpdateHealth] Attacker: {attacker.Instance.GetName(NpcNameSlot.Slot0)}, WeaponName: {(equippedWeapon != null ? equippedWeapon.Name : "None")}, Damage: {(equippedWeapon != null ? equippedWeapon.DamageTotal.ToString() : "N/A")}"); + 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 - Debug.Log($"[FightService.OnHitUpdateHealth] {target.Instance.GetName(NpcNameSlot.Slot0)}: {hitPoints} - {damage} dmg"); + Logger.Log($"[FightService.OnHitUpdateHealth] {target.Instance.GetName(NpcNameSlot.Slot0)}: {hitPoints} - {damage} dmg", LogCat.Npc); hitPoints -= damage; - Debug.Log($"[FightService.OnHitUpdateHealth] {target.Instance.GetName(NpcNameSlot.Slot0)} HP after: {hitPoints}/{maxHP}"); + Logger.Log($"[FightService.OnHitUpdateHealth] {target.Instance.GetName(NpcNameSlot.Slot0)} HP after: {hitPoints}/{maxHP}", LogCat.Npc); target.Vob.SetAttribute((int)NpcAttribute.HitPoints, hitPoints); var statusBar = target.Go.GetComponentInChildren(true); if (statusBar != null) { - Debug.Log($"[FightService.OnHitUpdateHealth] Updating HP bar for {target.Instance.GetName(NpcNameSlot.Slot0)}"); + Logger.Log($"[FightService.OnHitUpdateHealth] Updating HP bar for {target.Instance.GetName(NpcNameSlot.Slot0)}", LogCat.Npc); statusBar.SetFillAmount(hitPoints, maxHP); } else { - Debug.LogWarning($"[FightService.OnHitUpdateHealth] No StatusBar found for {target.Instance.GetName(NpcNameSlot.Slot0)}"); + Logger.LogWarning($"[FightService.OnHitUpdateHealth] No StatusBar found for {target.Instance.GetName(NpcNameSlot.Slot0)}", LogCat.Npc); } return hitPoints <= 0; From ecde8d81eb00e0bb8ee3bbf0611efa3711811b8c Mon Sep 17 00:00:00 2001 From: JaXt0r Date: Sat, 13 Jun 2026 10:51:03 +0200 Subject: [PATCH 16/39] feat: Move config to NPC-WIP section. --- .../Scripts/Models/Config/DeveloperConfig.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Models/Config/DeveloperConfig.cs b/Assets/Gothic-Core/Scripts/Models/Config/DeveloperConfig.cs index 1c445237a..07a3d3cfe 100644 --- a/Assets/Gothic-Core/Scripts/Models/Config/DeveloperConfig.cs +++ b/Assets/Gothic-Core/Scripts/Models/Config/DeveloperConfig.cs @@ -197,7 +197,13 @@ public class DebugChannelTypesCollection : CollectionWrapper Date: Sat, 13 Jun 2026 11:30:19 +0200 Subject: [PATCH 17/39] refactor: Remove NpcAttackAdapter for now and changed logging of other adapter to CatType.Fight. --- .../Scripts/Adapters/Npc/NpcAttackAdapter.cs | 163 ------------------ .../Adapters/Npc/NpcAttackAdapter.cs.meta | 2 - .../Adapters/Vob/Item/WeaponAttackAdapter.cs | 30 ++-- .../Resources/VR/Prefabs/Vobs/oCNpc.prefab | 13 -- 4 files changed, 15 insertions(+), 193 deletions(-) delete mode 100644 Assets/Gothic-Core/Scripts/Adapters/Npc/NpcAttackAdapter.cs delete mode 100644 Assets/Gothic-Core/Scripts/Adapters/Npc/NpcAttackAdapter.cs.meta diff --git a/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcAttackAdapter.cs b/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcAttackAdapter.cs deleted file mode 100644 index 24c789678..000000000 --- a/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcAttackAdapter.cs +++ /dev/null @@ -1,163 +0,0 @@ -using System.Linq; -using Gothic.Core.Const; -using Gothic.Core.Domain.Npc.Actions.AnimationActions; -using Gothic.Core.Logging; -using Gothic.Core.Models.Container; -using MyBox; -using UnityEngine; -using ZenKit.Daedalus; -using Logger = Gothic.Core.Logging.Logger; - -namespace Gothic.Core.Adapters.Npc -{ - /// - /// Attached to NPC/Monster root collider to allow them to deal melee damage via unarmed attacks. - /// Fires FightHit events when the NPC's collider hits an opponent during attack animations. - /// - public class NpcAttackAdapter : MonoBehaviour - { - private NpcContainer _npcContainer; - private BoxCollider _collider; - private float _attackHitCooldown; - - private void Start() - { - var npcLoader = GetComponentInParent(); - if (npcLoader != null) - { - _npcContainer = npcLoader.Container; - } - - _collider = GetComponent(); - } - - private void Update() - { - // Reduce cooldown timer each frame - if (_attackHitCooldown > 0) - { - _attackHitCooldown -= Time.deltaTime; - } - } - - /// - /// Called when the NPC's collider hits something during combat. - /// Checks if the NPC is actively attacking and if the target is valid. - /// - private void OnTriggerEnter(Collider other) - { - // Can't hit without a valid container - if (_npcContainer == null) - { - Logger.LogWarning($"[NpcAttackAdapter] No NPC container", LogCat.Npc); - return; - } - - // Check if this is within attack hit cooldown to prevent multiple hits per attack - if (_attackHitCooldown > 0) - { - Logger.LogWarning($"[NpcAttackAdapter] Hit in cooldown (remaining: {_attackHitCooldown:F2}s)", LogCat.Npc); - return; - } - - // The target must have a hitbox layer - if (other.gameObject.layer != Constants.VobHitbox) - { - Logger.LogWarning($"[NpcAttackAdapter] Wrong layer: {LayerMask.LayerToName(other.gameObject.layer)}", LogCat.Npc); - return; - } - - Logger.Log($"[NpcAttackAdapter] Collision with hitbox layer: {other.gameObject.name}", LogCat.Npc); - - // Try to get the target NPC/Player - var targetNpcLoader = other.GetComponentInParent(); - if (targetNpcLoader == null) - { - Logger.LogWarning($"[NpcAttackAdapter] No target NpcLoader found", LogCat.Npc); - return; - } - - var targetNpcContainer = targetNpcLoader.Container; - if (targetNpcContainer == null) - { - Logger.LogWarning($"[NpcAttackAdapter] No target NpcContainer found", LogCat.Npc); - return; - } - - Logger.Log($"[NpcAttackAdapter] Target NPC: {targetNpcContainer.Instance.GetName(NpcNameSlot.Slot0)}", LogCat.Npc); - - // Don't let NPCs hit themselves - if (targetNpcContainer == _npcContainer) - { - Logger.LogWarning($"[NpcAttackAdapter] Target is self, ignoring", LogCat.Npc); - return; - } - - Logger.Log($"[NpcAttackAdapter] Checking if {_npcContainer.Instance.GetName(NpcNameSlot.Slot0)} is attacking...", LogCat.Npc); - - // Check if the NPC is currently attacking - if (!IsNpcCurrentlyAttacking()) - { - Logger.LogWarning($"[NpcAttackAdapter] Not in attack state", LogCat.Npc); - return; - } - - // Fire the hit event - var hitPosition = transform.position; - Logger.Log($"[NpcAttackAdapter] *** HIT FIRED! {_npcContainer.Instance.GetName(NpcNameSlot.Slot0)} → {targetNpcContainer.Instance.GetName(NpcNameSlot.Slot0)} at {hitPosition}", LogCat.Npc); - GlobalEventDispatcher.FightHit.Invoke(_npcContainer, targetNpcContainer, hitPosition); - - // Set cooldown to prevent multiple hits in rapid succession (0.5 seconds) - _attackHitCooldown = 0.5f; - } - - /// - /// Determines if the NPC is currently in an attack animation. - /// Checks if the current animation action is an attack-related action. - /// - private bool IsNpcCurrentlyAttacking() - { - if (_npcContainer?.Props?.CurrentAction == null) - { - Logger.LogWarning($"[NpcAttackAdapter] No CurrentAction", LogCat.Npc); - return false; - } - - // Check if the current action is an attack-based action - // AttackPlayAni is used for all NPC melee attacks - var currentAction = _npcContainer.Props.CurrentAction; - var actionTypeName = currentAction.GetType().Name; - - Logger.Log($"[NpcAttackAdapter] CurrentAction type: {actionTypeName}", LogCat.Npc); - - // Direct check for AttackPlayAni type (preferred, most reliable) - if (actionTypeName == "AttackPlayAni") - { - Logger.Log($"[NpcAttackAdapter] IsAttacking=TRUE (AttackPlayAni)", LogCat.Npc); - return true; - } - - // Also support PlayAni with attack animations by checking the animation name - if (actionTypeName == "PlayAni") - { - // PlayAni stores animation name in Action.String0 - // Animation names for attacks typically contain "attack" or follow pattern like "s_1hAttack", "s_2hAttack" - if (currentAction.Action?.String0 != null) - { - var aniName = currentAction.Action.String0.ToLower(); - Logger.Log($"[NpcAttackAdapter] PlayAni animation: {aniName}", LogCat.Npc); - var isAttack = aniName.Contains("attack"); - Logger.Log($"[NpcAttackAdapter] IsAttacking={isAttack}", LogCat.Npc); - return isAttack; - } - else - { - Logger.LogWarning($"[NpcAttackAdapter] PlayAni but no animation name", LogCat.Npc); - } - } - - Logger.LogWarning($"[NpcAttackAdapter] IsAttacking=FALSE (type: {actionTypeName})", LogCat.Npc); - return false; - } - } -} diff --git a/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcAttackAdapter.cs.meta b/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcAttackAdapter.cs.meta deleted file mode 100644 index 0652e3be5..000000000 --- a/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcAttackAdapter.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: f9fe78d5279a6924e9ab8671790275d8 \ No newline at end of file diff --git a/Assets/Gothic-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs b/Assets/Gothic-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs index 351dfb979..50c9ec1fa 100644 --- a/Assets/Gothic-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs +++ b/Assets/Gothic-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs @@ -41,37 +41,37 @@ private void OnTriggerEnter(Collider other) { if (other.gameObject.layer != Constants.VobHitbox) { - Logger.LogWarning($"[WeaponAttackAdapter] Wrong layer: {LayerMask.LayerToName(other.gameObject.layer)}", LogCat.Npc); + Logger.LogWarning($"[WeaponAttackAdapter] Wrong layer: {LayerMask.LayerToName(other.gameObject.layer)}", LogCat.Fight); return; } - Logger.Log($"[WeaponAttackAdapter] Collision with VobHitbox: {other.gameObject.name}", LogCat.Npc); + 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.Npc); + Logger.LogWarning($"[WeaponAttackAdapter] No NpcLoader found", LogCat.Fight); return; } var targetNpcContainer = targetNpcLoader.Container; if (targetNpcContainer == null) { - Logger.LogWarning($"[WeaponAttackAdapter] No NpcContainer found", LogCat.Npc); + Logger.LogWarning("[WeaponAttackAdapter] No NpcContainer found", LogCat.Fight); return; } - Logger.Log($"[WeaponAttackAdapter] Target: {targetNpcContainer.Instance.GetName(NpcNameSlot.Slot0)}", LogCat.Npc); + 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.Npc); + Logger.Log($"[WeaponAttackAdapter] *** HIT FIRED (VR)", LogCat.Fight); return; } - Logger.LogWarning($"[WeaponAttackAdapter] Failed to fire hit (not in attack window or VR unavailable)", LogCat.Npc); + 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 } @@ -94,14 +94,14 @@ private bool TryFireHitViaVRWeaponService(NpcContainer targetNpcContainer) var vrWeaponServiceType = Type.GetType("Gothic.VR.Services.VRWeaponService, Gothic.VR"); if (vrWeaponServiceType == null) { - Logger.LogWarning($"[WeaponAttackAdapter] VRWeaponService not found (flat-screen mode?)", LogCat.Npc); + Logger.LogWarning("[WeaponAttackAdapter] VRWeaponService not found (flat-screen mode?)", LogCat.Fight); return false; } var vrWeaponService = ReflexProjectInstaller.DIContainer.Resolve(vrWeaponServiceType); if (vrWeaponService == null) { - Logger.LogWarning($"[WeaponAttackAdapter] VRWeaponService could not be resolved", LogCat.Npc); + Logger.LogWarning("[WeaponAttackAdapter] VRWeaponService could not be resolved", LogCat.Fight); return false; } @@ -111,16 +111,16 @@ private bool TryFireHitViaVRWeaponService(NpcContainer targetNpcContainer) if (isInAttackWindowMethod == null || getWeaponOwnerMethod == null) { - Logger.LogWarning($"[WeaponAttackAdapter] Required methods not found on VRWeaponService", LogCat.Npc); + Logger.LogWarning("[WeaponAttackAdapter] Required methods not found on VRWeaponService", LogCat.Fight); return false; } // Check if this weapon is currently in an active attack window var isInAttackWindow = (bool)isInAttackWindowMethod.Invoke(vrWeaponService, new object[] { _weaponVobContainer }); - Logger.Log($"[WeaponAttackAdapter] IsInAttackWindow: {isInAttackWindow}", LogCat.Npc); + Logger.Log($"[WeaponAttackAdapter] IsInAttackWindow: {isInAttackWindow}", LogCat.Fight); if (!isInAttackWindow) { - Logger.LogWarning($"[WeaponAttackAdapter] Weapon not in attack window", LogCat.Npc); + Logger.LogWarning("[WeaponAttackAdapter] Weapon not in attack window", LogCat.Fight); return false; } @@ -128,7 +128,7 @@ private bool TryFireHitViaVRWeaponService(NpcContainer targetNpcContainer) var attacker = (NpcContainer)getWeaponOwnerMethod.Invoke(vrWeaponService, new object[] { _weaponVobContainer }); if (attacker == null) { - Logger.LogWarning($"[WeaponAttackAdapter] No attacker found for weapon", LogCat.Npc); + Logger.LogWarning("[WeaponAttackAdapter] No attacker found for weapon", LogCat.Fight); return false; } @@ -136,14 +136,14 @@ private bool TryFireHitViaVRWeaponService(NpcContainer targetNpcContainer) var hitPosition = transform.position; // Fire the combat event - Logger.Log($"[WeaponAttackAdapter] FightHit event: {attacker.Instance.GetName(NpcNameSlot.Slot0)} → {targetNpcContainer.Instance.GetName(NpcNameSlot.Slot0)}", LogCat.Npc); + Logger.Log($"[WeaponAttackAdapter] FightHit event: {attacker.Instance.GetName(NpcNameSlot.Slot0)} → {targetNpcContainer.Instance.GetName(NpcNameSlot.Slot0)}", LogCat.Fight); GlobalEventDispatcher.FightHit.Invoke(attacker, targetNpcContainer, hitPosition); return true; } catch (Exception ex) { // VRWeaponService not available or DI resolution failed - Logger.LogError($"[WeaponAttackAdapter] Exception in TryFireHitViaVRWeaponService: {ex.Message}", LogCat.Npc); + Logger.LogError($"[WeaponAttackAdapter] Exception in TryFireHitViaVRWeaponService: {ex.Message}", LogCat.Fight); return false; } } diff --git a/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab b/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab index 2ce7f32c1..f0a06aff6 100644 --- a/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab +++ b/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab @@ -17,7 +17,6 @@ GameObject: - component: {fileID: 3792221975768868209} - component: {fileID: 7657019000195239257} - component: {fileID: 6519483080425844375} - - component: {fileID: 1346089012454031838} - component: {fileID: 8467261965850404422} m_Layer: 0 m_Name: oCNpc @@ -152,18 +151,6 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: _injectionStrategy: 2 ---- !u!114 &1346089012454031838 -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: f9fe78d5279a6924e9ab8671790275d8, type: 3} - m_Name: - m_EditorClassIdentifier: Gothic.Core::Gothic.Core.Adapters.Npc.NpcAttackAdapter --- !u!114 &8467261965850404422 MonoBehaviour: m_ObjectHideFlags: 0 From b10b80be9b71702831b8c10dc46a8921113faf6c Mon Sep 17 00:00:00 2001 From: JaXt0r Date: Sat, 13 Jun 2026 12:07:27 +0200 Subject: [PATCH 18/39] refactor: Move WeaponAttackAdapter to VR module. --- .../Resources/DeveloperConfigs/Production.asset | 2 ++ .../Scripts/Models/Config/DeveloperConfig.cs | 5 +++-- .../Resources/VR/Prefabs/Vobs/oCItem/Weapon.prefab | 11 +++++++++-- .../Scripts/Adapters/Player/VRFistAttackAdapter.cs | 4 ++-- .../Adapters/Vob/VobItem/VRWeaponAttackAdapter.cs} | 8 ++++++-- .../Vob/VobItem/VRWeaponAttackAdapter.cs.meta} | 0 6 files changed, 22 insertions(+), 8 deletions(-) rename Assets/{Gothic-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs => Gothic-VR/Scripts/Adapters/Vob/VobItem/VRWeaponAttackAdapter.cs} (97%) rename Assets/{Gothic-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs.meta => Gothic-VR/Scripts/Adapters/Vob/VobItem/VRWeaponAttackAdapter.cs.meta} (100%) 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/Models/Config/DeveloperConfig.cs b/Assets/Gothic-Core/Scripts/Models/Config/DeveloperConfig.cs index 07a3d3cfe..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 /// 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-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs b/Assets/Gothic-VR/Scripts/Adapters/Vob/VobItem/VRWeaponAttackAdapter.cs similarity index 97% rename from Assets/Gothic-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs rename to Assets/Gothic-VR/Scripts/Adapters/Vob/VobItem/VRWeaponAttackAdapter.cs index 50c9ec1fa..05c5f9456 100644 --- a/Assets/Gothic-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs +++ b/Assets/Gothic-VR/Scripts/Adapters/Vob/VobItem/VRWeaponAttackAdapter.cs @@ -1,5 +1,8 @@ +#if GOTHIC_HVR_INSTALLED using System; +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; @@ -7,7 +10,7 @@ using ZenKit.Daedalus; using Logger = Gothic.Core.Logging.Logger; -namespace Gothic.Core.Adapters.Vob.Item +namespace Gothic.VR.Adapters.Vob.VobItem { /// /// The validity check for a hit requires answering: "Is this attack currently active?" @@ -15,7 +18,7 @@ namespace Gothic.Core.Adapters.Vob.Item /// 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 + public class VRWeaponAttackAdapter : MonoBehaviour { private VobContainer _weaponVobContainer; private NpcContainer _targetNpcContainer; @@ -149,3 +152,4 @@ private bool TryFireHitViaVRWeaponService(NpcContainer targetNpcContainer) } } } +#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 From 2cb2a49857db13d588a663a95c51b1e299ad30f2 Mon Sep 17 00:00:00 2001 From: JaXt0r Date: Sat, 13 Jun 2026 12:12:06 +0200 Subject: [PATCH 19/39] refactor: Remove reflections. --- .../Vob/VobItem/VRWeaponAttackAdapter.cs | 80 +++++-------------- 1 file changed, 22 insertions(+), 58 deletions(-) diff --git a/Assets/Gothic-VR/Scripts/Adapters/Vob/VobItem/VRWeaponAttackAdapter.cs b/Assets/Gothic-VR/Scripts/Adapters/Vob/VobItem/VRWeaponAttackAdapter.cs index 05c5f9456..62d6ef39f 100644 --- a/Assets/Gothic-VR/Scripts/Adapters/Vob/VobItem/VRWeaponAttackAdapter.cs +++ b/Assets/Gothic-VR/Scripts/Adapters/Vob/VobItem/VRWeaponAttackAdapter.cs @@ -1,11 +1,12 @@ #if GOTHIC_HVR_INSTALLED -using System; 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; @@ -20,6 +21,8 @@ namespace Gothic.VR.Adapters.Vob.VobItem /// public class VRWeaponAttackAdapter : MonoBehaviour { + [Inject] private readonly VRWeaponService _vrWeaponService; + private VobContainer _weaponVobContainer; private NpcContainer _targetNpcContainer; @@ -84,71 +87,32 @@ private void OnTriggerExit(Collider other) return; } - /// - /// Attempts to validate and fire a hit using VRWeaponService (VR mode only). - /// Returns true if the hit was processed, false otherwise. - /// private bool TryFireHitViaVRWeaponService(NpcContainer targetNpcContainer) { - try + if (_vrWeaponService == null) { - // Try to dynamically get the VRWeaponService through the Reflex DI container - // Using reflection to avoid direct assembly dependency on Gothic.VR - var vrWeaponServiceType = Type.GetType("Gothic.VR.Services.VRWeaponService, Gothic.VR"); - if (vrWeaponServiceType == null) - { - Logger.LogWarning("[WeaponAttackAdapter] VRWeaponService not found (flat-screen mode?)", LogCat.Fight); - return false; - } - - var vrWeaponService = ReflexProjectInstaller.DIContainer.Resolve(vrWeaponServiceType); - if (vrWeaponService == null) - { - Logger.LogWarning("[WeaponAttackAdapter] VRWeaponService could not be resolved", LogCat.Fight); - return false; - } - - // Use reflection to call the methods - var isInAttackWindowMethod = vrWeaponServiceType.GetMethod("IsWeaponInAttackWindow"); - var getWeaponOwnerMethod = vrWeaponServiceType.GetMethod("GetWeaponOwner"); - - if (isInAttackWindowMethod == null || getWeaponOwnerMethod == null) - { - Logger.LogWarning("[WeaponAttackAdapter] Required methods not found on VRWeaponService", LogCat.Fight); - return false; - } - - // Check if this weapon is currently in an active attack window - var isInAttackWindow = (bool)isInAttackWindowMethod.Invoke(vrWeaponService, new object[] { _weaponVobContainer }); - Logger.Log($"[WeaponAttackAdapter] IsInAttackWindow: {isInAttackWindow}", LogCat.Fight); - if (!isInAttackWindow) - { - Logger.LogWarning("[WeaponAttackAdapter] Weapon not in attack window", LogCat.Fight); - return false; - } - - // Get who is attacking with this weapon - var attacker = (NpcContainer)getWeaponOwnerMethod.Invoke(vrWeaponService, new object[] { _weaponVobContainer }); - if (attacker == null) - { - Logger.LogWarning("[WeaponAttackAdapter] No attacker found for weapon", LogCat.Fight); - return false; - } - - // Get the hit position for effects/knockback later - var hitPosition = transform.position; + Logger.LogWarning("[WeaponAttackAdapter] VRWeaponService not injected (flat-screen mode?)", LogCat.Fight); + return false; + } - // Fire the combat event - Logger.Log($"[WeaponAttackAdapter] FightHit event: {attacker.Instance.GetName(NpcNameSlot.Slot0)} → {targetNpcContainer.Instance.GetName(NpcNameSlot.Slot0)}", LogCat.Fight); - GlobalEventDispatcher.FightHit.Invoke(attacker, targetNpcContainer, hitPosition); - return true; + 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; } - catch (Exception ex) + + var attacker = _vrWeaponService.GetWeaponOwner(_weaponVobContainer); + if (attacker == null) { - // VRWeaponService not available or DI resolution failed - Logger.LogError($"[WeaponAttackAdapter] Exception in TryFireHitViaVRWeaponService: {ex.Message}", LogCat.Fight); + 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; } } } From 27f90a1321c9f93ee428bc1a44de0d97e8ac766b Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Sat, 13 Jun 2026 15:30:37 +0100 Subject: [PATCH 20/39] requested cleanup: moved populate sockets logic into its own method --- .../Gothic-VR/Scripts/Adapters/VRNpcLoot.cs | 43 +++++-------------- 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/Assets/Gothic-VR/Scripts/Adapters/VRNpcLoot.cs b/Assets/Gothic-VR/Scripts/Adapters/VRNpcLoot.cs index 3069e1733..8181fe115 100644 --- a/Assets/Gothic-VR/Scripts/Adapters/VRNpcLoot.cs +++ b/Assets/Gothic-VR/Scripts/Adapters/VRNpcLoot.cs @@ -180,45 +180,24 @@ private List BuildLootList() private IEnumerator FillSockets() { - _tempIgnoreSocketing = true; - _vrWeaponService.DrawSoundsActive = false; - - 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; + yield return PopulateSockets(clearFirst: false); } private IEnumerator ClearAndRefill() + { + yield return PopulateSockets(clearFirst: true); + } + + private IEnumerator PopulateSockets(bool clearFirst) { _tempIgnoreSocketing = true; _vrWeaponService.DrawSoundsActive = false; - ClearSocketContents(); - yield return null; + if (clearFirst) + { + ClearSocketContents(); + yield return null; + } foreach (var entry in BuildLootList().Take(MaxVisibleSlots)) { From 28b273001adac94293ad6c3e0250b9fa29468bdb Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Fri, 12 Jun 2026 01:17:57 +0100 Subject: [PATCH 21/39] fix: restore GlobalOther/GlobalVictim correctly after perception call, use hero Index for distance check Previously oldOther was incorrectly assigned from GlobalVictim instead of GlobalOther, causing both globals to be restored to the same (victim) value after every perception call. Also replaces the fragile npc2.Id==0 hero check with an Index comparison against GlobalHero. --- .../Scripts/Services/Npc/NpcAiService.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs b/Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs index 971fd2c8e..e22244c7a 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; } @@ -395,7 +395,7 @@ public bool ExtNpcIsDead(NpcInstance npcInstance) public bool ExtNpcIsInState(NpcInstance npc, int state) { - return npc.GetUserData().Vob.CurrentStateIndex == state; + return npc.GetUserData()?.Vob.CurrentStateIndex == state; } public bool ExtNpcIsPlayer(NpcInstance npc) From 2e4feff883795d78267cec499f4fc9324715b2eb Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Fri, 12 Jun 2026 01:18:12 +0100 Subject: [PATCH 22/39] fix: NpcHelperService aiState filter compared wrong NPC's state index Filter was comparing npcVob.CurrentStateIndex (the calling NPC's index) against each candidate's state, instead of comparing the requested aiState value. --- Assets/Gothic-Core/Scripts/Services/Npc/NpcHelperService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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(); From aa507e7a76b34e7f0a1fe08571299c381ac23528 Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Fri, 12 Jun 2026 01:21:13 +0100 Subject: [PATCH 23/39] fix: death/hit animation bypasses queue, guard double-hit on dead NPC, skip AI_Attack for humans - Death: clear queue + StopAllAnimations then play directly so dying always wins - Hit: overlay hurt anim without interrupting current action - OnHit: early-out if target already dead to prevent double damage/animation - Attack: skip with log for human NPCs (guild < GIL_SEPERATOR_HUM), not yet implemented --- .../Npc/Actions/AnimationActions/Attack.cs | 7 +++++++ .../Scripts/Services/Npc/FightService.cs | 20 +++++++++---------- 2 files changed, 17 insertions(+), 10 deletions(-) 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/Services/Npc/FightService.cs b/Assets/Gothic-Core/Scripts/Services/Npc/FightService.cs index 674d5d0a4..163910d20 100644 --- a/Assets/Gothic-Core/Scripts/Services/Npc/FightService.cs +++ b/Assets/Gothic-Core/Scripts/Services/Npc/FightService.cs @@ -1,11 +1,9 @@ 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; @@ -17,7 +15,6 @@ 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; @@ -33,6 +30,9 @@ public void Init() 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)) { @@ -90,21 +90,21 @@ private bool OnHitUpdateHealth(NpcContainer attacker, NpcContainer target) 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) From 44129093250d666b3753ad92989c98f43fdab4ea Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Fri, 12 Jun 2026 01:25:19 +0100 Subject: [PATCH 24/39] fix: release FreePoint when NPC switches routine, remove stale commented-out ClearState block --- .../Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs b/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs index e35458390..c03289dd9 100644 --- a/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs +++ b/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs @@ -254,14 +254,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 +287,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; } From 9145046dac22ff7a9d9e1e4f73ccbc4b4ef9d5a9 Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Fri, 12 Jun 2026 01:26:19 +0100 Subject: [PATCH 25/39] fix: GoToNpc stops 1.5m before target, GoToWp guards null path from FindFastestPath --- .../Domain/Npc/Actions/AnimationActions/GoToNpc.cs | 8 +++++++- .../Domain/Npc/Actions/AnimationActions/GoToWp.cs | 10 ++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) 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(); } From af38def00e9bf46a2b7078ee407c21e351917dab Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Fri, 12 Jun 2026 01:27:44 +0100 Subject: [PATCH 26/39] fix: FormatException when parsing animation event slots with trailing spaces Split(' ') on strings like '5 ' produces an empty token; added Where filter to skip empty entries before Convert.ToInt32 on OptimalFrame, HitEnd, ComboWindow. --- .../Scripts/Adapters/Animations/AnimationSystem.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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: From d172a4f4b2c8941dd7e80dcadbcaa428f43d6572 Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Fri, 12 Jun 2026 01:37:54 +0100 Subject: [PATCH 27/39] fix: NpcHeadMeshBuilder null-safe NpcLoader/Npc access during async head build --- .../Scripts/Domain/Meshes/Builder/NpcHeadMeshBuilder.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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; From 181d2c6a744da708a8793b644f01f7882e3b1ea8 Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Fri, 12 Jun 2026 01:39:24 +0100 Subject: [PATCH 28/39] feat: pre-warm TMP font atlas from Daedalus VM string symbols on startup Iterates all string-type Daedalus symbols and collects unique characters, then calls TryAddCharacters to bake them into the atlas before any UI renders. Prevents TMP fallback glyphs on first display of Gothic NPC/item names. --- .../Scripts/Services/UI/FontService.cs | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs b/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs index 327456552..5d4816a30 100644 --- a/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs +++ b/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Reflection; using Gothic.Core.Const; using Gothic.Core.Logging; @@ -8,6 +10,7 @@ using TMPro; using UnityEngine; using UnityEngine.TextCore; +using ZenKit; using Logger = Gothic.Core.Logging.Logger; namespace Gothic.Core.Services.UI @@ -30,6 +33,30 @@ public void Create() TMP_Settings.defaultSpriteAsset = DefaultSpriteAsset; TMP_Settings.defaultFontAsset = DefaultFont; + + WarmAtlasFromVm(); + } + + private void WarmAtlasFromVm() + { + if (DefaultFont == null || _gameStateService.GothicVm == null) + return; + + var uniqueChars = new HashSet(); + foreach (var symbol in _gameStateService.GothicVm.Symbols) + { + if (symbol.Type != DaedalusDataType.String || symbol.Size == 0) continue; + for (ushort i = 0; i < symbol.Size; i++) + { + var str = symbol.GetString(i); + if (string.IsNullOrEmpty(str)) continue; + foreach (var c in str) + uniqueChars.Add(c); + } + } + + DefaultFont.TryAddCharacters(new string(uniqueChars.ToArray())); + Logger.Log($"Font atlas pre-warmed with {uniqueChars.Count} unique chars", LogCat.Misc); } [CanBeNull] @@ -109,13 +136,13 @@ public TMP_SpriteAsset TryGetFont(string fontName) return spriteAsset; } - private Material GetDefaultSpriteMaterial(Texture2D spriteSheet = null) + private UnityEngine.Material GetDefaultSpriteMaterial(Texture2D spriteSheet = null) { ShaderUtilities.GetShaderPropertyIDs(); // Add a new material var shader = Constants.ShaderTMPSprite; - var tempMaterial = new Material(shader); + var tempMaterial = new UnityEngine.Material(shader); tempMaterial.SetTexture(ShaderUtilities.ID_MainTex, spriteSheet); return tempMaterial; From bb6a9cc2acb2d35cd345c76639f9109d39420921 Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Fri, 12 Jun 2026 17:08:48 +0100 Subject: [PATCH 29/39] rollback font --- .../Scripts/Services/UI/FontService.cs | 32 ++----------------- 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs b/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs index 5d4816a30..6796a4104 100644 --- a/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs +++ b/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using Gothic.Core.Const; using Gothic.Core.Logging; @@ -10,7 +7,6 @@ using TMPro; using UnityEngine; using UnityEngine.TextCore; -using ZenKit; using Logger = Gothic.Core.Logging.Logger; namespace Gothic.Core.Services.UI @@ -33,30 +29,6 @@ public void Create() TMP_Settings.defaultSpriteAsset = DefaultSpriteAsset; TMP_Settings.defaultFontAsset = DefaultFont; - - WarmAtlasFromVm(); - } - - private void WarmAtlasFromVm() - { - if (DefaultFont == null || _gameStateService.GothicVm == null) - return; - - var uniqueChars = new HashSet(); - foreach (var symbol in _gameStateService.GothicVm.Symbols) - { - if (symbol.Type != DaedalusDataType.String || symbol.Size == 0) continue; - for (ushort i = 0; i < symbol.Size; i++) - { - var str = symbol.GetString(i); - if (string.IsNullOrEmpty(str)) continue; - foreach (var c in str) - uniqueChars.Add(c); - } - } - - DefaultFont.TryAddCharacters(new string(uniqueChars.ToArray())); - Logger.Log($"Font atlas pre-warmed with {uniqueChars.Count} unique chars", LogCat.Misc); } [CanBeNull] @@ -136,13 +108,13 @@ public TMP_SpriteAsset TryGetFont(string fontName) return spriteAsset; } - private UnityEngine.Material GetDefaultSpriteMaterial(Texture2D spriteSheet = null) + private Material GetDefaultSpriteMaterial(Texture2D spriteSheet = null) { ShaderUtilities.GetShaderPropertyIDs(); // Add a new material var shader = Constants.ShaderTMPSprite; - var tempMaterial = new UnityEngine.Material(shader); + var tempMaterial = new Material(shader); tempMaterial.SetTexture(ShaderUtilities.ID_MainTex, spriteSheet); return tempMaterial; From 5941ca50ea4f95999eb01eb1a231fb36c4aa22f8 Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Fri, 12 Jun 2026 17:41:20 +0100 Subject: [PATCH 30/39] fix for hp bar - they appeared empty in actual VR --- .../Resources/Prefabs/UI/StatusBars/StatusBar.prefab | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 80e70c26ebe83aa84be55c48d490881155fc08b8 Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Fri, 12 Jun 2026 17:44:54 +0100 Subject: [PATCH 31/39] fix: set LiberationSans SDF empty to Static mode so Gothic bitmap chars always fall through to sprite asset TMP's GetTextElement() checks the font asset before the sprite asset. In Dynamic mode the empty Liberation Sans atlas was being populated with Gothic characters on first render, causing them to render in Liberation Sans instead of the Gothic bitmap font. Switching to Static prevents any atlas population so all characters fall through to the Gothic TMP_SpriteAsset set by AbstractMenu/dialogs. Also cleared the placeholder texts ("x/y", "{{CATEGORY}}") in BackPack.prefab; they were triggering TMP warnings before boot because they rendered before FontService set the default sprite asset. --- .../FontAsset/LiberationSans SDF empty.asset | 45 +++++++++---------- .../Prefabs/Player-Elements/BackPack.prefab | 8 ++-- 2 files changed, 26 insertions(+), 27 deletions(-) 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-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 From 5ac25ed11304ebbda0dd1f0ba9f844e3335ae55a Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Fri, 12 Jun 2026 22:11:40 +0100 Subject: [PATCH 32/39] fix: Npc_HasItems now reads packed inventory; world item pickup amount clamped to 1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ExtNpcHasItems was reading from GetItem/ItemCount (VOB world-file list) which is never populated by runtime CreateInvItems calls. Rewrote to iterate all InvCats and read from GetPacked via GetInventoryItems — the same path used by all other inventory operations. VRBackpack was passing IItem.Amount directly to AddItem/RemoveItem. In ZenKit, world VOB items have Amount=0 (Gothic convention: single item = amount 0), so items picked up from the floor were stored as e.g. ITFOBEERx0 in packed inventory. Applied Mathf.Max(1, amount) to all four socket methods to treat 0 as 1. Fixes Npc_HasItems always returning 0 for player inventory, breaking dialogs that check whether the player has items (Fisk ore purchase, Bloodwyn extortion, Drax beer, etc.). --- .../Services/Npc/NpcInventoryService.cs | 23 ++++++++++++++----- .../Scripts/Adapters/Player/VRBackpack.cs | 12 +++++----- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Services/Npc/NpcInventoryService.cs b/Assets/Gothic-Core/Scripts/Services/Npc/NpcInventoryService.cs index ccd9edb66..0017f8d19 100644 --- a/Assets/Gothic-Core/Scripts/Services/Npc/NpcInventoryService.cs +++ b/Assets/Gothic-Core/Scripts/Services/Npc/NpcInventoryService.cs @@ -137,15 +137,26 @@ public List GetAllInventoryItems(NpcInstance npc) 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-VR/Scripts/Adapters/Player/VRBackpack.cs b/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs index 8ad6da4bc..ff31989d0 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(); } @@ -116,21 +116,21 @@ public void OnItemPutOutOfHolster(HVRGrabberBase grabber, HVRGrabbable grabbable var vobLoader = grabbable.GetComponentInParent(); 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(); } From cdff9e9ecc40e66b6cccac50a0ae9a69fec9a07d Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Sat, 13 Jun 2026 00:51:20 +0100 Subject: [PATCH 33/39] fix: prevent InitVobCoroutine crash on broken VOBs; fix null GetFirstSound in GetSoundClip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InitVobCoroutine now wraps each InitVob call in try/catch so a single broken VOB can't halt all lazy loading. Root cause was BIRD/OWL NEAR MARKET referencing wood_night2 which doesn't exist in Gothic's SfxInst.d — GetFirstSound() returned null and silently killed the coroutine. SfxModel now logs a warning when an SFX symbol is missing from the VM. --- .../Scripts/Domain/Vobs/VobInitializerDomain.cs | 10 +++++++--- Assets/Gothic-Core/Scripts/Models/Audio/SfxModel.cs | 7 +++++-- .../Gothic-Core/Scripts/Services/Vobs/VobService.cs | 11 +++++++++-- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Domain/Vobs/VobInitializerDomain.cs b/Assets/Gothic-Core/Scripts/Domain/Vobs/VobInitializerDomain.cs index 3e37bb0cd..1a1a9110d 100644 --- a/Assets/Gothic-Core/Scripts/Domain/Vobs/VobInitializerDomain.cs +++ b/Assets/Gothic-Core/Scripts/Domain/Vobs/VobInitializerDomain.cs @@ -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/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(); } From 9717be15cc540b5a56b5ef62a0de46a872452101 Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Sat, 13 Jun 2026 00:52:21 +0100 Subject: [PATCH 34/39] fix: release HVR grab and restore kinematic when opening dead NPC loot HVR sets isKinematic=false on the grabbed Rigidbody during the grab, which caused the corpse to be affected by gravity and pushable by the player capsule after releasing. ForceRelease + DisablePhysicsForNpc ensures the body stays frozen after the loot panel opens. --- Assets/Gothic-VR/Scripts/Adapters/VRNpc.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Assets/Gothic-VR/Scripts/Adapters/VRNpc.cs b/Assets/Gothic-VR/Scripts/Adapters/VRNpc.cs index f0e0e1eac..8664f2289 100644 --- a/Assets/Gothic-VR/Scripts/Adapters/VRNpc.cs +++ b/Assets/Gothic-VR/Scripts/Adapters/VRNpc.cs @@ -6,6 +6,7 @@ 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 HurricaneVR.Framework.Core; @@ -22,6 +23,7 @@ public class VRNpc : MonoBehaviour [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; @@ -39,6 +41,9 @@ public void OnGrabbed(HVRGrabberBase grabber, HVRGrabbable grabbable) 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; } From e0f805dc5f90eaaab2370e54ae106b055e9f613f Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Sat, 13 Jun 2026 00:57:32 +0100 Subject: [PATCH 35/39] fix: null guards in VobMeshCullingDomain, VRBackpack, AiHandler - VobMeshCullingDomain: skip destroyed Rigidbodies in StopVobTrackingBasedOnVelocity - VRBackpack: guard null vobLoader in OnItemPutOutOfHolster - AiHandler: guard null waypoint in ReEnableNpc, log warning so bad re-enables are visible --- Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs | 7 +++++-- .../Scripts/Domain/Culling/VobMeshCullingDomain.cs | 5 +++++ Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs | 2 ++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs b/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs index c03289dd9..21af979c0 100644 --- a/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs +++ b/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs @@ -334,8 +334,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/Domain/Culling/VobMeshCullingDomain.cs b/Assets/Gothic-Core/Scripts/Domain/Culling/VobMeshCullingDomain.cs index 6b94a11a2..c7c726a92 100644 --- a/Assets/Gothic-Core/Scripts/Domain/Culling/VobMeshCullingDomain.cs +++ b/Assets/Gothic-Core/Scripts/Domain/Culling/VobMeshCullingDomain.cs @@ -572,6 +572,11 @@ 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; diff --git a/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs b/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs index ff31989d0..fc5275e31 100644 --- a/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs +++ b/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs @@ -114,6 +114,8 @@ 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, Mathf.Max(1, vobContainer.VobAs().Amount)); From 3bfcc9ca9b38f4db7d88e5c25db42be94829ac9c Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Sat, 13 Jun 2026 00:58:12 +0100 Subject: [PATCH 36/39] oops typo --- Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs b/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs index 21af979c0..23a9d8917 100644 --- a/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs +++ b/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs @@ -338,7 +338,7 @@ public void ReEnableNpc() 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); + Logger.LogWarning($"ReEnableNpc: waypoint '{currentRoutine.Waypoint}' not found for {gameObject.name} — NPC will re-enable at current position.", LogCat.Npc); } // Animation state handling From 8682f60d8332a5da04527c67689839f06874e7a5 Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Sat, 13 Jun 2026 03:12:10 +0100 Subject: [PATCH 37/39] fix: Npc_IsDead now checks BodyState instead of returning false Was a FIXME stub hardcoded to return false, preventing any dead-NPC checks from working (e.g. Thorus couldn't recognize Mordrag as dead after being killed). Now checks Props.BodyState == BsDead, which FightService sets when HP reaches 0. Note: BodyState is runtime-only - a world reload will respawn the NPC alive. A permanent death flag in SaveGame state is needed as a follow-up fix. --- Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs b/Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs index e22244c7a..0adbe470f 100644 --- a/Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs +++ b/Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs @@ -388,9 +388,9 @@ 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) From 3c57992fc61c08604d76fb3eed9ab62d2378fa38 Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Sat, 13 Jun 2026 03:13:04 +0100 Subject: [PATCH 38/39] fix: set GlobalOther=hero before ZS_* loops and dialog condition evaluation After commit 69d7707c correctly fixed the GlobalOther save/restore in ExecutePerception, a stale NPC (e.g. a Shadow guard from a recent perception) remained in GlobalOther between perception calls. This caused two visible bugs: 1. NPCs turned their backs during conversation - routine states like ZS_*_Loop called B_AssessTalk/B_SmartTurnToNpc using 'other', which pointed to the stale Shadow instead of the player. NPCs would physically rotate toward that guard. 2. Important dialogs refused to trigger - conditions like Npc_GetDistToNpc(self,other) or guild attitude checks ran against the Shadow's position/guild, not the hero's, causing them to return false and skip the dialog entirely. Fix in AiHandler: set GlobalOther=GlobalHero before executing ZS_* state functions. This is just a sensible default - perception calls override GlobalOther via their own save/restore, so combat/assess perceptions are unaffected. Fix in DialogService: save/restore GlobalOther and explicitly set it to GlobalHero around Info_*_Condition calls. In Gothic's convention, self=NPC and other=hero in all dialog condition functions. --- Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs | 3 +++ Assets/Gothic-Core/Scripts/Services/Player/DialogService.cs | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs b/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs index 23a9d8917..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; 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) { From fb8f50f368b43269a19ac3a80ab9a8782a68fcfa Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Sat, 13 Jun 2026 19:21:50 +0100 Subject: [PATCH 39/39] simplest fix is usually the best.... turns out to fix healthbar on PCVR you had to move it forward by 0.02 :) --- Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab b/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab index f0a06aff6..3ea4fa907 100644 --- a/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab +++ b/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab @@ -764,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