Skip to content

Commit f164ca9

Browse files
fix: walk/turn/use-mob action sequencing and stepwise stop
Fixes GoToFp orphan-walk slide, UseMob slot tags with stepwise stop, and turn/walk action transitions.
1 parent 8e3241c commit f164ca9

9 files changed

Lines changed: 108 additions & 69 deletions

File tree

Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/AbstractRotateAnimationAction.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using Gothic.Core.Models.Vm;
33
using Gothic.Core.Services;
44
using Gothic.Core.Services.Npc;
5-
using Reflex.Attributes;
65
using UnityEngine;
76

87
namespace Gothic.Core.Domain.Npc.Actions.AnimationActions
@@ -53,9 +52,9 @@ public override void Start()
5352
return;
5453
}
5554

56-
// https://discussions.unity.com/t/determining-whether-to-rotate-left-or-right/44021
57-
var cross = Vector3.Cross(NpcGo.transform.forward, _finalRotation.eulerAngles);
58-
_isRotateLeft = cross.y >= 0;
55+
// Negative signed angle around the up axis means the target direction is to our left.
56+
var targetForward = _finalRotation * Vector3.forward;
57+
_isRotateLeft = Vector3.SignedAngle(NpcGo.transform.forward, targetForward, Vector3.up) < 0;
5958

6059
if (PlayAnimation)
6160
{
@@ -92,7 +91,8 @@ private void HandleRotation(Transform npcTransform)
9291
// Check if rotation is done.
9392
if (Quaternion.Angle(npcTransform.rotation, _finalRotation) < 1f)
9493
{
95-
PrefabProps.AnimationSystem.StopAnimation(_rotationAnimationName);
94+
if (_rotationAnimationName != null)
95+
PrefabProps.AnimationSystem.StopAnimation(_rotationAnimationName);
9696

9797
IsFinishedFlag = true;
9898
}

Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/AbstractWalkAnimationAction.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,7 @@ public override void Tick()
8181
private string GetWalkModeAnimationString()
8282
{
8383
var fightMode = (VmGothicEnums.WeaponState)Vob.FightMode;
84-
var weaponState = fightMode == VmGothicEnums.WeaponState.NoWeapon
85-
? ""
86-
: fightMode.ToString();
84+
var weaponState = Services.Npc.AnimationService.GetWeaponAnimationPrefix(fightMode);
8785
var walkMode = (VmGothicEnums.WalkMode)Vob.AiHuman.WalkMode;
8886
switch (walkMode)
8987
{

Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/AbstractWalkAnimationAction2.cs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using Gothic.Core.Models.Container;
33
using Gothic.Core.Models.Vm;
44
using Gothic.Core.Services.Npc;
5-
using Reflex.Attributes;
65
using UnityEngine;
76

87
namespace Gothic.Core.Domain.Npc.Actions.AnimationActions
@@ -12,6 +11,11 @@ public abstract class AbstractWalkAnimationAction2 : AbstractAnimationAction
1211
protected Transform NpcTransform => NpcGo.transform;
1312
protected bool IsDestReached;
1413

14+
// Name of the animation StartWalk() actually played. StopWalk() must stop exactly this one:
15+
// recalculating the name would stop the wrong animation when walk/fight mode changed mid-walk
16+
// (e.g. via an immediately executed AI_SetWalkmode), leaving the walk loop sliding the NPC forever.
17+
private string _startedWalkAnimationName;
18+
1519
protected AbstractWalkAnimationAction2(AnimationAction action, NpcContainer npcContainer) : base(action, npcContainer)
1620
{
1721
}
@@ -34,6 +38,16 @@ public override void Start()
3438
if (IsDestinationReached())
3539
{
3640
OnDestinationReached();
41+
42+
// Already at the final destination (e.g. a FP_ROAM FreePoint right next to the NPC):
43+
// never start the walk loop - nobody would stop it again and its root motion
44+
// would slide the NPC around (visible e.g. on roaming Molerats).
45+
// IsDestReached covers subclasses which continue at the spot without finishing
46+
// (e.g. UseMob playing its transition animation) - the walk loop would blend
47+
// that animation out again. Only a multi-stop route (GoToWp) resets the flag
48+
// and walks on.
49+
if (IsFinishedFlag || IsDestReached)
50+
return;
3751
}
3852

3953
StartWalk();
@@ -59,16 +73,18 @@ protected virtual void StartWalk()
5973
{
6074
PhysicsService.EnablePhysicsForNpc(PrefabProps);
6175

62-
var animName = AnimationService.GetAnimationName(VmGothicEnums.AnimationType.Move, NpcContainer);
63-
PrefabProps.AnimationSystem.PlayAnimation(animName);
76+
_startedWalkAnimationName = AnimationService.GetAnimationName(VmGothicEnums.AnimationType.Move, NpcContainer);
77+
PrefabProps.AnimationSystem.PlayAnimation(_startedWalkAnimationName);
6478
}
6579

6680
protected virtual void StopWalk()
6781
{
6882
PhysicsService.EnablePhysicsForNpc(PrefabProps);
6983

70-
var animName = AnimationService.GetAnimationName(VmGothicEnums.AnimationType.Move, NpcContainer);
71-
PrefabProps.AnimationSystem.StopAnimation(animName);
84+
if (_startedWalkAnimationName != null)
85+
{
86+
PrefabProps.AnimationSystem.StopAnimation(_startedWalkAnimationName);
87+
}
7288
}
7389

7490
private bool IsDestinationReached()

Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/GoToFp.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ public class GoToFp : AbstractWalkAnimationAction2
1010

1111
private string _destination => Action.String0;
1212

13-
private FreePoint _freePoint;
14-
1513
public GoToFp(AnimationAction action, NpcContainer npcContainer) : base(action, npcContainer)
1614
{
1715
}
@@ -27,9 +25,14 @@ public override void Start()
2725
return;
2826
}
2927

28+
// Free the FP we still hold from a previous GoToFp. Otherwise every FP an NPC ever
29+
// visited stays locked until the NPC is culled, and roaming runs out of free FPs.
30+
if (Props.CurrentFreePoint != null && Props.CurrentFreePoint != _fp)
31+
Props.CurrentFreePoint.IsLocked = false;
32+
3033
_fp.IsLocked = true;
3134
Props.CurrentFreePoint = _fp;
32-
35+
3336
base.Start();
3437
}
3538

Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/GoToWp.cs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,6 @@ public override void Start()
3939
base.Start();
4040
}
4141

