diff --git a/Assets/Gothic-Core/Resources/DeveloperConfigs/Production.asset b/Assets/Gothic-Core/Resources/DeveloperConfigs/Production.asset
index cb9935cc9..5903c4676 100644
--- a/Assets/Gothic-Core/Resources/DeveloperConfigs/Production.asset
+++ b/Assets/Gothic-Core/Resources/DeveloperConfigs/Production.asset
@@ -51,6 +51,7 @@ MonoBehaviour:
ShowVOBMeshCullingGizmos: 0
ShowCapsuleOverlapGizmos: 0
EnableNpcs: 1
+ EnableCombatSystem: 1
EnableNpcMeshCulling: 1
NpcCullingDistance: 50
SpawnNpcInstances:
@@ -58,6 +59,7 @@ MonoBehaviour:
SpawnMonsterInstances:
Value: 00000000
EnableNpcEyeBlinking: 0
+ EnableNpcLooting: 1
ShowNpcColliders: 0
ShowFreePoints: 0
ShowWayPoints: 0
diff --git a/Assets/Gothic-Core/Resources/FontAsset/LiberationSans SDF empty.asset b/Assets/Gothic-Core/Resources/FontAsset/LiberationSans SDF empty.asset
index 858a10bdb..c111263b5 100644
--- a/Assets/Gothic-Core/Resources/FontAsset/LiberationSans SDF empty.asset
+++ b/Assets/Gothic-Core/Resources/FontAsset/LiberationSans SDF empty.asset
@@ -37,6 +37,25 @@ MonoBehaviour:
m_TabWidth: 29
m_Material: {fileID: 5152873236376385906}
m_SourceFontFileGUID: e3265ab4bf004d28a9537516768c1c75
+ m_CreationSettings:
+ sourceFontFileName:
+ sourceFontFileGUID: e3265ab4bf004d28a9537516768c1c75
+ faceIndex: 0
+ pointSizeSamplingMode: 0
+ pointSize: 104
+ padding: 8
+ paddingMode: 1
+ packingMode: 0
+ atlasWidth: 512
+ atlasHeight: 256
+ characterSetSelectionMode: 5
+ characterSequence:
+ referencedFontAssetGUID:
+ referencedTextAssetGUID:
+ fontStyle: 0
+ fontStyleModifier: 0
+ renderMode: 4165
+ includeFontFeatures: 0
m_SourceFontFile: {fileID: 0}
m_SourceFontFilePath:
m_AtlasPopulationMode: 0
@@ -67,25 +86,6 @@ MonoBehaviour:
m_MarkToMarkAdjustmentRecords: []
m_ShouldReimportFontFeatures: 0
m_FallbackFontAssetTable: []
- m_CreationSettings:
- sourceFontFileName:
- sourceFontFileGUID: e3265ab4bf004d28a9537516768c1c75
- faceIndex: 0
- pointSizeSamplingMode: 0
- pointSize: 104
- padding: 8
- paddingMode: 1
- packingMode: 0
- atlasWidth: 512
- atlasHeight: 256
- characterSetSelectionMode: 5
- characterSequence:
- referencedFontAssetGUID:
- referencedTextAssetGUID:
- fontStyle: 0
- fontStyleModifier: 0
- renderMode: 4165
- includeFontFeatures: 0
m_FontWeightTable:
- regularTypeface: {fileID: 0}
italicTypeface: {fileID: 0}
@@ -151,17 +151,15 @@ Texture2D:
m_ImageContentsHash:
serializedVersion: 2
Hash: 00000000000000000000000000000000
- m_ForcedFallbackFormat: 4
- m_DownscaleFallback: 0
m_IsAlphaChannelOptional: 0
- serializedVersion: 2
+ serializedVersion: 4
m_Width: 512
m_Height: 256
m_CompleteImageSize: 131072
m_MipsStripped: 0
m_TextureFormat: 1
m_MipCount: 1
- m_IsReadable: 0
+ m_IsReadable: 1
m_IsPreProcessed: 0
m_IgnoreMipmapLimit: 0
m_MipmapLimitGroupName:
@@ -297,3 +295,4 @@ Material:
- _SpecularColor: {r: 1, g: 1, b: 1, a: 1}
- _UnderlayColor: {r: 0, g: 0, b: 0, a: 0.5}
m_BuildTextureStacks: []
+ m_AllowLocking: 1
diff --git a/Assets/Gothic-Core/Resources/Prefabs/UI/StatusBars/StatusBar.prefab b/Assets/Gothic-Core/Resources/Prefabs/UI/StatusBars/StatusBar.prefab
index b7cdcf879..8a7cc1894 100644
--- a/Assets/Gothic-Core/Resources/Prefabs/UI/StatusBars/StatusBar.prefab
+++ b/Assets/Gothic-Core/Resources/Prefabs/UI/StatusBars/StatusBar.prefab
@@ -225,7 +225,7 @@ Canvas:
m_OverridePixelPerfect: 0
m_SortingBucketNormalizedSize: 0
m_VertexColorAlwaysGammaSpace: 1
- m_AdditionalShaderChannelsFlag: 0
+ m_AdditionalShaderChannelsFlag: 25
m_UpdateRectTransformForStandalone: 0
m_SortingLayerID: 0
m_SortingOrder: 0
diff --git a/Assets/Gothic-Core/Scenes/MainMenu.unity b/Assets/Gothic-Core/Scenes/MainMenu.unity
index 23dcdfba3..e682b6026 100644
--- a/Assets/Gothic-Core/Scenes/MainMenu.unity
+++ b/Assets/Gothic-Core/Scenes/MainMenu.unity
@@ -287,7 +287,7 @@ MeshRenderer:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 775636288}
- m_Enabled: 1
+ m_Enabled: 0
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
@@ -300,6 +300,8 @@ MeshRenderer:
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
+ m_ForceMeshLod: -1
+ m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
@@ -321,9 +323,11 @@ MeshRenderer:
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
+ m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
+ m_MaskInteraction: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!33 &775636291
MeshFilter:
@@ -388,7 +392,7 @@ Transform:
m_GameObject: {fileID: 783007466}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
- m_LocalPosition: {x: -15.407947, y: 2.073237, z: -29.998268}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
@@ -467,6 +471,8 @@ MeshRenderer:
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
+ m_ForceMeshLod: -1
+ m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
@@ -488,9 +494,11 @@ MeshRenderer:
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
+ m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
+ m_MaskInteraction: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!33 &791865609
MeshFilter:
@@ -565,17 +573,23 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 474bcb49853aa07438625e644c072ee6, type: 3}
m_Name:
m_EditorClassIdentifier:
- m_Version: 3
m_UsePipelineSettings: 1
m_AdditionalLightsShadowResolutionTier: 2
- m_LightLayerMask: 1
- m_RenderingLayers: 1
m_CustomShadowLayers: 0
- m_ShadowLayerMask: 1
- m_ShadowRenderingLayers: 1
m_LightCookieSize: {x: 1, y: 1}
m_LightCookieOffset: {x: 0, y: 0}
m_SoftShadowQuality: 0
+ m_RenderingLayersMask:
+ serializedVersion: 0
+ m_Bits: 1
+ m_ShadowRenderingLayersMask:
+ serializedVersion: 0
+ m_Bits: 1
+ m_Version: 4
+ m_LightLayerMask: 1
+ m_ShadowLayerMask: 1
+ m_RenderingLayers: 1
+ m_ShadowRenderingLayers: 1
--- !u!108 &1308095012
Light:
m_ObjectHideFlags: 0
@@ -584,14 +598,14 @@ Light:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1308095009}
m_Enabled: 1
- serializedVersion: 11
+ serializedVersion: 12
m_Type: 1
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_Intensity: 1
m_Range: 10
m_SpotAngle: 30
m_InnerSpotAngle: 21.80208
- m_CookieSize: 10
+ m_CookieSize2D: {x: 10, y: 10}
m_Shadows:
m_Type: 0
m_Resolution: -1
@@ -667,13 +681,13 @@ RectTransform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2031667426}
- m_LocalRotation: {x: -0.7071068, y: -0, z: -0, w: 0.7071068}
- m_LocalPosition: {x: 0, y: 0, z: 0.02}
- m_LocalScale: {x: -40, y: 8, z: 32}
+ m_LocalRotation: {x: 0, y: 0.7071068, z: 0.7071068, w: 0}
+ m_LocalPosition: {x: 0, y: 0, z: 10}
+ m_LocalScale: {x: 2, y: 1, z: 2}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 783007468}
- m_LocalEulerAnglesHint: {x: -90, y: 0, z: 0}
+ m_LocalEulerAnglesHint: {x: -90, y: 180, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
@@ -721,6 +735,8 @@ MeshRenderer:
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
+ m_ForceMeshLod: -1
+ m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials: []
@@ -741,9 +757,11 @@ MeshRenderer:
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
+ m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
+ m_MaskInteraction: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!33 &2031667430
MeshFilter:
@@ -761,10 +779,38 @@ PrefabInstance:
serializedVersion: 3
m_TransformParent: {fileID: 505140342}
m_Modifications:
+ - target: {fileID: 514510020229674838, guid: a0af29d97abd24d4c8382b6da4017660, type: 3}
+ propertyPath: m_fontSize
+ value: 44.75
+ objectReference: {fileID: 0}
+ - target: {fileID: 819469628458212509, guid: a0af29d97abd24d4c8382b6da4017660, type: 3}
+ propertyPath: m_fontSize
+ value: 44.75
+ objectReference: {fileID: 0}
+ - target: {fileID: 1099075632631060353, guid: a0af29d97abd24d4c8382b6da4017660, type: 3}
+ propertyPath: m_fontSize
+ value: 44.75
+ objectReference: {fileID: 0}
+ - target: {fileID: 1395391690268023265, guid: a0af29d97abd24d4c8382b6da4017660, type: 3}
+ propertyPath: m_fontSize
+ value: 44.75
+ objectReference: {fileID: 0}
+ - target: {fileID: 1684002802907956876, guid: a0af29d97abd24d4c8382b6da4017660, type: 3}
+ propertyPath: m_fontSize
+ value: 44.75
+ objectReference: {fileID: 0}
- target: {fileID: 1879303798920566507, guid: a0af29d97abd24d4c8382b6da4017660, type: 3}
propertyPath: m_Name
value: Tutorials
objectReference: {fileID: 0}
+ - target: {fileID: 1894576811620697740, guid: a0af29d97abd24d4c8382b6da4017660, type: 3}
+ propertyPath: m_fontSize
+ value: 44.75
+ objectReference: {fileID: 0}
+ - target: {fileID: 1988018937390540162, guid: a0af29d97abd24d4c8382b6da4017660, type: 3}
+ propertyPath: m_fontSize
+ value: 22.9
+ objectReference: {fileID: 0}
- target: {fileID: 2283093842980008316, guid: a0af29d97abd24d4c8382b6da4017660, type: 3}
propertyPath: m_LocalPosition.x
value: 0
@@ -805,6 +851,58 @@ PrefabInstance:
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
+ - target: {fileID: 3406562529197016604, guid: a0af29d97abd24d4c8382b6da4017660, type: 3}
+ propertyPath: m_fontSize
+ value: 44.75
+ objectReference: {fileID: 0}
+ - target: {fileID: 3521149265392391147, guid: a0af29d97abd24d4c8382b6da4017660, type: 3}
+ propertyPath: m_fontSize
+ value: 22.35
+ objectReference: {fileID: 0}
+ - target: {fileID: 4641005291164401446, guid: a0af29d97abd24d4c8382b6da4017660, type: 3}
+ propertyPath: m_fontSize
+ value: 44.75
+ objectReference: {fileID: 0}
+ - target: {fileID: 6503434739734083609, guid: a0af29d97abd24d4c8382b6da4017660, type: 3}
+ propertyPath: m_fontSize
+ value: 44.75
+ objectReference: {fileID: 0}
+ - target: {fileID: 6916374624597502023, guid: a0af29d97abd24d4c8382b6da4017660, type: 3}
+ propertyPath: m_fontSize
+ value: 36.75
+ objectReference: {fileID: 0}
+ - target: {fileID: 6924097052499452501, guid: a0af29d97abd24d4c8382b6da4017660, type: 3}
+ propertyPath: m_fontSize
+ value: 44.75
+ objectReference: {fileID: 0}
+ - target: {fileID: 7063008752886569165, guid: a0af29d97abd24d4c8382b6da4017660, type: 3}
+ propertyPath: m_fontSize
+ value: 44.75
+ objectReference: {fileID: 0}
+ - target: {fileID: 7255685370734901281, guid: a0af29d97abd24d4c8382b6da4017660, type: 3}
+ propertyPath: m_fontSize
+ value: 44.75
+ objectReference: {fileID: 0}
+ - target: {fileID: 7407273248831804694, guid: a0af29d97abd24d4c8382b6da4017660, type: 3}
+ propertyPath: m_fontSize
+ value: 22.35
+ objectReference: {fileID: 0}
+ - target: {fileID: 8256774338473347927, guid: a0af29d97abd24d4c8382b6da4017660, type: 3}
+ propertyPath: m_fontSize
+ value: 44.75
+ objectReference: {fileID: 0}
+ - target: {fileID: 8591534907977924294, guid: a0af29d97abd24d4c8382b6da4017660, type: 3}
+ propertyPath: m_fontSize
+ value: 44.75
+ objectReference: {fileID: 0}
+ - target: {fileID: 9011431278706922558, guid: a0af29d97abd24d4c8382b6da4017660, type: 3}
+ propertyPath: m_fontSize
+ value: 44.75
+ objectReference: {fileID: 0}
+ - target: {fileID: 9118953017428058395, guid: a0af29d97abd24d4c8382b6da4017660, type: 3}
+ propertyPath: m_fontSize
+ value: 44.75
+ objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
@@ -828,11 +926,11 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 7389606469834037353, guid: 962bda2b335a56f48a6f7159a9e824d8, type: 3}
propertyPath: m_LocalScale.x
- value: 0.7
+ value: 1
objectReference: {fileID: 0}
- target: {fileID: 7389606469834037353, guid: 962bda2b335a56f48a6f7159a9e824d8, type: 3}
propertyPath: m_LocalScale.y
- value: 0.7
+ value: 1
objectReference: {fileID: 0}
- target: {fileID: 7389606469834037353, guid: 962bda2b335a56f48a6f7159a9e824d8, type: 3}
propertyPath: m_LocalPosition.x
@@ -840,7 +938,7 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 7389606469834037353, guid: 962bda2b335a56f48a6f7159a9e824d8, type: 3}
propertyPath: m_LocalPosition.y
- value: 1.6
+ value: 1.5
objectReference: {fileID: 0}
- target: {fileID: 7389606469834037353, guid: 962bda2b335a56f48a6f7159a9e824d8, type: 3}
propertyPath: m_LocalPosition.z
diff --git a/Assets/Gothic-Core/Scripts/Adapters/Animations/AnimationSystem.cs b/Assets/Gothic-Core/Scripts/Adapters/Animations/AnimationSystem.cs
index 357f76a9e..3ca3351ec 100644
--- a/Assets/Gothic-Core/Scripts/Adapters/Animations/AnimationSystem.cs
+++ b/Assets/Gothic-Core/Scripts/Adapters/Animations/AnimationSystem.cs
@@ -565,13 +565,13 @@ private void ApplyEventTags(AnimationTrackInstance trackInstance)
AttackAnimation = trackInstance.AnimationName;
break;
case EventType.OptimalFrame:
- AttackOptFrame = eventTag.Slots.Item1.Split(' ').Select(i => Convert.ToInt32(i)).ToList();
+ AttackOptFrame = eventTag.Slots.Item1.Split(' ').Where(s => !string.IsNullOrEmpty(s)).Select(i => Convert.ToInt32(i)).ToList();
break;
case EventType.HitEnd:
- AttackHitEnd = eventTag.Slots.Item1.Split(' ').Select(i => Convert.ToInt32(i)).ToList();
+ AttackHitEnd = eventTag.Slots.Item1.Split(' ').Where(s => !string.IsNullOrEmpty(s)).Select(i => Convert.ToInt32(i)).ToList();
break;
case EventType.ComboWindow:
- AttackWindowFrames = eventTag.Slots.Item1.Split(' ').Select(i => Convert.ToInt32(i)).ToList();
+ AttackWindowFrames = eventTag.Slots.Item1.Split(' ').Where(s => !string.IsNullOrEmpty(s)).Select(i => Convert.ToInt32(i)).ToList();
break;
// Unused. @see: https://gothic-modding-community.github.io/gmc/zengin/anims/events/#def_dir
case EventType.HitDirection:
diff --git a/Assets/Gothic-Core/Scripts/Adapters/Animations/Morph/AbstractMorphAnimation.cs b/Assets/Gothic-Core/Scripts/Adapters/Animations/Morph/AbstractMorphAnimation.cs
index c203c4b3d..9cae98670 100644
--- a/Assets/Gothic-Core/Scripts/Adapters/Animations/Morph/AbstractMorphAnimation.cs
+++ b/Assets/Gothic-Core/Scripts/Adapters/Animations/Morph/AbstractMorphAnimation.cs
@@ -57,6 +57,12 @@ protected void StartAnimation(string morphMeshName, [CanBeNull] string animation
var newMorph = new MorphAnimationData();
newMorph.MeshMetadata = _resourceCacheService.TryGetMorphMesh(morphMeshName);
+ if (newMorph.MeshMetadata == null)
+ {
+ Logger.LogWarning($"MorphMesh not found: {morphMeshName}", LogCat.Mesh);
+ return;
+ }
+
newMorph.AnimationMetadata = animationName == null
? newMorph.MeshMetadata.Animations.First()
: newMorph.MeshMetadata.Animations.First(anim => anim.Name.EqualsIgnoreCase(animationName));
diff --git a/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs b/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs
index e35458390..9a683c22e 100644
--- a/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs
+++ b/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs
@@ -79,9 +79,12 @@ private void Update()
if (Properties.AnimationQueue.Count == 0)
{
// We always need to set "self" before executing any Daedalus function.
+ // "other" defaults to hero here so routine states (ZS_*_Loop) have a sensible fallback.
+ // Perception calls (ExecutePerception) override GlobalOther themselves with their own save/restore.
if (NpcInstance != null)
{
Vm.GlobalSelf = NpcInstance;
+ Vm.GlobalOther = Vm.GlobalHero;
}
DaedalusSymbol loopSymbol;
@@ -254,14 +257,6 @@ public void StartRoutine(int action, string wayPointName)
public void StartRoutine(int action)
{
- // End original loop first
- // TODO - Calling ClearState(false) was buggy when e.g. Diego dialog "END" was clicked. Then the dialog lines were skipped.
- // if (Properties.CurrentLoopState == NpcProperties.LoopState.Loop)
- // {
- // // We reuse this function as it is doing what we need.
- // ClearState(false);
- // }
-
var didRoutineChange = Vob.CurrentStateIndex != action;
Vob.LastAiState = Vob.CurrentStateIndex;
@@ -295,6 +290,11 @@ public void StartRoutine(int action)
// When we reached end of ZS_*_END, we also call this method. Check if we really altered the routine action or just restarted it.
if (didRoutineChange)
{
+ if (Properties.CurrentFreePoint != null)
+ {
+ Properties.CurrentFreePoint.IsLocked = false;
+ Properties.CurrentFreePoint = null;
+ }
Logger.Log($"Start new routine >{routineSymbol.Name}< on >{Go.transform.parent.name}<", LogCat.Ai);
Properties.StateTime = 0;
}
@@ -337,8 +337,11 @@ public void ReEnableNpc()
var currentRoutine = Properties.RoutineCurrent;
if (currentRoutine != null)
{
- var wpPos = _wayNetService.GetWayNetPoint(currentRoutine.Waypoint).Position;
- gameObject.transform.position = _npcService.GetFreeAreaAtSpawnPoint(wpPos);
+ var wp = _wayNetService.GetWayNetPoint(currentRoutine.Waypoint);
+ if (wp != null)
+ gameObject.transform.position = _npcService.GetFreeAreaAtSpawnPoint(wp.Position);
+ else
+ Logger.LogWarning($"ReEnableNpc: waypoint '{currentRoutine.Waypoint}' not found for {gameObject.name} — NPC will re-enable at current position.", LogCat.Npc);
}
// Animation state handling
diff --git a/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcLoader.cs b/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcLoader.cs
index e0aebaba0..55e32a039 100644
--- a/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcLoader.cs
+++ b/Assets/Gothic-Core/Scripts/Adapters/Npc/NpcLoader.cs
@@ -12,3 +12,4 @@ public class NpcLoader : MonoBehaviour
public bool IsLoaded;
}
}
+
diff --git a/Assets/Gothic-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs b/Assets/Gothic-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs
deleted file mode 100644
index a42664a2a..000000000
--- a/Assets/Gothic-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-using Gothic.Core.Const;
-using UnityEngine;
-
-namespace Gothic.Core.Adapters.Vob.Item
-{
- ///
- /// The validity check for a hit requires answering: "Is this attack currently active?"
- /// That state lives on the attacker (DEF_OPT_FRAME window, DEF_HIT_LIMB, "already connected" flag).
- /// If we put the logic on the receiver, we must reach across to the attacker's component to get that state
- /// every time any contact happens. If we put it on the attacker, all required state is already local.
- ///
- public class WeaponAttackAdapter : MonoBehaviour
- {
- ///
- /// TODO - Need to be updated to support fist collider from monsters and player as well
- /// Is the other who's hitting me?:
- /// 1. A VobItem (aka weapon)
- /// 2. Is the attacker in attack window state
- ///
- private void OnTriggerEnter(Collider other)
- {
- if (other.gameObject.layer != Constants.VobHitbox)
- return;
-
- Debug.Log("OnTriggerEnter - VobHitbox");
- // if (other.gameObject.layer != Constants.VobItemLayer)
- // return;
- //
- // var vobContainer = other.GetComponentInParent()?.Container;
- //
- // if (!_vrWeaponService.IsWeaponInAttackWindow(vobContainer))
- // return;
- //
- // var attacker = _vrWeaponService.GetWeaponOwner(vobContainer);
- // if (attacker == null)
- // return;
- //
- // var hitPosition = other.ClosestPoint(transform.position);
- // GlobalEventDispatcher.FightHit.Invoke(attacker, _npcContainer, hitPosition);
- }
-
- private void OnTriggerExit(Collider other)
- {
- if (other.gameObject.layer != Constants.VobHitbox)
- return;
-
- Debug.Log("OnTriggerExit - VobHitbox");
- }
- }
-}
diff --git a/Assets/Gothic-Core/Scripts/Domain/Audio/MusicDomain.cs b/Assets/Gothic-Core/Scripts/Domain/Audio/MusicDomain.cs
index 34490efb1..4187d0038 100644
--- a/Assets/Gothic-Core/Scripts/Domain/Audio/MusicDomain.cs
+++ b/Assets/Gothic-Core/Scripts/Domain/Audio/MusicDomain.cs
@@ -148,9 +148,9 @@ private void PlayInternal(MusicThemeInstance theme)
// DMusic crashes for some unfinished small themes (<3kB). Therefore, skipping them now with a warning until fixed.
// Issue report: https://github.com/GothicKit/dmusic-cs/issues/1
- if (_resourceCacheService.Vfs.Find(theme.File)?.Buffer.Bytes.Length < 3000)
+ if (_resourceCacheService.Vfs.Find(theme.File)?.Buffer.Bytes.Length < 1000)
{
- Logger.LogWarning($"Music theme >{theme.File}< might be broken with less than 3kB size. Safety skip", LogCat.Audio);
+ Logger.LogWarning($"Music theme >{theme.File}< might be broken with less than 1kB size. Safety skip", LogCat.Audio);
return;
}
diff --git a/Assets/Gothic-Core/Scripts/Domain/Culling/VobMeshCullingDomain.cs b/Assets/Gothic-Core/Scripts/Domain/Culling/VobMeshCullingDomain.cs
index 15814e263..c7c726a92 100644
--- a/Assets/Gothic-Core/Scripts/Domain/Culling/VobMeshCullingDomain.cs
+++ b/Assets/Gothic-Core/Scripts/Domain/Culling/VobMeshCullingDomain.cs
@@ -489,8 +489,13 @@ public void StartTrackVobPositionUpdates(GameObject go)
}
if (index == -1)
- Logger.LogError($"Couldn't find object in Culling list {rootGo.name}. Culling updates will break.",
+ {
+ // Dynamically spawned items (loot panel, backpack fill) are not registered in the culling lists.
+ // This is expected — just skip tracking for them.
+ Logger.LogWarning($"VOB {rootGo.name} not in culling list — skipping position tracking (dynamically spawned).",
LogCat.Vob);
+ return;
+ }
_pausedVobs.Add(rootGo, new Tuple(vobType, index));
}
@@ -542,6 +547,11 @@ private IEnumerator StopTrackVobPositionUpdatesDelayed(GameObject rootGo)
{
yield return new WaitForSeconds(1f);
_pausedVobsToReenableCoroutine.Remove(rootGo);
+
+ // GO may have been destroyed (e.g., loot panel closed) during the 1-second delay.
+ if (rootGo == null)
+ yield break;
+
if (!_pausedVobsToReenable.ContainsKey(rootGo))
{
_pausedVobsToReenable.Add(rootGo, rootGo.GetComponentInChildren());
@@ -562,14 +572,21 @@ private IEnumerator StopVobTrackingBasedOnVelocity()
{
var key = _pausedVobsToReenable.Keys.ElementAt(i);
var rigidBody = _pausedVobsToReenable[key];
+ if (rigidBody == null)
+ {
+ _pausedVobsToReenable.Remove(key);
+ continue;
+ }
if (rigidBody.linearVelocity != Vector3.zero)
{
continue;
}
- UpdateSpherePosition(key);
- rigidBody.isKinematic = true;
+ // Item may not be in _pausedVobs if it was dynamically spawned and never registered in culling lists.
+ if (_pausedVobs.ContainsKey(key))
+ UpdateSpherePosition(key);
+ rigidBody.isKinematic = true;
_pausedVobs.Remove(key);
_pausedVobsToReenable.Remove(key);
}
@@ -593,6 +610,10 @@ private void UpdateSpherePosition(GameObject go)
_ => throw new ArgumentOutOfRangeException()
};
+ // Index may be stale if other VOBs were removed from the list after this item was grabbed.
+ if (index >= sphereList.Count)
+ return;
+
sphereList[index] = new BoundingSphere(go.transform.position, sphereList[index].radius);
}
diff --git a/Assets/Gothic-Core/Scripts/Domain/Meshes/Builder/NpcHeadMeshBuilder.cs b/Assets/Gothic-Core/Scripts/Domain/Meshes/Builder/NpcHeadMeshBuilder.cs
index 5b0e7e89d..920fe944e 100644
--- a/Assets/Gothic-Core/Scripts/Domain/Meshes/Builder/NpcHeadMeshBuilder.cs
+++ b/Assets/Gothic-Core/Scripts/Domain/Meshes/Builder/NpcHeadMeshBuilder.cs
@@ -20,9 +20,14 @@ public override GameObject Build()
return RootGo;
}
- var npcContainer = RootGo.GetComponentInParent().Npc.GetUserData();
+ var npcContainer = RootGo.GetComponentInParent()?.Npc?.GetUserData();
+ if (npcContainer == null)
+ {
+ Logger.LogWarning($"NpcContainer not available during head build for {RootGo.name} — skipping head component setup.", LogCat.Mesh);
+ return RootGo;
+ }
- // Cache it f1or faster use during runtime
+ // Cache it for faster use during runtime
npcContainer.PrefabProps.Head = headGo.transform;
npcContainer.PrefabProps.HeadMorph = headGo.AddComponent().Inject();
npcContainer.PrefabProps.HeadMorph.HeadName = npcContainer.Props.BodyData.Head;
diff --git a/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/Attack.cs b/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/Attack.cs
index 0c133159d..32da7ff9f 100644
--- a/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/Attack.cs
+++ b/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/Attack.cs
@@ -32,6 +32,13 @@ public Attack(AnimationAction action, NpcContainer npcData) : base(action, npcDa
public override void Start()
{
+ if (Vob.GuildTrue < (int)VmGothicEnums.Guild.GIL_SEPERATOR_HUM)
+ {
+ Logger.Log($"AI_Attack() on human NPC (guild={Vob.GuildTrue}) — not yet implemented, skipping.", LogCat.Ai);
+ IsFinishedFlag = true;
+ return;
+ }
+
var aiFunctionTemplate = FindAiFunctionTemplate();
_move = VmCacheService.TryGetFightAiData(aiFunctionTemplate, Vob.FightTactic).GetRandomMove();
StartAttackAction();
diff --git a/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/GoToNpc.cs b/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/GoToNpc.cs
index 29b970bc6..d2a0e2cc5 100644
--- a/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/GoToNpc.cs
+++ b/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/GoToNpc.cs
@@ -6,6 +6,8 @@ namespace Gothic.Core.Domain.Npc.Actions.AnimationActions
{
public class GoToNpc : AbstractWalkAnimationAction
{
+ private const float ConversationDistance = 1.5f;
+
private Transform _destinationTransform;
public GoToNpc(AnimationAction action, NpcContainer npcContainer) : base(action, npcContainer)
@@ -21,7 +23,11 @@ public override void Start()
protected override Vector3 GetWalkDestination()
{
- return _destinationTransform.position;
+ var targetPos = _destinationTransform.position;
+ var toTarget = targetPos - NpcGo.transform.position;
+ if (toTarget.sqrMagnitude < 0.001f)
+ return targetPos;
+ return targetPos + toTarget.normalized * -ConversationDistance;
}
diff --git a/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/GoToWp.cs b/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/GoToWp.cs
index e889e8861..2cf9ab28e 100644
--- a/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/GoToWp.cs
+++ b/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/GoToWp.cs
@@ -33,8 +33,14 @@ public override void Start()
}
// We need to set the route now to ensure base.Start() can check if NPC is already _on_ the final destination.
- _route = new Stack(WayNetService.FindFastestPath(currentWaypoint.Name,
- destinationWaypoint.Name));
+ var path = WayNetService.FindFastestPath(currentWaypoint.Name, destinationWaypoint.Name);
+ if (path == null)
+ {
+ IsFinishedFlag = true;
+ return;
+ }
+
+ _route = new Stack(path);
base.Start();
}
diff --git a/Assets/Gothic-Core/Scripts/Domain/Vobs/VobInitializerDomain.cs b/Assets/Gothic-Core/Scripts/Domain/Vobs/VobInitializerDomain.cs
index 6f6273b22..1a1a9110d 100644
--- a/Assets/Gothic-Core/Scripts/Domain/Vobs/VobInitializerDomain.cs
+++ b/Assets/Gothic-Core/Scripts/Domain/Vobs/VobInitializerDomain.cs
@@ -466,7 +466,7 @@ private GameObject CreateItemMesh(ItemInstance item, GameObject go, GameObject p
if (mrm != null)
{
- return _meshService.CreateVob(item.Visual, mrm, parent: parent, rootGo: go, useColliderCache: true);
+ return _meshService.CreateVob(item.Visual, mrm, parent: parent, rootGo: go, useColliderCache: true, useTextureArray: false);
}
// shortbow (itrw_bow_l_01) has no mrm, but has mmb
@@ -581,14 +581,18 @@ public AudioClip GetSoundClip(string soundName)
if (sfxContainer == null)
return null;
+ var firstSound = sfxContainer.GetFirstSound();
+ if (firstSound == null)
+ return null;
+
// Instead of decoding nosound.wav which might be decoded incorrectly, just return null.
- if (sfxContainer.GetFirstSound().File.EqualsIgnoreCase(AudioService.NoSoundName))
+ if (firstSound.File.EqualsIgnoreCase(AudioService.NoSoundName))
return null;
if (sfxContainer.Count > 1)
- Logger.LogWarning($"Multiple random elements exist for >{sfxContainer.GetFirstSound().File}< but only first is selected.", LogCat.Audio);
+ Logger.LogWarning($"Multiple random elements exist for >{firstSound.File}< but only first is selected.", LogCat.Audio);
- clip = _audioService.CreateAudioClip(sfxContainer.GetFirstSound().File);
+ clip = _audioService.CreateAudioClip(firstSound.File);
}
return clip;
diff --git a/Assets/Gothic-Core/Scripts/Models/Audio/SfxModel.cs b/Assets/Gothic-Core/Scripts/Models/Audio/SfxModel.cs
index c02327a76..b2285618b 100644
--- a/Assets/Gothic-Core/Scripts/Models/Audio/SfxModel.cs
+++ b/Assets/Gothic-Core/Scripts/Models/Audio/SfxModel.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Gothic.Core.Logging;
using Gothic.Core.Services;
using Gothic.Core.Const;
using Gothic.Core.Extensions;
@@ -8,6 +9,7 @@
using MyBox;
using Reflex.Attributes;
using ZenKit.Daedalus;
+using Logger = Gothic.Core.Logging.Logger;
namespace Gothic.Core.Models.Audio
{
@@ -64,9 +66,10 @@ private void LoadSoundEffects()
var firstSound = _gameStateService.SfxVm.InitInstance(soundKey);
sounds.Add(firstSound);
}
- catch (Exception e)
+ catch (Exception)
{
- // If the key itself doesn't exist, then we don't need to look further.
+ // SFX symbol missing from Gothic's SFX scripts — expected for some broken VOBs (e.g. Wood_Night2 was never defined).
+ Logger.LogWarning($"SFX symbol not found in VM: '{soundKey}' — no sound will play.", LogCat.Audio);
_soundEffects = Array.Empty();
return;
}
diff --git a/Assets/Gothic-Core/Scripts/Models/Config/DeveloperConfig.cs b/Assets/Gothic-Core/Scripts/Models/Config/DeveloperConfig.cs
index dfac35707..97a281486 100644
--- a/Assets/Gothic-Core/Scripts/Models/Config/DeveloperConfig.cs
+++ b/Assets/Gothic-Core/Scripts/Models/Config/DeveloperConfig.cs
@@ -175,6 +175,9 @@ public class DebugChannelTypesCollection : CollectionWrapper i.sharedMaterial.SetFloat(shaderProperty, shaderValue));
+ entry.Renderers.ForEach(i => { if (i != null) i.sharedMaterial.SetFloat(shaderProperty, shaderValue); });
// And we add the property to the list of "changed" properties.
entry.AlteredShaderProperties.Add(shaderProperty);
@@ -70,7 +70,7 @@ public void ResetDynamicValue(GameObject go, int shaderProperty, float shaderVal
}
// Reset values
- entry.Renderers.ForEach(i => i.sharedMaterial.SetFloat(shaderProperty, shaderValue));
+ entry.Renderers.ForEach(i => { if (i != null) i.sharedMaterial.SetFloat(shaderProperty, shaderValue); });
entry.AlteredShaderProperties.Remove(shaderProperty);
if (entry.AlteredShaderProperties.IsEmpty())
@@ -144,6 +144,9 @@ private void ActivateDynamicRenderers(CacheEntry entry)
for (var i = 0; i < entry.Renderers.Count; i++)
{
+ if (entry.Renderers[i] == null)
+ continue;
+
var dynamicMaterial = entry.DynamicMaterials[i];
// It's a shader we didn't touch.
@@ -165,12 +168,15 @@ private void DeactivateDynamicRenderers(CacheEntry entry)
for (var i = 0; i < entry.Renderers.Count; i++)
{
+ if (entry.Renderers[i] == null)
+ continue;
+
var defaultMaterial = entry.DefaultMaterials[i];
// It's a shader we didn't touch.
if (defaultMaterial == null)
continue;
-
+
entry.Renderers[i].sharedMaterial = defaultMaterial;
}
diff --git a/Assets/Gothic-Core/Scripts/Services/Npc/FightService.cs b/Assets/Gothic-Core/Scripts/Services/Npc/FightService.cs
index 355b98919..163910d20 100644
--- a/Assets/Gothic-Core/Scripts/Services/Npc/FightService.cs
+++ b/Assets/Gothic-Core/Scripts/Services/Npc/FightService.cs
@@ -1,38 +1,48 @@
using Gothic.Core.Adapters.UI.StatusBars;
-using Gothic.Core.Domain.Npc.Actions.AnimationActions;
+using Gothic.Core.Logging;
using Gothic.Core.Manager;
using Gothic.Core.Models.Container;
using Gothic.Core.Models.Vm;
-using Gothic.Core.Services.Vm;
+using Gothic.Core.Services.Config;
using Gothic.Core.Services.World;
using Reflex.Attributes;
using UnityEngine;
using ZenKit.Daedalus;
+using Logger = Gothic.Core.Logging.Logger;
namespace Gothic.Core.Services.Npc
{
public class FightService
{
[Inject] private AudioService _audioService;
- [Inject] private VmService _vmService;
[Inject] private AnimationService _animationService;
[Inject] private PhysicsService _physicsService;
[Inject] private NpcHelperService _npcHelperService;
+ [Inject] private readonly ConfigService _configService;
public void Init()
{
+ if (!_configService.Dev.EnableCombatSystem)
+ return;
+
GlobalEventDispatcher.FightHit.AddListener(OnHit);
}
private void OnHit(NpcContainer attacker, NpcContainer target, Vector3 __)
{
+ if (target.Props.BodyState == VmGothicEnums.BodyState.BsDead)
+ return;
+
+ Logger.Log($"[FightService.OnHit] *** {attacker.Instance.GetName(NpcNameSlot.Slot0)} HIT {target.Instance.GetName(NpcNameSlot.Slot0)}", LogCat.Npc);
if (OnHitUpdateHealth(attacker, target))
{
+ Logger.Log($"[FightService.OnHit] {target.Instance.GetName(NpcNameSlot.Slot0)} is DEAD", LogCat.Npc);
target.Props.BodyState = VmGothicEnums.BodyState.BsDead;
OnDyingChangeAnimation(target);
}
else
{
+ Logger.Log($"[FightService.OnHit] {target.Instance.GetName(NpcNameSlot.Slot0)} took damage, playing hurt animation", LogCat.Npc);
OnHitChangeAnimation(target);
OnHitPlaySound(target);
}
@@ -46,38 +56,55 @@ private bool OnHitUpdateHealth(NpcContainer attacker, NpcContainer target)
{
// FIXME - We need to handle this via power and skill level of attacker, not weapon alone.
var hitPoints = target.Vob.GetAttribute((int)NpcAttribute.HitPoints);
+ var maxHP = target.Vob.GetAttribute((int)NpcAttribute.HitPointsMax);
var equippedWeapon = _npcHelperService.ExtNpcGetEquippedMeleeWeapon(attacker.Instance);
+ Logger.Log($"[FightService.OnHitUpdateHealth] Attacker: {attacker.Instance.GetName(NpcNameSlot.Slot0)}, WeaponName: {(equippedWeapon != null ? equippedWeapon.Name : "None")}, Damage: {(equippedWeapon != null ? equippedWeapon.DamageTotal.ToString() : "N/A")}", LogCat.Npc);
// FIXME - Instead of 0, use fist value
// FIXME - Instead of DamageTotal, use calculated NPC/Hero value
var damage = equippedWeapon?.DamageTotal ?? 0;
+ if (damage <= 0)
+ damage = 10; // debug: force minimum 10 until proper damage calculation is implemented
+
+ Logger.Log($"[FightService.OnHitUpdateHealth] {target.Instance.GetName(NpcNameSlot.Slot0)}: {hitPoints} - {damage} dmg", LogCat.Npc);
hitPoints -= damage;
+ Logger.Log($"[FightService.OnHitUpdateHealth] {target.Instance.GetName(NpcNameSlot.Slot0)} HP after: {hitPoints}/{maxHP}", LogCat.Npc);
+
target.Vob.SetAttribute((int)NpcAttribute.HitPoints, hitPoints);
- target.Go.GetComponentInChildren(true)?.SetFillAmount(hitPoints, target.Vob.GetAttribute((int)NpcAttribute.HitPointsMax));
+ var statusBar = target.Go.GetComponentInChildren(true);
+ if (statusBar != null)
+ {
+ Logger.Log($"[FightService.OnHitUpdateHealth] Updating HP bar for {target.Instance.GetName(NpcNameSlot.Slot0)}", LogCat.Npc);
+ statusBar.SetFillAmount(hitPoints, maxHP);
+ }
+ else
+ {
+ Logger.LogWarning($"[FightService.OnHitUpdateHealth] No StatusBar found for {target.Instance.GetName(NpcNameSlot.Slot0)}", LogCat.Npc);
+ }
return hitPoints <= 0;
}
private void OnDyingChangeAnimation(NpcContainer target)
{
- // Stop current (attack) animation.
- target.Props.CurrentAction.StopImmediately();
+ // Clear pending AI queue and stop all running animations (e.g. s_walk still looping).
+ // Death takes priority over everything — bypass the queue and play directly.
+ target.Props.AnimationQueue.Clear();
+ target.PrefabProps.AnimationSystem.StopAllAnimations();
_physicsService.DisablePhysicsForNpc(target.PrefabProps);
var animName = _animationService.GetAnimationName(VmGothicEnums.AnimationType.DeadB, target);
- target.Props.AnimationQueue.Enqueue(new PlayAni(new(animName), target));
+ target.PrefabProps.AnimationSystem.PlayAnimation(animName);
}
private void OnHitChangeAnimation(NpcContainer target)
{
- // Stop current (attack) animation.
- target.Props.CurrentAction.StopImmediately();
-
+ // Play hurt on top of whatever is currently running — don't interrupt the current action.
var animName = _animationService.GetAnimationName(VmGothicEnums.AnimationType.StumbleA, target);
- target.Props.AnimationQueue.Enqueue(new PlayAni(new(animName), target));
+ target.PrefabProps.AnimationSystem.PlayAnimation(animName);
}
private void OnHitPlaySound(NpcContainer target)
diff --git a/Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs b/Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs
index 971fd2c8e..0adbe470f 100644
--- a/Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs
+++ b/Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs
@@ -50,12 +50,12 @@ public void ExecutePerception(VmGothicEnums.PerceptionType type, NpcProperties p
}
var oldSelf = _gameStateService.GothicVm.GlobalSelf;
+ var oldOther = _gameStateService.GothicVm.GlobalOther;
var oldVictim = _gameStateService.GothicVm.GlobalVictim;
- var oldOther = _gameStateService.GothicVm.GlobalVictim;
_gameStateService.GothicVm.GlobalSelf = self;
-
- if(other != null)
+
+ if(other != null)
{
_gameStateService.GothicVm.GlobalOther = other;
}
@@ -66,10 +66,10 @@ public void ExecutePerception(VmGothicEnums.PerceptionType type, NpcProperties p
}
_gameStateService.GothicVm.Call(perceptionFunction);
-
+
_gameStateService.GothicVm.GlobalSelf = oldSelf;
+ _gameStateService.GothicVm.GlobalOther = oldOther;
_gameStateService.GothicVm.GlobalVictim = oldVictim;
- _gameStateService.GothicVm.GlobalVictim = oldOther;
}
public void ExtNpcSetPerceptionTime(NpcInstance npc, float time)
@@ -341,8 +341,8 @@ public int ExtNpcGetDistToNpc(NpcInstance npc1, NpcInstance npc2)
var npc1Pos = npc1.GetUserData().Go.transform.position;
Vector3 npc2Pos;
- // If hero
- if (npc2.Id == 0)
+ // If hero: use camera position (VR head position is most accurate)
+ if (npc2.Index == _gameStateService.GothicVm.GlobalHero?.Index)
{
npc2Pos = Camera.main!.transform.position;
}
@@ -388,14 +388,14 @@ public void ExtAiDrawWeapon(NpcInstance npc)
public bool ExtNpcIsDead(NpcInstance npcInstance)
{
- // FIXME - We need to implement it properly. Just fixing NPEs for now!
- // FIXME - e.g. used for PC_Thief_AFTERTROLL_Condition() from Daedalus.
- return false;
+ // FIXME - BodyState is runtime-only and lost on NPC reload (e.g. world reload respawns the NPC alive).
+ // A permanent death flag needs to be persisted in SaveGame state and checked here instead.
+ return npcInstance.GetUserData()?.Props.BodyState == VmGothicEnums.BodyState.BsDead;
}
public bool ExtNpcIsInState(NpcInstance npc, int state)
{
- return npc.GetUserData().Vob.CurrentStateIndex == state;
+ return npc.GetUserData()?.Vob.CurrentStateIndex == state;
}
public bool ExtNpcIsPlayer(NpcInstance npc)
diff --git a/Assets/Gothic-Core/Scripts/Services/Npc/NpcHelperService.cs b/Assets/Gothic-Core/Scripts/Services/Npc/NpcHelperService.cs
index ac0354fe6..b3a41e31f 100644
--- a/Assets/Gothic-Core/Scripts/Services/Npc/NpcHelperService.cs
+++ b/Assets/Gothic-Core/Scripts/Services/Npc/NpcHelperService.cs
@@ -152,7 +152,7 @@ public bool ExtWldDetectNpcEx(NpcInstance npcInstance, int specificNpcIndex, int
_gameStateService.GothicVm.GlobalHero!.Index) // if we don't detect player, then skip it
.Where(i => specificNpcIndex < 0 ||
specificNpcIndex == i.Instance.Index) // Specific NPC is found right now?
- .Where(i => aiState < 0 || npcVob.CurrentStateIndex == i.Vob.CurrentStateIndex)
+ .Where(i => aiState < 0 || aiState == i.Vob.CurrentStateIndex)
.Where(i => guild < 0 || i.Instance.Guild == guild) // check guild
.OrderBy(i => Vector3.Distance(i.Go.transform.position, npcPos)) // get nearest
.FirstOrDefault();
diff --git a/Assets/Gothic-Core/Scripts/Services/Npc/NpcInventoryService.cs b/Assets/Gothic-Core/Scripts/Services/Npc/NpcInventoryService.cs
index 038d4bd80..0017f8d19 100644
--- a/Assets/Gothic-Core/Scripts/Services/Npc/NpcInventoryService.cs
+++ b/Assets/Gothic-Core/Scripts/Services/Npc/NpcInventoryService.cs
@@ -111,17 +111,52 @@ public List GetInventoryItems(NpcInstance npc, VmGothicEnums.InvCat
return _vobService.UnpackItems(npcVob.GetPacked((int)category));
}
+ ///
+ /// Returns all items across every category. Each InvCats slot only exists in ZenKit if
+ /// SetPacked was previously called for it — accessing a missing slot throws from native code.
+ /// This method silently skips slots that were never initialized.
+ ///
+ public List GetAllInventoryItems(NpcInstance npc)
+ {
+ var items = new List();
+ foreach (VmGothicEnums.InvCats cat in System.Enum.GetValues(typeof(VmGothicEnums.InvCats)))
+ {
+ if (cat == VmGothicEnums.InvCats.InvCatMax)
+ continue;
+ try
+ {
+ items.AddRange(GetInventoryItems(npc, cat));
+ }
+ catch
+ {
+ // Slot was never initialized for this NPC — expected when a category has no items
+ }
+ }
+ return items;
+ }
+
public int ExtNpcHasItems(NpcInstance npc, int itemId)
{
- var npcVob = npc.GetUserData()!.Vob;
var itemInstanceName = _gameStateService.GothicVm.GetSymbolByIndex(itemId)!.Name;
-
- for (var i = 0; i < npcVob.ItemCount; i++)
+
+ foreach (InvCats cat in System.Enum.GetValues(typeof(InvCats)))
{
- if (npcVob.GetItem(i).Name == itemInstanceName)
- return npcVob.GetItem(i).Amount;
+ if (cat == InvCats.InvCatMax)
+ continue;
+ try
+ {
+ foreach (var item in GetInventoryItems(npc, cat))
+ {
+ if (string.Equals(item.Name, itemInstanceName, System.StringComparison.OrdinalIgnoreCase))
+ return item.Amount;
+ }
+ }
+ catch
+ {
+ // Category slot was never initialized for this NPC
+ }
}
-
+
return 0;
}
diff --git a/Assets/Gothic-Core/Scripts/Services/Player/DialogService.cs b/Assets/Gothic-Core/Scripts/Services/Player/DialogService.cs
index 63cdc66e0..6635f89e6 100644
--- a/Assets/Gothic-Core/Scripts/Services/Player/DialogService.cs
+++ b/Assets/Gothic-Core/Scripts/Services/Player/DialogService.cs
@@ -99,9 +99,12 @@ public void StartDialog(NpcContainer npcContainer, bool initialDialogStarting)
// TODO - Should be outsourced to some VmManager.Call function which sets and resets values.
var oldSelf = _gameStateService.GothicVm.GlobalSelf;
+ var oldOther = _gameStateService.GothicVm.GlobalOther;
_gameStateService.GothicVm.GlobalSelf = npcContainer.Instance;
+ _gameStateService.GothicVm.GlobalOther = _gameStateService.GothicVm.GlobalHero;
var conditionResult = _gameStateService.GothicVm.Call(dialog.Condition);
_gameStateService.GothicVm.GlobalSelf = oldSelf;
+ _gameStateService.GothicVm.GlobalOther = oldOther;
// Dialog condition is false
if (conditionResult == 0)
@@ -146,9 +149,12 @@ private bool TryGetImportant(NpcContainer npcContainer, out InfoInstance item)
// TODO - Should be outsourced to some VmManager.Call function which sets and resets values.
var oldSelf = _gameStateService.GothicVm.GlobalSelf;
+ var oldOther = _gameStateService.GothicVm.GlobalOther;
_gameStateService.GothicVm.GlobalSelf = npcContainer.Instance;
+ _gameStateService.GothicVm.GlobalOther = _gameStateService.GothicVm.GlobalHero;
var conditionResult = _gameStateService.GothicVm.Call(dialog.Condition);
_gameStateService.GothicVm.GlobalSelf = oldSelf;
+ _gameStateService.GothicVm.GlobalOther = oldOther;
if (conditionResult == 0)
{
diff --git a/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs b/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs
index caa1709cb..6796a4104 100644
--- a/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs
+++ b/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs
@@ -1,4 +1,3 @@
-using System;
using System.Reflection;
using Gothic.Core.Const;
using Gothic.Core.Logging;
@@ -58,8 +57,6 @@ public TMP_SpriteAsset TryGetFont(string fontName)
var y = font.Glyphs[i].topLeft.Y * fontTexture.height;
var w = font.Glyphs[i].width;
var h = font.Height;
- var newSprite = Sprite.Create(fontTexture, new Rect(x, y, w, h),
- new Vector2(font.Glyphs[i].topLeft.X, font.Glyphs[i].bottomRight.Y));
var spriteGlyph = new TMP_SpriteGlyph
{
@@ -79,11 +76,10 @@ public TMP_SpriteAsset TryGetFont(string fontName)
horizontalAdvance = w
},
index = (uint)i,
- sprite = newSprite,
+ sprite = null,
scale = -1
};
-
// Convert the glyph index (treated as a codepage-byte) to its Unicode equivalent
var unicodeChars = _gameStateService.Encoding.GetChars(new[]{(byte)i});
var unicodeValue = (uint)unicodeChars[0]; // Return the Unicode character's code point
diff --git a/Assets/Gothic-Core/Scripts/Services/Vobs/VobService.cs b/Assets/Gothic-Core/Scripts/Services/Vobs/VobService.cs
index 28b608a1e..c6ea15d55 100644
--- a/Assets/Gothic-Core/Scripts/Services/Vobs/VobService.cs
+++ b/Assets/Gothic-Core/Scripts/Services/Vobs/VobService.cs
@@ -176,12 +176,19 @@ private IEnumerator InitVobCoroutine()
// }
var item = _objectsToInitQueue.Dequeue();
-
+
item.IsLoaded = true;
// We assume that each loaded VOB is centered at parent=0,0,0.
// Should work smoothly until we start lazy loading sub-vobs ;-)
- _initializerDomain.InitVob(item.Container.Vob, item.gameObject, default, true);
+ try
+ {
+ _initializerDomain.InitVob(item.Container.Vob, item.gameObject, default, true);
+ }
+ catch (Exception e)
+ {
+ Logger.LogError($"Failed to init VOB {item.name}: {e}", LogCat.Vob);
+ }
yield return _frameSkipperService.TrySkipToNextFrameCoroutine();
}
diff --git a/Assets/Gothic-Core/Scripts/Services/World/WayNetService.cs b/Assets/Gothic-Core/Scripts/Services/World/WayNetService.cs
index 758ec0477..a1ce67074 100644
--- a/Assets/Gothic-Core/Scripts/Services/World/WayNetService.cs
+++ b/Assets/Gothic-Core/Scripts/Services/World/WayNetService.cs
@@ -228,9 +228,16 @@ public FreePoint FindNearestFreePoint(Vector3 lookupPosition, string fpNamePart,
public DijkstraWaypoint[] FindFastestPath(string startWaypoint, string endWaypoint)
{
- // Get the start and end waypoints from the DijkstraWaypoints dictionary
- var startDijkstraWaypoint = _gameStateService.DijkstraWaypoints[startWaypoint];
- var endDijkstraWaypoint = _gameStateService.DijkstraWaypoints[endWaypoint];
+ if (!_gameStateService.DijkstraWaypoints.ContainsKey(startWaypoint))
+ {
+ Logger.LogWarning($"FindFastestPath: start waypoint '{startWaypoint}' not found in WayNet.", LogCat.Npc);
+ return null;
+ }
+ if (!_gameStateService.DijkstraWaypoints.TryGetValue(endWaypoint, out var endDijkstraWaypoint))
+ {
+ Logger.LogWarning($"FindFastestPath: end waypoint '{endWaypoint}' not found in WayNet.", LogCat.Npc);
+ return null;
+ }
// Initialize the previousNodes dictionary to keep track of the path
var previousNodes = new Dictionary();
diff --git a/Assets/Gothic-VR/Resources/VR/Prefabs/Player-Elements/BackPack.prefab b/Assets/Gothic-VR/Resources/VR/Prefabs/Player-Elements/BackPack.prefab
index b6b86c804..07854330c 100644
--- a/Assets/Gothic-VR/Resources/VR/Prefabs/Player-Elements/BackPack.prefab
+++ b/Assets/Gothic-VR/Resources/VR/Prefabs/Player-Elements/BackPack.prefab
@@ -1293,15 +1293,15 @@ PrefabInstance:
m_Modifications:
- target: {fileID: 6401121504383089494, guid: 4293d805a850fb84885c55f47b7299bd, type: 3}
propertyPath: m_text
- value: x/y
+ value: ''
objectReference: {fileID: 0}
- target: {fileID: 6401121504383089494, guid: 4293d805a850fb84885c55f47b7299bd, type: 3}
propertyPath: m_fontAsset
- value:
+ value:
objectReference: {fileID: 11400000, guid: ed34b05229166f3469c21b7208879736, type: 2}
- target: {fileID: 6401121504383089494, guid: 4293d805a850fb84885c55f47b7299bd, type: 3}
propertyPath: m_sharedMaterial
- value:
+ value:
objectReference: {fileID: 5152873236376385906, guid: ed34b05229166f3469c21b7208879736, type: 2}
- target: {fileID: 6401121504383089494, guid: 4293d805a850fb84885c55f47b7299bd, type: 3}
propertyPath: m_VerticalAlignment
@@ -3692,7 +3692,7 @@ PrefabInstance:
m_Modifications:
- target: {fileID: 6401121504383089494, guid: 4293d805a850fb84885c55f47b7299bd, type: 3}
propertyPath: m_text
- value: '{{CATEGORY}}'
+ value: ''
objectReference: {fileID: 0}
- target: {fileID: 6401121504383089494, guid: 4293d805a850fb84885c55f47b7299bd, type: 3}
propertyPath: m_fontSize
diff --git a/Assets/Gothic-VR/Resources/VR/Prefabs/Story/IntroduceChapter.prefab b/Assets/Gothic-VR/Resources/VR/Prefabs/Story/IntroduceChapter.prefab
index 39dcc335f..e5a60535f 100644
--- a/Assets/Gothic-VR/Resources/VR/Prefabs/Story/IntroduceChapter.prefab
+++ b/Assets/Gothic-VR/Resources/VR/Prefabs/Story/IntroduceChapter.prefab
@@ -103,6 +103,7 @@ MonoBehaviour:
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
+ m_characterHorizontalScale: 1
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
@@ -239,6 +240,7 @@ MonoBehaviour:
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
+ m_characterHorizontalScale: 1
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
@@ -444,6 +446,7 @@ GameObject:
- component: {fileID: 7369722077890996068}
- component: {fileID: 390066666418957288}
- component: {fileID: 5615940322375626677}
+ - component: {fileID: 1602513700271543697}
m_Layer: 5
m_Name: IntroduceChapter
m_TagString: Untagged
@@ -495,6 +498,7 @@ AudioSource:
serializedVersion: 4
OutputAudioMixerGroup: {fileID: 0}
m_audioClip: {fileID: 0}
+ m_Resource: {fileID: 0}
m_PlayOnAwake: 1
m_Volume: 1
m_Pitch: 1
@@ -580,3 +584,16 @@ AudioSource:
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
+--- !u!114 &1602513700271543697
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 7339275886835513150}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 20620ae30fdd0164c9bb5bdc717d3ada, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Reflex::Reflex.Components.GameObjectSelfInjector
+ _injectionStrategy: 2
diff --git a/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCItem/Weapon.prefab b/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCItem/Weapon.prefab
index 0ee845892..d4655c452 100644
--- a/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCItem/Weapon.prefab
+++ b/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCItem/Weapon.prefab
@@ -34,7 +34,7 @@ Transform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!96 &4431916770342467661
TrailRenderer:
- serializedVersion: 3
+ serializedVersion: 4
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
@@ -53,6 +53,8 @@ TrailRenderer:
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
+ m_ForceMeshLod: -1
+ m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
@@ -74,9 +76,11 @@ TrailRenderer:
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
+ m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
+ m_MaskInteraction: 0
m_Time: 0.77
m_PreviewTimeScale: 1
m_Parameters:
@@ -135,7 +139,6 @@ TrailRenderer:
shadowBias: 0.5
generateLightingData: 0
m_MinVertexDistance: 0.1
- m_MaskInteraction: 0
m_Autodestruct: 0
m_Emitting: 1
m_ApplyActiveColorSpace: 1
@@ -191,6 +194,10 @@ PrefabInstance:
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
+ - target: {fileID: 5406461743051542922, guid: b8c776d1865a6014385673931a399ebc, type: 3}
+ propertyPath: m_PresetInfoIsWorld
+ value: 1
+ objectReference: {fileID: 0}
- target: {fileID: 6503575194566409312, guid: b8c776d1865a6014385673931a399ebc, type: 3}
propertyPath: m_Name
value: Weapon
diff --git a/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab b/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab
index f4064916a..3ea4fa907 100644
--- a/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab
+++ b/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab
@@ -17,6 +17,7 @@ GameObject:
- component: {fileID: 3792221975768868209}
- component: {fileID: 7657019000195239257}
- component: {fileID: 6519483080425844375}
+ - component: {fileID: 8467261965850404422}
m_Layer: 0
m_Name: oCNpc
m_TagString: GothicVob
@@ -150,6 +151,19 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
_injectionStrategy: 2
+--- !u!114 &8467261965850404422
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2202926895728732233}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 9c814ba80f440864495cb75d4515235f, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Gothic.VR::Gothic.VR.Adapters.VRNpcLoot
+ _socketPrefab: {fileID: 5861164055357080340, guid: 2dd29fe4a7e819740af535267b7cc113, type: 3}
--- !u!1 &2815092147823170342
GameObject:
m_ObjectHideFlags: 0
@@ -750,6 +764,10 @@ PrefabInstance:
propertyPath: m_SizeDelta.y
value: 2.8
objectReference: {fileID: 0}
+ - target: {fileID: 2805224566998289421, guid: 6ce7e724ba52cae47a499f87c612cd47, type: 3}
+ propertyPath: m_LocalPosition.z
+ value: 0.02
+ objectReference: {fileID: 0}
- target: {fileID: 8230739814078363531, guid: 6ce7e724ba52cae47a499f87c612cd47, type: 3}
propertyPath: m_Pivot.x
value: 0.5
diff --git a/Assets/Gothic-VR/Scripts/Adapters/HVROverrides/VRSocket.cs b/Assets/Gothic-VR/Scripts/Adapters/HVROverrides/VRSocket.cs
index fb9260e49..fa22e5deb 100644
--- a/Assets/Gothic-VR/Scripts/Adapters/HVROverrides/VRSocket.cs
+++ b/Assets/Gothic-VR/Scripts/Adapters/HVROverrides/VRSocket.cs
@@ -56,10 +56,21 @@ protected override void OnGrabbed(HVRGrabArgs args)
protected override void OnReleased(HVRGrabbable grabbable)
{
var tmpPreviousParent = _previousParent;
- var itemRoot = grabbable.GetComponentInParent(true).transform;
+ var vobLoader = grabbable.GetComponentInParent(true);
+ if (vobLoader == null)
+ {
+ base.OnReleased(grabbable);
+ return;
+ }
+ var itemRoot = vobLoader.transform;
base.OnReleased(grabbable);
-
+
+ // Skip re-parenting when the VobLoader was already deactivated (being destroyed via ClearSocketContents).
+ // OnGrabbableDestroyed can fire during Destroy() after SetActive(false) — re-parenting a destroying GO throws.
+ if (!vobLoader.gameObject.activeSelf)
+ return;
+
grabbable.transform.parent = itemRoot;
itemRoot.parent = tmpPreviousParent;
}
diff --git a/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs b/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs
index 219547f3e..fc5275e31 100644
--- a/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs
+++ b/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs
@@ -87,7 +87,7 @@ public void OnItemPutIntoHolster(HVRGrabberBase grabber, HVRGrabbable grabbable)
var vobLoader = grabbable.GetComponentInParent();
var vobContainer = vobLoader.Container;
- _playerService.AddItem(vobContainer.Vob.Name, vobContainer.VobAs().Amount);
+ _playerService.AddItem(vobContainer.Vob.Name, Mathf.Max(1, vobContainer.VobAs().Amount));
}
@@ -102,8 +102,8 @@ public void OnItemPutIntoBackpack(HVRGrabberBase grabber, HVRGrabbable grabbable
_vobMeshCullingService.RemoveCullingEntry(vobContainer);
_saveGameService.CurrentWorldData.Vobs.Remove(vobContainer.Vob);
- _playerService.AddItem(vobContainer.Vob.Name, vobContainer.VobAs().Amount);
-
+ _playerService.AddItem(vobContainer.Vob.Name, Mathf.Max(1, vobContainer.VobAs().Amount));
+
UpdateInventoryView();
}
@@ -114,23 +114,25 @@ public void OnItemPutIntoBackpack(HVRGrabberBase grabber, HVRGrabbable grabbable
public void OnItemPutOutOfHolster(HVRGrabberBase grabber, HVRGrabbable grabbable)
{
var vobLoader = grabbable.GetComponentInParent();
+ if (vobLoader == null)
+ return;
var vobContainer = vobLoader.Container;
- _playerService.RemoveItem(vobContainer.Vob.Name, vobContainer.VobAs().Amount);
+ _playerService.RemoveItem(vobContainer.Vob.Name, Mathf.Max(1, vobContainer.VobAs().Amount));
}
public void OnItemPutOutOfBackpack(HVRGrabberBase grabber, HVRGrabbable grabbable)
{
if (_tempIgnoreSocketing)
return;
-
+
var vobLoader = grabbable.GetComponentInParent();
var vobContainer = vobLoader.Container;
_vobMeshCullingService.AddCullingEntry(vobContainer);
_saveGameService.CurrentWorldData.Vobs.Add(vobContainer.Vob);
- _playerService.RemoveItem(vobContainer.Vob.Name, vobContainer.VobAs().Amount);
+ _playerService.RemoveItem(vobContainer.Vob.Name, Mathf.Max(1, vobContainer.VobAs().Amount));
UpdateInventoryView();
}
@@ -267,7 +269,14 @@ private void RefillSockets(List inventory)
private void SubtractItemFromHand(List inventory, GameObject handItem)
{
- var item = handItem?.GetComponentInParent().Container.VobAs();
+ if (handItem == null)
+ return;
+
+ var vobLoader = handItem.GetComponentInParent();
+ if (vobLoader == null)
+ return;
+
+ var item = vobLoader.Container?.VobAs();
if (item == null)
return;
diff --git a/Assets/Gothic-VR/Scripts/Adapters/Player/VRFistAttackAdapter.cs b/Assets/Gothic-VR/Scripts/Adapters/Player/VRFistAttackAdapter.cs
index d4ea646e6..48cefc69c 100644
--- a/Assets/Gothic-VR/Scripts/Adapters/Player/VRFistAttackAdapter.cs
+++ b/Assets/Gothic-VR/Scripts/Adapters/Player/VRFistAttackAdapter.cs
@@ -1,13 +1,13 @@
#if GOTHIC_HVR_INSTALLED
using Gothic.Core.Const;
-using UnityEngine;
+using Gothic.VR.Adapters.Vob.VobItem;
namespace Gothic.Core.Adapters.Vob.Item
{
///
/// Basically a WeaponAttackAdapter for HVR Hands. But some tweaks are needed to fake the object into being an oCVobItem to fight.
///
- public class VRFistAttackAdapter : WeaponAttackAdapter
+ public class VRFistAttackAdapter : VRWeaponAttackAdapter
{
private void Awake()
{
diff --git a/Assets/Gothic-VR/Scripts/Adapters/Player/VRPlayerWeaponInteraction.cs b/Assets/Gothic-VR/Scripts/Adapters/Player/VRPlayerWeaponInteraction.cs
index 30fd4c8c5..4bb3622e3 100644
--- a/Assets/Gothic-VR/Scripts/Adapters/Player/VRPlayerWeaponInteraction.cs
+++ b/Assets/Gothic-VR/Scripts/Adapters/Player/VRPlayerWeaponInteraction.cs
@@ -1,8 +1,10 @@
#if GOTHIC_HVR_INSTALLED
using System.Collections.Generic;
using Gothic.Core.Adapters.Vob;
+using Gothic.Core.Extensions;
using Gothic.Core.Models.Marvin;
using Gothic.Core.Models.Vm;
+using Gothic.Core.Services;
using Gothic.VR.Models.Vob;
using Gothic.VR.Services;
using HurricaneVR.Framework.Core;
@@ -16,6 +18,7 @@ namespace Gothic.VR.Adapters.Player
public class VRPlayerWeaponInteraction : MonoBehaviour, IMarvinPropertyCollector
{
[Inject] private readonly VRWeaponService _weaponService;
+ [Inject] private readonly GameStateService _gameStateService;
// FIXME - All of these values will be dynamic in the future. Based on skill level and weapon type.
@@ -47,11 +50,14 @@ public void OnGrabbed(HVRGrabberBase hand, HVRGrabbable item)
if (vobContainer == null || vobContainer.Vob.Type != VirtualObjectType.oCItem)
return;
+ var itemInstance = vobContainer.GetItemInstance();
+
// We currently handle melee weapons only.
- if (vobContainer.GetItemInstance()!.MainFlag != (int)VmGothicEnums.ItemFlags.ItemKatNf)
+ if (itemInstance!.MainFlag != (int)VmGothicEnums.ItemFlags.ItemKatNf)
return;
_weaponService.OnGrabbed(((HVRHandGrabber)hand).HandSide, vobContainer, GetWeaponPhysicsConfig());
+ SyncEquippedWeaponToHero(itemInstance, equip: true);
}
public void OnReleased(HVRGrabberBase hand, HVRGrabbable item)
@@ -63,11 +69,30 @@ public void OnReleased(HVRGrabberBase hand, HVRGrabbable item)
if (vobContainer == null || vobContainer.Vob.Type != VirtualObjectType.oCItem)
return;
+ var itemInstance = vobContainer.GetItemInstance();
+
// Currently we handle melee weapons only.
- if (vobContainer.GetItemInstance()!.MainFlag != (int)VmGothicEnums.ItemFlags.ItemKatNf)
+ if (itemInstance!.MainFlag != (int)VmGothicEnums.ItemFlags.ItemKatNf)
return;
_weaponService.OnReleased(((HVRHandGrabber)hand).HandSide, GetWeaponPhysicsConfig());
+ SyncEquippedWeaponToHero(itemInstance, equip: false);
+ }
+
+ private void SyncEquippedWeaponToHero(ZenKit.Daedalus.ItemInstance itemInstance, bool equip)
+ {
+ var hero = _gameStateService.GothicVm?.GlobalHero as ZenKit.Daedalus.NpcInstance;
+ if (hero == null)
+ return;
+
+ var heroContainer = hero.GetUserData();
+ if (heroContainer == null)
+ return;
+
+ var equippedItems = heroContainer.Props.EquippedItems;
+ equippedItems.RemoveAll(i => i.MainFlag == itemInstance.MainFlag);
+ if (equip)
+ equippedItems.Add(itemInstance);
}
///
diff --git a/Assets/Gothic-VR/Scripts/Adapters/VRNpc.cs b/Assets/Gothic-VR/Scripts/Adapters/VRNpc.cs
index 80dbcd99a..8664f2289 100644
--- a/Assets/Gothic-VR/Scripts/Adapters/VRNpc.cs
+++ b/Assets/Gothic-VR/Scripts/Adapters/VRNpc.cs
@@ -4,10 +4,11 @@
using Gothic.Core.Models.Container;
using Gothic.Core.Models.Vm;
using Gothic.Core.Services;
+using Gothic.Core.Services.Config;
using Gothic.Core.Services.Npc;
+using Gothic.Core.Services.World;
using Gothic.Core;
using Gothic.Core.Extensions;
-using Gothic.Core.Const;
using HurricaneVR.Framework.Core;
using HurricaneVR.Framework.Core.Grabbers;
using Reflex.Attributes;
@@ -21,16 +22,31 @@ public class VRNpc : MonoBehaviour
[Inject] private readonly GameStateService _gameStateService;
[Inject] private readonly DialogService _dialogService;
[Inject] private readonly NpcAiService _npcAiService;
+ [Inject] private readonly ConfigService _configService;
+ [Inject] private readonly PhysicsService _physicsService;
private NpcContainer _npcData;
+ private VRNpcLoot _npcLoot;
private void Awake()
{
_npcData = GetComponentInParent().Npc.GetUserData();
+ _npcLoot = GetComponent();
}
public void OnGrabbed(HVRGrabberBase grabber, HVRGrabbable grabbable)
{
+ var isDead = _npcData.Props.BodyState == VmGothicEnums.BodyState.BsDead;
+
+ if (isDead && _configService.Dev.EnableNpcLooting && _npcLoot != null)
+ {
+ _npcLoot.Toggle(_npcData);
+ // HVR sets isKinematic=false during grab; release immediately and freeze the corpse
+ grabber.ForceRelease();
+ _physicsService.DisablePhysicsForNpc(_npcData.PrefabProps);
+ return;
+ }
+
if (_gameStateService.Dialogs.IsInDialog)
{
_dialogService.SkipCurrentDialogLine(_npcData.Props);
diff --git a/Assets/Gothic-VR/Scripts/Adapters/VRNpcLoot.cs b/Assets/Gothic-VR/Scripts/Adapters/VRNpcLoot.cs
new file mode 100644
index 000000000..8181fe115
--- /dev/null
+++ b/Assets/Gothic-VR/Scripts/Adapters/VRNpcLoot.cs
@@ -0,0 +1,388 @@
+#if GOTHIC_HVR_INSTALLED
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using Assets.HurricaneVR.Framework.Shared.Utilities;
+using Gothic.Core;
+using Gothic.Core.Adapters.Npc;
+using Gothic.Core.Adapters.Vob;
+using Gothic.Core.Extensions;
+using Gothic.Core.Manager;
+using Gothic.Core.Models.Container;
+using Gothic.Core.Models.Vm;
+using Gothic.Core.Models.Vob;
+using Gothic.Core.Services;
+using Gothic.Core.Services.Caches;
+using Gothic.Core.Services.Npc;
+using Gothic.Core.Services.Vobs;
+using Gothic.VR.Services;
+using HurricaneVR.Framework.Core;
+using HurricaneVR.Framework.Core.Grabbers;
+using HurricaneVR.Framework.Core.Sockets;
+using Reflex.Attributes;
+using TMPro;
+using UnityEngine;
+using ZenKit.Daedalus;
+using ZenKit.Vobs;
+
+namespace Gothic.VR.Adapters
+{
+ public class VRNpcLoot : MonoBehaviour
+ {
+ private const int MaxVisibleSlots = 5;
+ private const float RefreshDelay = 0.6f;
+ private const float SocketHeight = 0.8f;
+ private const float SocketSpacing = 0.22f;
+
+ [SerializeField] private GameObject _socketPrefab;
+
+ [Inject] private readonly NpcInventoryService _npcInventoryService;
+ [Inject] private readonly VobService _vobService;
+ [Inject] private readonly AudioService _audioService;
+ [Inject] private readonly VmCacheService _vmCacheService;
+ [Inject] private readonly VRWeaponService _vrWeaponService;
+ [Inject] private readonly GameStateService _gameStateService;
+
+ private NpcContainer _npcContainer;
+ private NpcLoader _npcLoader;
+ private readonly List _sockets = new();
+ private readonly List _socketRoots = new();
+ private bool _isOpen;
+ private bool _tempIgnoreSocketing;
+ private Coroutine _pendingRefresh;
+
+ private readonly struct LootEntry
+ {
+ public readonly ContentItem Item;
+ public readonly bool IsEquipped;
+
+ public LootEntry(ContentItem item, bool isEquipped)
+ {
+ Item = item;
+ IsEquipped = isEquipped;
+ }
+ }
+
+ private void Awake()
+ {
+ _npcLoader = GetComponentInParent();
+ }
+
+ public void Toggle(NpcContainer npc)
+ {
+ if (_isOpen)
+ Close();
+ else
+ Open(npc);
+ }
+
+ public void Open(NpcContainer npc)
+ {
+ _npcContainer = npc;
+ _isOpen = true;
+ PlayOpenSound();
+ CreateSockets();
+ StartCoroutine(FillSockets());
+ }
+
+ public void Close()
+ {
+ _isOpen = false;
+ if (_pendingRefresh != null)
+ {
+ StopCoroutine(_pendingRefresh);
+ _pendingRefresh = null;
+ }
+ StartCoroutine(CloseSockets());
+ }
+
+ private void PlayOpenSound()
+ {
+ var clip = _audioService.CreateAudioClip(_audioService.InvOpen.File);
+ if (clip == null)
+ return;
+
+ var audioSource = GetComponentInChildren();
+ audioSource?.PlayOneShot(clip);
+ }
+
+ private void CreateSockets()
+ {
+ var totalWidth = (MaxVisibleSlots - 1) * SocketSpacing;
+
+ for (var i = 0; i < MaxVisibleSlots; i++)
+ {
+ var socketGo = Instantiate(_socketPrefab, transform);
+ var xOffset = -totalWidth / 2f + i * SocketSpacing;
+ socketGo.transform.localPosition = new Vector3(xOffset, SocketHeight, 0f);
+ socketGo.transform.localRotation = Quaternion.identity;
+ socketGo.transform.localScale = Vector3.one * 1.6f;
+
+ var socket = socketGo.GetComponentInChildren();
+ socket.Released.AddListener(OnItemTakenFromLoot);
+
+ _socketRoots.Add(socketGo);
+ _sockets.Add(socket);
+ }
+ }
+
+ private void DestroySockets()
+ {
+ foreach (var root in _socketRoots)
+ {
+ if (root != null)
+ Destroy(root);
+ }
+ _socketRoots.Clear();
+ _sockets.Clear();
+ }
+
+ private List BuildLootList()
+ {
+ var result = new List();
+ var equippedNames = new HashSet(StringComparer.OrdinalIgnoreCase);
+
+ // Equipped weapons first (melee + ranged) — shown with [E] badge and GO removal on take
+ foreach (var equipped in _npcContainer.Props.EquippedItems)
+ {
+ var mainFlag = (VmGothicEnums.ItemFlags)equipped.MainFlag;
+ if (mainFlag != VmGothicEnums.ItemFlags.ItemKatNf && mainFlag != VmGothicEnums.ItemFlags.ItemKatFf)
+ continue;
+
+ var symbolName = _gameStateService.GothicVm.GetSymbolByIndex(equipped.Index)?.Name;
+ if (symbolName == null)
+ continue;
+
+ equippedNames.Add(symbolName);
+ result.Add(new LootEntry(new ContentItem(symbolName, 1), isEquipped: true));
+ }
+
+ // All inventory items: skip armor, skip equipped weapons already listed above
+ foreach (var invItem in _npcInventoryService.GetAllInventoryItems(_npcContainer.Instance))
+ {
+ if (equippedNames.Contains(invItem.Name))
+ continue;
+
+ var itemData = _vmCacheService.TryGetItemData(invItem.Name);
+ if (itemData != null)
+ {
+ var cat = ((VmGothicEnums.ItemFlags)itemData.MainFlag).ToInventoryCategory();
+ if (cat == VmGothicEnums.InvCats.InvArmor)
+ continue;
+ }
+
+ result.Add(new LootEntry(invItem, isEquipped: false));
+ }
+
+ return result;
+ }
+
+ private IEnumerator FillSockets()
+ {
+ yield return PopulateSockets(clearFirst: false);
+ }
+
+ private IEnumerator ClearAndRefill()
+ {
+ yield return PopulateSockets(clearFirst: true);
+ }
+
+ private IEnumerator PopulateSockets(bool clearFirst)
+ {
+ _tempIgnoreSocketing = true;
+ _vrWeaponService.DrawSoundsActive = false;
+
+ if (clearFirst)
+ {
+ ClearSocketContents();
+ yield return null;
+ }
+
+ foreach (var entry in BuildLootList().Take(MaxVisibleSlots))
+ {
+ var vobContainer = _vobService.CreateItem(new Item
+ {
+ Name = entry.Item.Name,
+ Visual = new VisualMesh(),
+ Instance = entry.Item.Name,
+ Amount = entry.Item.Amount
+ });
+
+ vobContainer.Go.GetComponentInChildren().isKinematic = false;
+
+ yield return null;
+
+ var grabbable = vobContainer.Go.GetComponentInChildren(true);
+ var freeSocket = _sockets.FirstOrDefault(s => !s.IsGrabbing);
+ if (freeSocket != null)
+ {
+ freeSocket.TryGrab(grabbable, true, true);
+ if (entry.IsEquipped)
+ AddEquippedLabel(GetSocketRoot(freeSocket));
+ }
+ }
+
+ yield return null;
+ _tempIgnoreSocketing = false;
+ _vrWeaponService.DrawSoundsActive = true;
+ }
+
+ private void ClearSocketContents()
+ {
+ // Remove equipped labels before clearing items
+ foreach (var root in _socketRoots)
+ {
+ var label = root.transform.Find("EquippedLabel");
+ if (label != null)
+ Destroy(label.gameObject);
+ }
+
+ foreach (var socket in _sockets)
+ {
+ if (!socket.IsGrabbing)
+ continue;
+
+ var heldRoot = socket.HeldObject.transform.parent.gameObject;
+ socket.ForceRelease();
+ heldRoot.SetActive(false);
+ this.ExecuteNextUpdate(() => Destroy(heldRoot));
+ }
+ }
+
+ private IEnumerator CloseSockets()
+ {
+ _tempIgnoreSocketing = true;
+ _vrWeaponService.DrawSoundsActive = false;
+
+ ClearSocketContents();
+ yield return null;
+
+ DestroySockets();
+ _tempIgnoreSocketing = false;
+ _vrWeaponService.DrawSoundsActive = true;
+ }
+
+ public void OnItemTakenFromLoot(HVRGrabberBase grabber, HVRGrabbable grabbable)
+ {
+ if (_tempIgnoreSocketing)
+ return;
+
+ var vobLoader = grabbable.GetComponentInParent();
+ if (vobLoader?.Container == null)
+ return;
+
+ var item = vobLoader.Container.VobAs();
+ if (item == null)
+ return;
+
+ var itemName = vobLoader.Container.Vob.Name;
+ var itemData = _vmCacheService.TryGetItemData(itemName);
+ if (itemData == null)
+ return;
+
+ // If this was an equipped weapon: destroy the mesh from NPC body and remove from equipped list
+ var equippedMatch = _npcContainer.Props.EquippedItems
+ .FirstOrDefault(e => string.Equals(
+ _gameStateService.GothicVm.GetSymbolByIndex(e.Index)?.Name,
+ itemName,
+ StringComparison.OrdinalIgnoreCase));
+ if (equippedMatch != null)
+ {
+ _npcContainer.Props.EquippedItems.Remove(equippedMatch);
+ var weaponGo = FindEquippedWeaponGo(equippedMatch);
+ if (weaponGo != null)
+ Destroy(weaponGo);
+ }
+
+ _npcInventoryService.ExtRemoveInvItems(_npcContainer.Instance, itemData.Index, item.Amount);
+
+ if (_pendingRefresh != null)
+ StopCoroutine(_pendingRefresh);
+ _pendingRefresh = StartCoroutine(RefreshAfterDelay());
+ }
+
+ private IEnumerator RefreshAfterDelay()
+ {
+ yield return new WaitForSeconds(RefreshDelay);
+ _pendingRefresh = null;
+ StartCoroutine(ClearAndRefill());
+ }
+
+ ///
+ /// Finds the weapon GameObject in the NPC skeleton using the same slot mapping as NpcWeaponMeshBuilder.
+ /// Tries the holster slot first, then ZS_RIGHTHAND as fallback (for NPCs that died mid-draw).
+ ///
+ private GameObject FindEquippedWeaponGo(ItemInstance equipped)
+ {
+ if (_npcLoader == null)
+ return null;
+
+ var mainFlag = (VmGothicEnums.ItemFlags)equipped.MainFlag;
+ var flags = (VmGothicEnums.ItemFlags)equipped.Flags;
+ var npcRoot = _npcLoader.gameObject;
+
+ string holsterSlot;
+ switch (mainFlag)
+ {
+ case VmGothicEnums.ItemFlags.ItemKatNf:
+ switch (flags)
+ {
+ case VmGothicEnums.ItemFlags.Item2HdAxe:
+ case VmGothicEnums.ItemFlags.Item2HdSwd:
+ holsterSlot = "ZS_LONGSWORD";
+ break;
+ default:
+ holsterSlot = "ZS_SWORD";
+ break;
+ }
+ break;
+ case VmGothicEnums.ItemFlags.ItemKatFf:
+ holsterSlot = flags == VmGothicEnums.ItemFlags.ItemCrossbow ? "ZS_CROSSBOW" : "ZS_BOW";
+ break;
+ default:
+ return null;
+ }
+
+ // Try holster slot, fall back to drawn position
+ foreach (var slotName in new[] { holsterSlot, "ZS_RIGHTHAND" })
+ {
+ var slotGo = npcRoot.FindChildRecursively(slotName);
+ if (slotGo != null && slotGo.transform.childCount > 0)
+ return slotGo.transform.GetChild(0).gameObject;
+ }
+
+ return null;
+ }
+
+ private GameObject GetSocketRoot(HVRSocket socket)
+ {
+ var idx = _sockets.IndexOf(socket);
+ return idx >= 0 && idx < _socketRoots.Count ? _socketRoots[idx] : null;
+ }
+
+ private static void AddEquippedLabel(GameObject socketRoot)
+ {
+ if (socketRoot == null)
+ return;
+
+ var labelGo = new GameObject("EquippedLabel");
+ labelGo.transform.SetParent(socketRoot.transform, false);
+ labelGo.transform.localPosition = new Vector3(0f, 0.09f, 0f);
+ labelGo.transform.localScale = Vector3.one * 0.013f;
+
+ var tmp = labelGo.AddComponent();
+ // Assign font immediately after AddComponent to suppress repeated OnPreRenderObject warnings.
+ var font = Resources.Load("Fonts & Materials/LiberationSans SDF");
+ if (font != null)
+ tmp.font = font;
+ tmp.text = "[E]";
+ tmp.fontSize = 12;
+ tmp.color = new Color(1f, 0.65f, 0f, 1f);
+ tmp.alignment = TextAlignmentOptions.Center;
+ tmp.textWrappingMode = TextWrappingModes.NoWrap;
+ tmp.fontStyle = FontStyles.Bold;
+ }
+ }
+}
+#endif
diff --git a/Assets/Gothic-VR/Scripts/Adapters/VRNpcLoot.cs.meta b/Assets/Gothic-VR/Scripts/Adapters/VRNpcLoot.cs.meta
new file mode 100644
index 000000000..40fe5bc38
--- /dev/null
+++ b/Assets/Gothic-VR/Scripts/Adapters/VRNpcLoot.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 9c814ba80f440864495cb75d4515235f
\ No newline at end of file
diff --git a/Assets/Gothic-VR/Scripts/Adapters/Vob/Container/VRVobContainerSocketInventory.cs b/Assets/Gothic-VR/Scripts/Adapters/Vob/Container/VRVobContainerSocketInventory.cs
index 2f3f7c72d..5c43a6669 100644
--- a/Assets/Gothic-VR/Scripts/Adapters/Vob/Container/VRVobContainerSocketInventory.cs
+++ b/Assets/Gothic-VR/Scripts/Adapters/Vob/Container/VRVobContainerSocketInventory.cs
@@ -1,5 +1,7 @@
#if GOTHIC_HVR_INSTALLED
using Gothic.Core.Adapters.Vob;
+using Gothic.Core.Models.Container;
+using Gothic.Core.Models.Vm;
using Gothic.VR.Services;
using HurricaneVR.Framework.Core;
using HurricaneVR.Framework.Core.Grabbers;
@@ -14,17 +16,29 @@ namespace Gothic.VR.Adapters.Vob.Container
public class VRVobContainerSocketInventory : MonoBehaviour
{
[Inject] private VRWeaponService _vrWeaponService;
-
+
public void OnBeforeGrabbed(HVRGrabberBase grabber, HVRGrabbable grabbable)
{
- Debug.Log("Undraw");
- _vrWeaponService.PlayUndrawSound(grabbable.GetComponentInParent().Container);
+ var container = grabbable.GetComponentInParent()?.Container;
+ if (!IsWeapon(container))
+ return;
+ _vrWeaponService.PlayUndrawSound(container);
}
public void OnReleased(HVRGrabberBase grabber, HVRGrabbable grabbable)
{
- Debug.Log("Draw");
- _vrWeaponService.PlayDrawSound(grabbable.GetComponentInParent().Container);
+ var container = grabbable.GetComponentInParent()?.Container;
+ if (!IsWeapon(container))
+ return;
+ _vrWeaponService.PlayDrawSound(container);
+ }
+
+ private bool IsWeapon(VobContainer container)
+ {
+ var itemInstance = container?.GetItemInstance();
+ if (itemInstance == null) return false;
+ var flag = (VmGothicEnums.ItemFlags)itemInstance.MainFlag;
+ return flag is VmGothicEnums.ItemFlags.ItemKatNf or VmGothicEnums.ItemFlags.ItemKatFf;
}
}
}
diff --git a/Assets/Gothic-VR/Scripts/Adapters/Vob/LockPicking/VRContainerDoorPickingInteraction.cs b/Assets/Gothic-VR/Scripts/Adapters/Vob/LockPicking/VRContainerDoorPickingInteraction.cs
index b4b5710d0..528706556 100644
--- a/Assets/Gothic-VR/Scripts/Adapters/Vob/LockPicking/VRContainerDoorPickingInteraction.cs
+++ b/Assets/Gothic-VR/Scripts/Adapters/Vob/LockPicking/VRContainerDoorPickingInteraction.cs
@@ -67,7 +67,10 @@ private void Start()
// Stop this handler if the object is already unlocked.
if (!_isLocked)
+ {
gameObject.SetActive(false);
+ return;
+ }
StartCoroutine(StartDelayed());
}
diff --git a/Assets/Gothic-VR/Scripts/Adapters/Vob/VobItem/VRMouth.cs b/Assets/Gothic-VR/Scripts/Adapters/Vob/VobItem/VRMouth.cs
index 9cd5ce671..91e1d688b 100644
--- a/Assets/Gothic-VR/Scripts/Adapters/Vob/VobItem/VRMouth.cs
+++ b/Assets/Gothic-VR/Scripts/Adapters/Vob/VobItem/VRMouth.cs
@@ -14,6 +14,7 @@
using UnityEngine;
using ZenKit;
using ZenKit.Daedalus;
+using ZenKit.Vobs;
using Logger = Gothic.Core.Logging.Logger;
namespace Gothic.VR.Adapters.Vob.VobItem
@@ -60,8 +61,8 @@ private void OnTriggerEnter(Collider other)
GameObject rootGo = go;
var vobLoaderComp = go.GetComponentInParent();
- // LAB - Fallback as objects aren't LazyLoaded in here.
- if (vobLoaderComp != null)
+ // Only use the VobLoader root if it belongs to this specific item (not a parent chest/container).
+ if (vobLoaderComp != null && vobLoaderComp.Container.Vob.Type == VirtualObjectType.oCItem)
rootGo = vobLoaderComp.gameObject;
StartCoroutine(ConsumeObject(rootGo, clip, destroyTime));
diff --git a/Assets/Gothic-VR/Scripts/Adapters/Vob/VobItem/VRWeaponAttackAdapter.cs b/Assets/Gothic-VR/Scripts/Adapters/Vob/VobItem/VRWeaponAttackAdapter.cs
new file mode 100644
index 000000000..62d6ef39f
--- /dev/null
+++ b/Assets/Gothic-VR/Scripts/Adapters/Vob/VobItem/VRWeaponAttackAdapter.cs
@@ -0,0 +1,119 @@
+#if GOTHIC_HVR_INSTALLED
+using Gothic.Core;
+using Gothic.Core.Adapters.Npc;
+using Gothic.Core.Adapters.Vob;
+using Gothic.Core.Const;
+using Gothic.Core.Logging;
+using Gothic.Core.Models.Container;
+using Gothic.VR.Services;
+using Reflex.Attributes;
+using UnityEngine;
+using ZenKit.Daedalus;
+using Logger = Gothic.Core.Logging.Logger;
+
+namespace Gothic.VR.Adapters.Vob.VobItem
+{
+ ///
+ /// The validity check for a hit requires answering: "Is this attack currently active?"
+ /// That state lives on the attacker (DEF_OPT_FRAME window, DEF_HIT_LIMB, "already connected" flag).
+ /// If we put the logic on the receiver, we must reach across to the attacker's component to get that state
+ /// every time any contact happens. If we put it on the attacker, all required state is already local.
+ ///
+ public class VRWeaponAttackAdapter : MonoBehaviour
+ {
+ [Inject] private readonly VRWeaponService _vrWeaponService;
+
+ private VobContainer _weaponVobContainer;
+ private NpcContainer _targetNpcContainer;
+
+ private void Start()
+ {
+ // Get reference to this weapon's VobContainer if it exists
+ var vobLoader = GetComponentInParent();
+ if (vobLoader != null)
+ {
+ _weaponVobContainer = vobLoader.Container;
+ }
+ }
+
+ ///
+ /// TODO - We need to handle multiple hitboxes on the same target (e.g. head vs body) and ensure we don't apply multiple hits from one attack.
+ /// TODO - figure out how to do fists
+ /// TODO - figure out how to do NPC-to-NPC hits (e.g. monster attacking hero or monster attacking monster)
+ /// Handles collision between weapon and potential targets (NPCs/Monsters).
+ /// Validates the hit and fires FightHit event if conditions are met.
+ ///
+ private void OnTriggerEnter(Collider other)
+ {
+ if (other.gameObject.layer != Constants.VobHitbox)
+ {
+ Logger.LogWarning($"[WeaponAttackAdapter] Wrong layer: {LayerMask.LayerToName(other.gameObject.layer)}", LogCat.Fight);
+ return;
+ }
+
+ Logger.Log($"[WeaponAttackAdapter] Collision with VobHitbox: {other.gameObject.name}", LogCat.Fight);
+
+ // Try to get the target NPC/Monster from the hitbox
+ var targetNpcLoader = other.GetComponentInParent();
+ if (targetNpcLoader == null)
+ {
+ Logger.LogWarning($"[WeaponAttackAdapter] No NpcLoader found", LogCat.Fight);
+ return;
+ }
+
+ var targetNpcContainer = targetNpcLoader.Container;
+ if (targetNpcContainer == null)
+ {
+ Logger.LogWarning("[WeaponAttackAdapter] No NpcContainer found", LogCat.Fight);
+ return;
+ }
+
+ Logger.Log($"[WeaponAttackAdapter] Target: {targetNpcContainer.Instance.GetName(NpcNameSlot.Slot0)}", LogCat.Fight);
+
+ // Try to fire the hit through VR weapon service if available
+ if (TryFireHitViaVRWeaponService(targetNpcContainer))
+ {
+ Logger.Log($"[WeaponAttackAdapter] *** HIT FIRED (VR)", LogCat.Fight);
+ return;
+ }
+
+ Logger.LogWarning("[WeaponAttackAdapter] Failed to fire hit (not in attack window or VR unavailable)", LogCat.Fight);
+ // TODO - Add support for flat-screen weapon hits and NPC-to-NPC hits here
+ }
+
+ private void OnTriggerExit(Collider other)
+ {
+ if (other.gameObject.layer != Constants.VobHitbox)
+ return;
+ }
+
+ private bool TryFireHitViaVRWeaponService(NpcContainer targetNpcContainer)
+ {
+ if (_vrWeaponService == null)
+ {
+ Logger.LogWarning("[WeaponAttackAdapter] VRWeaponService not injected (flat-screen mode?)", LogCat.Fight);
+ return false;
+ }
+
+ var isInAttackWindow = _vrWeaponService.IsWeaponInAttackWindow(_weaponVobContainer);
+ Logger.Log($"[WeaponAttackAdapter] IsInAttackWindow: {isInAttackWindow}", LogCat.Fight);
+ if (!isInAttackWindow)
+ {
+ Logger.LogWarning("[WeaponAttackAdapter] Weapon not in attack window", LogCat.Fight);
+ return false;
+ }
+
+ var attacker = _vrWeaponService.GetWeaponOwner(_weaponVobContainer);
+ if (attacker == null)
+ {
+ Logger.LogWarning("[WeaponAttackAdapter] No attacker found for weapon", LogCat.Fight);
+ return false;
+ }
+
+ Logger.Log($"[WeaponAttackAdapter] FightHit event: {attacker.Instance.GetName(NpcNameSlot.Slot0)} → {targetNpcContainer.Instance.GetName(NpcNameSlot.Slot0)}", LogCat.Fight);
+ GlobalEventDispatcher.FightHit.Invoke(attacker, targetNpcContainer, transform.position);
+ return true;
+ }
+ }
+}
+#endif
diff --git a/Assets/Gothic-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs.meta b/Assets/Gothic-VR/Scripts/Adapters/Vob/VobItem/VRWeaponAttackAdapter.cs.meta
similarity index 100%
rename from Assets/Gothic-Core/Scripts/Adapters/Vob/Item/WeaponAttackAdapter.cs.meta
rename to Assets/Gothic-VR/Scripts/Adapters/Vob/VobItem/VRWeaponAttackAdapter.cs.meta
diff --git a/Assets/Gothic-VR/Scripts/Services/VRPlayerService.cs b/Assets/Gothic-VR/Scripts/Services/VRPlayerService.cs
index b9ba5237c..754090e60 100644
--- a/Assets/Gothic-VR/Scripts/Services/VRPlayerService.cs
+++ b/Assets/Gothic-VR/Scripts/Services/VRPlayerService.cs
@@ -45,6 +45,9 @@ public void SetGrab(HVRGrabberBase grabber, HVRGrabbable grabbable)
else
handGrabber = grabber as HVRHandGrabber;
+ if (handGrabber == null)
+ return;
+
if (handGrabber.IsLeftHand)
{
// If we did remote grabbing, this function is called twice (remote grabber+hand grabber).
@@ -70,7 +73,8 @@ public void SetGrab(HVRGrabberBase grabber, HVRGrabbable grabbable)
// Otherwise alter inventory count
var vobItem = grabbable.GetComponentInParent().Container.VobAs();
- _playerService.AddItem(vobItem.Instance, vobItem.Amount);
+ var instanceName = !string.IsNullOrEmpty(vobItem.Instance) ? vobItem.Instance : vobItem.Name;
+ _playerService.AddItem(instanceName, vobItem.Amount);
}
public void UnsetGrab(HVRGrabberBase grabber, HVRGrabbable grabbable)
@@ -83,6 +87,9 @@ public void UnsetGrab(HVRGrabberBase grabber, HVRGrabbable grabbable)
else
handGrabber = grabber as HVRHandGrabber;
+ if (handGrabber == null)
+ return;
+
if (handGrabber.IsLeftHand)
{
// If we did remote grabbing, this function is called twice (remote grabber+hand grabber).
@@ -108,7 +115,8 @@ public void UnsetGrab(HVRGrabberBase grabber, HVRGrabbable grabbable)
// Otherwise alter inventory count
var vobItem = grabbable.GetComponentInParent().Container.VobAs();
- _playerService.RemoveItem(vobItem.Instance, vobItem.Amount);
+ var instanceName = !string.IsNullOrEmpty(vobItem.Instance) ? vobItem.Instance : vobItem.Name;
+ _playerService.RemoveItem(instanceName, vobItem.Amount);
}
public HVRController GetHand(HVRHandSide side)
diff --git a/ProjectSettings/DynamicsManager.asset b/ProjectSettings/DynamicsManager.asset
index b55c6c701..c81ca078f 100644
--- a/ProjectSettings/DynamicsManager.asset
+++ b/ProjectSettings/DynamicsManager.asset
@@ -17,7 +17,7 @@ PhysicsManager:
m_EnableAdaptiveForce: 0
m_ClothInterCollisionDistance: 0
m_ClothInterCollisionStiffness: 0
- m_LayerCollisionMatrix: 6fefffff09e0cfffedefffffffffffff08e0cfff0de0cfff4dfaffff0ce2efff0de6cfffcdefffff0de7ffff4deaffff48e0cfffffffffffffffffffffffffffffffffffffffffffffffffffffffffff4deeffffcdeeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+ m_LayerCollisionMatrix: 6fefffffc9e0cfffedefffffffffffff08e0cfff0de0cfff4ffaffff0ee2efff0de6cfffcdefffff0de7ffff4deaffff48e0cfffffffffffffffffffffffffffffffffffffffffffffffffffffffffff4deeffffcdeeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
m_SimulationMode: 0
m_AutoSyncTransforms: 0
m_ReuseCollisionCallbacks: 1