42-
/// <summary>
43-
/// Skip animation setting if we're on the final destination right from the start.
44-
/// </summary>
45-
protected override void StartWalk()
46-
{
47-
if (!IsFinishedFlag)
48-
{
49-
base.StartWalk();
50-
}
51-
}
52-
5342
protected override Vector3 GetWalkDestination()
5443
{
5544
return _route.Peek().Position;
@@ -73,6 +62,7 @@ protected override void OnDestinationReached()
7362
return;
7463
}
7564

65+
StopWalk();
7666
AnimationEnd();
7767

7868
IsFinishedFlag = true;

Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/Output.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
using Gothic.Core.Services.Caches;
77
using Gothic.Core.Services.Config;
88
using Gothic.Core.Services.Npc;
9-
using Gothic.Core.Adapters.Npc;
109
using Gothic.Core.Extensions;
11-
using Gothic.Core.Const;
10+
using Gothic.Core.Logging;
1211
using Reflex.Attributes;
1312
using UnityEngine;
13+
using Logger = Gothic.Core.Logging.Logger;
1414
using Random = UnityEngine.Random;
1515

1616
namespace Gothic.Core.Domain.Npc.Actions.AnimationActions
@@ -50,6 +50,14 @@ public override void Start()
5050
}
5151

5252
var audioClip = _audioService.CreateAudioClip(OutputName);
53+
54+
if (audioClip == null)
55+
{
56+
Logger.LogWarning($"AudioClip >{OutputName}< not found. Skipping speech output.", LogCat.Dialog);
57+
IsFinishedFlag = true;
58+
return;
59+
}
60+
5361
_audioPlaySeconds = audioClip.length;
5462

5563
// Hero

Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/PlayAni.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,21 @@ public override void Start()
2121
}
2222
ActionEndEventTime = PrefabProps.AnimationSystem.GetAnimationDuration(_animName);
2323
}
24+
25+
public override void Tick()
26+
{
27+
base.Tick();
28+
29+
if (IsFinishedFlag)
30+
return;
31+
32+
// An external stop (e.g. AI_StopAni) triggered the track's blend-out before
33+
// the natural duration elapsed — finish the action now rather than waiting
34+
// for the timer.
35+
if (PrefabProps.AnimationSystem.IsAnimationBlendingOut(_animName))
36+
{
37+
AnimationEnd();
38+
}
39+
}
2440
}
2541
}

Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/StandUp.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Gothic.Core.Models.Container;
2+
using Gothic.Core.Models.Vm;
23

34
namespace Gothic.Core.Domain.Npc.Actions.AnimationActions
45
{
@@ -10,6 +11,18 @@ public StandUp(AnimationAction action, NpcContainer npcContainer) : base(action,
1011

1112
public override void Start()
1213
{
14+
// G1 documentation: while using a Mobsi, AI_StandUp pops the NPC to standing without back-transitions.
15+
if (PrefabProps.CurrentInteractable != null)
16+
{
17+
PrefabProps.CurrentInteractable = null;
18+
PrefabProps.CurrentInteractableSlot = null;
19+
Props.CurrentInteractableStateId = -1;
20+
Props.BodyState = VmGothicEnums.BodyState.BsStand;
21+
22+
PhysicsService.EnablePhysicsForNpc(PrefabProps);
23+
}
24+
25+
// Playing the idle blends out a possibly running Mobsi loop animation on the same layer.
1326
PrefabProps.AnimationSystem.PlayIdleAnimation();
1427
}
1528

Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/UseMob.cs

Lines changed: 35 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace Gothic.Core.Domain.Npc.Actions.AnimationActions
1313
public class UseMob : AbstractWalkAnimationAction2
1414
{
1515
private const string _mobTransitionAnimationString = "T_{0}{1}{2}_2_{3}";
16-
private const string _mobLoopAnimationString = "S_{0}_S{1}";
16+
private const string _mobLoopAnimationString = "S_{0}{1}S{2}";
1717
private VobContainer _mobContainer;
1818
private GameObject _slotGo;
1919
private Vector3 _destination;
@@ -23,6 +23,9 @@ public class UseMob : AbstractWalkAnimationAction2
2323
private int _desiredState => Action.Int0;
2424
private bool IsStopUsingMob => _desiredState <= -1;
2525

26+
// -1 is the not-in-use state; scripts may send any negative value to stop.
27+
private int TargetState => IsStopUsingMob ? -1 : _desiredState;
28+
2629
private bool _isMobFoundButNotYetInitialized;
2730
private string _currentMobAnimation;
2831

@@ -40,6 +43,11 @@ public override void Start()
4043
_slotGo = PrefabProps.CurrentInteractableSlot;
4144
_mobsiScheme = _mobContainer.Props.GetVisualScheme();
4245

46+
// We already stand at the slot. Without this, Tick() would treat the unset
47+
// _destination (0,0,0) as walk target and never reach TickMobUsage().
48+
_destination = _slotGo.transform.position;
49+
IsDestReached = true;
50+
4351
StartMobUseAnimation();
4452
return;
4553
}
@@ -49,7 +57,8 @@ public override void Start()
4957
_mobContainer = container;
5058
_mobsiScheme = _mobContainer?.Props.GetVisualScheme();
5159

52-
if (container!.Go == null)
60+
// No free Mobsi of this scheme within reach (e.g. all occupied by other NPCs).
61+
if (container == null || !container.Go)
5362
{
5463
IsFinishedFlag = true;
5564
return;
@@ -63,9 +72,6 @@ public override void Start()
6372

6473
private void StartNow()
6574
{
66-
// We call Start only if the Mobsi is already available.
67-
base.Start();
68-
6975
_isMobFoundButNotYetInitialized = false;
7076

7177
var slot = GetNearestMobSlot();
@@ -83,6 +89,10 @@ private void StartNow()
8389
PrefabProps.CurrentInteractableSlot = _slotGo;
8490

8591
SetBodyState();
92+
93+
// base.Start() checks the walk destination and may start the walk loop - it must only
94+
// run once _destination is set, and not at all when no slot was found.
95+
base.Start();
8696
}
8797

8898
private void SetBodyState()
@@ -136,11 +146,13 @@ private void TickMobUsage()
136146
{
137147
if (PrefabProps.AnimationSystem.IsPlaying(_currentMobAnimation))
138148
return;
139-
140-
UpdateState();
149+
150+
// A finished transition moves the state one step toward the target (e.g. S1 -> S0 -> Stand when stopping).
151+
if (Props.CurrentInteractableStateId != TargetState)
152+
UpdateState();
141153

142154
// If we arrived at the Mobsi, we will further execute the transitions step-by-step until demanded state is reached.
143-
if (Props.CurrentInteractableStateId != _desiredState)
155+
if (Props.CurrentInteractableStateId != TargetState)
144156
{
145157
PlayTransitionAnimation();
146158
return;
@@ -151,14 +163,16 @@ private void TickMobUsage()
151163
{
152164
PrefabProps.CurrentInteractable = null;
153165
PrefabProps.CurrentInteractableSlot = null;
166+
Props.CurrentItem = -1;
154167
Props.BodyState = VmGothicEnums.BodyState.BsStand;
155168

156169
PhysicsService.EnablePhysicsForNpc(PrefabProps);
157170
}
158171
// Loop Mobsi animation until the same UseMob with -1 is called.
159172
else
160173
{
161-
var animName = string.Format(_mobLoopAnimationString, _mobsiScheme, _desiredState);
174+
// Loop animations carry the slot position as well (e.g. s_Bed_Front_S1 vs. s_Cauldron_S1).
175+
var animName = string.Format(_mobLoopAnimationString, _mobsiScheme, GetSlotPositionTag(_slotGo.name), TargetState);
162176
PrefabProps.AnimationSystem.PlayAnimation(animName);
163177
}
164178

@@ -205,7 +219,10 @@ private void StartMobUseAnimation()
205219

206220
NpcGo.transform.SetPositionAndRotation(_slotGo.transform.position, _slotGo.transform.rotation);
207221

208-
PlayTransitionAnimation();
222+
// Already in the demanded state (e.g. a repeated AI_UseMob with the same state):
223+
// TickMobUsage() will replay the loop animation and finish without a transition.
224+
if (Props.CurrentInteractableStateId != TargetState)
225+
PlayTransitionAnimation();
209226
}
210227

211228
private string GetSlotPositionTag(string name)
@@ -230,41 +247,19 @@ protected override Vector3 GetWalkDestination()
230247

231248
private void UpdateState()
232249
{
233-
// FIXME - We need to check. For Cauldron/Cook we have only t_s0_2_Stand, but not t_s1_2_s0 - But is it for all of them?
234-
if (IsStopUsingMob)
235-
{
236-
Props.CurrentInteractableStateId = -1;
237-
Props.CurrentItem = -1;
238-
}
239-
else
240-
{
241-
var newStateAddition = Props.CurrentInteractableStateId > _desiredState ? -1 : +1;
242-
Props.CurrentInteractableStateId += newStateAddition;
243-
}
250+
var step = Props.CurrentInteractableStateId > TargetState ? -1 : +1;
251+
Props.CurrentInteractableStateId += step;
244252
}
245253

246254
private void PlayTransitionAnimation()
247255
{
248-
string from;
249-
string to;
256+
var current = Props.CurrentInteractableStateId;
257+
var next = current + (TargetState > current ? +1 : -1);
250258

251-
// FIXME - We need to check. For Cauldron/Cook we have only t_s0_2_Stand, but not t_s1_2_s0 - But is it for all of them?
252-
if (IsStopUsingMob)
253-
{
254-
from = "S0";
255-
to = "Stand";
256-
}
257-
else
258-
{
259-
from = Props.CurrentInteractableStateId.ToString();
260-
to = $"S{Props.CurrentInteractableStateId + 1}";
261-
262-
from = from switch
263-
{
264-
"-1" => "Stand",
265-
_ => $"S{from}"
266-
};
267-
}
259+
// -1 is the not-in-use state and is named Stand inside the animations.
260+
// Both directions exist as own animations (e.g. t_Cauldron_Stand_2_S0, t_Cauldron_S0_2_S1, t_Cauldron_S1_2_S0).
261+
var from = current == -1 ? "Stand" : $"S{current}";
262+
var to = next == -1 ? "Stand" : $"S{next}";
268263

269264
var slotPositionName = GetSlotPositionTag(_slotGo.name);
270265
var animName = string.Format(_mobTransitionAnimationString, _mobsiScheme, slotPositionName, from, to);

0 commit comments

Comments
 (0)